tor_hsclient/
isol_map.rs

1//! Data structure `MultikeyIsolatedMap` indexed by multiple keys and an isolation
2
3use std::collections::HashMap;
4use std::hash::Hash;
5
6use derive_more::{Deref, DerefMut};
7use educe::Educe;
8
9use slotmap_careful::{DenseSlotMap, Key};
10
11use tor_circmgr::isolation::Isolation;
12
13/// Data structure indexed by multiple keys and an isolation
14///
15/// This is a map keyed by:
16///  * `K1`, a hashable key type
17///  * `K2`, a possibly-non-hashable key type,
18///  * [`Box<dyn Isolation>`](Isolation), a circuit/client isolation
19///
20/// Lookup/inserts will look for a suitable entry with a compatible isolation
21/// and, if found, narrow that entry's isolation.
22///
23/// A lookup/insert yields a **table index** `I`,
24/// which can later be used to do an O(1) lookup of the same entry.
25/// (This is particularly useful given that an isolation cannot be simply compared;
26/// a proper lookup might narrow the isolation of an existing record.)
27///
28/// `I` must implement `slotmap:Key`.
29///
30/// The values are `V`.
31///
32/// Internally, it looks like this:
33///
34/// ```text
35///           index                                         table
36///           HashMap           Vec_______________          SlotMap_____________________
37///           |     | contains  | table_index    |  t._i.   | K2, isol, V  /  <vacant> |
38///     K1 -> |  ---+---------> | table_index    | -------> | K2, isol, V  /  <vacant> |
39///           |_____|           | table_index    | 1      1 | K2, isol, V  /  <vacant> |
40///                             | table_index    |          | K2, isol, V  /  <vacant> |
41///   K2, isol ---------------> | .............. |          | K2, isol, V  /  <vacant> |
42///             linear search   |________________|          | ...             ....     |
43/// ```                                                     |__________________________|
44#[derive(Debug, Educe)]
45#[educe(Default)]
46pub(crate) struct MultikeyIsolatedMap<I, K1, K2, V>
47where
48    I: Key,
49    K1: Hash + Eq,
50    K2: Eq,
51{
52    /// The first stage index, mapping `K1` to `I`
53    index: HashMap<K1, Vec<I>>,
54
55    /// Actual table containing the entries, including `K2` and the isolation, and `V`
56    ///
57    /// ### Invariant
58    ///
59    /// Entries in `table` and `index` correspond precisely, one-to-one:
60    /// each `Vec` element in `index` refers to an (occupied) entry in `table`, and
61    /// each (occupied) entry in `table` is referenced precisely once from `index`.
62    table: DenseSlotMap<I, Record<K2, V>>,
63}
64
65/// Record within a `MultikeyIsolatedMap`
66///
67/// This contains `K2`, the isolation, and the value.
68/// It derefs to the value, which you may mutate.
69///
70/// (You can't mutate the key parts; that would be wrong.)
71#[derive(Debug, Deref, DerefMut)]
72pub(crate) struct Record<K2, V>
73where
74    K2: Eq,
75{
76    /// K2 (part of the data structure key)
77    k2: K2,
78    /// Circuit isolation (part of the data structure key)
79    isolation: Box<dyn Isolation>,
80    /// Actual value
81    #[deref]
82    #[deref_mut]
83    value: V,
84}
85
86impl<K2, V> Record<K2, V>
87where
88    K2: Eq,
89{
90    /// Obtain a reference to this record's 2nd-stage key `K2`
91    #[allow(dead_code)] // TODO remove if and when we make this all pub
92    pub(crate) fn k2(&self) -> &K2 {
93        &self.k2
94    }
95
96    /// Obtain a reference to this record's isolation
97    #[allow(dead_code)] // TODO remove if and when we make this all pub
98    pub(crate) fn isolation(&self) -> &dyn Isolation {
99        &*self.isolation
100    }
101}
102
103impl<I, K1, K2, V> MultikeyIsolatedMap<I, K1, K2, V>
104where
105    I: Key,
106    K1: Hash + Eq,
107    K2: Eq,
108{
109    /// Lookup, or insert, an entry
110    ///
111    /// Looks for an entry with keys `k1` and `k2` and a compatible isolation.
112    /// If it finds one, narrows the isolation and returns the entry's index.
113    ///
114    /// If no entry is found, inserts a new value made by `create`.
115    pub(crate) fn index_or_insert_with(
116        &mut self,
117        k1: &K1,
118        k2: &K2,
119        isolation: Box<dyn Isolation>,
120        create: impl FnOnce() -> V,
121    ) -> I
122    where
123        K1: Clone,
124        K2: Clone,
125    {
126        let indices = self.index.entry(k1.clone()).or_default();
127
128        match indices.iter().find_map(|&t_index| {
129            // Deconstruct so that we can't accidentally fail to check some of the key fields
130            let Record {
131                k2: t_k2,
132                isolation: t_isolation,
133                value: _,
134            } = self.table.get(t_index)
135                // should be Some, unless data structure corrupted, but don't panic here
136                    ?;
137            (t_k2 == k2).then_some(())?;
138            let new_isolation = t_isolation.join(&*isolation)?;
139            Some((t_index, new_isolation))
140        }) {
141            Some((t_index, new_isolation)) => {
142                self.table
143                    .get_mut(t_index)
144                    .expect("table entry disappeared")
145                    .isolation = new_isolation;
146                t_index
147            }
148            None => {
149                let value = create();
150                let record = Record {
151                    k2: k2.clone(),
152                    isolation,
153                    value,
154                };
155                let table_index = self.table.insert(record);
156                indices.push(table_index);
157                table_index
158            }
159        }
160    }
161
162    /// Look up an existing entry by index
163    ///
164    /// If the entry was removed in the meantime, will return `None`
165    #[allow(dead_code)] // TODO remove if and when we make this all pub
166    pub(crate) fn by_index(&self, t_index: I) -> Option<&Record<K2, V>> {
167        self.table.get(t_index)
168    }
169
170    /// Look up an existing entry by index (mutably)
171    ///
172    /// If the entry was removed in the meantime, will return `None`
173    pub(crate) fn by_index_mut(&mut self, t_index: I) -> Option<&mut Record<K2, V>> {
174        self.table.get_mut(t_index)
175    }
176
177    /// Keep only entries that match a predicate
178    ///
179    /// Each entry is passed to `test`, and removed unless `test` returned `true`.
180    #[allow(dead_code)] // TODO HS remove
181    pub(crate) fn retain(&mut self, mut test: impl FnMut(&K1, &Record<K2, V>, I) -> bool) {
182        self.index.retain(|k1, indices| {
183            indices.retain(|&t_index| {
184                let record = match self.table.get(t_index) {
185                    Some(record) => record,
186                    None => return false, // shouldn't happen
187                };
188                let keep = test(k1, record, t_index);
189                if !keep {
190                    self.table.remove(t_index);
191                }
192                keep
193            });
194            !indices.is_empty()
195        });
196    }
197
198    /// Checks that the structure is consistent
199    ///
200    /// # Panics
201    ///
202    /// If it is found not to be.
203    #[cfg(test)]
204    fn check_or_panic(&self) {
205        let mut referenced = slotmap_careful::SecondaryMap::default();
206
207        for indices in self.index.values() {
208            assert!(!indices.is_empty(), "empty Vec not GC'd");
209            for (vi1, &ti1) in indices.iter().enumerate() {
210                let rec1 = self.table.get(ti1).expect("dangling index");
211                match referenced.entry(ti1) {
212                    Some(slotmap_careful::secondary::Entry::Vacant(ve)) => ve.insert(()),
213                    _ => panic!("colliding references or something {ti1:?}"),
214                };
215                for &ti2 in &indices[vi1 + 1..] {
216                    let rec2 = &self.table[ti2];
217                    assert!(
218                        !(rec1.k2 == rec2.k2 && rec1.isolation.compatible(&*rec2.isolation)),
219                        "Vec contains entries that should have been merged",
220                    );
221                }
222            }
223        }
224
225        for ti in self.table.keys() {
226            let () = referenced.get(ti).expect("unreferenced entry");
227        }
228    }
229}
230
231// TODO HS: Currently tested via state.rs's stests, which do exercise this fairly well,
232// but some more dedicated unit tests would be good.
233#[cfg(test)]
234mod test {
235    // @@ begin test lint list maintained by maint/add_warning @@
236    #![allow(clippy::bool_assert_comparison)]
237    #![allow(clippy::clone_on_copy)]
238    #![allow(clippy::dbg_macro)]
239    #![allow(clippy::mixed_attributes_style)]
240    #![allow(clippy::print_stderr)]
241    #![allow(clippy::print_stdout)]
242    #![allow(clippy::single_char_pattern)]
243    #![allow(clippy::unwrap_used)]
244    #![allow(clippy::unchecked_duration_subtraction)]
245    #![allow(clippy::useless_vec)]
246    #![allow(clippy::needless_pass_by_value)]
247    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
248    use super::*;
249
250    slotmap_careful::new_key_type! {
251        struct Idx;
252    }
253
254    use crate::state::test::NarrowableIsolation;
255
256    fn mk_isol(s: impl Into<String>) -> Box<dyn Isolation> {
257        NarrowableIsolation(s.into()).into()
258    }
259
260    fn mk() -> MultikeyIsolatedMap<Idx, u32, u16, String> {
261        let mut out = MultikeyIsolatedMap::<Idx, u32, u16, String>::default();
262        let ti = out.index_or_insert_with(&1, &22, mk_isol("a"), || "hi".into());
263        assert_eq!(out.by_index(ti).unwrap().k2(), &22);
264        out.check_or_panic();
265        out
266    }
267
268    #[test]
269    fn simple() {
270        mk();
271    }
272
273    #[test]
274    fn retain() {
275        let mut m = mk();
276        m.index_or_insert_with(&2, &22, mk_isol("ab"), || "22".into());
277        m.check_or_panic();
278        m.index_or_insert_with(&2, &23, mk_isol("ac"), || "23".into());
279        m.check_or_panic();
280        m.index_or_insert_with(&2, &24, mk_isol("dd"), || "24".into());
281        m.check_or_panic();
282        dbg!(&m);
283        m.retain(|_k1, rec, _ti| (rec.k2 % 2) == 1);
284        dbg!(&m);
285        m.check_or_panic();
286    }
287}