mirror of
https://github.com/DeBrosOfficial/network.git
synced 2025-10-06 08:19:07 +00:00
feat: add configurable database endpoints with multiaddr to HTTP URL conversion
This commit is contained in:
parent
26e2bbb477
commit
7bcf32e527
6
Makefile
6
Makefile
@ -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:
|
||||||
|
@ -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
102
pkg/client/defaults.go
Normal 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
|
||||||
|
}
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user