- Add BUILD.bazel across tidal, tidal-net, tidal-server, tidalctl for bzlmod build - Add tidal/ crate docs (README, CHANGELOG, CONTRIBUTING, AGENTS, CLAUDE, API, ARCHITECTURE) and ai-lookup reference - Add docker standalone/cluster/deploy images, compose, and prometheus config - Harden WAL (batch format, writer, dedup, diagnostics), text syncer/collectors, and vector registry - Expand tidalctl CLI and tests; restructure WAL/visibility integration test suites - Refine tidal-net transport/client/server and tidal-server cluster/scatter-gather
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