diff --git a/Makefile b/Makefile index 294a9d8..d01320a 100644 --- a/Makefile +++ b/Makefile @@ -26,21 +26,21 @@ test: # Run bootstrap node explicitly run-node: @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 # Usage: make run-node2 BOOTSTRAP=/ip4/127.0.0.1/tcp/4001/p2p/ HTTP=5002 RAFT=7002 P2P=4002 run-node2: @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/ [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 # Usage: make run-node3 BOOTSTRAP=/ip4/127.0.0.1/tcp/4001/p2p/ HTTP=5003 RAFT=7003 P2P=4003 run-node3: @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/ [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-example: diff --git a/pkg/client/client.go b/pkg/client/client.go index 60a27ce..0360031 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -97,6 +97,23 @@ func (c *Client) Network() NetworkInfo { 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 func (c *Client) Connect() error { c.mu.Lock() diff --git a/pkg/client/defaults.go b/pkg/client/defaults.go new file mode 100644 index 0000000..0c6bd3e --- /dev/null +++ b/pkg/client/defaults.go @@ -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 +} diff --git a/pkg/client/implementations.go b/pkg/client/implementations.go index 59a423c..6267054 100644 --- a/pkg/client/implementations.go +++ b/pkg/client/implementations.go @@ -3,12 +3,12 @@ package client import ( "context" "fmt" + "net/url" + "os" "strings" "sync" "time" - "git.debros.io/DeBros/network/pkg/constants" - "git.debros.io/DeBros/network/pkg/storage" "github.com/libp2p/go-libp2p/core/peer" @@ -154,12 +154,16 @@ func (d *DatabaseClientImpl) isWriteOperation(sql string) bool { } return false -} // clearConnection clears the cached connection to force reconnection +} + +// clearConnection clears the cached connection to force reconnection func (d *DatabaseClientImpl) clearConnection() { d.mu.Lock() defer d.mu.Unlock() 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) { d.mu.Lock() defer d.mu.Unlock() @@ -169,6 +173,75 @@ func (d *DatabaseClientImpl) getRQLiteConnection() (*gorqlite.Connection, error) 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 func (d *DatabaseClientImpl) connectToAvailableNode() (*gorqlite.Connection, error) { // 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) } -// 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 func (d *DatabaseClientImpl) testConnection(conn *gorqlite.Connection) error { // Try a simple read query first (works even without leadership) diff --git a/pkg/client/interface.go b/pkg/client/interface.go index b5c71c9..a69f4ec 100644 --- a/pkg/client/interface.go +++ b/pkg/client/interface.go @@ -24,6 +24,9 @@ type NetworkClient interface { Connect() error Disconnect() error Health() (*HealthStatus, error) + + // Config access (snapshot copy) + Config() *ClientConfig } // DatabaseClient provides database operations for applications @@ -121,6 +124,7 @@ type ClientConfig struct { AppName string `json:"app_name"` DatabaseName string `json:"database_name"` BootstrapPeers []string `json:"bootstrap_peers"` + DatabaseEndpoints []string `json:"database_endpoints"` ConnectTimeout time.Duration `json:"connect_timeout"` RetryAttempts int `json:"retry_attempts"` RetryDelay time.Duration `json:"retry_delay"` @@ -129,12 +133,13 @@ type ClientConfig struct { // DefaultClientConfig returns a default client configuration func DefaultClientConfig(appName string) *ClientConfig { - return &ClientConfig{ - AppName: appName, - DatabaseName: fmt.Sprintf("%s_db", appName), - BootstrapPeers: []string{}, - ConnectTimeout: time.Second * 30, - RetryAttempts: 3, - RetryDelay: time.Second * 5, - } + return &ClientConfig{ + AppName: appName, + DatabaseName: fmt.Sprintf("%s_db", appName), + BootstrapPeers: []string{}, + DatabaseEndpoints: DefaultDatabaseEndpoints(), + ConnectTimeout: time.Second * 30, + RetryAttempts: 3, + RetryDelay: time.Second * 5, + } }