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>
126 lines
3.6 KiB
Markdown
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
|