Milestone 8 (phases 1-4): - Shard-aware WAL segment naming, BatchHeader v2, ShardRouter - Transport trait, InProcessTransport, WalShipper, FollowerDb - HLC, PNCounter, LWWRegister, CrdtSignalState, ReconciliationEngine - Session replication bridge with SeqNo/HWM, idempotency store Forage application: - Multi-source discovery engine with MAB exploration - Embedding-based label system, server handlers, UI refresh Other: - QUICKSTART.md, README.md, milestone-8 planning docs - Hard negative union semantics, RLHF export enhancements - Recovery benchmark and visibility test expansions - Split 8 oversized source files per CODING_GUIDELINES §9 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
3.6 KiB
3.6 KiB
Task 05: FollowerDb
Delivers
Wire TidalDb to support NodeRole::Follower startup in tidal/src/db/open.rs. Guard all write methods (signal, write_item, write_creator, etc.) to return TidalError::ReadOnly when role is Follower. Start SegmentReceiver on open for follower nodes.
Complexity: M
Dependencies
- Task 04 (SegmentReceiver)
- Phase 8.1 (NodeConfig, NodeRole)
Technical Design
Write guards in TidalDb
// tidal/src/db/mod.rs
impl TidalDb {
/// Guard that returns ReadOnly if this node is a follower.
fn require_writeable(&self) -> crate::Result<()> {
if !self.config.cluster.accepts_writes() {
return Err(TidalError::ReadOnly);
}
Ok(())
}
pub fn signal(
&self,
signal_type: &str,
entity_id: EntityId,
weight: f64,
timestamp: Timestamp,
) -> crate::Result<()> {
self.require_writeable()?;
// ... existing implementation ...
}
pub fn write_item(
&self,
entity_id: EntityId,
metadata: &HashMap<String, String>,
) -> crate::Result<()> {
self.require_writeable()?;
// ... existing implementation ...
}
// All other write methods follow the same pattern.
}
Follower startup in open.rs
// tidal/src/db/open.rs
pub fn open_db(config: Config) -> crate::Result<TidalDb> {
// ... existing open logic ...
let db = TidalDb { /* ... */ };
if config.cluster.role == NodeRole::Follower {
// Start segment receiver background task.
// The transport is set by the caller via db.start_replication(transport).
tracing::info!("TidalDb: starting as follower for shard {:?}", config.cluster.shard_id);
}
Ok(db)
}
TidalDb::start_replication
impl TidalDb {
/// Wire up replication transport for follower nodes.
///
/// Must be called after open() for NodeRole::Follower nodes.
/// No-op for NodeRole::Single or NodeRole::Leader.
pub fn start_replication(
&self,
transport: Arc<dyn Transport>,
leader_shard: ShardId,
shutdown_rx: tokio::sync::watch::Receiver<bool>,
) {
if self.config.cluster.role != NodeRole::Follower {
return;
}
let receiver = Arc::new(SegmentReceiver::new(
transport,
Arc::clone(&self.signal_ledger),
Arc::clone(&self.replication_state),
leader_shard,
));
receiver.start(shutdown_rx);
}
}
TidalError::ReadOnly
// tidal/src/error.rs (or wherever TidalError is defined)
#[derive(Debug, thiserror::Error)]
pub enum TidalError {
// ... existing variants ...
/// This node is a read-only follower; write operations are not permitted.
#[error("this node is read-only (follower)")]
ReadOnly,
}
Acceptance Criteria
TidalError::ReadOnlyvariant added to the error enum- All write methods (
signal,write_item,write_creator,write_item_embedding,write_creator_embedding,close_session, etc.) returnErr(TidalError::ReadOnly)whenrole == Follower - Read methods (
retrieve,search,read_decay_score, etc.) work normally on followers TidalDb::start_replication(transport, leader_shard, shutdown_rx)wiresSegmentReceiverfor follower nodes; is a no-op forSingle/Leader- Integration test: open as Follower, verify all writes fail with ReadOnly; open as Leader, verify writes succeed
- All existing tests pass (they use Single node, unaffected)
cargo clippy -D warningsandcargo fmtpass