feat(federation): add dolt sql-server mode for daemon (bd-wkumz.2)
Add --federation flag to bd daemon start that runs dolt sql-server instead of the embedded driver. Enables multi-writer support and exposes remotesapi on port 8080 for peer-to-peer push/pull. Changes: - Add --federation flag to daemon start command - Create dolt server manager (internal/storage/dolt/server.go) - Update DoltStore to support server mode via MySQL protocol - Integrate server lifecycle into daemon (auto-start/stop) - Add tests for server management and server mode connections Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Steve Yegge
parent
458fb7197a
commit
da4584ae57
@@ -26,8 +26,10 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
// Import Dolt driver
|
||||
// Import Dolt embedded driver
|
||||
_ "github.com/dolthub/driver"
|
||||
// Import MySQL driver for server mode connections
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
|
||||
"github.com/steveyegge/beads/internal/storage"
|
||||
)
|
||||
@@ -56,6 +58,12 @@ type Config struct {
|
||||
Remote string // Default remote name (e.g., "origin")
|
||||
Database string // Database name within Dolt (default: "beads")
|
||||
ReadOnly bool // Open in read-only mode (skip schema init)
|
||||
|
||||
// Server mode options (federation)
|
||||
ServerMode bool // Connect to dolt sql-server instead of embedded
|
||||
ServerHost string // Server host (default: 127.0.0.1)
|
||||
ServerPort int // Server port (default: 3306)
|
||||
ServerUser string // MySQL user (default: root)
|
||||
}
|
||||
|
||||
// New creates a new Dolt storage backend
|
||||
@@ -84,44 +92,39 @@ func New(ctx context.Context, cfg *Config) (*DoltStore, error) {
|
||||
cfg.Remote = "origin"
|
||||
}
|
||||
|
||||
// Server mode defaults
|
||||
if cfg.ServerMode {
|
||||
if cfg.ServerHost == "" {
|
||||
cfg.ServerHost = "127.0.0.1"
|
||||
}
|
||||
if cfg.ServerPort == 0 {
|
||||
cfg.ServerPort = DefaultSQLPort
|
||||
}
|
||||
if cfg.ServerUser == "" {
|
||||
cfg.ServerUser = "root"
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
if err := os.MkdirAll(cfg.Path, 0o750); err != nil {
|
||||
return nil, fmt.Errorf("failed to create database directory: %w", err)
|
||||
}
|
||||
|
||||
// First, connect without specifying a database to create it if needed
|
||||
initConnStr := fmt.Sprintf(
|
||||
"file://%s?commitname=%s&commitemail=%s",
|
||||
cfg.Path, cfg.CommitterName, cfg.CommitterEmail)
|
||||
var db *sql.DB
|
||||
var connStr string
|
||||
var err error
|
||||
|
||||
initDB, err := sql.Open("dolt", initConnStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open Dolt for initialization: %w", err)
|
||||
if cfg.ServerMode {
|
||||
// Server mode: connect via MySQL protocol to dolt sql-server
|
||||
db, connStr, err = openServerConnection(ctx, cfg)
|
||||
} else {
|
||||
// Embedded mode: use Dolt driver directly
|
||||
db, connStr, err = openEmbeddedConnection(ctx, cfg)
|
||||
}
|
||||
|
||||
// Create the database if it doesn't exist
|
||||
_, err = initDB.ExecContext(ctx, fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", cfg.Database))
|
||||
if err != nil {
|
||||
_ = initDB.Close() // nolint:gosec // G104: error ignored on early return
|
||||
return nil, fmt.Errorf("failed to create database: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
_ = initDB.Close() // nolint:gosec // G104: connection no longer needed
|
||||
|
||||
// Now connect with the database specified
|
||||
connStr := fmt.Sprintf(
|
||||
"file://%s?commitname=%s&commitemail=%s&database=%s",
|
||||
cfg.Path, cfg.CommitterName, cfg.CommitterEmail, cfg.Database)
|
||||
|
||||
db, err := sql.Open("dolt", connStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open Dolt database: %w", err)
|
||||
}
|
||||
|
||||
// Configure connection pool
|
||||
// Dolt embedded mode is single-writer like SQLite
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(1)
|
||||
db.SetConnMaxLifetime(0)
|
||||
|
||||
// Test connection
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
@@ -155,6 +158,82 @@ func New(ctx context.Context, cfg *Config) (*DoltStore, error) {
|
||||
return store, nil
|
||||
}
|
||||
|
||||
// openEmbeddedConnection opens a connection using the embedded Dolt driver
|
||||
func openEmbeddedConnection(ctx context.Context, cfg *Config) (*sql.DB, string, error) {
|
||||
// First, connect without specifying a database to create it if needed
|
||||
initConnStr := fmt.Sprintf(
|
||||
"file://%s?commitname=%s&commitemail=%s",
|
||||
cfg.Path, cfg.CommitterName, cfg.CommitterEmail)
|
||||
|
||||
initDB, err := sql.Open("dolt", initConnStr)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to open Dolt for initialization: %w", err)
|
||||
}
|
||||
|
||||
// Create the database if it doesn't exist
|
||||
_, err = initDB.ExecContext(ctx, fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", cfg.Database))
|
||||
if err != nil {
|
||||
_ = initDB.Close()
|
||||
return nil, "", fmt.Errorf("failed to create database: %w", err)
|
||||
}
|
||||
_ = initDB.Close()
|
||||
|
||||
// Now connect with the database specified
|
||||
connStr := fmt.Sprintf(
|
||||
"file://%s?commitname=%s&commitemail=%s&database=%s",
|
||||
cfg.Path, cfg.CommitterName, cfg.CommitterEmail, cfg.Database)
|
||||
|
||||
db, err := sql.Open("dolt", connStr)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to open Dolt database: %w", err)
|
||||
}
|
||||
|
||||
// Configure connection pool
|
||||
// Dolt embedded mode is single-writer like SQLite
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(1)
|
||||
db.SetConnMaxLifetime(0)
|
||||
|
||||
return db, connStr, nil
|
||||
}
|
||||
|
||||
// openServerConnection opens a connection to a dolt sql-server via MySQL protocol
|
||||
func openServerConnection(ctx context.Context, cfg *Config) (*sql.DB, string, error) {
|
||||
// DSN format: user@tcp(host:port)/database?parseTime=true
|
||||
// parseTime=true tells the MySQL driver to parse DATETIME/TIMESTAMP to time.Time
|
||||
connStr := fmt.Sprintf("%s@tcp(%s:%d)/%s?parseTime=true",
|
||||
cfg.ServerUser, cfg.ServerHost, cfg.ServerPort, cfg.Database)
|
||||
|
||||
db, err := sql.Open("mysql", connStr)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to open Dolt server connection: %w", err)
|
||||
}
|
||||
|
||||
// Server mode supports multi-writer, configure reasonable pool size
|
||||
db.SetMaxOpenConns(10)
|
||||
db.SetMaxIdleConns(5)
|
||||
db.SetConnMaxLifetime(5 * time.Minute)
|
||||
|
||||
// Ensure database exists (may need to create it)
|
||||
// First connect without database to create it
|
||||
initConnStr := fmt.Sprintf("%s@tcp(%s:%d)/?parseTime=true",
|
||||
cfg.ServerUser, cfg.ServerHost, cfg.ServerPort)
|
||||
initDB, err := sql.Open("mysql", initConnStr)
|
||||
if err != nil {
|
||||
_ = db.Close()
|
||||
return nil, "", fmt.Errorf("failed to open init connection: %w", err)
|
||||
}
|
||||
defer func() { _ = initDB.Close() }()
|
||||
|
||||
_, err = initDB.ExecContext(ctx, fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", cfg.Database))
|
||||
if err != nil {
|
||||
_ = db.Close()
|
||||
return nil, "", fmt.Errorf("failed to create database: %w", err)
|
||||
}
|
||||
|
||||
return db, connStr, nil
|
||||
}
|
||||
|
||||
// initSchema creates all tables if they don't exist
|
||||
func (s *DoltStore) initSchema(ctx context.Context) error {
|
||||
// Execute schema creation - split into individual statements
|
||||
|
||||
Reference in New Issue
Block a user