Skip to main content

rings_core/message/handlers/storage/
mod.rs

1#![deny(missing_docs)]
2
3use std::sync::Arc;
4
5use async_recursion::async_recursion;
6use async_trait::async_trait;
7
8use crate::dht::entry::Entry;
9use crate::dht::entry::EntryKind;
10use crate::dht::entry::EntryOperation;
11use crate::dht::entry::PlacedEntryOperation;
12use crate::dht::entry::SyncedEntryAck;
13use crate::dht::ChordStorage;
14use crate::dht::ChordStorageCache;
15use crate::dht::ChordStorageRepair;
16use crate::dht::ChordStorageSync;
17use crate::dht::Did;
18use crate::dht::PeerRing;
19use crate::dht::PeerRingAction;
20use crate::dht::PeerRingRemoteAction;
21use crate::dht::StorageSyncDestination;
22use crate::dht::StorageSyncPurpose;
23use crate::error::Error;
24use crate::error::Result;
25use crate::message::effects::core_actor_steps;
26use crate::message::effects::yield_core_actor_step;
27use crate::message::effects::CoreEffect;
28use crate::message::types::FoundEntry;
29use crate::message::types::Message;
30use crate::message::types::SearchEntry;
31use crate::message::types::SyncEntriesWithSuccessor;
32use crate::message::types::SyncEntriesWithSuccessorReport;
33use crate::message::Encoded;
34use crate::message::HandleMsg;
35use crate::message::MessageHandler;
36use crate::message::MessagePayload;
37use crate::message::MessageVerificationExt;
38use crate::message::PayloadSender;
39use crate::swarm::transport::SwarmTransport;
40use crate::swarm::Swarm;
41
42/// ChordStorageInterface should imply necessary method for DHT storage
43#[cfg_attr(all(feature = "wasm", target_family = "wasm"), async_trait(?Send))]
44#[cfg_attr(not(all(feature = "wasm", target_family = "wasm")), async_trait)]
45pub trait ChordStorageInterface<const REDUNDANT: u16> {
46    /// Fetch an entry from DHT storage.
47    async fn storage_fetch(&self, entry_key: Did) -> Result<()>;
48    /// Store an entry on DHT storage.
49    async fn storage_store(&self, entry: Entry) -> Result<()>;
50    /// Append data to a Data kind entry.
51    async fn storage_append_data(&self, topic: &str, data: Encoded) -> Result<()>;
52    /// Append data to a Data kind entry uniquely.
53    async fn storage_touch_data(&self, topic: &str, data: Encoded) -> Result<()>;
54    /// Tombstone observed data in a Data kind entry.
55    async fn storage_tombstone_data(&self, topic: &str, data: Encoded) -> Result<()>;
56    /// Compact a Data kind entry after removing listed payloads.
57    async fn storage_compact_data(&self, topic: &str, removals: Vec<Encoded>) -> Result<()>;
58}
59
60/// ChordStorageInterfaceCacheChecker defines the interface for checking the local cache of the DHT.
61#[cfg_attr(all(feature = "wasm", target_family = "wasm"), async_trait(?Send))]
62#[cfg_attr(not(all(feature = "wasm", target_family = "wasm")), async_trait)]
63pub trait ChordStorageInterfaceCacheChecker {
64    /// Check the local cache of the DHT for a specific entry key.
65    ///
66    /// Returns an optional `Entry` representing the cached data, or `None` if it is not found.
67    async fn storage_check_cache(&self, entry_key: Did) -> Option<Entry>;
68}
69
70fn finish_storage_action(act: PeerRingAction) -> Result<()> {
71    match act {
72        PeerRingAction::None => Ok(()),
73        act => Err(Error::unexpected_peer_ring_action(act)),
74    }
75}
76
77async fn reset_storage_relay_destination(
78    handler: &MessageHandler,
79    ctx: &MessagePayload,
80    next: Did,
81) -> Result<()> {
82    handler
83        .run_effects([CoreEffect::reset_destination(ctx, next)])
84        .await
85}
86
87async fn repair_observed_storage_misses(
88    transport: Arc<SwarmTransport>,
89    entry: Entry,
90    redundancy: u16,
91) -> Result<()> {
92    let misses = transport.take_storage_misses(entry.did, redundancy)?;
93    let repair = transport
94        .dht
95        .read_repair_entry(entry, &misses, redundancy)
96        .await?;
97    run_storage_repair_transport_effects(transport, repair).await
98}
99
100/// Execute storage fetch actions for the Swarm-facing storage API.
101#[cfg_attr(all(feature = "wasm", target_family = "wasm"), async_recursion(?Send))]
102#[cfg_attr(not(all(feature = "wasm", target_family = "wasm")), async_recursion)]
103async fn handle_storage_fetch_act<const REDUNDANT: u16>(
104    transport: Arc<SwarmTransport>,
105    resource: Did,
106    act: PeerRingAction,
107) -> Result<()> {
108    match act {
109        PeerRingAction::SomeEntry(evidence) => {
110            transport
111                .dht
112                .local_cache_put(evidence.entry.clone())
113                .await?;
114            let misses = evidence.misses;
115            let repair = transport
116                .dht
117                .read_repair_entry(evidence.entry, &misses, REDUNDANT)
118                .await?;
119            run_storage_repair_transport_effects(transport.clone(), repair).await?;
120        }
121        PeerRingAction::RemoteAction(next, dht_act) => {
122            if let PeerRingRemoteAction::FindEntry(query) = dht_act {
123                tracing::debug!(
124                    "storage_fetch send_message: SearchEntry({:?}) to {:?}",
125                    query,
126                    next
127                );
128                transport
129                    .send_message(
130                        Message::SearchEntry(SearchEntry {
131                            resource: query.resource,
132                            placement: query.placement,
133                            redundancy: REDUNDANT,
134                        }),
135                        next,
136                    )
137                    .await?;
138            }
139        }
140        PeerRingAction::MultiActions(acts) => {
141            for (act, has_next) in core_actor_steps(acts) {
142                handle_storage_fetch_act::<REDUNDANT>(transport.clone(), resource, act).await?;
143                if has_next {
144                    yield_core_actor_step().await;
145                }
146            }
147        }
148        PeerRingAction::EntryMisses(misses) => {
149            transport.observe_storage_misses(resource, REDUNDANT, misses)?;
150        }
151        act => finish_storage_action(act)?,
152    }
153    Ok(())
154}
155
156/// Execute storage store actions for the Swarm-facing storage API.
157#[cfg_attr(all(feature = "wasm", target_family = "wasm"), async_recursion(?Send))]
158#[cfg_attr(not(all(feature = "wasm", target_family = "wasm")), async_recursion)]
159pub(super) async fn handle_storage_store_act(
160    transport: Arc<SwarmTransport>,
161    act: PeerRingAction,
162) -> Result<()> {
163    match act {
164        PeerRingAction::RemoteAction(target, PeerRingRemoteAction::FindEntryForOperate(op)) => {
165            transport
166                .send_message(Message::OperateEntry(op), target)
167                .await?;
168        }
169        PeerRingAction::MultiActions(acts) => {
170            for (act, has_next) in core_actor_steps(acts) {
171                handle_storage_store_act(transport.clone(), act).await?;
172                if has_next {
173                    yield_core_actor_step().await;
174                }
175            }
176        }
177        act => finish_storage_action(act)?,
178    }
179    Ok(())
180}
181
182async fn operate_entry_at_placement(
183    dht: &PeerRing,
184    placement: Did,
185    op: EntryOperation,
186) -> Result<()> {
187    let op = op.stamped(dht.did)?;
188    let this = match dht.storage.get(&placement.to_string()).await? {
189        Some(this) => this,
190        None => op.clone().gen_default_entry()?,
191    };
192    let entry = this.operate(op, dht.did)?;
193    dht.join_storage_entry(placement, entry).await?;
194    Ok(())
195}
196
197async fn handle_placed_entry_operation(
198    handler: &MessageHandler,
199    ctx: &MessagePayload,
200    msg: &PlacedEntryOperation,
201) -> Result<()> {
202    msg.validate_placement(handler.transport.storage_redundancy())?;
203
204    match handler.dht.find_storage_owner(msg.placement)? {
205        PeerRingAction::Some(_) => {
206            operate_entry_at_placement(&handler.dht, msg.placement, msg.op.clone()).await
207        }
208        PeerRingAction::RemoteAction(next, PeerRingRemoteAction::FindSuccessor(_)) => {
209            reset_storage_relay_destination(handler, ctx, next).await
210        }
211        action => Err(Error::unexpected_peer_ring_action(action)),
212    }
213}
214
215/// Execute copy-only storage repair actions at the Swarm API adapter boundary.
216async fn run_storage_repair_transport_effects(
217    transport: Arc<SwarmTransport>,
218    act: PeerRingAction,
219) -> Result<()> {
220    for (delivery, has_next) in core_actor_steps(act.coalesced_storage_sync_deliveries()?) {
221        let msg = SyncEntriesWithSuccessor::from_delivery(delivery);
222        transport
223            .send_storage_sync_or_defer(msg, "storage_repair")
224            .await?;
225        if has_next {
226            yield_core_actor_step().await;
227        }
228    }
229    Ok(())
230}
231
232/// Execute storage search actions emitted by inbound message handlers.
233#[cfg_attr(all(feature = "wasm", target_family = "wasm"), async_recursion(?Send))]
234#[cfg_attr(not(all(feature = "wasm", target_family = "wasm")), async_recursion)]
235async fn handle_storage_search_act(
236    handler: &MessageHandler,
237    ctx: &MessagePayload,
238    act: PeerRingAction,
239    resource: Did,
240    redundancy: u16,
241) -> Result<()> {
242    match act {
243        PeerRingAction::SomeEntry(evidence) => {
244            handler
245                .run_effects([CoreEffect::send_report_message(
246                    ctx,
247                    Message::FoundEntry(FoundEntry {
248                        data: vec![evidence.entry],
249                        misses: evidence.misses,
250                        resource,
251                        redundancy,
252                    }),
253                )])
254                .await
255        }
256        PeerRingAction::EntryMisses(misses) => {
257            handler
258                .run_effects([CoreEffect::send_report_message(
259                    ctx,
260                    Message::FoundEntry(FoundEntry {
261                        data: vec![],
262                        misses,
263                        resource,
264                        redundancy,
265                    }),
266                )])
267                .await
268        }
269        PeerRingAction::RemoteAction(next, _) => {
270            reset_storage_relay_destination(handler, ctx, next).await
271        }
272        PeerRingAction::MultiActions(acts) => {
273            for (act, has_next) in core_actor_steps(acts) {
274                handle_storage_search_act(handler, ctx, act, resource, redundancy).await?;
275                if has_next {
276                    yield_core_actor_step().await;
277                }
278            }
279
280            Ok(())
281        }
282        act => finish_storage_action(act),
283    }
284}
285
286async fn operate_storage_entry<const REDUNDANT: u16>(
287    swarm: &Swarm,
288    operation: EntryOperation,
289) -> Result<()> {
290    swarm.transport.ensure_storage_redundancy::<REDUNDANT>()?;
291    let action =
292        <PeerRing as ChordStorage<_, REDUNDANT>>::entry_operate(&swarm.dht, operation).await?;
293    handle_storage_store_act(swarm.transport.clone(), action).await
294}
295
296fn next_hop_for_sync_entries(
297    handler: &MessageHandler,
298    ctx: &MessagePayload,
299    msg: &SyncEntriesWithSuccessor,
300) -> Result<Option<Did>> {
301    if msg.destination.did() != ctx.relay.destination {
302        return Err(Error::InvalidMessage(format!(
303            "sync destination {:?} does not match relay destination {}",
304            msg.destination, ctx.relay.destination
305        )));
306    }
307
308    if ctx.is_relay_destination_for(handler.dht.did) {
309        return Ok(None);
310    }
311
312    handler.dht.next_hop_for_storage_sync(msg.destination)
313}
314
315async fn report_synced_entries(
316    handler: &MessageHandler,
317    ctx: &MessagePayload,
318    purpose: StorageSyncPurpose,
319    destination: StorageSyncDestination,
320    acks: Vec<SyncedEntryAck>,
321) -> Result<()> {
322    handler
323        .run_effects([CoreEffect::send_report_message(
324            ctx,
325            Message::SyncEntriesWithSuccessorReport(SyncEntriesWithSuccessorReport::new(
326                purpose,
327                destination,
328                handler.dht.did,
329                acks,
330            )),
331        )])
332        .await
333}
334
335#[cfg_attr(all(feature = "wasm", target_family = "wasm"), async_trait(?Send))]
336#[cfg_attr(not(all(feature = "wasm", target_family = "wasm")), async_trait)]
337impl ChordStorageInterfaceCacheChecker for Swarm {
338    /// Check local cache
339    async fn storage_check_cache(&self, entry_key: Did) -> Option<Entry> {
340        self.dht.local_cache_get(entry_key).await.ok().flatten()
341    }
342}
343
344#[cfg_attr(all(feature = "wasm", target_family = "wasm"), async_trait(?Send))]
345#[cfg_attr(not(all(feature = "wasm", target_family = "wasm")), async_trait)]
346impl<const REDUNDANT: u16> ChordStorageInterface<REDUNDANT> for Swarm {
347    /// Fetch an entry. If it exists in local storage, copy it to the cache;
348    /// otherwise query the responsible remote node.
349    async fn storage_fetch(&self, entry_key: Did) -> Result<()> {
350        self.transport.ensure_storage_redundancy::<REDUNDANT>()?;
351        self.transport.start_storage_lookup(entry_key, REDUNDANT)?;
352        // If peer found that data is on it's localstore, copy it to the cache
353        let act = self
354            .dht
355            .entry_lookup_for_fetch::<REDUNDANT>(entry_key)
356            .await?;
357        handle_storage_fetch_act::<REDUNDANT>(self.transport.clone(), entry_key, act).await?;
358        Ok(())
359    }
360
361    /// Store Entry, `TryInto<Entry>` is implemented for alot of types
362    async fn storage_store(&self, entry: Entry) -> Result<()> {
363        operate_storage_entry::<REDUNDANT>(self, EntryOperation::Overwrite(entry)).await
364    }
365
366    async fn storage_append_data(&self, topic: &str, data: Encoded) -> Result<()> {
367        let entry: Entry = (topic.to_string(), data).try_into()?;
368        operate_storage_entry::<REDUNDANT>(self, EntryOperation::Extend(entry)).await
369    }
370
371    async fn storage_touch_data(&self, topic: &str, data: Encoded) -> Result<()> {
372        let entry: Entry = (topic.to_string(), data).try_into()?;
373        operate_storage_entry::<REDUNDANT>(self, EntryOperation::Touch(entry)).await
374    }
375
376    async fn storage_tombstone_data(&self, topic: &str, data: Encoded) -> Result<()> {
377        let entry: Entry = (topic.to_string(), data).try_into()?;
378        operate_storage_entry::<REDUNDANT>(self, EntryOperation::Tombstone(entry)).await
379    }
380
381    async fn storage_compact_data(&self, topic: &str, removals: Vec<Encoded>) -> Result<()> {
382        let entry = Entry::new(Entry::gen_did(topic)?, removals, EntryKind::Data);
383        operate_storage_entry::<REDUNDANT>(self, EntryOperation::CompactData(entry)).await
384    }
385}
386
387#[cfg_attr(all(feature = "wasm", target_family = "wasm"), async_trait(?Send))]
388#[cfg_attr(not(all(feature = "wasm", target_family = "wasm")), async_trait)]
389impl HandleMsg<SearchEntry> for MessageHandler {
390    /// Search Entry via successor
391    /// If a Entry is storead local, it will response immediately.(See Chordstorageinterface::storage_fetch)
392    async fn handle(&self, ctx: &MessagePayload, msg: &SearchEntry) -> Result<()> {
393        // For relay message, set redundant to 1
394        match <PeerRing as ChordStorage<_, 1>>::entry_lookup(&self.dht, msg.placement).await {
395            Ok(action) => {
396                handle_storage_search_act(self, ctx, action, msg.resource, msg.redundancy).await
397            }
398            Err(e) => Err(e),
399        }
400    }
401}
402
403#[cfg_attr(all(feature = "wasm", target_family = "wasm"), async_trait(?Send))]
404#[cfg_attr(not(all(feature = "wasm", target_family = "wasm")), async_trait)]
405impl HandleMsg<FoundEntry> for MessageHandler {
406    async fn handle(&self, ctx: &MessagePayload, msg: &FoundEntry) -> Result<()> {
407        if ctx.should_forward_from(self.dht.did) {
408            return self
409                .run_effects([CoreEffect::forward_payload(ctx, None)])
410                .await;
411        }
412        // Pre: this node started a local lookup for (resource, redundancy).
413        // Preservation: all remote-controlled FoundEntry fields are validated
414        // before local_cache_put or read-repair can write storage state.
415        let found_entry = msg.single_entry()?;
416        self.transport
417            .ensure_storage_lookup_active(msg.resource, msg.redundancy)?;
418        self.transport.observe_storage_misses(
419            msg.resource,
420            msg.redundancy,
421            msg.misses.iter().copied(),
422        )?;
423        if let Some(data) = found_entry {
424            self.dht.local_cache_put(data.clone()).await?;
425            repair_observed_storage_misses(self.transport.clone(), data.clone(), msg.redundancy)
426                .await?;
427        } else if !msg.misses.is_empty() {
428            if let Some(entry) = self.dht.local_cache_get(msg.resource).await? {
429                repair_observed_storage_misses(self.transport.clone(), entry, msg.redundancy)
430                    .await?;
431            }
432        }
433        Ok(())
434    }
435}
436
437#[cfg_attr(all(feature = "wasm", target_family = "wasm"), async_trait(?Send))]
438#[cfg_attr(not(all(feature = "wasm", target_family = "wasm")), async_trait)]
439impl HandleMsg<PlacedEntryOperation> for MessageHandler {
440    async fn handle(&self, ctx: &MessagePayload, msg: &PlacedEntryOperation) -> Result<()> {
441        handle_placed_entry_operation(self, ctx, msg).await
442    }
443}
444
445#[cfg_attr(all(feature = "wasm", target_family = "wasm"), async_trait(?Send))]
446#[cfg_attr(not(all(feature = "wasm", target_family = "wasm")), async_trait)]
447impl HandleMsg<SyncEntriesWithSuccessor> for MessageHandler {
448    // received remote sync entry request
449    async fn handle(&self, ctx: &MessagePayload, msg: &SyncEntriesWithSuccessor) -> Result<()> {
450        if let Some(next) = next_hop_for_sync_entries(self, ctx, msg)? {
451            return self
452                .run_effects([CoreEffect::forward_payload(ctx, Some(next))])
453                .await;
454        }
455
456        let acks = self.transport.persist_storage_sync_entries(msg).await?;
457        if msg.purpose.permits_source_cleanup() {
458            if let Err(e) =
459                report_synced_entries(self, ctx, msg.purpose, msg.destination, acks).await
460            {
461                tracing::warn!("Failed to report synced entries: {e:?}");
462            }
463        }
464        Ok(())
465    }
466}
467
468#[cfg_attr(all(feature = "wasm", target_family = "wasm"), async_trait(?Send))]
469#[cfg_attr(not(all(feature = "wasm", target_family = "wasm")), async_trait)]
470impl HandleMsg<SyncEntriesWithSuccessorReport> for MessageHandler {
471    async fn handle(
472        &self,
473        ctx: &MessagePayload,
474        msg: &SyncEntriesWithSuccessorReport,
475    ) -> Result<()> {
476        if ctx.should_forward_from(self.dht.did) {
477            return self
478                .run_effects([CoreEffect::forward_payload(ctx, None)])
479                .await;
480        }
481
482        let signer = ctx.transaction.signer();
483        let origin = ctx.relay.try_origin_sender()?;
484        if signer != msg.receiver || origin != msg.receiver {
485            return Err(Error::InvalidMessage(
486                "storage sync report receiver does not match signed report origin".to_string(),
487            ));
488        }
489        let acks =
490            self.transport
491                .take_pending_storage_sync_ack(ctx.transaction.tx_id, signer, msg)?;
492        let action = self.dht.acknowledge_synced_entries(&acks).await?;
493        finish_storage_action(action)
494    }
495}
496
497#[cfg(not(all(feature = "wasm", target_family = "wasm")))]
498#[cfg(test)]
499mod tests;