# 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 ```rust // 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, ) -> crate::Result<()> { self.require_writeable()?; // ... existing implementation ... } // All other write methods follow the same pattern. } ``` ### Follower startup in open.rs ```rust // tidal/src/db/open.rs pub fn open_db(config: Config) -> crate::Result { // ... 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 ```rust 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, leader_shard: ShardId, shutdown_rx: tokio::sync::watch::Receiver, ) { 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 ```rust // 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::ReadOnly` variant added to the error enum - [ ] All write methods (`signal`, `write_item`, `write_creator`, `write_item_embedding`, `write_creator_embedding`, `close_session`, etc.) return `Err(TidalError::ReadOnly)` when `role == Follower` - [ ] Read methods (`retrieve`, `search`, `read_decay_score`, etc.) work normally on followers - [ ] `TidalDb::start_replication(transport, leader_shard, shutdown_rx)` wires `SegmentReceiver` for follower nodes; is a no-op for `Single`/`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 warnings` and `cargo fmt` pass