network/pkg/rqlite/consensus.go
2025-10-13 07:41:46 +03:00

181 lines
4.2 KiB
Go

package rqlite
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"sort"
"time"
)
// SelectCoordinator deterministically selects a coordinator from a list of node IDs
// Uses lexicographic ordering (lowest ID wins)
func SelectCoordinator(nodeIDs []string) string {
if len(nodeIDs) == 0 {
return ""
}
sorted := make([]string, len(nodeIDs))
copy(sorted, nodeIDs)
sort.Strings(sorted)
return sorted[0]
}
// ResolveConflict resolves a conflict between two database metadata entries
// Returns the winning metadata entry
func ResolveConflict(local, remote *DatabaseMetadata) *DatabaseMetadata {
if local == nil {
return remote
}
if remote == nil {
return local
}
// Compare vector clocks
localVC := VectorClock(local.VectorClock)
remoteVC := VectorClock(remote.VectorClock)
comparison := localVC.Compare(remoteVC)
if comparison == -1 {
// Local happens before remote, remote wins
return remote
} else if comparison == 1 {
// Remote happens before local, local wins
return local
}
// Concurrent: use version number as tiebreaker
if remote.Version > local.Version {
return remote
} else if local.Version > remote.Version {
return local
}
// Same version: use timestamp as tiebreaker
if remote.CreatedAt.After(local.CreatedAt) {
return remote
} else if local.CreatedAt.After(remote.CreatedAt) {
return local
}
// Same timestamp: use lexicographic comparison of database name
if remote.DatabaseName < local.DatabaseName {
return remote
}
return local
}
// MetadataChecksum represents a checksum of database metadata
type MetadataChecksum struct {
DatabaseName string `json:"database_name"`
Version uint64 `json:"version"`
Hash string `json:"hash"`
}
// ComputeMetadataChecksum computes a checksum for database metadata
func ComputeMetadataChecksum(db *DatabaseMetadata) MetadataChecksum {
if db == nil {
return MetadataChecksum{}
}
// Create a canonical representation for hashing
canonical := struct {
DatabaseName string
NodeIDs []string
PortMappings map[string]PortPair
Status DatabaseStatus
}{
DatabaseName: db.DatabaseName,
NodeIDs: make([]string, len(db.NodeIDs)),
PortMappings: db.PortMappings,
Status: db.Status,
}
// Sort node IDs for deterministic hashing
copy(canonical.NodeIDs, db.NodeIDs)
sort.Strings(canonical.NodeIDs)
// Serialize and hash
data, _ := json.Marshal(canonical)
hash := sha256.Sum256(data)
return MetadataChecksum{
DatabaseName: db.DatabaseName,
Version: db.Version,
Hash: hex.EncodeToString(hash[:]),
}
}
// ComputeFullStateChecksum computes checksums for all databases in the store
func ComputeFullStateChecksum(store *MetadataStore) []MetadataChecksum {
checksums := make([]MetadataChecksum, 0)
for _, name := range store.ListDatabases() {
if db := store.GetDatabase(name); db != nil {
checksums = append(checksums, ComputeMetadataChecksum(db))
}
}
// Sort by database name for deterministic ordering
sort.Slice(checksums, func(i, j int) bool {
return checksums[i].DatabaseName < checksums[j].DatabaseName
})
return checksums
}
// SelectNodesForDatabase selects N nodes from the list of healthy nodes
// Returns up to replicationFactor nodes
func SelectNodesForDatabase(healthyNodes []string, replicationFactor int) []string {
if len(healthyNodes) == 0 {
return []string{}
}
// Sort for deterministic selection
sorted := make([]string, len(healthyNodes))
copy(sorted, healthyNodes)
sort.Strings(sorted)
// Select first N nodes
n := replicationFactor
if n > len(sorted) {
n = len(sorted)
}
return sorted[:n]
}
// IsNodeInCluster checks if a node is part of a database cluster
func IsNodeInCluster(nodeID string, db *DatabaseMetadata) bool {
if db == nil {
return false
}
for _, id := range db.NodeIDs {
if id == nodeID {
return true
}
}
return false
}
// UpdateDatabaseMetadata updates metadata with vector clock and version increment
func UpdateDatabaseMetadata(db *DatabaseMetadata, nodeID string) {
if db.VectorClock == nil {
db.VectorClock = NewVectorClock()
}
// Increment vector clock for this node
vc := VectorClock(db.VectorClock)
vc.Increment(nodeID)
// Increment version
db.Version++
// Update last accessed time
db.LastAccessed = time.Now()
}