tidaldb/docs/planning/milestone-8/phase-2/task-05-follower-db.md
jordan f4cfd6c81f feat: complete M8 replication primitives + forage enhancements + docs
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>
2026-02-24 13:17:19 -07:00

126 lines
3.6 KiB
Markdown

# 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<String, String>,
) -> 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<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
```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<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
```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