Skip to main content

dns_resolver/
cache.rs

1use priority_queue::PriorityQueue;
2use std::cmp::Eq;
3use std::cmp::Reverse;
4use std::collections::HashMap;
5use std::hash::Hash;
6use std::marker::Copy;
7use std::sync::{Arc, Mutex};
8use std::time::{Duration, Instant};
9
10use dns_types::protocol::types::*;
11
12/// A convenience wrapper around a `Cache` which lets it be shared
13/// between threads.
14///
15/// Invoking `clone` on a `SharedCache` gives a new instance which
16/// refers to the same underlying `Cache` object.
17#[derive(Debug, Clone)]
18pub struct SharedCache {
19    cache: Arc<Mutex<Cache>>,
20}
21
22const MUTEX_POISON_MESSAGE: &str =
23    "[INTERNAL ERROR] cache mutex poisoned, cannot recover from this - aborting";
24
25impl SharedCache {
26    /// Make a new, empty, shared cache.
27    pub fn new() -> Self {
28        SharedCache {
29            cache: Arc::new(Mutex::new(Cache::new())),
30        }
31    }
32
33    /// Create a new cache with the given desired size.
34    pub fn with_desired_size(desired_size: usize) -> Self {
35        SharedCache {
36            cache: Arc::new(Mutex::new(Cache::with_desired_size(desired_size))),
37        }
38    }
39
40    /// Get an entry from the cache.
41    ///
42    /// The TTL in the returned `ResourceRecord` is relative to the
43    /// current time - not when the record was inserted into the
44    /// cache.
45    ///
46    /// # Panics
47    ///
48    /// If the mutex has been poisoned.
49    pub fn get(&self, name: &DomainName, qtype: QueryType) -> Vec<ResourceRecord> {
50        self.cache
51            .lock()
52            .expect(MUTEX_POISON_MESSAGE)
53            .get(name, qtype)
54    }
55
56    /// Like `get`, but may return expired entries.
57    ///
58    /// Consumers MUST check that the TTL of a record is nonzero
59    /// before using it!
60    ///
61    /// # Panics
62    ///
63    /// If the mutex has been poisoned.
64    pub fn get_without_checking_expiration(
65        &self,
66        name: &DomainName,
67        qtype: QueryType,
68    ) -> Vec<ResourceRecord> {
69        self.cache
70            .lock()
71            .expect(MUTEX_POISON_MESSAGE)
72            .get_without_checking_expiration(name, qtype)
73    }
74
75    /// Insert an entry into the cache.
76    ///
77    /// It is not inserted if its TTL is zero or negative.
78    ///
79    /// This may make the cache grow beyond the desired size.
80    ///
81    /// # Panics
82    ///
83    /// If the mutex has been poisoned.
84    pub fn insert(&self, record: ResourceRecord) {
85        if record.ttl > 0 {
86            let mut cache = self.cache.lock().expect(MUTEX_POISON_MESSAGE);
87            cache.insert(record);
88        }
89    }
90
91    /// Insert multiple entries into the cache.
92    ///
93    /// This is more efficient than calling `insert` multiple times, as it locks
94    /// the cache just once.
95    ///
96    /// Records with a TTL of zero or negative are skipped.
97    ///
98    /// This may make the cache grow beyond the desired size.
99    ///
100    /// # Panics
101    ///
102    /// If the mutex has been poisoned.
103    pub fn insert_all(&self, records: Vec<ResourceRecord>) {
104        let mut cache = self.cache.lock().expect(MUTEX_POISON_MESSAGE);
105        for record in records {
106            if record.ttl > 0 {
107                cache.insert(record);
108            }
109        }
110    }
111
112    /// Atomically clears expired entries and, if the cache has grown
113    /// beyond its desired size, prunes entries to get down to size.
114    ///
115    /// Returns `(has overflowed?, current size, num expired, num pruned)`.
116    ///
117    /// # Panics
118    ///
119    /// If the mutex has been poisoned.
120    pub fn prune(&self) -> (bool, usize, usize, usize) {
121        self.cache.lock().expect(MUTEX_POISON_MESSAGE).prune()
122    }
123}
124
125impl Default for SharedCache {
126    fn default() -> Self {
127        Self::new()
128    }
129}
130
131/// Caching for `ResourceRecord`s.
132///
133/// You probably want to use `SharedCache` instead.
134#[derive(Debug, Clone)]
135pub struct Cache {
136    inner: PartitionedCache<DomainName, RecordType, RecordTypeWithData>,
137}
138
139impl Default for Cache {
140    fn default() -> Self {
141        Self::new()
142    }
143}
144
145impl Cache {
146    /// Create a new cache with a default desired size.
147    pub fn new() -> Self {
148        Self {
149            inner: PartitionedCache::new(),
150        }
151    }
152
153    /// Create a new cache with the given desired size.
154    ///
155    /// The `prune` method will remove expired entries, and also enough entries
156    /// (in least-recently-used order) to get down to this size.
157    pub fn with_desired_size(desired_size: usize) -> Self {
158        Self {
159            inner: PartitionedCache::with_desired_size(desired_size),
160        }
161    }
162
163    /// Get RRs from the cache.
164    ///
165    /// The TTL in the returned `ResourceRecord` is relative to the
166    /// current time - not when the record was inserted into the
167    /// cache.
168    pub fn get(&mut self, name: &DomainName, qtype: QueryType) -> Vec<ResourceRecord> {
169        let mut rrs = self.get_without_checking_expiration(name, qtype);
170        rrs.retain(|rr| rr.ttl > 0);
171        rrs
172    }
173
174    /// Like `get`, but may return expired RRs.
175    ///
176    /// Consumers MUST check that the TTL of a record is nonzero before using
177    /// it!
178    pub fn get_without_checking_expiration(
179        &mut self,
180        name: &DomainName,
181        qtype: QueryType,
182    ) -> Vec<ResourceRecord> {
183        let now = Instant::now();
184        let mut rrs = Vec::new();
185        match qtype {
186            QueryType::Wildcard => {
187                if let Some(records) = self.inner.get_partition_without_checking_expiration(name) {
188                    for tuples in records.values() {
189                        to_rrs(name, now, tuples, &mut rrs);
190                    }
191                }
192            }
193            QueryType::Record(rtype) => {
194                if let Some(tuples) = self.inner.get_without_checking_expiration(name, &rtype) {
195                    to_rrs(name, now, tuples, &mut rrs);
196                }
197            }
198            _ => (),
199        }
200
201        rrs
202    }
203
204    /// Insert an RR into the cache.
205    pub fn insert(&mut self, record: ResourceRecord) {
206        self.inner.upsert(
207            record.name,
208            record.rtype_with_data.rtype(),
209            record.rtype_with_data,
210            Duration::from_secs(record.ttl.into()),
211        );
212    }
213
214    /// Clear expired RRs and, if the cache has grown beyond its desired size,
215    /// prunes domains to get down to size.
216    ///
217    /// Returns `(has overflowed?, current size, num expired, num pruned)`.
218    pub fn prune(&mut self) -> (bool, usize, usize, usize) {
219        self.inner.prune()
220    }
221}
222
223/// Helper for `get_without_checking_expiration`: converts the cached
224/// record tuples into RRs.
225fn to_rrs(
226    name: &DomainName,
227    now: Instant,
228    tuples: &[(RecordTypeWithData, Instant)],
229    rrs: &mut Vec<ResourceRecord>,
230) {
231    for (rtype, expires) in tuples {
232        rrs.push(ResourceRecord {
233            name: name.clone(),
234            rtype_with_data: rtype.clone(),
235            rclass: RecordClass::IN,
236            ttl: expires
237                .saturating_duration_since(now)
238                .as_secs()
239                .try_into()
240                .unwrap_or(u32::MAX),
241        });
242    }
243}
244
245#[derive(Debug, Clone)]
246pub struct PartitionedCache<K1: Eq + Hash, K2: Eq + Hash, V> {
247    /// Cached entries, indexed by partition key.
248    partitions: HashMap<K1, Partition<K2, V>>,
249
250    /// Priority queue of partition keys ordered by access times.
251    ///
252    /// When the cache is full and there are no expired records to prune,
253    /// partitions will instead be pruned in LRU order.
254    ///
255    /// INVARIANT: the keys in here are exactly the keys in `partitions`.
256    access_priority: PriorityQueue<K1, Reverse<Instant>>,
257
258    /// Priority queue of partition keys ordered by expiry time.
259    ///
260    /// When the cache is pruned, expired records are removed first.
261    ///
262    /// INVARIANT: the keys in here are exactly the keys in `partitions`.
263    expiry_priority: PriorityQueue<K1, Reverse<Instant>>,
264
265    /// The number of records in the cache, across all partitions.
266    ///
267    /// INVARIANT: this is the sum of the `size` fields of the `partitions`.
268    current_size: usize,
269
270    /// The desired maximum number of records in the cache.
271    desired_size: usize,
272}
273
274/// The cached records for a domain.
275#[derive(Debug, Clone, Eq, PartialEq)]
276struct Partition<K: Eq + Hash, V> {
277    /// The time this partition was last read at.
278    last_read: Instant,
279
280    /// When the next record expires.
281    ///
282    /// INVARIANT: this is the minimum of the expiry times of the `records`.
283    next_expiry: Instant,
284
285    /// How many records there are.
286    ///
287    /// INVARIANT: this is the sum of the vector lengths in `records`.
288    size: usize,
289
290    /// The records, further divided by record key.
291    records: HashMap<K, Vec<(V, Instant)>>,
292}
293
294impl<K1: Clone + Eq + Hash, K2: Copy + Eq + Hash, V: PartialEq> Default
295    for PartitionedCache<K1, K2, V>
296{
297    fn default() -> Self {
298        Self::new()
299    }
300}
301
302impl<K1: Clone + Eq + Hash, K2: Copy + Eq + Hash, V: PartialEq> PartitionedCache<K1, K2, V> {
303    /// Create a new cache with a default desired size.
304    pub fn new() -> Self {
305        Self::with_desired_size(512)
306    }
307
308    /// Create a new cache with the given desired size.
309    ///
310    /// The `prune` method will remove expired records, and also enough records
311    /// (in least-recently-used order) to get down to this size.
312    pub fn with_desired_size(desired_size: usize) -> Self {
313        Self {
314            // `desired_size / 2` is a compromise: most partitions will have
315            // more than one record, so `desired_size` would be too big for the
316            // `partitions`.
317            partitions: HashMap::with_capacity(desired_size / 2),
318            access_priority: PriorityQueue::with_capacity(desired_size),
319            expiry_priority: PriorityQueue::with_capacity(desired_size),
320            current_size: 0,
321            desired_size,
322        }
323    }
324
325    /// Get all records for the given partition key from the cache, along with
326    /// their expiration times.
327    ///
328    /// These records may have expired if `prune` has not been called recently.
329    pub fn get_partition_without_checking_expiration(
330        &mut self,
331        partition_key: &K1,
332    ) -> Option<&HashMap<K2, Vec<(V, Instant)>>> {
333        if let Some(partition) = self.partitions.get_mut(partition_key) {
334            partition.last_read = Instant::now();
335            self.access_priority
336                .change_priority(partition_key, Reverse(partition.last_read));
337            return Some(&partition.records);
338        }
339
340        None
341    }
342
343    /// Get all records for the given partition and record key from the cache,
344    /// along with their expiration times.
345    ///
346    /// These records may have expired if `prune` has not been called recently.
347    pub fn get_without_checking_expiration(
348        &mut self,
349        partition_key: &K1,
350        record_key: &K2,
351    ) -> Option<&[(V, Instant)]> {
352        if let Some(partition) = self.partitions.get_mut(partition_key) {
353            if let Some(tuples) = partition.records.get(record_key) {
354                partition.last_read = Instant::now();
355                self.access_priority
356                    .change_priority(partition_key, Reverse(partition.last_read));
357                return Some(tuples);
358            }
359        }
360
361        None
362    }
363
364    /// Insert a record into the cache, or reset the expiry time if already
365    /// present.
366    pub fn upsert(&mut self, partition_key: K1, record_key: K2, value: V, ttl: Duration) {
367        let now = Instant::now();
368        let expiry = now + ttl;
369        let tuple = (value, expiry);
370        if let Some(partition) = self.partitions.get_mut(&partition_key) {
371            let mut recalculate_next_expiry = false;
372
373            if let Some(tuples) = partition.records.get_mut(&record_key) {
374                for i in 0..tuples.len() {
375                    let t = &tuples[i];
376                    if t.0 == tuple.0 {
377                        partition.size -= 1;
378                        self.current_size -= 1;
379                        recalculate_next_expiry = t.1 == partition.next_expiry;
380                        tuples.swap_remove(i);
381                        break;
382                    }
383                }
384
385                tuples.push(tuple);
386            } else {
387                partition.records.insert(record_key, vec![tuple]);
388            }
389            partition.last_read = now;
390            partition.size += 1;
391            self.access_priority
392                .change_priority(&partition_key, Reverse(partition.last_read));
393            if expiry < partition.next_expiry {
394                partition.next_expiry = expiry;
395                self.expiry_priority
396                    .change_priority(&partition_key, Reverse(partition.next_expiry));
397            } else if recalculate_next_expiry {
398                // the next record to expire was popped so we need to examine
399                // all current records to determine the new next expiry
400                partition.next_expiry = expiry;
401                for tuples in partition.records.values() {
402                    for (_, expires) in tuples {
403                        if *expires < partition.next_expiry {
404                            partition.next_expiry = *expires;
405                        }
406                    }
407                }
408                self.expiry_priority
409                    .change_priority(&partition_key, Reverse(partition.next_expiry));
410            }
411        } else {
412            let mut records = HashMap::new();
413            records.insert(record_key, vec![tuple]);
414            let partition = Partition {
415                last_read: now,
416                next_expiry: expiry,
417                size: 1,
418                records,
419            };
420            self.access_priority
421                .push(partition_key.clone(), Reverse(partition.last_read));
422            self.expiry_priority
423                .push(partition_key.clone(), Reverse(partition.next_expiry));
424            self.partitions.insert(partition_key, partition);
425        }
426
427        self.current_size += 1;
428    }
429
430    /// Delete all expired records.
431    ///
432    /// Returns the number of records deleted.
433    pub fn remove_expired(&mut self) -> usize {
434        let mut pruned = 0;
435
436        loop {
437            let before = pruned;
438            pruned += self.remove_expired_step();
439            if before == pruned {
440                break;
441            }
442        }
443
444        pruned
445    }
446
447    /// Delete all expired records, and then enough
448    /// least-recently-used records to reduce the cache to the desired
449    /// size.
450    ///
451    /// Returns `(has overflowed?, current size, num expired, num pruned)`.
452    pub fn prune(&mut self) -> (bool, usize, usize, usize) {
453        let has_overflowed = self.current_size > self.desired_size;
454        let num_expired = self.remove_expired();
455        let mut num_pruned = 0;
456
457        while self.current_size > self.desired_size {
458            num_pruned += self.remove_least_recently_used();
459        }
460
461        (has_overflowed, self.current_size, num_expired, num_pruned)
462    }
463
464    /// Helper for `remove_expired`: looks at the next-to-expire
465    /// domain and cleans up expired records from it.  This may delete
466    /// more than one record, and may even delete the whole domain.
467    ///
468    /// Returns the number of records removed.
469    fn remove_expired_step(&mut self) -> usize {
470        if let Some((partition_key, Reverse(expiry))) = self.expiry_priority.pop() {
471            let now = Instant::now();
472
473            if expiry > now {
474                self.expiry_priority.push(partition_key, Reverse(expiry));
475                return 0;
476            }
477
478            if let Some(partition) = self.partitions.get_mut(&partition_key) {
479                let mut pruned = 0;
480
481                let record_keys = partition.records.keys().copied().collect::<Vec<K2>>();
482                let mut next_expiry = None;
483                for rkey in record_keys {
484                    if let Some(tuples) = partition.records.get_mut(&rkey) {
485                        let len = tuples.len();
486                        tuples.retain(|(_, expiry)| expiry > &now);
487                        pruned += len - tuples.len();
488                        for (_, expiry) in tuples {
489                            match next_expiry {
490                                None => next_expiry = Some(*expiry),
491                                Some(t) if *expiry < t => next_expiry = Some(*expiry),
492                                _ => (),
493                            }
494                        }
495                    }
496                }
497
498                partition.size -= pruned;
499
500                if let Some(ne) = next_expiry {
501                    partition.next_expiry = ne;
502                    self.expiry_priority.push(partition_key, Reverse(ne));
503                } else {
504                    self.partitions.remove(&partition_key);
505                    self.access_priority.remove(&partition_key);
506                }
507
508                self.current_size -= pruned;
509                pruned
510            } else {
511                self.access_priority.remove(&partition_key);
512                0
513            }
514        } else {
515            0
516        }
517    }
518
519    /// Helper for `prune`: deletes all records associated with the
520    /// least recently used domain.
521    ///
522    /// Returns the number of records removed.
523    fn remove_least_recently_used(&mut self) -> usize {
524        if let Some((partition_key, _)) = self.access_priority.pop() {
525            self.expiry_priority.remove(&partition_key);
526
527            if let Some(partition) = self.partitions.remove(&partition_key) {
528                let pruned = partition.size;
529                self.current_size -= pruned;
530                pruned
531            } else {
532                0
533            }
534        } else {
535            0
536        }
537    }
538}
539
540#[allow(clippy::missing_panics_doc)]
541pub mod test_util {
542    use super::*;
543
544    /// Assert that the cache response has exactly one element and
545    /// that it matches the original (all fields equal except TTL,
546    /// where the original is >=).
547    pub fn assert_cache_response(original: &ResourceRecord, response: &[ResourceRecord]) {
548        assert_eq!(1, response.len());
549        let cached = response[0].clone();
550
551        assert_eq!(original.name, cached.name);
552        assert_eq!(original.rtype_with_data, cached.rtype_with_data);
553        assert_eq!(RecordClass::IN, cached.rclass);
554        assert!(original.ttl >= cached.ttl);
555    }
556
557    /// Assert that the cache has a given number of records.
558    pub fn assert_current_size(expected: usize, cache: &Cache) {
559        if expected != cache.inner.current_size {
560            dbg!(&cache.inner.partitions);
561            assert_eq!(expected, cache.inner.current_size);
562        }
563    }
564
565    /// Expire stale records and assert the count expired.
566    pub fn assert_expires(expected: usize, cache: &mut Cache) {
567        assert_eq!(expected, cache.inner.remove_expired());
568    }
569
570    /// Assert that the cache invariants are met:
571    ///
572    /// - `current_size` is the number of records
573    /// - `next_expiry` of each partition is the min of its expiry times
574    /// - `access_priority` has all the partitions in the correct order
575    /// - `expiry_priority` has all the partitions in the correct order
576    pub fn assert_invariants(cache: &Cache) {
577        assert_eq!(
578            cache.inner.current_size,
579            cache
580                .inner
581                .partitions
582                .values()
583                .map(|e| e.size)
584                .sum::<usize>()
585        );
586
587        assert_eq!(
588            cache.inner.partitions.len(),
589            cache.inner.access_priority.len()
590        );
591        assert_eq!(
592            cache.inner.partitions.len(),
593            cache.inner.expiry_priority.len()
594        );
595
596        let mut access_priority = PriorityQueue::new();
597        let mut expiry_priority = PriorityQueue::new();
598
599        for (name, partition) in &cache.inner.partitions {
600            assert_eq!(
601                partition.size,
602                partition.records.values().map(Vec::len).sum::<usize>()
603            );
604
605            let mut min_expires = None;
606            for (rtype, tuples) in &partition.records {
607                for (rtype_with_data, expires) in tuples {
608                    assert_eq!(*rtype, rtype_with_data.rtype());
609
610                    if let Some(e) = min_expires {
611                        if *expires < e {
612                            min_expires = Some(*expires);
613                        }
614                    } else {
615                        min_expires = Some(*expires);
616                    }
617                }
618            }
619
620            assert_eq!(Some(partition.next_expiry), min_expires);
621
622            access_priority.push(name.clone(), Reverse(partition.last_read));
623            expiry_priority.push(name.clone(), Reverse(partition.next_expiry));
624        }
625
626        assert_eq!(cache.inner.access_priority, access_priority);
627        assert_eq!(cache.inner.expiry_priority, expiry_priority);
628    }
629}