//! MetadataActor — object metadata index with DHT overlay. //! //! Owns: //! - Local object index: `HashMap` keyed by content hash //! - Dissemination queue for DHT replication (reuses `ClusterRegistry` pattern) //! //! The metadata DHT is a separate Kademlia overlay from the actor directory. //! Objects are keyed by `blake3(blob_bytes)` — the content hash of the entire blob. use std::collections::{HashMap, HashSet}; use swactor::actor::{ActorAddress, ActorInterface, Ctx}; use distribution::types::NodeId; use crate::messages::{BlobStoreMsg, DatastoreResponse, MetadataMsg}; use crate::types::{ContentHash, DatastoreConfig, ObjectEntry, ObjectManifest}; // ─── Dissemination entry (reuses registry.rs pattern) ─────────────────────── #[derive(Debug, Clone)] struct DisseminationEntry { entry: ObjectEntry, manifest: Option, remaining: usize, } // ─── MetadataActor ────────────────────────────────────────────────────────── /// Manages the object metadata index for a single node. pub struct MetadataActor { /// This node's identity. node_id: NodeId, /// Local object index: content_hash → ObjectEntry. entries: HashMap, /// Local manifest cache: content_hash → ObjectManifest. manifests: HashMap, /// Pending entries to disseminate to DHT peers. dissemination: Vec, /// Tick counter for periodic GC. tick_count: u64, /// Configuration. gc_interval: u64, /// Dissemination multiplier (Λ) — same role as in SWIM. dissemination_lambda: usize, /// Address of the local BlobStoreActor (for forwarding manifest writes). blob_store_addr: Option, /// Addresses of peer MetadataActors for epidemic dissemination. peers: Vec, } impl MetadataActor { pub fn new(node_id: NodeId, config: &DatastoreConfig) -> Self { Self { node_id, entries: HashMap::new(), manifests: HashMap::new(), dissemination: Vec::new(), tick_count: 0, gc_interval: config.gc_interval, dissemination_lambda: 3, blob_store_addr: None, peers: Vec::new(), } } /// Set the address of the co-located BlobStoreActor. pub fn set_blob_store(&mut self, addr: ActorAddress) { self.blob_store_addr = Some(addr); } fn transmit_budget(&self, cluster_size: usize) -> usize { let n = cluster_size.max(2) as f64; let log_n = n.log2().ceil() as usize; self.dissemination_lambda * log_n.max(1) } fn enqueue(&mut self, entry: ObjectEntry, manifest: Option, cluster_size: usize) { let budget = self.transmit_budget(cluster_size); // Replace existing entry for same content hash if present. if let Some(existing) = self .dissemination .iter_mut() .find(|e| e.entry.content_hash == entry.content_hash) { existing.entry = entry; if manifest.is_some() { existing.manifest = manifest; } existing.remaining = budget; return; } self.dissemination.push(DisseminationEntry { entry, manifest, remaining: budget, }); } /// Take pending entries for dissemination, up to `max_count`. /// Returns `(ObjectEntry, Option)` pairs. pub fn take_pending(&mut self, max_count: usize) -> Vec<(ObjectEntry, Option)> { let count = max_count.min(self.dissemination.len()); let mut result = Vec::with_capacity(count); for entry in self.dissemination.iter_mut().take(count) { result.push((entry.entry.clone(), entry.manifest.clone())); entry.remaining = entry.remaining.saturating_sub(1); } // Evict exhausted entries. self.dissemination.retain(|e| e.remaining > 0); result } /// Periodic GC: build referenced chunk set from all manifests and send /// `GcUnreferenced` to BlobStoreActor to delete orphaned chunks. fn gc_tick(&mut self, ctx: &Ctx) { self.tick_count += 1; if self.tick_count % self.gc_interval != 0 { return; } let blob_store_addr = match self.blob_store_addr { Some(addr) => addr, None => return, }; let mut referenced = HashSet::new(); for manifest in self.manifests.values() { for chunk_ref in &manifest.chunks { referenced.insert(chunk_ref.hash); } } let _ = ctx.send(blob_store_addr, BlobStoreMsg::GcUnreferenced { referenced }); } // ─── Message handlers ─────────────────────────────────────────────── fn handle_put_object( &mut self, ctx: &Ctx, entry: ObjectEntry, manifest: ObjectManifest, reply_to: ActorAddress, ) { let content_hash = entry.content_hash; // Store manifest locally. self.manifests.insert(content_hash, manifest.clone()); // Insert entry keyed by content hash. let mut entry = entry; entry.node_id = self.node_id; self.entries.insert(content_hash, entry.clone()); // Enqueue for DHT dissemination (include manifest for peer replication). self.enqueue(entry, Some(manifest), 3); let _ = ctx.send( reply_to, DatastoreResponse::PutOk { content_hash }, ); } fn handle_get_object(&self, ctx: &Ctx, content_hash: ContentHash, reply_to: ActorAddress) { match self.entries.get(&content_hash) { Some(entry) => { if let Some(manifest) = self.manifests.get(&content_hash) { let _ = ctx.send( reply_to, DatastoreResponse::GetOk { entry: entry.clone(), manifest: manifest.clone(), }, ); } else { let _ = ctx.send( reply_to, DatastoreResponse::Error { reason: format!("manifest not found for content hash: {content_hash}"), }, ); } } None => { let _ = ctx.send(reply_to, DatastoreResponse::NotFound); } } } fn handle_delete_object(&mut self, ctx: &Ctx, content_hash: ContentHash, reply_to: ActorAddress) { if self.entries.remove(&content_hash).is_some() { self.manifests.remove(&content_hash); let _ = ctx.send(reply_to, DatastoreResponse::DeleteOk { content_hash }); } else { let _ = ctx.send(reply_to, DatastoreResponse::NotFound); } } fn handle_list_local( &self, ctx: &Ctx, name_filter: Option, reply_to: ActorAddress, ) { let entries: Vec = self .entries .values() .filter(|e| { match (&name_filter, &e.name) { (Some(filter), Some(name)) => name.contains(filter.as_str()), (Some(_), None) => false, (None, _) => true, } }) .cloned() .collect(); let _ = ctx.send(reply_to, DatastoreResponse::ListOk { entries }); } fn handle_list_swarm( &self, ctx: &Ctx, name_filter: Option, reply_to: ActorAddress, ) { // Delegates to local index. Swarm-wide fan-out to peer MetadataActors // will be wired when networking integration is added. self.handle_list_local(ctx, name_filter, reply_to); } fn handle_find_object( &self, ctx: &Ctx, _from: NodeId, content_hash: ContentHash, reply_to: ActorAddress, ) { match self.entries.get(&content_hash) { Some(entry) => { let _ = ctx.send( reply_to, DatastoreResponse::GetOk { entry: entry.clone(), manifest: self .manifests .get(&content_hash) .cloned() .unwrap_or_else(|| ObjectManifest { content_hash, chunks: vec![], total_size: entry.size_bytes, chunk_size: 0, content_type: None, }), }, ); } None => { let _ = ctx.send(reply_to, DatastoreResponse::NotFound); } } } fn handle_set_peers(&mut self, peers: Vec) { self.peers = peers; } fn handle_disseminate_tick(&mut self, ctx: &Ctx) { if self.peers.is_empty() { return; } let pending = self.take_pending(10); for (entry, manifest) in pending { for &peer in &self.peers { let _ = ctx.send(peer, MetadataMsg::HandleStoreObject { entry: entry.clone(), manifest: manifest.clone(), }); } } } fn handle_store_object(&mut self, entry: ObjectEntry, manifest: Option) { // Insert if absent — content-addressed entries don't conflict. let content_hash = entry.content_hash; if !self.entries.contains_key(&content_hash) { if let Some(ref m) = manifest { self.manifests.insert(content_hash, m.clone()); } self.entries.insert(content_hash, entry.clone()); self.enqueue(entry, manifest, 3); } } } impl ActorInterface for MetadataActor { type Incoming = MetadataMsg; type Response = DatastoreResponse; fn handle(&mut self, ctx: &Ctx, msg: MetadataMsg) { match msg { MetadataMsg::PutObject { entry, manifest, reply_to, } => self.handle_put_object(ctx, entry, manifest, reply_to), MetadataMsg::GetObject { content_hash, reply_to } => { self.handle_get_object(ctx, content_hash, reply_to) } MetadataMsg::DeleteObject { content_hash, reply_to } => { self.handle_delete_object(ctx, content_hash, reply_to) } MetadataMsg::ListLocal { name_filter, reply_to } => { self.handle_list_local(ctx, name_filter, reply_to) } MetadataMsg::ListSwarm { name_filter, reply_to } => { self.handle_list_swarm(ctx, name_filter, reply_to) } MetadataMsg::HandleFindObject { from, content_hash, reply_to, } => self.handle_find_object(ctx, from, content_hash, reply_to), MetadataMsg::HandleStoreObject { entry, manifest } => { self.handle_store_object(entry, manifest) } MetadataMsg::SetPeers { peers } => self.handle_set_peers(peers), MetadataMsg::DisseminateTick => self.handle_disseminate_tick(ctx), MetadataMsg::GcTick => self.gc_tick(ctx), } } }