1
//! Data structure `MultikeyIsolatedMap` indexed by multiple keys and an isolation
2

            
3
use std::collections::HashMap;
4
use std::hash::Hash;
5

            
6
use derive_more::{Deref, DerefMut};
7
use educe::Educe;
8

            
9
use slotmap_careful::{DenseSlotMap, Key};
10

            
11
use 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
20
#[derive(Debug, Educe)]
45
#[educe(Default)]
46
pub(crate) struct MultikeyIsolatedMap<I, K1, K2, V>
47
where
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)]
72
pub(crate) struct Record<K2, V>
73
where
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

            
86
impl<K2, V> Record<K2, V>
87
where
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
4
    pub(crate) fn k2(&self) -> &K2 {
93
4
        &self.k2
94
4
    }
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

            
103
impl<I, K1, K2, V> MultikeyIsolatedMap<I, K1, K2, V>
104
where
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
50
    pub(crate) fn index_or_insert_with(
116
50
        &mut self,
117
50
        k1: &K1,
118
50
        k2: &K2,
119
50
        isolation: Box<dyn Isolation>,
120
50
        create: impl FnOnce() -> V,
121
50
    ) -> I
122
50
    where
123
50
        K1: Clone,
124
50
        K2: Clone,
125
50
    {
126
50
        let indices = self.index.entry(k1.clone()).or_default();
127
50

            
128
50
        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
34
                k2: t_k2,
132
34
                isolation: t_isolation,
133
                value: _,
134
34
            } = self.table.get(t_index)
135
                // should be Some, unless data structure corrupted, but don't panic here
136
                    ?;
137
34
            (t_k2 == k2).then_some(())?;
138
24
            let new_isolation = t_isolation.join(&*isolation)?;
139
22
            Some((t_index, new_isolation))
140
50
        }) {
141
22
            Some((t_index, new_isolation)) => {
142
22
                self.table
143
22
                    .get_mut(t_index)
144
22
                    .expect("table entry disappeared")
145
22
                    .isolation = new_isolation;
146
22
                t_index
147
            }
148
            None => {
149
28
                let value = create();
150
28
                let record = Record {
151
28
                    k2: k2.clone(),
152
28
                    isolation,
153
28
                    value,
154
28
                };
155
28
                let table_index = self.table.insert(record);
156
28
                indices.push(table_index);
157
28
                table_index
158
            }
159
        }
160
50
    }
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
4
    pub(crate) fn by_index(&self, t_index: I) -> Option<&Record<K2, V>> {
167
4
        self.table.get(t_index)
168
4
    }
169

            
170
    /// Look up an existing entry by index (mutably)
171
    ///
172
    /// If the entry was removed in the meantime, will return `None`
173
136
    pub(crate) fn by_index_mut(&mut self, t_index: I) -> Option<&mut Record<K2, V>> {
174
136
        self.table.get_mut(t_index)
175
136
    }
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
22
    pub(crate) fn retain(&mut self, mut test: impl FnMut(&K1, &Record<K2, V>, I) -> bool) {
182
24
        self.index.retain(|k1, indices| {
183
28
            indices.retain(|&t_index| {
184
28
                let record = match self.table.get(t_index) {
185
28
                    Some(record) => record,
186
                    None => return false, // shouldn't happen
187
                };
188
28
                let keep = test(k1, record, t_index);
189
28
                if !keep {
190
10
                    self.table.remove(t_index);
191
18
                }
192
28
                keep
193
28
            });
194
24
            !indices.is_empty()
195
24
        });
196
22
    }
197

            
198
    /// Checks that the structure is consistent
199
    ///
200
    /// # Panics
201
    ///
202
    /// If it is found not to be.
203
    #[cfg(test)]
204
12
    fn check_or_panic(&self) {
205
12
        let mut referenced = slotmap_careful::SecondaryMap::default();
206

            
207
18
        for indices in self.index.values() {
208
18
            assert!(!indices.is_empty(), "empty Vec not GC'd");
209
24
            for (vi1, &ti1) in indices.iter().enumerate() {
210
24
                let rec1 = self.table.get(ti1).expect("dangling index");
211
24
                match referenced.entry(ti1) {
212
24
                    Some(slotmap_careful::secondary::Entry::Vacant(ve)) => ve.insert(()),
213
                    _ => panic!("colliding references or something {ti1:?}"),
214
                };
215
24
                for &ti2 in &indices[vi1 + 1..] {
216
8
                    let rec2 = &self.table[ti2];
217
8
                    assert!(
218
8
                        !(rec1.k2 == rec2.k2 && rec1.isolation.compatible(&*rec2.isolation)),
219
                        "Vec contains entries that should have been merged",
220
                    );
221
                }
222
            }
223
        }
224

            
225
24
        for ti in self.table.keys() {
226
24
            let () = referenced.get(ti).expect("unreferenced entry");
227
24
        }
228
12
    }
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)]
234
mod 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
}