tidaldb/tidal/docs/planning/milestone-8/phase-2/task-05-follower-db.md
jx12n 3bcfb3c576 feat: Bazel build, crate docs/ai-lookup, docker images, and engine hardening
- 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
2026-06-07 18:29:38 -06:00

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::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