Skip to main content

aranya_runtime/client/
session.rs

1//! Ephemeral sessions for off-graph commands.
2//!
3//! See [`ClientState::session`] and [`Session`].
4//!
5//! Design doc: [Aranya Sessions](https://github.com/aranya-project/aranya-docs/blob/main/src/Aranya-Sessions-note.md)
6
7use alloc::{
8    collections::{BTreeMap, btree_map},
9    string::String,
10    sync::Arc,
11    vec::Vec,
12};
13use core::{cmp::Ordering, iter::Peekable, marker::PhantomData, mem, ops::Bound};
14
15use buggy::{Bug, bug};
16use yoke::{Yoke, Yokeable};
17
18use crate::{
19    Address, Bytes, Checkpoint, ClientError, ClientState, CmdId, Command, Fact, FactPerspective,
20    GraphId, Keys, MaxCut, NullSink, Perspective, Policy, PolicyId, PolicyStore, Prior, Priority,
21    Query, QueryMut, Revertable, Segment as _, Sink, Storage, StorageError, StorageProvider,
22    policy::{ActionPlacement, CommandPlacement},
23};
24
25/// Ephemeral session used to handle/generate off-graph commands.
26pub struct Session<SP: StorageProvider, PS> {
27    /// The ID of the associated graph.
28    graph_id: GraphId,
29    /// The policy ID for the session.
30    policy_id: PolicyId,
31
32    /// The prior facts from the graph head.
33    base_facts: <SP::Storage as Storage>::FactIndex,
34    /// The log of facts in insertion order.
35    fact_log: Vec<(String, Keys, Option<Bytes>)>,
36    /// The current facts of the session, relative to `base_facts`.
37    current_facts: Arc<BTreeMap<String, BTreeMap<Keys, Option<Bytes>>>>,
38
39    /// Tag for associated policy store.
40    _policy_store: PhantomData<PS>,
41}
42
43struct SessionPerspective<'a, SP: StorageProvider, PS, MS> {
44    session: &'a mut Session<SP, PS>,
45    message_sink: &'a mut MS,
46}
47
48impl<SP: StorageProvider, PS> Session<SP, PS> {
49    pub(super) fn new(provider: &mut SP, graph_id: GraphId) -> Result<Self, ClientError> {
50        let storage = provider.get_storage(graph_id)?;
51        let head_loc = storage.get_head()?;
52        let seg = storage.get_segment(head_loc)?;
53
54        let base_facts = seg.facts()?;
55
56        let result = Self {
57            graph_id,
58            policy_id: seg.policy(),
59            base_facts,
60            fact_log: Vec::new(),
61            current_facts: Arc::default(),
62            _policy_store: PhantomData,
63        };
64
65        Ok(result)
66    }
67}
68
69impl<SP: StorageProvider, PS: PolicyStore> Session<SP, PS> {
70    /// Evaluate an action on the ephemeral session and generate serialized
71    /// commands, so another client can [`Session::receive`] them.
72    pub fn action<ES, MS>(
73        &mut self,
74        client: &ClientState<PS, SP>,
75        effect_sink: &mut ES,
76        message_sink: &mut MS,
77        action: <PS::Policy as Policy>::Action<'_>,
78    ) -> Result<(), ClientError>
79    where
80        ES: Sink<PS::Effect>,
81        MS: for<'b> Sink<&'b [u8]>,
82    {
83        let policy = client.policy_store.get_policy(self.policy_id)?;
84
85        // Use a special perspective so we can send to the message sink.
86        let mut perspective = SessionPerspective {
87            session: self,
88            message_sink,
89        };
90        let checkpoint = perspective.checkpoint();
91        effect_sink.begin();
92
93        // Try to perform action.
94        match policy.call_action(
95            action,
96            &mut perspective,
97            effect_sink,
98            ActionPlacement::OffGraph,
99        ) {
100            Ok(()) => {
101                // Success, commit effects
102                effect_sink.commit();
103                Ok(())
104            }
105            Err(e) => {
106                // Other error, revert all? See #513.
107                perspective.revert(checkpoint)?;
108                perspective.message_sink.rollback();
109                effect_sink.rollback();
110                Err(e.into())
111            }
112        }
113    }
114
115    /// Handle a command from another client generated by [`Session::action`].
116    ///
117    /// You do NOT need to reprocess the commands from actions generated in the
118    /// same session.
119    pub fn receive(
120        &mut self,
121        client: &ClientState<PS, SP>,
122        sink: &mut impl Sink<PS::Effect>,
123        command_bytes: &[u8],
124    ) -> Result<(), ClientError> {
125        let command = SessionCommand::deserialize(self.graph_id, command_bytes)
126            .ok_or(ClientError::SessionDeserialize)?;
127
128        let policy = client.policy_store.get_policy(self.policy_id)?;
129
130        // Use a special perspective which doesn't check the head
131        let mut perspective = SessionPerspective {
132            session: self,
133            message_sink: &mut NullSink,
134        };
135
136        // Try to evaluate command.
137        sink.begin();
138        let checkpoint = perspective.checkpoint();
139        if let Err(e) =
140            policy.call_rule(&command, &mut perspective, sink, CommandPlacement::OffGraph)
141        {
142            perspective.revert(checkpoint)?;
143            sink.rollback();
144            return Err(e.into());
145        }
146        sink.commit();
147
148        Ok(())
149    }
150}
151
152/// Fake session parent value.
153///
154/// Since we don't track the parent for session commands, we pretend that they all have this same
155/// parent. This makes the existing sign/verify work as expected without supplying a true value.
156///
157/// By using the graph ID as the parent, the command signature will include it, binding this
158/// command to that graph.
159fn session_parent(graph_id: GraphId) -> Prior<Address> {
160    Prior::Single(Address {
161        id: CmdId::transmute(graph_id),
162        max_cut: MaxCut::new(0),
163    })
164}
165
166/// Used for serializing session commands
167struct SessionCommand<'a> {
168    graph_id: GraphId,
169    id: CmdId,
170    data: &'a [u8],
171}
172
173impl Command for SessionCommand<'_> {
174    fn priority(&self) -> Priority {
175        Priority::Basic(0)
176    }
177
178    fn id(&self) -> CmdId {
179        self.id
180    }
181
182    fn parent(&self) -> Prior<Address> {
183        session_parent(self.graph_id)
184    }
185
186    fn policy(&self) -> Option<&[u8]> {
187        // Session commands should never have policy?
188        None
189    }
190
191    fn bytes(&self) -> &[u8] {
192        self.data
193    }
194}
195
196impl<'sc> SessionCommand<'sc> {
197    fn from_cmd(graph_id: GraphId, command: &'sc impl Command) -> Result<Self, Bug> {
198        if command.policy().is_some() {
199            bug!("session command should have no policy");
200        }
201        if !matches!(command.priority(), Priority::Basic(_)) {
202            bug!("session command has bad priority");
203        }
204        if command.parent() != session_parent(graph_id) {
205            bug!("session command has bad parent");
206        }
207        Ok(SessionCommand {
208            graph_id,
209            id: command.id(),
210            data: command.bytes(),
211        })
212    }
213
214    fn serialize(&self) -> Vec<u8> {
215        [self.id.as_bytes(), self.data].concat()
216    }
217
218    fn deserialize(graph_id: GraphId, bytes: &'sc [u8]) -> Option<Self> {
219        let (id, data) = bytes.split_first_chunk()?;
220        Some(Self {
221            graph_id,
222            id: CmdId::from_bytes(*id),
223            data,
224        })
225    }
226}
227
228/// Query iterator for SessionPerspective which wraps an inner query iterator
229struct QueryIterator<I1: Iterator, I2: Iterator> {
230    prior: Peekable<I1>,
231    current: Peekable<I2>,
232}
233
234impl<I1, I2> QueryIterator<I1, I2>
235where
236    I1: Iterator<Item = Result<Fact, StorageError>>,
237    I2: Iterator<Item = (Keys, Option<Bytes>)>,
238{
239    fn new(prior: I1, current: I2) -> Self {
240        Self {
241            prior: prior.peekable(),
242            current: current.peekable(),
243        }
244    }
245}
246
247impl<I1, I2> Iterator for QueryIterator<I1, I2>
248where
249    I1: Iterator<Item = Result<Fact, StorageError>>,
250    I2: Iterator<Item = (Keys, Option<Bytes>)>,
251{
252    type Item = Result<Fact, StorageError>;
253
254    fn next(&mut self) -> Option<Self::Item> {
255        // We find the next lowest item between the two iterators,
256        // while also ensuring that newer entries overwrite older.
257        // We loop so we can skip over deleted facts.
258
259        loop {
260            let Some(new) = self.current.peek() else {
261                // If current has run out, just use prior.
262                return self.prior.next();
263            };
264            if let Some(old) = self.prior.peek() {
265                let Ok(old) = old else {
266                    // Bubble up errors as soon as possible, instead of returning `new`.
267                    return self.prior.next();
268                };
269                match new.0.cmp(&old.key) {
270                    Ordering::Equal => {
271                        // new overwrites old.
272                        let _ = self.prior.next();
273                    }
274                    Ordering::Greater => {
275                        // old comes next in sorted order.
276                        return self.prior.next();
277                    }
278                    Ordering::Less => {
279                        // new comes next in sorted order.
280                    }
281                }
282            }
283            let Some(slot) = self.current.next() else {
284                bug!("expected Some after peek")
285            };
286            if let (k, Some(v)) = slot {
287                return Some(Ok(Fact {
288                    key: k.iter().cloned().collect(),
289                    value: v,
290                }));
291            }
292        }
293    }
294}
295
296impl<SP, PS, MS> FactPerspective for SessionPerspective<'_, SP, PS, MS> where SP: StorageProvider {}
297
298impl<SP, PS, MS> Query for SessionPerspective<'_, SP, PS, MS>
299where
300    SP: StorageProvider,
301{
302    fn query(&self, name: &str, keys: &[Bytes]) -> Result<Option<Bytes>, StorageError> {
303        if let Some(slot) = self
304            .session
305            .current_facts
306            .get(name)
307            .and_then(|m| m.get(keys))
308        {
309            return Ok(slot.clone());
310        }
311        self.session.base_facts.query(name, keys)
312    }
313
314    type QueryIterator = QueryIterator<
315        <<SP::Storage as Storage>::FactIndex as Query>::QueryIterator,
316        YokeIter<PrefixIter<'static>, Arc<BTreeMap<String, BTreeMap<Keys, Option<Bytes>>>>>,
317    >;
318    fn query_prefix(
319        &self,
320        name: &str,
321        prefix: &[Bytes],
322    ) -> Result<Self::QueryIterator, StorageError> {
323        let prior = self.session.base_facts.query_prefix(name, prefix)?;
324        let current = Yoke::<PrefixIter<'static>, _>::attach_to_cart(
325            Arc::clone(&self.session.current_facts),
326            |map| match map.get(name) {
327                Some(facts) => PrefixIter::new(facts, prefix.iter().cloned().collect()),
328                None => PrefixIter::default(),
329            },
330        );
331        Ok(QueryIterator::new(prior, YokeIter::new(current)))
332    }
333}
334
335/// Iterator over matching prefix of a [`BTreeMap`].
336///
337/// Equivalent to `map.range(&prefix..).take_while(move |(k, _)| k.starts_with(prefix))`,
338/// but nameable and [`Yokeable`].
339#[derive(Default, Yokeable)]
340struct PrefixIter<'map> {
341    range: btree_map::Range<'map, Keys, Option<Bytes>>,
342    prefix: Keys,
343}
344
345impl<'map> PrefixIter<'map> {
346    fn new(map: &'map BTreeMap<Keys, Option<Bytes>>, prefix: Keys) -> Self {
347        let range = map.range::<[Bytes], _>((Bound::Included(prefix.as_ref()), Bound::Unbounded));
348        Self { range, prefix }
349    }
350}
351
352impl Iterator for PrefixIter<'_> {
353    type Item = (Keys, Option<Bytes>);
354
355    fn next(&mut self) -> Option<Self::Item> {
356        self.range
357            .next()
358            .filter(|(k, _)| k.starts_with(&self.prefix))
359            .map(|(k, v)| (k.clone(), v.clone()))
360    }
361}
362
363/// Wrapper around [`Yoke`] which implements [`Iterator`].
364struct YokeIter<I: for<'a> Yokeable<'a>, C>(Option<Yoke<I, C>>);
365
366impl<I: for<'a> Yokeable<'a>, C> YokeIter<I, C> {
367    fn new(yoke: Yoke<I, C>) -> Self {
368        Self(Some(yoke))
369    }
370}
371
372impl<I, C> Iterator for YokeIter<I, C>
373where
374    I: Iterator + for<'a> Yokeable<'a>,
375    for<'a> <I as Yokeable<'a>>::Output: Iterator<Item = I::Item>,
376{
377    type Item = I::Item;
378
379    fn next(&mut self) -> Option<Self::Item> {
380        // `Yoke::map_project` is currently the only way to mutate something in a yoke and get out a value.
381        // It takes the yoke by value though, so we have to `take` it so we can own it temporarily.
382        let mut item = None;
383        self.0 = Some(self.0.take()?.map_project::<I, _>(|mut it, _| {
384            item = it.next();
385            it
386        }));
387        item
388    }
389}
390
391impl<SP: StorageProvider, PS, MS> QueryMut for SessionPerspective<'_, SP, PS, MS> {
392    fn insert(&mut self, name: String, keys: Keys, value: Bytes) -> Result<(), StorageError> {
393        self.session
394            .fact_log
395            .push((name.clone(), keys.clone(), Some(value.clone())));
396        Arc::make_mut(&mut self.session.current_facts)
397            .entry(name)
398            .or_default()
399            .insert(keys, Some(value));
400        Ok(())
401    }
402
403    fn delete(&mut self, name: String, keys: Keys) -> Result<(), StorageError> {
404        self.session
405            .fact_log
406            .push((name.clone(), keys.clone(), None));
407        Arc::make_mut(&mut self.session.current_facts)
408            .entry(name)
409            .or_default()
410            .insert(keys, None);
411        Ok(())
412    }
413}
414
415impl<SP, PS, MS> Perspective for SessionPerspective<'_, SP, PS, MS>
416where
417    SP: StorageProvider,
418    MS: for<'b> Sink<&'b [u8]>,
419{
420    fn policy(&self) -> PolicyId {
421        self.session.policy_id
422    }
423
424    fn add_command(&mut self, command: &impl Command) -> Result<usize, StorageError> {
425        let command = SessionCommand::from_cmd(self.session.graph_id, command)?;
426        self.message_sink.consume(&command.serialize());
427
428        Ok(0)
429    }
430
431    fn includes(&self, _id: CmdId) -> bool {
432        debug_assert!(false, "only used in transactions");
433
434        false
435    }
436
437    fn head_address(&self) -> Result<Prior<Address>, Bug> {
438        Ok(session_parent(self.session.graph_id))
439    }
440}
441
442impl<SP, PS, MS> Revertable for SessionPerspective<'_, SP, PS, MS>
443where
444    SP: StorageProvider,
445{
446    fn checkpoint(&self) -> Checkpoint {
447        Checkpoint {
448            index: self.session.fact_log.len(),
449        }
450    }
451
452    fn revert(&mut self, checkpoint: Checkpoint) -> Result<(), StorageError> {
453        if checkpoint.index == self.session.fact_log.len() {
454            return Ok(());
455        }
456
457        if checkpoint.index > self.session.fact_log.len() {
458            bug!(
459                "A checkpoint's index should always be less than or equal to the length of a session's fact log!"
460            );
461        }
462
463        self.session.fact_log.truncate(checkpoint.index);
464        // Create empty map, but reuse allocation if not shared
465        let mut facts =
466            Arc::get_mut(&mut self.session.current_facts).map_or_else(BTreeMap::new, mem::take);
467        facts.clear();
468        for (n, k, v) in self.session.fact_log.iter().cloned() {
469            facts.entry(n).or_default().insert(k, v);
470        }
471        self.session.current_facts = Arc::new(facts);
472
473        Ok(())
474    }
475}
476
477#[cfg(test)]
478mod test {
479    use super::*;
480
481    #[test]
482    fn test_query_iterator() {
483        #![allow(clippy::type_complexity)]
484
485        let prior: Vec<Result<(&[&[u8]], &[u8]), _>> = vec![
486            Ok((&[b"a"], b"a0")),
487            Ok((&[b"c"], b"c0")),
488            Ok((&[b"d"], b"d0")),
489            Ok((&[b"f"], b"f0")),
490            Err(StorageError::IoError),
491        ];
492        let current: Vec<([Bytes; 1], Option<&[u8]>)> = vec![
493            ([Bytes::from(*b"a")], None),
494            ([Bytes::from(*b"b")], Some(b"b1")),
495            ([Bytes::from(*b"e")], None),
496            ([Bytes::from(*b"j")], None),
497        ];
498        let merged: Vec<Result<(&[&[u8]], &[u8]), _>> = vec![
499            Ok((&[b"b"], b"b1")),
500            Ok((&[b"c"], b"c0")),
501            Ok((&[b"d"], b"d0")),
502            Ok((&[b"f"], b"f0")),
503            Err(StorageError::IoError),
504        ];
505
506        let got: Vec<_> = QueryIterator::new(
507            prior.into_iter().map(|r| {
508                r.map(|(k, v)| Fact {
509                    key: k.into(),
510                    value: v.into(),
511                })
512            }),
513            current
514                .into_iter()
515                .map(|(k, v)| (k.into_iter().collect(), v.map(Bytes::from))),
516        )
517        .collect();
518        let want: Vec<_> = merged
519            .into_iter()
520            .map(|r| {
521                r.map(|(k, v)| Fact {
522                    key: k.into(),
523                    value: v.into(),
524                })
525            })
526            .collect();
527
528        assert_eq!(got, want);
529    }
530}