feat: add configurable database endpoints with multiaddr to HTTP URL conversion

This commit is contained in:
anonpenguin 2025-08-09 17:04:36 +03:00
parent 26e2bbb477
commit 7bcf32e527
5 changed files with 212 additions and 46 deletions

View File

@ -26,21 +26,21 @@ test:
# Run bootstrap node explicitly # Run bootstrap node explicitly
run-node: run-node:
@echo "Starting BOOTSTRAP node (role=bootstrap)..." @echo "Starting BOOTSTRAP node (role=bootstrap)..."
go run cmd/node/main.go -role bootstrap -data ./data/bootstrap -advertise localhost -p2p-port $${P2P:-4001} go run ./cmd/node -role bootstrap -data ./data/bootstrap -advertise localhost -p2p-port $${P2P:-4001}
# Run second node (regular) - requires BOOTSTRAP multiaddr # Run second node (regular) - requires BOOTSTRAP multiaddr
# Usage: make run-node2 BOOTSTRAP=/ip4/127.0.0.1/tcp/4001/p2p/<ID> HTTP=5002 RAFT=7002 P2P=4002 # Usage: make run-node2 BOOTSTRAP=/ip4/127.0.0.1/tcp/4001/p2p/<ID> HTTP=5002 RAFT=7002 P2P=4002
run-node2: run-node2:
@echo "Starting REGULAR node2 (role=node)..." @echo "Starting REGULAR node2 (role=node)..."
@if [ -z "$(BOOTSTRAP)" ]; then echo "ERROR: Provide BOOTSTRAP multiaddr: make run-node2 BOOTSTRAP=/ip4/127.0.0.1/tcp/4001/p2p/<ID> [HTTP=5002 RAFT=7002 P2P=4002]"; exit 1; fi @if [ -z "$(BOOTSTRAP)" ]; then echo "ERROR: Provide BOOTSTRAP multiaddr: make run-node2 BOOTSTRAP=/ip4/127.0.0.1/tcp/4001/p2p/<ID> [HTTP=5002 RAFT=7002 P2P=4002]"; exit 1; fi
go run cmd/node/main.go -role node -id node2 -data ./data/node2 -bootstrap $(BOOTSTRAP) -rqlite-http-port $${HTTP:-5002} -rqlite-raft-port $${RAFT:-7002} -p2p-port $${P2P:-4002} -advertise $${ADVERTISE:-localhost} go run ./cmd/node -role node -id node2 -data ./data/node2 -bootstrap $(BOOTSTRAP) -rqlite-http-port $${HTTP:-5002} -rqlite-raft-port $${RAFT:-7002} -p2p-port $${P2P:-4002} -advertise $${ADVERTISE:-localhost}
# Run third node (regular) - requires BOOTSTRAP multiaddr # Run third node (regular) - requires BOOTSTRAP multiaddr
# Usage: make run-node3 BOOTSTRAP=/ip4/127.0.0.1/tcp/4001/p2p/<ID> HTTP=5003 RAFT=7003 P2P=4003 # Usage: make run-node3 BOOTSTRAP=/ip4/127.0.0.1/tcp/4001/p2p/<ID> HTTP=5003 RAFT=7003 P2P=4003
run-node3: run-node3:
@echo "Starting REGULAR node3 (role=node)..." @echo "Starting REGULAR node3 (role=node)..."
@if [ -z "$(BOOTSTRAP)" ]; then echo "ERROR: Provide BOOTSTRAP multiaddr: make run-node3 BOOTSTRAP=/ip4/127.0.0.1/tcp/4001/p2p/<ID> [HTTP=5003 RAFT=7003 P2P=4003]"; exit 1; fi @if [ -z "$(BOOTSTRAP)" ]; then echo "ERROR: Provide BOOTSTRAP multiaddr: make run-node3 BOOTSTRAP=/ip4/127.0.0.1/tcp/4001/p2p/<ID> [HTTP=5003 RAFT=7003 P2P=4003]"; exit 1; fi
go run cmd/node/main.go -role node -id node3 -data ./data/node3 -bootstrap $(BOOTSTRAP) -rqlite-http-port $${HTTP:-5003} -rqlite-raft-port $${RAFT:-7003} -p2p-port $${P2P:-4003} -advertise $${ADVERTISE:-localhost} go run ./cmd/node -role node -id node3 -data ./data/node3 -bootstrap $(BOOTSTRAP) -rqlite-http-port $${HTTP:-5003} -rqlite-raft-port $${RAFT:-7003} -p2p-port $${P2P:-4003} -advertise $${ADVERTISE:-localhost}
# Run basic usage example # Run basic usage example
run-example: run-example:

