Phase 2d (0.6.1-beta): route manifest + blob ops through file_holders

Switch ALL propagation-decision reads to the flat holder set.

push_manifest_to_downstream now targets file_holders instead of
blob_downstream. ManifestPush receive-side relay likewise — known
holders fan out to up to 5 most-recent peers instead of a directional
tree.

Blob delete notices: single flat fan-out to file_holders; the legacy
upstream_node tree-healing field is emitted as None (wire-stable via
serde default) and ignored on receive — the post-0.6 flat model
doesn't need sender-role distinction. send_blob_delete_notices keeps
its Option<&Upstream> parameter as an unused placeholder for signature
stability with the call sites in this commit.

Other reads migrated:
- blob fetch cascade: step 2 now tries "known holders" (up to 5)
  instead of a single upstream
- manifest refresh: downstream_count reported from file_holder_count
- web/http post holder enumeration
- Worm search post/blob holder fallback (both connection.rs paths)
- DeleteRecord fan-out rewires to file_holders
- Under-replication replication check: < 2 holders

Storage additions:
- get_file_holder_count(file_id)
- remove_file_holder(file_id, peer_id)

Legacy upstream/downstream writes are still happening from Phase 2b;
those + the tables themselves go in 2e.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-04-21 21:09:45 -04:00
parent 3a0d2e93ab
commit 60463d1817
6 changed files with 103 additions and 128 deletions

View file

@ -4470,6 +4470,14 @@ impl Storage {
Ok(())
}
/// Count file holders (bounded at 5 by touch_file_holder's LRU cap).
pub fn get_file_holder_count(&self, file_id: &[u8; 32]) -> anyhow::Result<u32> {
let count: i64 = self.conn.prepare(
"SELECT COUNT(*) FROM file_holders WHERE file_id = ?1",
)?.query_row(params![file_id.as_slice()], |row| row.get(0))?;
Ok(count as u32)
}
/// Return the up-to-5 most recently interacted holders of a file.
pub fn get_file_holders(&self, file_id: &[u8; 32]) -> anyhow::Result<Vec<(NodeId, Vec<String>)>> {
let mut stmt = self.conn.prepare(
@ -4504,6 +4512,15 @@ impl Storage {
Ok(())
}
/// Remove a single peer's holder entry for a file.
pub fn remove_file_holder(&self, file_id: &[u8; 32], peer_id: &NodeId) -> anyhow::Result<()> {
self.conn.execute(
"DELETE FROM file_holders WHERE file_id = ?1 AND peer_id = ?2",
params![file_id.as_slice(), peer_id.as_slice()],
)?;
Ok(())
}
/// One-time migration: seed file_holders from the legacy upstream/downstream
/// tables so a user upgrading from pre-0.6.1 doesn't start with empty holder
/// sets. Idempotent — inserts use ON CONFLICT DO NOTHING semantics via the