Skip to main content

rings_core/dht/storage/
mod.rs

1//! DHT storage ownership, repair, and sync transitions.
2//!
3//! The Chord ring decides physical successor topology. This module decides
4//! storage-specific ownership on top of that topology: affine replica
5//! placement, storage virtual-node ownership, read repair, and sync hand-off.
6
7use std::collections::BTreeMap;
8use std::collections::BTreeSet;
9
10use serde::Deserialize;
11use serde::Serialize;
12
13use super::chord::PeerRing;
14use super::chord::PeerRingAction;
15use super::chord::RemoteAction;
16use super::entry::PlacedEntry;
17use super::topology;
18use super::topology::FindSuccessorStep;
19use super::topology::TopologyState;
20use super::types::Chord;
21use super::virtual_node::StorageVirtualNodes;
22use super::virtual_node::VirtualNode;
23use super::Did;
24use crate::error::Error;
25use crate::error::Result;
26
27mod repair;
28mod sync;
29
30/// Storage-sync transition kind.
31///
32/// Cleanup law: only [`StorageSyncPurpose::OwnershipHandoff`] reports can prove
33/// source-side deletion. [`StorageSyncPurpose::AdditiveRepair`] is copy-only and
34/// must never create a delete-capable ack.
35#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
36pub enum StorageSyncPurpose {
37    /// Ownership changed and the sender may delete after a durable matching ack.
38    OwnershipHandoff,
39    /// Additive read-repair or anti-entropy copy.
40    AdditiveRepair,
41}
42
43impl StorageSyncPurpose {
44    /// Returns whether reports for this sync kind may drive source cleanup.
45    pub const fn permits_source_cleanup(self) -> bool {
46        matches!(self, Self::OwnershipHandoff)
47    }
48}
49
50/// Destination semantics for a storage sync hand-off.
51///
52/// Routing law:
53/// - [`StorageSyncDestination::PhysicalOwner`] is routed as a node DID through
54///   physical Chord membership.
55/// - [`StorageSyncDestination::PlacementKey`] is routed through storage
56///   ownership for that placement key.
57///
58/// Safety: a physical-owner receiver still validates each placement before
59/// acking, so a stale sender cannot trigger local cleanup for a key the receiver
60/// does not own.
61#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
62pub enum StorageSyncDestination {
63    /// Route to a physical node DID, then let the receiver validate entry ownership.
64    PhysicalOwner(Did),
65    /// Route through storage ownership for this placement key.
66    PlacementKey(Did),
67}
68
69impl StorageSyncDestination {
70    /// Build a physical-owner sync destination.
71    pub const fn physical_owner(did: Did) -> Self {
72        Self::PhysicalOwner(did)
73    }
74
75    /// Build a placement-key sync destination.
76    pub const fn placement_key(did: Did) -> Self {
77        Self::PlacementKey(did)
78    }
79
80    /// Return the DID placed in the relay destination.
81    pub fn did(self) -> Did {
82        match self {
83            Self::PhysicalOwner(did) | Self::PlacementKey(did) => did,
84        }
85    }
86
87    /// Return the routing semantics for this destination.
88    pub const fn route(self) -> StorageSyncRoute {
89        match self {
90            Self::PhysicalOwner(_) => StorageSyncRoute::PhysicalOwner,
91            Self::PlacementKey(_) => StorageSyncRoute::PlacementKey,
92        }
93    }
94}
95
96/// Routing semantics for a storage sync hand-off.
97///
98/// The route is paired with the outer [`PeerRingAction::RemoteAction`] target,
99/// so the action tree carries the destination DID exactly once.
100#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
101pub enum StorageSyncRoute {
102    /// Interpret the action target as a physical node DID.
103    PhysicalOwner,
104    /// Interpret the action target as a storage placement key.
105    PlacementKey,
106}
107
108impl StorageSyncRoute {
109    /// Combine this route with the action target DID to form a wire destination.
110    pub const fn destination(self, target: Did) -> StorageSyncDestination {
111        match self {
112            Self::PhysicalOwner => StorageSyncDestination::physical_owner(target),
113            Self::PlacementKey => StorageSyncDestination::placement_key(target),
114        }
115    }
116}
117
118/// Lowered storage-sync delivery ready for the message layer.
119#[derive(Debug, PartialEq, Eq)]
120pub(crate) struct StorageSyncDelivery {
121    purpose: StorageSyncPurpose,
122    destination: StorageSyncDestination,
123    data: Vec<PlacedEntry>,
124}
125
126/// Stable identity used to continue bounded storage repair across changing plans.
127#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
128pub(crate) struct StorageSyncDeliveryCursor {
129    purpose: StorageSyncPurpose,
130    destination: StorageSyncDestination,
131    placement_keys: Vec<Did>,
132}
133
134impl StorageSyncDelivery {
135    fn from_parts(
136        purpose: StorageSyncPurpose,
137        destination: StorageSyncDestination,
138        data: Vec<PlacedEntry>,
139    ) -> Self {
140        Self {
141            purpose,
142            destination,
143            data,
144        }
145    }
146
147    fn from_route(
148        purpose: StorageSyncPurpose,
149        target: Did,
150        route: StorageSyncRoute,
151        data: Vec<PlacedEntry>,
152    ) -> Self {
153        // Invariant: `destination` is the unique wire interpretation of
154        // `(target, route)`. Transport computes the physical next hop from the
155        // destination at send time, so this lowered value does not pretend that
156        // the action target is already a relay hop.
157        Self {
158            purpose,
159            destination: route.destination(target),
160            data,
161        }
162    }
163
164    /// Consume this delivery into the wire purpose, destination, and payload data.
165    pub(crate) fn into_message_parts(
166        self,
167    ) -> (StorageSyncPurpose, StorageSyncDestination, Vec<PlacedEntry>) {
168        (self.purpose, self.destination, self.data)
169    }
170
171    /// Return the stable repair cursor key for this delivery.
172    ///
173    /// Entry values are intentionally excluded. Replacing a value at the same
174    /// placement preserves the delivery's scheduling identity while changes to
175    /// batch membership produce a distinct key.
176    pub(crate) fn cursor_key(&self) -> StorageSyncDeliveryCursor {
177        let mut placement_keys = self
178            .data
179            .iter()
180            .map(|placed| placed.key)
181            .collect::<Vec<_>>();
182        placement_keys.sort_unstable();
183        StorageSyncDeliveryCursor {
184            purpose: self.purpose,
185            destination: self.destination,
186            placement_keys,
187        }
188    }
189}
190
191pub(super) enum StorageSyncTarget {
192    Local,
193    Remote(StorageSyncDestination),
194}
195
196impl PeerRingAction {
197    pub(crate) fn sync_entries_for_handoff(
198        destination: StorageSyncDestination,
199        data: Vec<PlacedEntry>,
200    ) -> Self {
201        Self::sync_entries(StorageSyncPurpose::OwnershipHandoff, destination, data)
202    }
203
204    pub(crate) fn sync_entries_for_repair(
205        destination: StorageSyncDestination,
206        data: Vec<PlacedEntry>,
207    ) -> Self {
208        Self::sync_entries(StorageSyncPurpose::AdditiveRepair, destination, data)
209    }
210
211    fn sync_entries(
212        purpose: StorageSyncPurpose,
213        destination: StorageSyncDestination,
214        data: Vec<PlacedEntry>,
215    ) -> Self {
216        Self::RemoteAction(destination.did(), RemoteAction::SyncEntriesWithSuccessor {
217            purpose,
218            route: destination.route(),
219            data,
220        })
221    }
222
223    /// Lower this action tree into storage-sync deliveries.
224    pub(crate) fn storage_sync_deliveries(self) -> Result<Vec<StorageSyncDelivery>> {
225        let mut deliveries = Vec::new();
226        self.collect_storage_sync_deliveries(&mut deliveries)?;
227        Ok(deliveries)
228    }
229
230    /// Lower this action tree into storage-sync deliveries, merging delivery
231    /// leaves that share the same wire purpose and destination.
232    ///
233    /// Safety law: coalescing is restricted to identical `(purpose,
234    /// destination)` pairs. `PlacementKey` destinations therefore keep their
235    /// placement identity, and physical-owner batches still let the receiver
236    /// validate each placement independently before acking.
237    pub(crate) fn coalesced_storage_sync_deliveries(self) -> Result<Vec<StorageSyncDelivery>> {
238        let mut by_route =
239            BTreeMap::<(StorageSyncPurpose, StorageSyncDestination), Vec<PlacedEntry>>::new();
240        for delivery in self.storage_sync_deliveries()? {
241            let (purpose, destination, data) = delivery.into_message_parts();
242            by_route
243                .entry((purpose, destination))
244                .or_default()
245                .extend(data);
246        }
247
248        let mut deliveries = Vec::new();
249        for ((purpose, destination), data) in by_route {
250            for batch in sync::sync_entries_batches(data, sync::SYNC_BATCH_MAX_BYTES)? {
251                deliveries.push(StorageSyncDelivery::from_parts(purpose, destination, batch));
252            }
253        }
254        Ok(deliveries)
255    }
256
257    fn collect_storage_sync_deliveries(
258        self,
259        deliveries: &mut Vec<StorageSyncDelivery>,
260    ) -> Result<()> {
261        match self {
262            Self::None => Ok(()),
263            Self::RemoteAction(
264                target,
265                RemoteAction::SyncEntriesWithSuccessor {
266                    purpose,
267                    route,
268                    data,
269                },
270            ) => {
271                deliveries.push(StorageSyncDelivery::from_route(
272                    purpose, target, route, data,
273                ));
274                Ok(())
275            }
276            Self::MultiActions(actions) => {
277                for action in actions {
278                    action.collect_storage_sync_deliveries(deliveries)?;
279                }
280                Ok(())
281            }
282            action => Err(Error::unexpected_peer_ring_action(action)),
283        }
284    }
285}
286
287impl PeerRing {
288    /// Return whether the storage virtual-node registry is enabled.
289    pub fn storage_virtual_nodes_enabled(&self) -> Result<bool> {
290        Ok(self.storage_virtual_node_config().is_enabled())
291    }
292
293    /// Return virtual storage positions owned by `owner`.
294    pub fn storage_virtual_positions(&self, owner: Did) -> Result<Vec<VirtualNode>> {
295        Ok(self.storage_virtual_nodes()?.positions_for_owner(owner))
296    }
297
298    pub(super) fn observed_storage_virtual_owner(&self, placement_key: Did) -> Result<Option<Did>> {
299        Ok(self.storage_virtual_nodes()?.owner_for_key(placement_key))
300    }
301
302    pub(super) fn observed_storage_virtual_owner_registered(&self, owner: Did) -> Result<bool> {
303        Ok(self.storage_virtual_nodes()?.contains_owner(owner))
304    }
305
306    fn storage_virtual_nodes(&self) -> Result<StorageVirtualNodes> {
307        let state = self.topology_state()?;
308        Ok(self.storage_virtual_nodes_for_topology(&state))
309    }
310
311    fn storage_virtual_nodes_for_topology(&self, state: &TopologyState) -> StorageVirtualNodes {
312        let mut owners = BTreeSet::new();
313        // Pre: `state` is this node's authenticated topology view.
314        // Post: the virtual-owner set is exactly the physical DIDs currently
315        // visible to storage routing: local, successors, predecessor, and
316        // fingers. It is an observed view, not a global registry.
317        owners.insert(state.local);
318        owners.extend(state.successors.iter().copied());
319        owners.extend(state.predecessor);
320        owners.extend(state.fingers.iter().flatten().copied());
321        StorageVirtualNodes::from_owners(self.storage_virtual_node_config(), owners)
322    }
323
324    pub(crate) fn find_storage_owner(&self, placement_key: Did) -> Result<PeerRingAction> {
325        if let Some(owner) = self.observed_storage_virtual_owner(placement_key)? {
326            if owner == self.did {
327                Ok(PeerRingAction::Some(owner))
328            } else {
329                Ok(PeerRingAction::RemoteAction(
330                    owner,
331                    RemoteAction::FindSuccessor(placement_key),
332                ))
333            }
334        } else {
335            self.find_successor(placement_key)
336        }
337    }
338
339    pub(super) fn storage_sync_target(&self, placement_key: Did) -> Result<StorageSyncTarget> {
340        if let Some(owner) = self.observed_storage_virtual_owner(placement_key)? {
341            if owner == self.did {
342                Ok(StorageSyncTarget::Local)
343            } else {
344                Ok(StorageSyncTarget::Remote(
345                    StorageSyncDestination::PhysicalOwner(owner),
346                ))
347            }
348        } else {
349            match self.find_successor(placement_key)? {
350                // In non-virtual storage, `Some(_)` means this node's local
351                // Chord view has reached the terminal storage branch. The
352                // witness DID may be the successor for lookup fallback, not a
353                // remote owner that should receive this placement.
354                PeerRingAction::Some(_) => Ok(StorageSyncTarget::Local),
355                PeerRingAction::RemoteAction(_, RemoteAction::FindSuccessor(_)) => Ok(
356                    StorageSyncTarget::Remote(StorageSyncDestination::PlacementKey(placement_key)),
357                ),
358                action => Err(Error::unexpected_peer_ring_action(action)),
359            }
360        }
361    }
362
363    pub(crate) fn next_hop_for_storage_sync(
364        &self,
365        destination: StorageSyncDestination,
366    ) -> Result<Option<Did>> {
367        let state = self.topology_state()?;
368        Ok(self.next_hop_for_storage_sync_in(&state, destination))
369    }
370
371    pub(crate) fn storage_sync_route_still_permits(
372        &self,
373        destination: StorageSyncDestination,
374        next_hop: Did,
375    ) -> Result<bool> {
376        self.with_topology_state(|state| {
377            self.storage_sync_route_permits_in(state, destination, next_hop)
378        })
379    }
380
381    /// Execute `operation` only while one topology snapshot proves this route.
382    ///
383    /// The topology transition lock remains held through `operation`. This is
384    /// the DHT half of final transport admission: the caller can synchronously
385    /// check connection ownership and readiness without a route transition
386    /// crossing that check.
387    pub(crate) fn with_permitted_storage_sync_route<T>(
388        &self,
389        destination: StorageSyncDestination,
390        next_hop: Did,
391        operation: impl FnOnce() -> T,
392    ) -> Result<Option<T>> {
393        self.with_topology_state(|state| {
394            self.storage_sync_route_permits_in(state, destination, next_hop)
395                .then(operation)
396        })
397    }
398
399    fn storage_sync_route_permits_in(
400        &self,
401        state: &TopologyState,
402        destination: StorageSyncDestination,
403        next_hop: Did,
404    ) -> bool {
405        Self::routing_peer_registered_in(state, next_hop)
406            && self.next_hop_for_storage_sync_in(state, destination) == Some(next_hop)
407    }
408
409    fn routing_peer_registered_in(state: &TopologyState, peer: Did) -> bool {
410        peer == state.local
411            || state.successors.contains(&peer)
412            || state.predecessor == Some(peer)
413            || state.fingers.iter().flatten().any(|did| *did == peer)
414    }
415
416    // Pre: `state` is one authenticated topology snapshot.
417    // Post: both route registration and next-hop selection use only `state`.
418    fn next_hop_for_storage_sync_in(
419        &self,
420        state: &TopologyState,
421        destination: StorageSyncDestination,
422    ) -> Option<Did> {
423        match destination {
424            StorageSyncDestination::PhysicalOwner(owner) => {
425                Self::next_hop_to_physical_owner_in(state, owner)
426            }
427            StorageSyncDestination::PlacementKey(key) => {
428                self.next_hop_to_storage_placement_in(state, key)
429            }
430        }
431    }
432
433    fn next_hop_to_physical_owner_in(state: &TopologyState, owner: Did) -> Option<Did> {
434        if owner == state.local {
435            return None;
436        }
437        match topology::find_successor(state, owner) {
438            // If this local view cannot prove a better physical next hop, try
439            // the target owner directly rather than accepting the payload as
440            // local work. Persisting happens only at relay destination.
441            FindSuccessorStep::Local(next) if next == state.local => Some(owner),
442            FindSuccessorStep::Local(next) | FindSuccessorStep::Remote { next, .. } => Some(next),
443        }
444    }
445
446    fn next_hop_to_storage_placement_in(&self, state: &TopologyState, key: Did) -> Option<Did> {
447        if let Some(owner) = self
448            .storage_virtual_nodes_for_topology(state)
449            .owner_for_key(key)
450        {
451            return (owner != state.local).then_some(owner);
452        }
453        match topology::find_successor(state, key) {
454            FindSuccessorStep::Local(_) => None,
455            FindSuccessorStep::Remote { next, .. } => Some(next),
456        }
457    }
458}
459
460#[cfg(all(not(all(feature = "wasm", target_family = "wasm")), test))]
461mod tests;