Skip to main content

rings_core/dht/storage/
sync.rs

1use std::str::FromStr;
2
3use async_trait::async_trait;
4use rings_transport::core::transport::MAX_DATA_CHANNEL_MESSAGE_SIZE;
5use serde::Serialize;
6
7use super::StorageSyncDestination;
8use super::StorageSyncPurpose;
9use super::StorageSyncTarget;
10use crate::consts::MAX_CHUNK_ENVELOPE_OVERHEAD;
11use crate::consts::TRANSPORT_CUSTOM_OVERHEAD;
12use crate::dht::chord::PeerRing;
13use crate::dht::chord::PeerRingAction;
14use crate::dht::did::BiasId;
15use crate::dht::entry::Entry;
16use crate::dht::entry::PlacedEntry;
17use crate::dht::entry::SyncedEntryAck;
18use crate::dht::ChordStorageSync;
19use crate::dht::Did;
20use crate::error::Error;
21use crate::error::Result;
22use crate::message::types::Message;
23use crate::message::types::SyncEntriesWithSuccessor;
24
25/// Maximum wire budget for one `SyncEntriesWithSuccessor` hand-off batch.
26///
27/// This stays below one interoperable WebRTC data-channel frame so storage
28/// anti-entropy cannot monopolize the chunk sender. The batch cost also
29/// reserves the payload/chunk envelope bytes below.
30pub(crate) const SYNC_BATCH_MAX_BYTES: usize = MAX_DATA_CHANNEL_MESSAGE_SIZE / 4;
31
32const SYNC_BATCH_ENVELOPE_HEADROOM_BYTES: usize =
33    MAX_CHUNK_ENVELOPE_OVERHEAD + TRANSPORT_CUSTOM_OVERHEAD;
34
35fn serialized_wire_size<T: Serialize>(value: &T) -> Result<usize> {
36    let bytes = rings_codec::serialized_size(value).map_err(Error::CodecSerialize)?;
37    usize::try_from(bytes).map_err(|_| Error::MessageSizeOverflow)
38}
39
40fn add_wire_cost(total: usize, next: usize) -> Result<usize> {
41    total.checked_add(next).ok_or(Error::MessageSizeOverflow)
42}
43
44fn sync_entries_fixed_wire_cost() -> Result<usize> {
45    let empty_message = Message::SyncEntriesWithSuccessor(SyncEntriesWithSuccessor {
46        purpose: StorageSyncPurpose::OwnershipHandoff,
47        destination: StorageSyncDestination::PhysicalOwner(Did::from(0u32)),
48        data: Vec::new(),
49    });
50    add_wire_cost(
51        serialized_wire_size(&empty_message)?,
52        SYNC_BATCH_ENVELOPE_HEADROOM_BYTES,
53    )
54}
55
56fn placed_entry_wire_cost(placed: &PlacedEntry) -> Result<usize> {
57    serialized_wire_size(placed)
58}
59
60#[cfg(all(test, not(all(feature = "wasm", target_family = "wasm"))))]
61pub(super) fn sync_entries_batch_wire_cost(data: &[PlacedEntry]) -> Result<usize> {
62    let mut cost = sync_entries_fixed_wire_cost()?;
63    for placed in data {
64        cost = add_wire_cost(cost, placed_entry_wire_cost(placed)?)?;
65    }
66    Ok(cost)
67}
68
69pub(super) fn sync_entries_batches(
70    data: Vec<PlacedEntry>,
71    max_batch_bytes: usize,
72) -> Result<Vec<Vec<PlacedEntry>>> {
73    let mut batches = Vec::new();
74    let mut current = Vec::new();
75    let fixed_cost = sync_entries_fixed_wire_cost()?;
76    let mut current_cost = fixed_cost;
77
78    // Pre: `data` is the migrating set M produced from local storage.
79    // Post Coverage: concatenating all returned batches yields exactly M in
80    // the same order; no PlacedEntry is duplicated or dropped.
81    // Post Budget: every non-singleton batch, and every singleton whose own
82    // cost fits, has sync_entries_batch_wire_cost(batch) <= max_batch_bytes.
83    // Post Atomicity: each PlacedEntry is moved as a whole; no entry is split
84    // across batches.
85    // Post Progress: if one PlacedEntry exceeds max_batch_bytes by itself, it
86    // is emitted as a one-entry batch so the chunk layer can still frame it.
87    for placed in data {
88        let placed_cost = placed_entry_wire_cost(&placed)?;
89        let candidate_cost = add_wire_cost(current_cost, placed_cost)?;
90        if current.is_empty() {
91            current.push(placed);
92            current_cost = candidate_cost;
93            continue;
94        }
95
96        if candidate_cost <= max_batch_bytes {
97            current.push(placed);
98            current_cost = candidate_cost;
99        } else {
100            batches.push(current);
101            current = vec![placed];
102            current_cost = add_wire_cost(fixed_cost, placed_cost)?;
103        }
104    }
105
106    if !current.is_empty() {
107        batches.push(current);
108    }
109
110    Ok(batches)
111}
112
113#[cfg_attr(all(feature = "wasm", target_family = "wasm"), async_trait(?Send))]
114#[cfg_attr(not(all(feature = "wasm", target_family = "wasm")), async_trait)]
115impl ChordStorageSync<PeerRingAction> for PeerRing {
116    /// When the successor of a node is updated, it needs to check if there are
117    /// `Entry`s that are no longer between current node and `new_successor`,
118    /// and copy them to the new successor.
119    async fn sync_entries_with_successor(&self, new_successor: Did) -> Result<PeerRingAction> {
120        if self.storage_virtual_nodes_enabled()? {
121            return self.copy_entries_to_observed_virtual_storage_owners().await;
122        }
123
124        let mut data = Vec::<PlacedEntry>::new();
125        let all_items: Vec<(String, Entry)> = self.storage.get_all().await?;
126
127        // Pre: new_successor is the successor adopted by stabilization.
128        // Post S1: forall key in local_before, local_after[key] =
129        // local_before[key]; this transition emits join deliveries only.
130        // Post S2(copy): every emitted PlacedEntry keeps the exact local
131        // placement key, so an eventual ack names the key whose durable copy was
132        // reported by the receiver.
133        // Preservation #611/#614: sync hand-off is join-before-ack-before-local
134        // cleanup. acknowledge_synced_entries is the only local cleanup
135        // transition and does not define storage convergence.
136        for (entry_key_str, entry) in all_items {
137            let entry_key = Did::from_str(&entry_key_str)?;
138            if BiasId::cmp_from_observer(self.did, entry_key, new_successor)
139                == std::cmp::Ordering::Greater
140            {
141                data.push(PlacedEntry::new(entry_key, entry));
142            }
143        }
144
145        let batches = sync_entries_batches(data, SYNC_BATCH_MAX_BYTES)?;
146        Ok(batches
147            .into_iter()
148            .map(|batch| {
149                PeerRingAction::sync_entries_for_handoff(
150                    StorageSyncDestination::PhysicalOwner(new_successor),
151                    batch,
152                )
153            })
154            .collect::<Vec<_>>()
155            .into())
156    }
157
158    async fn acknowledge_synced_entries(&self, acks: &[SyncedEntryAck]) -> Result<PeerRingAction> {
159        // Pre S2': each ack in acks is contained in a
160        // SyncEntriesWithSuccessorReport sent only after the receiver persisted
161        // SyncedEntryAck { key, entry } at key.
162        // Post S2': a local key is removed only if canonical(local_before[key])
163        // == canonical(ack.entry). If the canonical local value differs, the
164        // local value is preserved and will be offered again by a later
165        // sync_entries_with_successor transition.
166        // Preservation #614: a write racing between copy and ack changes the
167        // canonical local value, so confirms_local_value is false and delete
168        // is skipped.
169        for ack in acks {
170            let Some(local_entry) = self.storage.get(&ack.key.to_string()).await? else {
171                continue;
172            };
173            if ack.confirms_local_value(&local_entry)? {
174                self.storage.remove(&ack.key.to_string()).await?;
175            }
176        }
177
178        Ok(PeerRingAction::None)
179    }
180}
181
182impl PeerRing {
183    async fn copy_entries_to_observed_virtual_storage_owners(&self) -> Result<PeerRingAction> {
184        let all_items: Vec<(String, Entry)> = self.storage.get_all().await?;
185        let mut by_target =
186            std::collections::BTreeMap::<StorageSyncDestination, Vec<PlacedEntry>>::new();
187
188        // Pre: storage virtual nodes are enabled.
189        // Model: storage_sync_target computes ownership under this node's
190        // authenticated local topology view, not a globally complete membership
191        // relation.
192        // Post S1: local entries are retained without action; entries whose
193        // observed virtual owner is remote are emitted as additive anti-entropy
194        // copies to that physical owner.
195        // Preservation S1'': this transition cannot create a delete-capable
196        // report. Only non-virtual physical handoff has an ownership proof
197        // strong enough to permit source cleanup.
198        for (entry_key_str, entry) in all_items {
199            let entry_key = Did::from_str(&entry_key_str)?;
200            if let StorageSyncTarget::Remote(target) = self.storage_sync_target(entry_key)? {
201                by_target
202                    .entry(target)
203                    .or_default()
204                    .push(PlacedEntry::new(entry_key, entry));
205            }
206        }
207
208        let mut actions = Vec::new();
209        for (target, data) in by_target {
210            for batch in sync_entries_batches(data, SYNC_BATCH_MAX_BYTES)? {
211                actions.push(PeerRingAction::sync_entries_for_repair(target, batch));
212            }
213        }
214        Ok(actions.into())
215    }
216}