Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
8-task cluster completion bringing 3-replica StatefulSet from isolated nodes to fully functional cluster: 1. Fix Gateway /metrics 500 (wire PrometheusHandle) 2. gRPC server + SWIM background tasks (probe, suspicion, gossip dissemination) 3. join() registers peers in membership table via PingResponse fields 4. Shard rebalancing on membership changes (deterministic round-robin) 5. API cluster wiring (DNS resolution, Gateway, gRPC, gossip broadcaster) 6. Single binary merge (stemedb-api --features cluster replaces stemedb-node) 7. Auth header forwarding (X-API-Key passed through Gateway to backends) 8. CORS restriction (STEMEDB_ALLOWED_ORIGINS env var, permissive fallback)
153 lines
4.1 KiB
Protocol Buffer
153 lines
4.1 KiB
Protocol Buffer
syntax = "proto3";
|
|
package stemedb.sync.v1;
|
|
|
|
// SyncService enables node-to-node replication for StemeDB.
|
|
//
|
|
// The service supports two sync patterns:
|
|
// 1. Gossip: Push new assertions to peers immediately after ingestion
|
|
// 2. Anti-Entropy: Periodic Merkle root exchange and diff-based sync
|
|
service SyncService {
|
|
// Gossip pushes a new assertion to a peer.
|
|
// Called immediately after local ingestion to propagate data quickly.
|
|
rpc Gossip(GossipRequest) returns (GossipResponse);
|
|
|
|
// ExchangeRoots compares Merkle roots to detect divergence.
|
|
// If roots differ, the caller should fetch missing assertions.
|
|
rpc ExchangeRoots(RootExchangeRequest) returns (RootExchangeResponse);
|
|
|
|
// FetchAssertions retrieves assertion data by hash.
|
|
// Used after ExchangeRoots to pull missing assertions.
|
|
rpc FetchAssertions(FetchRequest) returns (FetchResponse);
|
|
|
|
// Ping checks if a peer is alive and returns basic metadata.
|
|
rpc Ping(PingRequest) returns (PingResponse);
|
|
|
|
// GetLeaves returns all Merkle tree leaf hashes.
|
|
// Used for computing the diff during anti-entropy sync.
|
|
rpc GetLeaves(GetLeavesRequest) returns (GetLeavesResponse);
|
|
}
|
|
|
|
// GossipRequest pushes a single assertion to a peer.
|
|
message GossipRequest {
|
|
// BLAKE3 hash of the assertion (32 bytes)
|
|
bytes assertion_hash = 1;
|
|
|
|
// Serialized assertion data (rkyv format)
|
|
bytes assertion_data = 2;
|
|
|
|
// HLC timestamp components for causal ordering
|
|
uint64 hlc_time = 3;
|
|
uint32 hlc_counter = 4;
|
|
bytes hlc_node_id = 5; // 16 bytes
|
|
}
|
|
|
|
message GossipResponse {
|
|
// True if the assertion was accepted (stored or already existed)
|
|
bool accepted = 1;
|
|
|
|
// Error message if rejected (e.g., validation failure)
|
|
string error = 2;
|
|
}
|
|
|
|
// RootExchangeRequest initiates Merkle root comparison.
|
|
message RootExchangeRequest {
|
|
// Local Merkle root hash (32 bytes)
|
|
bytes merkle_root = 1;
|
|
|
|
// Number of assertions in local tree
|
|
uint64 assertion_count = 2;
|
|
}
|
|
|
|
message RootExchangeResponse {
|
|
// Remote Merkle root hash (32 bytes)
|
|
bytes merkle_root = 1;
|
|
|
|
// Number of assertions in remote tree
|
|
uint64 assertion_count = 2;
|
|
|
|
// True if roots match (trees are identical)
|
|
bool roots_match = 3;
|
|
}
|
|
|
|
// FetchRequest asks for assertion data by hash.
|
|
message FetchRequest {
|
|
// List of assertion hashes to fetch (max 1000 per request)
|
|
repeated bytes hashes = 1;
|
|
}
|
|
|
|
message FetchResponse {
|
|
// Retrieved assertions (may be fewer than requested if not found)
|
|
repeated AssertionData assertions = 1;
|
|
}
|
|
|
|
// AssertionData pairs a hash with its serialized data.
|
|
message AssertionData {
|
|
// BLAKE3 hash of the assertion (32 bytes)
|
|
bytes hash = 1;
|
|
|
|
// Serialized assertion data (rkyv format)
|
|
bytes data = 2;
|
|
}
|
|
|
|
// PingRequest is a health check with node identity.
|
|
message PingRequest {
|
|
// Sender's node ID (16 bytes)
|
|
bytes node_id = 1;
|
|
|
|
// Piggybacked SWIM membership gossip updates
|
|
repeated MembershipUpdate updates = 2;
|
|
}
|
|
|
|
message PingResponse {
|
|
// Responder's node ID (16 bytes)
|
|
bytes node_id = 1;
|
|
|
|
// Number of assertions on this node
|
|
uint64 assertion_count = 2;
|
|
|
|
// Piggybacked SWIM membership gossip updates
|
|
repeated MembershipUpdate updates = 3;
|
|
|
|
// Responder's RPC address
|
|
string rpc_addr = 4;
|
|
|
|
// Responder's API address
|
|
string api_addr = 5;
|
|
}
|
|
|
|
// MembershipUpdate carries SWIM gossip state for a single node.
|
|
message MembershipUpdate {
|
|
// Node ID (16 bytes)
|
|
bytes node_id = 1;
|
|
|
|
// RPC address (e.g., "10.0.0.1:18182")
|
|
string rpc_addr = 2;
|
|
|
|
// API address (e.g., "10.0.0.1:18180")
|
|
string api_addr = 3;
|
|
|
|
// Node state: 0=Alive, 1=Suspect, 2=Dead, 3=Left
|
|
uint32 state = 4;
|
|
|
|
// Lamport clock time
|
|
uint64 lamport_time = 5;
|
|
|
|
// Incarnation number for crashing/rejoining detection
|
|
uint64 incarnation = 6;
|
|
}
|
|
|
|
// GetLeavesRequest requests all Merkle tree leaf hashes.
|
|
message GetLeavesRequest {
|
|
// Maximum number of leaves to return (0 = no limit, but capped at 10000)
|
|
uint64 max_leaves = 1;
|
|
}
|
|
|
|
// GetLeavesResponse returns Merkle tree leaf hashes.
|
|
message GetLeavesResponse {
|
|
// All leaf hashes (each 32 bytes)
|
|
repeated bytes leaves = 1;
|
|
|
|
// True if there are more leaves than max_leaves
|
|
bool truncated = 2;
|
|
}
|