Skip to main content

aranya_runtime/sync/
mod.rs

1//! Interface for syncing state between clients.
2//!
3//! Transports decode incoming bytes into a [`SyncIncoming`] and dispatch on
4//! its variants; the on-wire postcard layout is hidden behind that
5//! enumeration so it can evolve without breaking consumers.
6
7use core::time::Duration;
8
9use buggy::Bug;
10use heapless::Vec;
11use postcard::Error as PostcardError;
12use serde::{Deserialize, Serialize};
13
14use crate::{
15    Address, MaxCut, Prior,
16    command::{CmdId, Command, Priority},
17    storage::{GraphId, MAX_COMMAND_LENGTH, StorageError},
18};
19
20mod requester;
21mod responder;
22mod wire;
23
24use requester::SyncRequestMessage;
25pub use requester::SyncRequester;
26use responder::SyncResponseMessage;
27pub use responder::{PeerCache, SyncResponder};
28use wire::{SubscribeResult, SyncHelloType, SyncType};
29
30// TODO: These should all be compile time parameters
31
32/// The maximum number of heads that will be stored for a peer.
33pub const PEER_HEAD_MAX: usize = 10;
34
35/// The maximum number of samples in a request
36#[cfg(feature = "low-mem-usage")]
37const COMMAND_SAMPLE_MAX: usize = 20;
38#[cfg(not(feature = "low-mem-usage"))]
39const COMMAND_SAMPLE_MAX: usize = 100;
40
41/// The maximum number of missing segments that can be requested
42/// in a single message
43#[cfg(feature = "low-mem-usage")]
44const REQUEST_MISSING_MAX: usize = 1;
45#[cfg(not(feature = "low-mem-usage"))]
46const REQUEST_MISSING_MAX: usize = 100;
47
48/// The maximum number of commands in a response
49#[cfg(feature = "low-mem-usage")]
50pub const COMMAND_RESPONSE_MAX: usize = 5;
51#[cfg(not(feature = "low-mem-usage"))]
52pub const COMMAND_RESPONSE_MAX: usize = 100;
53
54/// The maximum number of segments which can be stored to send
55#[cfg(feature = "low-mem-usage")]
56const SEGMENT_BUFFER_MAX: usize = 10;
57#[cfg(not(feature = "low-mem-usage"))]
58const SEGMENT_BUFFER_MAX: usize = 100;
59
60/// The maximum size of a sync message
61// TODO: Use postcard to calculate max size (which accounts for overhead)
62// https://docs.rs/postcard/latest/postcard/experimental/max_size/index.html
63pub const MAX_SYNC_MESSAGE_SIZE: usize = 1024 + MAX_COMMAND_LENGTH * COMMAND_RESPONSE_MAX;
64
65/// An error returned by the syncer.
66#[derive(Debug, thiserror::Error)]
67#[non_exhaustive]
68pub enum SyncError {
69    #[error("sync session ID does not match")]
70    SessionMismatch,
71    #[error("missing sync response")]
72    MissingSyncResponse,
73    #[error("syncer state not valid for this message")]
74    SessionState,
75    #[error("syncer not ready for operation")]
76    NotReady,
77    #[error("too many commands sent")]
78    CommandOverflow,
79    #[error("storage error: {0}")]
80    Storage(#[from] StorageError),
81    #[error("serialize error: {0}")]
82    Serialize(#[from] PostcardError),
83    #[error(transparent)]
84    Bug(#[from] Bug),
85}
86
87/// Sync command to be committed to graph.
88#[derive(Serialize, Deserialize, Debug)]
89pub struct SyncCommand<'a> {
90    priority: Priority,
91    id: CmdId,
92    parent: Prior<Address>,
93    policy: Option<&'a [u8]>,
94    data: &'a [u8],
95    max_cut: MaxCut,
96}
97
98impl<'a> Command for SyncCommand<'a> {
99    fn priority(&self) -> Priority {
100        self.priority.clone()
101    }
102
103    fn id(&self) -> CmdId {
104        self.id
105    }
106
107    fn parent(&self) -> Prior<Address> {
108        self.parent
109    }
110
111    fn policy(&self) -> Option<&'a [u8]> {
112        self.policy
113    }
114
115    fn bytes(&self) -> &'a [u8] {
116        self.data
117    }
118
119    fn max_cut(&self) -> Result<MaxCut, Bug> {
120        Ok(self.max_cut)
121    }
122}
123
124/// A decoded incoming sync message.
125///
126/// Transports decode the raw bytes received over the network with
127/// [`SyncIncoming::decode`] and then dispatch on the returned variant.
128/// The on-wire postcard layout is encapsulated by this enum and the
129/// associated helper types — consumers don't depend on the wire
130/// representation directly.
131#[allow(clippy::large_enum_variant)]
132pub enum SyncIncoming<'a> {
133    /// A sync poll. Hand to [`SyncResponder::receive`].
134    Poll(PollIncoming),
135    /// A subscription request from a peer.
136    Subscribe(SubscribeIncoming),
137    /// An unsubscribe request from a peer.
138    Unsubscribe(UnsubscribeIncoming),
139    /// A push from a subscribed peer. Hand to [`SyncRequester::receive_push`].
140    Push(PushIncoming<'a>),
141    /// A subscription-control hello message.
142    Hello(SyncHello),
143}
144
145impl<'a> SyncIncoming<'a> {
146    /// Decode an incoming sync message from raw bytes.
147    pub fn decode(data: &'a [u8]) -> Result<Self, SyncError> {
148        let (sync_type, remaining) = postcard::take_from_bytes::<SyncType>(data)?;
149        Ok(match sync_type {
150            SyncType::Poll { request } => Self::Poll(PollIncoming {
151                session_id: request.session_id(),
152                message: request,
153            }),
154            SyncType::Subscribe {
155                remain_open,
156                max_bytes,
157                commands,
158                graph_id,
159            } => Self::Subscribe(SubscribeIncoming {
160                graph_id,
161                remain_open,
162                max_bytes,
163                heads: SyncHeads { inner: commands },
164            }),
165            SyncType::Unsubscribe { graph_id } => {
166                Self::Unsubscribe(UnsubscribeIncoming { graph_id })
167            }
168            SyncType::Push { message, graph_id } => Self::Push(PushIncoming {
169                graph_id,
170                session_id: message.session_id(),
171                message,
172                command_data: remaining,
173            }),
174            SyncType::Hello(hello) => Self::Hello(hello.into()),
175        })
176    }
177}
178
179/// A peer's sample of known graph heads, carried in [`SyncIncoming::Subscribe`].
180pub struct SyncHeads {
181    inner: Vec<Address, COMMAND_SAMPLE_MAX>,
182}
183
184impl SyncHeads {
185    /// Returns the heads as a slice.
186    pub fn as_slice(&self) -> &[Address] {
187        &self.inner
188    }
189
190    /// Iterates over the heads.
191    pub fn iter(&self) -> impl DoubleEndedIterator<Item = Address> + ExactSizeIterator + '_ {
192        self.inner.iter().copied()
193    }
194}
195
196/// A subscription-control message: subscribe, unsubscribe, or hello.
197#[derive(Debug)]
198pub enum SyncHello {
199    /// Subscribe to receive hello notifications from this peer.
200    Subscribe(HelloSubscribe),
201    /// Unsubscribe from hello notifications.
202    Unsubscribe(HelloUnsubscribe),
203    /// Notification message sent to subscribers.
204    Hello(HelloNotification),
205}
206
207impl From<SyncHelloType> for SyncHello {
208    fn from(t: SyncHelloType) -> Self {
209        match t {
210            SyncHelloType::Subscribe {
211                graph_id,
212                graph_change_delay,
213                duration,
214                schedule_delay,
215            } => Self::Subscribe(HelloSubscribe {
216                graph_id,
217                graph_change_delay,
218                duration,
219                schedule_delay,
220            }),
221            SyncHelloType::Unsubscribe { graph_id } => {
222                Self::Unsubscribe(HelloUnsubscribe { graph_id })
223            }
224            SyncHelloType::Hello { graph_id, head } => {
225                Self::Hello(HelloNotification { graph_id, head })
226            }
227        }
228    }
229}
230
231/// An opaque container for a hello-protocol subscribe request.
232#[derive(Debug)]
233pub struct HelloSubscribe {
234    graph_id: GraphId,
235    graph_change_delay: Duration,
236    duration: Duration,
237    schedule_delay: Duration,
238}
239
240impl HelloSubscribe {
241    /// Returns the graph being subscribed to.
242    pub fn graph_id(&self) -> GraphId {
243        self.graph_id
244    }
245
246    /// Returns the delay between notifications when the graph changes (rate limiting).
247    pub fn graph_change_delay(&self) -> Duration {
248        self.graph_change_delay
249    }
250
251    /// Returns how long the subscription should last.
252    pub fn duration(&self) -> Duration {
253        self.duration
254    }
255
256    /// Returns the schedule-based hello sending delay.
257    pub fn schedule_delay(&self) -> Duration {
258        self.schedule_delay
259    }
260}
261
262/// An opaque container for a hello-protocol unsubscribe request.
263#[derive(Debug)]
264pub struct HelloUnsubscribe {
265    graph_id: GraphId,
266}
267
268impl HelloUnsubscribe {
269    /// Returns the graph being unsubscribed from.
270    pub fn graph_id(&self) -> GraphId {
271        self.graph_id
272    }
273}
274
275/// An opaque container for a hello notification sent to subscribers.
276#[derive(Debug)]
277pub struct HelloNotification {
278    graph_id: GraphId,
279    head: Address,
280}
281
282impl HelloNotification {
283    /// Returns the graph this notification is for.
284    pub fn graph_id(&self) -> GraphId {
285        self.graph_id
286    }
287
288    /// Returns the current head of the sender's graph.
289    pub fn head(&self) -> Address {
290        self.head
291    }
292}
293
294/// An opaque container for a received poll message. Hand to
295/// [`SyncResponder::receive`] to update the responder's state.
296pub struct PollIncoming {
297    session_id: u128,
298    pub(crate) message: SyncRequestMessage,
299}
300
301impl PollIncoming {
302    /// Returns the sender's session identifier.
303    pub fn session_id(&self) -> u128 {
304        self.session_id
305    }
306}
307
308/// An opaque container for a received subscribe message.
309pub struct SubscribeIncoming {
310    graph_id: GraphId,
311    remain_open: u64,
312    max_bytes: u64,
313    heads: SyncHeads,
314}
315
316impl SubscribeIncoming {
317    /// Returns the graph being subscribed to.
318    pub fn graph_id(&self) -> GraphId {
319        self.graph_id
320    }
321
322    /// Returns how long the subscription should remain open.
323    pub fn remain_open(&self) -> Duration {
324        Duration::from_secs(self.remain_open)
325    }
326
327    /// Returns the maximum number of bytes the responder may push.
328    pub fn max_bytes(&self) -> u64 {
329        self.max_bytes
330    }
331
332    /// Returns the peer's known graph heads.
333    pub fn heads(&self) -> &SyncHeads {
334        &self.heads
335    }
336}
337
338/// An opaque container for a received unsubscribe message.
339pub struct UnsubscribeIncoming {
340    graph_id: GraphId,
341}
342
343impl UnsubscribeIncoming {
344    /// Returns the graph being unsubscribed from.
345    pub fn graph_id(&self) -> GraphId {
346        self.graph_id
347    }
348}
349
350/// An opaque container for a received push message. Hand to
351/// [`SyncRequester::receive_push`] to extract the contained commands.
352pub struct PushIncoming<'a> {
353    graph_id: GraphId,
354    session_id: u128,
355    pub(crate) message: SyncResponseMessage,
356    pub(crate) command_data: &'a [u8],
357}
358
359impl PushIncoming<'_> {
360    /// Returns the graph this push targets.
361    pub fn graph_id(&self) -> GraphId {
362        self.graph_id
363    }
364
365    /// Returns the sender's session identifier.
366    pub fn session_id(&self) -> u128 {
367        self.session_id
368    }
369}
370
371/// The result of a [`SyncIncoming::Subscribe`] dispatch, sent back to the
372/// requester so it knows whether the subscription was accepted.
373#[derive(Debug)]
374pub enum SubscribeResponse {
375    /// The subscription was accepted.
376    Success,
377    /// The responder is at its subscription limit.
378    TooManySubscriptions,
379}
380
381impl SubscribeResponse {
382    /// Encode into `target`. Returns the number of bytes written.
383    pub fn encode_to(self, target: &mut [u8]) -> Result<usize, SyncError> {
384        let inner = match self {
385            Self::Success => SubscribeResult::Success,
386            Self::TooManySubscriptions => SubscribeResult::TooManySubscriptions,
387        };
388        Ok(postcard::to_slice(&inner, target)?.len())
389    }
390
391    /// Decode from raw bytes.
392    pub fn decode(data: &[u8]) -> Result<Self, SyncError> {
393        let inner: SubscribeResult = postcard::from_bytes(data)?;
394        Ok(match inner {
395            SubscribeResult::Success => Self::Success,
396            SubscribeResult::TooManySubscriptions => Self::TooManySubscriptions,
397        })
398    }
399}