View File

@ -97,6 +97,23 @@ func (c *Client) Network() NetworkInfo {
return c.network return c.network
} }
// Config returns a snapshot copy of the client's configuration
func (c *Client) Config() *ClientConfig {
c.mu.RLock()
defer c.mu.RUnlock()
if c.config == nil {
return nil
}
cp := *c.config
if c.config.BootstrapPeers != nil {
cp.BootstrapPeers = append([]string(nil), c.config.BootstrapPeers...)
}
if c.config.DatabaseEndpoints != nil {
cp.DatabaseEndpoints = append([]string(nil), c.config.DatabaseEndpoints...)
}
return &cp
}
// Connect establishes connection to the network // Connect establishes connection to the network
func (c *Client) Connect() error { func (c *Client) Connect() error {
c.mu.Lock() c.mu.Lock()

102
pkg/client/defaults.go Normal file
View File

@ -0,0 +1,102 @@
package client
import (
"os"
"strconv"
"strings"
"git.debros.io/DeBros/network/pkg/constants"
"github.com/multiformats/go-multiaddr"
)
// DefaultBootstrapPeers returns the library's default bootstrap peer multiaddrs.
func DefaultBootstrapPeers() []string {
peers := constants.GetBootstrapPeers()
out := make([]string, len(peers))
copy(out, peers)
return out
}
// DefaultDatabaseEndpoints returns default DB HTTP endpoints derived from default bootstrap peers.
// Port defaults to RQLite HTTP 5001, or RQLITE_PORT if set.
func DefaultDatabaseEndpoints() []string {
port := 5001
if v := os.Getenv("RQLITE_PORT"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
port = n
}
}
peers := DefaultBootstrapPeers()
if len(peers) == 0 {
return []string{"http://localhost:" + strconv.Itoa(port)}
}
endpoints := make([]string, 0, len(peers))
for _, s := range peers {
ma, err := multiaddr.NewMultiaddr(s)
if err != nil {
continue
}
endpoints = append(endpoints, endpointFromMultiaddr(ma, port))
}
out := dedupeStrings(endpoints)
if len(out) == 0 {
out = []string{"http://localhost:" + strconv.Itoa(port)}
}
return out
}
// MapAddrsToDBEndpoints converts a set of peer multiaddrs to DB HTTP endpoints using dbPort.
func MapAddrsToDBEndpoints(addrs []multiaddr.Multiaddr, dbPort int) []string {
if dbPort <= 0 {
dbPort = 5001
}
eps := make([]string, 0, len(addrs))
for _, ma := range addrs {
eps = append(eps, endpointFromMultiaddr(ma, dbPort))
}
return dedupeStrings(eps)
}
func endpointFromMultiaddr(ma multiaddr.Multiaddr, port int) string {
var host string
// Prefer DNS if present, then IP
if v, err := ma.ValueForProtocol(multiaddr.P_DNS); err == nil && v != "" {
host = v
}
if host == "" {
if v, err := ma.ValueForProtocol(multiaddr.P_DNS4); err == nil && v != "" { host = v }
}
if host == "" {
if v, err := ma.ValueForProtocol(multiaddr.P_DNS6); err == nil && v != "" { host = v }
}
if host == "" {
if v, err := ma.ValueForProtocol(multiaddr.P_IP4); err == nil && v != "" { host = v }
}
if host == "" {
if v, err := ma.ValueForProtocol(multiaddr.P_IP6); err == nil && v != "" { host = v }
}
if host == "" {
host = "localhost"
}
return "http://" + host + ":" + strconv.Itoa(port)
}
func dedupeStrings(in []string) []string {
m := make(map[string]struct{}, len(in))
out := make([]string, 0, len(in))
for _, s := range in {
s = strings.TrimSpace(s)
if s == "" {
continue
}
if _, ok := m[s]; ok {
continue
}
m[s] = struct{}{}
out = append(out, s)
}
return out
}

View File

@ -3,12 +3,12 @@ package client
import ( import (
"context" "context"
"fmt" "fmt"
"net/url"
"os"
"strings" "strings"
"sync" "sync"
"time" "time"
"git.debros.io/DeBros/network/pkg/constants"
"git.debros.io/DeBros/network/pkg/storage" "git.debros.io/DeBros/network/pkg/storage"
"github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peer"
@ -154,12 +154,16 @@ func (d *DatabaseClientImpl) isWriteOperation(sql string) bool {
} }
return false return false
} // clearConnection clears the cached connection to force reconnection }
// clearConnection clears the cached connection to force reconnection
func (d *DatabaseClientImpl) clearConnection() { func (d *DatabaseClientImpl) clearConnection() {
d.mu.Lock() d.mu.Lock()
defer d.mu.Unlock() defer d.mu.Unlock()
d.connection = nil d.connection = nil
} // getRQLiteConnection returns a connection to RQLite, creating one if needed }
// getRQLiteConnection returns a connection to RQLite, creating one if needed
func (d *DatabaseClientImpl) getRQLiteConnection() (*gorqlite.Connection, error) { func (d *DatabaseClientImpl) getRQLiteConnection() (*gorqlite.Connection, error) {
d.mu.Lock() d.mu.Lock()
defer d.mu.Unlock() defer d.mu.Unlock()
@ -169,6 +173,75 @@ func (d *DatabaseClientImpl) getRQLiteConnection() (*gorqlite.Connection, error)
return d.connectToAvailableNode() return d.connectToAvailableNode()
} }
// getRQLiteNodes returns a list of RQLite node URLs with precedence:
// 1) client config DatabaseEndpoints
// 2) RQLITE_NODES env (comma/space separated)
// 3) library defaults via DefaultDatabaseEndpoints()
func (d *DatabaseClientImpl) getRQLiteNodes() []string {
// 1) Prefer explicit configuration on the client
if d.client != nil && d.client.config != nil && len(d.client.config.DatabaseEndpoints) > 0 {
return dedupeStrings(normalizeEndpoints(d.client.config.DatabaseEndpoints))
}
// 2) Backward compatibility: RQLITE_NODES environment variable
if raw := os.Getenv("RQLITE_NODES"); strings.TrimSpace(raw) != "" {
// split by comma or whitespace
parts := splitCSVOrSpace(raw)
if len(parts) > 0 {
return dedupeStrings(normalizeEndpoints(parts))
}
}
// 3) Fallback to library defaults derived from bootstrap peers
return DefaultDatabaseEndpoints()
}
// normalizeEndpoints ensures each endpoint has an http scheme and a port (defaults to 5001)
func normalizeEndpoints(in []string) []string {
out := make([]string, 0, len(in))
for _, s := range in {
s = strings.TrimSpace(s)
if s == "" {
continue
}
// Prepend scheme if missing so url.Parse handles host:port
if !strings.HasPrefix(s, "http://") && !strings.HasPrefix(s, "https://") {
s = "http://" + s
}
u, err := url.Parse(s)
if err != nil || u.Host == "" {
continue
}
// Ensure port present
if h := u.Host; !hasPort(h) {
u.Host = u.Host + ":5001"
}
out = append(out, u.String())
}
return out
}
func hasPort(hostport string) bool {
// cheap check for :port suffix (IPv6 with brackets handled by url.Parse earlier)
if i := strings.LastIndex(hostport, ":"); i > -1 && i < len(hostport)-1 {
// ensure the segment after ':' is numeric-ish
for _, c := range hostport[i+1:] {
if c < '0' || c > '9' {
return false
}
}
return true
}
return false
}
func splitCSVOrSpace(s string) []string {
// replace commas with spaces, then split on spaces
s = strings.ReplaceAll(s, ",", " ")
fields := strings.Fields(s)
return fields
}
// connectToAvailableNode tries to connect to any available RQLite node // connectToAvailableNode tries to connect to any available RQLite node
func (d *DatabaseClientImpl) connectToAvailableNode() (*gorqlite.Connection, error) { func (d *DatabaseClientImpl) connectToAvailableNode() (*gorqlite.Connection, error) {
// Get RQLite nodes from environment or use defaults // Get RQLite nodes from environment or use defaults
@ -197,37 +270,6 @@ func (d *DatabaseClientImpl) connectToAvailableNode() (*gorqlite.Connection, err
return nil, fmt.Errorf("failed to connect to any RQLite instance. Last error: %w", lastErr) return nil, fmt.Errorf("failed to connect to any RQLite instance. Last error: %w", lastErr)
} }
// getRQLiteNodes returns a list of RQLite node URLs using the peer IPs/hostnames from bootstrap.go, always on port 5001
func (d *DatabaseClientImpl) getRQLiteNodes() []string {
// Use bootstrap peer addresses from constants
// Import the constants package
// We'll extract the IP/host from the multiaddr and build the HTTP URL
var nodes []string
for _, addr := range constants.GetBootstrapPeers() {
// Example multiaddr: /ip4/57.129.81.31/tcp/4001/p2p/12D3KooWQRK2duw5B5LXi8gA7HBBFiCsLvwyph2ZU9VBmvbE1Nei
parts := strings.Split(addr, "/")
var host string
var port string = "5001" // always use RQLite HTTP 5001
for i := 0; i < len(parts); i++ {
if parts[i] == "ip4" || parts[i] == "ip6" {
host = parts[i+1]
}
if parts[i] == "dns" || parts[i] == "dns4" || parts[i] == "dns6" {
host = parts[i+1]
}
// ignore tcp port in multiaddr, always use 5001 for RQLite HTTP
}
if host != "" {
nodes = append(nodes, "http://"+host+":"+port)
}
}
// If no peers found, fallback to localhost:5001
if len(nodes) == 0 {
nodes = append(nodes, "http://localhost:5001")
}
return nodes
}
// testConnection performs a health check on the RQLite connection // testConnection performs a health check on the RQLite connection
func (d *DatabaseClientImpl) testConnection(conn *gorqlite.Connection) error { func (d *DatabaseClientImpl) testConnection(conn *gorqlite.Connection) error {
// Try a simple read query first (works even without leadership) // Try a simple read query first (works even without leadership)

View File

@ -24,6 +24,9 @@ type NetworkClient interface {
Connect() error Connect() error
Disconnect() error Disconnect() error
Health() (*HealthStatus, error) Health() (*HealthStatus, error)
// Config access (snapshot copy)
Config() *ClientConfig
} }
// DatabaseClient provides database operations for applications // DatabaseClient provides database operations for applications
@ -121,6 +124,7 @@ type ClientConfig struct {
AppName string `json:"app_name"` AppName string `json:"app_name"`
DatabaseName string `json:"database_name"` DatabaseName string `json:"database_name"`
BootstrapPeers []string `json:"bootstrap_peers"` BootstrapPeers []string `json:"bootstrap_peers"`
DatabaseEndpoints []string `json:"database_endpoints"`
ConnectTimeout time.Duration `json:"connect_timeout"` ConnectTimeout time.Duration `json:"connect_timeout"`
RetryAttempts int `json:"retry_attempts"` RetryAttempts int `json:"retry_attempts"`
RetryDelay time.Duration `json:"retry_delay"` RetryDelay time.Duration `json:"retry_delay"`
@ -129,12 +133,13 @@ type ClientConfig struct {
// DefaultClientConfig returns a default client configuration // DefaultClientConfig returns a default client configuration
func DefaultClientConfig(appName string) *ClientConfig { func DefaultClientConfig(appName string) *ClientConfig {
return &ClientConfig{ return &ClientConfig{
AppName: appName, AppName: appName,
DatabaseName: fmt.Sprintf("%s_db", appName), DatabaseName: fmt.Sprintf("%s_db", appName),
BootstrapPeers: []string{}, BootstrapPeers: []string{},
ConnectTimeout: time.Second * 30, DatabaseEndpoints: DefaultDatabaseEndpoints(),
RetryAttempts: 3, ConnectTimeout: time.Second * 30,
RetryDelay: time.Second * 5, RetryAttempts: 3,
} RetryDelay: time.Second * 5,
}
} }