Files
beads/cmd/bd/doctor/federation.go
emma f91bbf3a03 fix(build): add CGO build constraints for Dolt-dependent files
The dolthub/gozstd dependency requires CGO. Several files were importing
the dolt package without build constraints, causing CI failures when
building with CGO_ENABLED=0 for Linux, FreeBSD, and Android.

Changes:
- Add //go:build cgo to federation.go and doctor/federation.go
- Create dolt_server_cgo.go/nocgo.go to abstract dolt.Server usage
- Create federation_nocgo.go with stub command explaining CGO requirement
- Create doctor/federation_nocgo.go with stub health checks
- Update daemon.go to use the dolt server abstraction

Federation and Dolt-specific features are unavailable in non-CGO builds.
Users are directed to pre-built binaries from GitHub releases.

Fixes v0.49.0 CI failure.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 00:01:37 -08:00

577 lines
16 KiB
Go

//go:build cgo
package doctor
import (
"context"
"fmt"
"net"
"os"
"path/filepath"
"strings"
"time"
"github.com/steveyegge/beads/internal/configfile"
"github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/storage/dolt"
storagefactory "github.com/steveyegge/beads/internal/storage/factory"
)
// CheckFederationRemotesAPI checks if the remotesapi port is accessible for federation.
// This is the port used for peer-to-peer sync operations.
func CheckFederationRemotesAPI(path string) DoctorCheck {
backend, beadsDir := getBackendAndBeadsDir(path)
// Only relevant for Dolt backend
if backend != configfile.BackendDolt {
return DoctorCheck{
Name: "Federation remotesapi",
Status: StatusOK,
Message: "N/A (SQLite backend)",
Category: CategoryFederation,
}
}
// Check if dolt directory exists
doltPath := filepath.Join(beadsDir, "dolt")
if _, err := os.Stat(doltPath); os.IsNotExist(err) {
return DoctorCheck{
Name: "Federation remotesapi",
Status: StatusOK,
Message: "N/A (no dolt database)",
Category: CategoryFederation,
}
}
// Check if server PID file exists (indicates server mode might be running)
pidFile := filepath.Join(doltPath, "dolt-server.pid")
serverPID := dolt.GetRunningServerPID(doltPath)
if serverPID == 0 {
// No server running - check if we have remotes configured
ctx := context.Background()
store, err := storagefactory.NewFromConfigWithOptions(ctx, beadsDir, storagefactory.Options{ReadOnly: true})
if err != nil {
return DoctorCheck{
Name: "Federation remotesapi",
Status: StatusOK,
Message: "N/A (embedded mode, no remotes check needed)",
Category: CategoryFederation,
}
}
defer func() { _ = store.Close() }()
fedStore, ok := storage.AsFederated(store)
if !ok {
return DoctorCheck{
Name: "Federation remotesapi",
Status: StatusOK,
Message: "N/A (storage does not support federation)",
Category: CategoryFederation,
}
}
remotes, err := fedStore.ListRemotes(ctx)
if err != nil || len(remotes) == 0 {
return DoctorCheck{
Name: "Federation remotesapi",
Status: StatusOK,
Message: "N/A (no peers configured)",
Category: CategoryFederation,
}
}
// Has remotes but no server running - suggest starting in federation mode
return DoctorCheck{
Name: "Federation remotesapi",
Status: StatusWarning,
Message: fmt.Sprintf("Server not running (%d peers configured)", len(remotes)),
Detail: "Federation requires dolt sql-server for peer sync",
Fix: "Run 'bd daemon start --federation' to enable peer-to-peer sync",
Category: CategoryFederation,
}
}
// Server is running - check if remotesapi port is accessible
// Default remotesapi port is 8080
remotesAPIPort := dolt.DefaultRemotesAPIPort
host := "127.0.0.1"
addr := fmt.Sprintf("%s:%d", host, remotesAPIPort)
conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
if err != nil {
return DoctorCheck{
Name: "Federation remotesapi",
Status: StatusError,
Message: fmt.Sprintf("remotesapi port %d not accessible", remotesAPIPort),
Detail: fmt.Sprintf("Server PID %d found in %s but port unreachable: %v", serverPID, pidFile, err),
Fix: "Check if dolt sql-server is running with --remotesapi-port flag",
Category: CategoryFederation,
}
}
_ = conn.Close()
return DoctorCheck{
Name: "Federation remotesapi",
Status: StatusOK,
Message: fmt.Sprintf("Port %d accessible", remotesAPIPort),
Category: CategoryFederation,
}
}
// CheckFederationPeerConnectivity checks if configured peer remotes are reachable.
func CheckFederationPeerConnectivity(path string) DoctorCheck {
backend, beadsDir := getBackendAndBeadsDir(path)
// Only relevant for Dolt backend
if backend != configfile.BackendDolt {
return DoctorCheck{
Name: "Peer Connectivity",
Status: StatusOK,
Message: "N/A (SQLite backend)",
Category: CategoryFederation,
}
}
// Check if dolt directory exists
doltPath := filepath.Join(beadsDir, "dolt")
if _, err := os.Stat(doltPath); os.IsNotExist(err) {
return DoctorCheck{
Name: "Peer Connectivity",
Status: StatusOK,
Message: "N/A (no dolt database)",
Category: CategoryFederation,
}
}
ctx := context.Background()
store, err := storagefactory.NewFromConfigWithOptions(ctx, beadsDir, storagefactory.Options{ReadOnly: true})
if err != nil {
return DoctorCheck{
Name: "Peer Connectivity",
Status: StatusWarning,
Message: "Unable to open database",
Detail: err.Error(),
Category: CategoryFederation,
}
}
defer func() { _ = store.Close() }()
fedStore, ok := storage.AsFederated(store)
if !ok {
return DoctorCheck{
Name: "Peer Connectivity",
Status: StatusOK,
Message: "N/A (storage does not support federation)",
Category: CategoryFederation,
}
}
remotes, err := fedStore.ListRemotes(ctx)
if err != nil {
return DoctorCheck{
Name: "Peer Connectivity",
Status: StatusWarning,
Message: "Unable to list remotes",
Detail: err.Error(),
Category: CategoryFederation,
}
}
if len(remotes) == 0 {
return DoctorCheck{
Name: "Peer Connectivity",
Status: StatusOK,
Message: "No peers configured",
Category: CategoryFederation,
}
}
// Try to get sync status for each peer (this doesn't require network for cached data)
var reachable, unreachable []string
var statusDetails []string
for _, remote := range remotes {
// Skip origin - it's typically the DoltHub remote, not a peer
if remote.Name == "origin" {
continue
}
status, err := fedStore.SyncStatus(ctx, remote.Name)
if err != nil {
unreachable = append(unreachable, remote.Name)
statusDetails = append(statusDetails, fmt.Sprintf("%s: %v", remote.Name, err))
} else {
reachable = append(reachable, remote.Name)
if status.LocalAhead > 0 || status.LocalBehind > 0 {
statusDetails = append(statusDetails, fmt.Sprintf("%s: %d ahead, %d behind",
remote.Name, status.LocalAhead, status.LocalBehind))
}
}
}
// If no peers (only origin), report as OK
if len(reachable) == 0 && len(unreachable) == 0 {
return DoctorCheck{
Name: "Peer Connectivity",
Status: StatusOK,
Message: "No federation peers configured (only origin remote)",
Category: CategoryFederation,
}
}
if len(unreachable) > 0 {
return DoctorCheck{
Name: "Peer Connectivity",
Status: StatusWarning,
Message: fmt.Sprintf("%d/%d peers unreachable", len(unreachable), len(reachable)+len(unreachable)),
Detail: strings.Join(statusDetails, "\n"),
Fix: "Check peer URLs and network connectivity",
Category: CategoryFederation,
}
}
msg := fmt.Sprintf("%d peers reachable", len(reachable))
detail := ""
if len(statusDetails) > 0 {
detail = strings.Join(statusDetails, "\n")
}
return DoctorCheck{
Name: "Peer Connectivity",
Status: StatusOK,
Message: msg,
Detail: detail,
Category: CategoryFederation,
}
}
// CheckFederationSyncStaleness checks for stale sync status with peers.
func CheckFederationSyncStaleness(path string) DoctorCheck {
backend, beadsDir := getBackendAndBeadsDir(path)
// Only relevant for Dolt backend
if backend != configfile.BackendDolt {
return DoctorCheck{
Name: "Sync Staleness",
Status: StatusOK,
Message: "N/A (SQLite backend)",
Category: CategoryFederation,
}
}
// Check if dolt directory exists
doltPath := filepath.Join(beadsDir, "dolt")
if _, err := os.Stat(doltPath); os.IsNotExist(err) {
return DoctorCheck{
Name: "Sync Staleness",
Status: StatusOK,
Message: "N/A (no dolt database)",
Category: CategoryFederation,
}
}
ctx := context.Background()
store, err := storagefactory.NewFromConfigWithOptions(ctx, beadsDir, storagefactory.Options{ReadOnly: true})
if err != nil {
return DoctorCheck{
Name: "Sync Staleness",
Status: StatusWarning,
Message: "Unable to open database",
Detail: err.Error(),
Category: CategoryFederation,
}
}
defer func() { _ = store.Close() }()
fedStore, ok := storage.AsFederated(store)
if !ok {
return DoctorCheck{
Name: "Sync Staleness",
Status: StatusOK,
Message: "N/A (storage does not support federation)",
Category: CategoryFederation,
}
}
remotes, err := fedStore.ListRemotes(ctx)
if err != nil || len(remotes) == 0 {
return DoctorCheck{
Name: "Sync Staleness",
Status: StatusOK,
Message: "No peers configured",
Category: CategoryFederation,
}
}
// Check sync status for each peer
var staleWarnings []string
var totalBehind int
for _, remote := range remotes {
// Skip origin - check only federation peers
if remote.Name == "origin" {
continue
}
status, err := fedStore.SyncStatus(ctx, remote.Name)
if err != nil {
continue // Already handled in peer connectivity check
}
// Warn if significantly behind
if status.LocalBehind > 0 {
totalBehind += status.LocalBehind
staleWarnings = append(staleWarnings, fmt.Sprintf("%s: %d commits behind",
remote.Name, status.LocalBehind))
}
// Note: LastSync time tracking is not yet implemented in SyncStatus
// When it is, we can add time-based staleness warnings here
}
if len(staleWarnings) == 0 {
return DoctorCheck{
Name: "Sync Staleness",
Status: StatusOK,
Message: "Sync is up to date",
Category: CategoryFederation,
}
}
return DoctorCheck{
Name: "Sync Staleness",
Status: StatusWarning,
Message: fmt.Sprintf("%d total commits behind peers", totalBehind),
Detail: strings.Join(staleWarnings, "\n"),
Fix: "Run 'bd federation sync' to synchronize with peers",
Category: CategoryFederation,
}
}
// CheckFederationConflicts checks for unresolved merge conflicts.
func CheckFederationConflicts(path string) DoctorCheck {
backend, beadsDir := getBackendAndBeadsDir(path)
// Only relevant for Dolt backend
if backend != configfile.BackendDolt {
return DoctorCheck{
Name: "Federation Conflicts",
Status: StatusOK,
Message: "N/A (SQLite backend)",
Category: CategoryFederation,
}
}
// Check if dolt directory exists
doltPath := filepath.Join(beadsDir, "dolt")
if _, err := os.Stat(doltPath); os.IsNotExist(err) {
return DoctorCheck{
Name: "Federation Conflicts",
Status: StatusOK,
Message: "N/A (no dolt database)",
Category: CategoryFederation,
}
}
ctx := context.Background()
store, err := storagefactory.NewFromConfigWithOptions(ctx, beadsDir, storagefactory.Options{ReadOnly: true})
if err != nil {
return DoctorCheck{
Name: "Federation Conflicts",
Status: StatusWarning,
Message: "Unable to open database",
Detail: err.Error(),
Category: CategoryFederation,
}
}
defer func() { _ = store.Close() }()
// Check if storage supports versioning (needed for conflict detection)
verStore, ok := storage.AsVersioned(store)
if !ok {
return DoctorCheck{
Name: "Federation Conflicts",
Status: StatusOK,
Message: "N/A (storage does not support versioning)",
Category: CategoryFederation,
}
}
conflicts, err := verStore.GetConflicts(ctx)
if err != nil {
// Some errors are expected (e.g., no conflicts table)
if strings.Contains(err.Error(), "no such table") || strings.Contains(err.Error(), "doesn't exist") {
return DoctorCheck{
Name: "Federation Conflicts",
Status: StatusOK,
Message: "No conflicts",
Category: CategoryFederation,
}
}
return DoctorCheck{
Name: "Federation Conflicts",
Status: StatusWarning,
Message: "Unable to check conflicts",
Detail: err.Error(),
Category: CategoryFederation,
}
}
if len(conflicts) == 0 {
return DoctorCheck{
Name: "Federation Conflicts",
Status: StatusOK,
Message: "No conflicts",
Category: CategoryFederation,
}
}
// Group conflicts by issue ID
issueConflicts := make(map[string][]string)
for _, c := range conflicts {
issueConflicts[c.IssueID] = append(issueConflicts[c.IssueID], c.Field)
}
var details []string
for issueID, fields := range issueConflicts {
details = append(details, fmt.Sprintf("%s: %s", issueID, strings.Join(fields, ", ")))
}
return DoctorCheck{
Name: "Federation Conflicts",
Status: StatusError,
Message: fmt.Sprintf("%d unresolved conflicts in %d issues", len(conflicts), len(issueConflicts)),
Detail: strings.Join(details, "\n"),
Fix: "Run 'bd federation sync --strategy ours|theirs' to resolve conflicts",
Category: CategoryFederation,
}
}
// CheckDoltServerModeMismatch checks for mismatch between Dolt init and server mode.
// This detects cases where:
// - Server mode is expected but no server is running
// - Embedded mode is being used when server mode should be used (federation with peers)
func CheckDoltServerModeMismatch(path string) DoctorCheck {
backend, beadsDir := getBackendAndBeadsDir(path)
// Only relevant for Dolt backend
if backend != configfile.BackendDolt {
return DoctorCheck{
Name: "Dolt Mode",
Status: StatusOK,
Message: "N/A (SQLite backend)",
Category: CategoryFederation,
}
}
// Check if dolt directory exists
doltPath := filepath.Join(beadsDir, "dolt")
if _, err := os.Stat(doltPath); os.IsNotExist(err) {
return DoctorCheck{
Name: "Dolt Mode",
Status: StatusOK,
Message: "N/A (no dolt database)",
Category: CategoryFederation,
}
}
// Check for server PID file
serverPID := dolt.GetRunningServerPID(doltPath)
// Open storage to check for remotes
ctx := context.Background()
store, err := storagefactory.NewFromConfigWithOptions(ctx, beadsDir, storagefactory.Options{ReadOnly: true})
if err != nil {
// If we can't open the store, check if there's a lock file indicating embedded mode
lockFile := filepath.Join(doltPath, ".dolt", "lock")
if _, lockErr := os.Stat(lockFile); lockErr == nil && serverPID == 0 {
return DoctorCheck{
Name: "Dolt Mode",
Status: StatusWarning,
Message: "Embedded mode with lock file",
Detail: "Another process may be using the database in embedded mode",
Fix: "Close other bd processes or start daemon with --federation",
Category: CategoryFederation,
}
}
return DoctorCheck{
Name: "Dolt Mode",
Status: StatusWarning,
Message: "Unable to open database",
Detail: err.Error(),
Category: CategoryFederation,
}
}
defer func() { _ = store.Close() }()
// Check if storage supports federation
fedStore, isFederated := storage.AsFederated(store)
if !isFederated {
if serverPID > 0 {
return DoctorCheck{
Name: "Dolt Mode",
Status: StatusOK,
Message: "Server mode (no federation)",
Category: CategoryFederation,
}
}
return DoctorCheck{
Name: "Dolt Mode",
Status: StatusOK,
Message: "Embedded mode",
Category: CategoryFederation,
}
}
// Check for configured remotes
remotes, err := fedStore.ListRemotes(ctx)
if err != nil {
return DoctorCheck{
Name: "Dolt Mode",
Status: StatusWarning,
Message: "Unable to list remotes",
Detail: err.Error(),
Category: CategoryFederation,
}
}
// Count federation peers (exclude origin)
peerCount := 0
for _, r := range remotes {
if r.Name != "origin" {
peerCount++
}
}
// Determine expected vs actual mode
if peerCount > 0 && serverPID == 0 {
return DoctorCheck{
Name: "Dolt Mode",
Status: StatusWarning,
Message: fmt.Sprintf("Embedded mode with %d peers configured", peerCount),
Detail: "Federation with peers requires server mode for multi-writer support",
Fix: "Run 'bd daemon start --federation' to enable server mode",
Category: CategoryFederation,
}
}
if serverPID > 0 {
return DoctorCheck{
Name: "Dolt Mode",
Status: StatusOK,
Message: fmt.Sprintf("Server mode (PID %d)", serverPID),
Detail: fmt.Sprintf("%d peers configured", peerCount),
Category: CategoryFederation,
}
}
return DoctorCheck{
Name: "Dolt Mode",
Status: StatusOK,
Message: "Embedded mode",
Detail: "No federation peers configured",
Category: CategoryFederation,
}
}