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>
577 lines
16 KiB
Go
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,
|
|
}
|
|
}
|