ifc_lite_server/in_flight.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! In-memory marker for "a data-model write is running right now for this
6//! cache key" (issue #5129).
7//!
8//! The key is the data-model ENTRY key (`data_model_cache_key`), not the
9//! request key it is built from (#6034): the full and the rooted-only tables
10//! are separate entries, and a fill of one says nothing about the other.
11//!
12//! `get_data_model` needs to tell a client "not yet, keep polling" apart from
13//! "not for this key, stop asking": the first is 202, the second 404. A cache
14//! miss alone cannot make that distinction — the entry could be absent because
15//! nothing ever asked for it, which is exactly the case the issue's 404 is
16//! for. This map is the missing signal: a key is present in it for the
17//! lifetime of every background task that could still write it
18//! (`parse_parquet_stream`'s data-model fill), and gone only once every such
19//! task for that key has ended, however each one ends.
20//!
21//! Refcounted, not a plain set (PR #5134 review): two concurrent uploads of
22//! the same content each spawn their own fill and each call `begin` with the
23//! same cache key. A plain set's `remove` on either `Drop` would clear the
24//! key while the other fill is still running, and a `GET` racing that window
25//! would see 404 for a fill that is, in fact, still in progress.
26
27use rustc_hash::FxHashMap;
28use std::sync::{Arc, Mutex};
29
30/// The refcounted set of cache keys with a data-model write in flight. One
31/// instance lives on `AppState`, shared by every request. The count is how
32/// many live guards currently name that key, not a byte size or anything
33/// meaningful beyond "still positive".
34#[derive(Default)]
35pub struct InFlightKeys(Mutex<FxHashMap<String, usize>>);
36
37impl InFlightKeys {
38 /// Mark `key` as in flight and return a guard that un-marks it on drop.
39 ///
40 /// Takes `self` as `Arc` so the returned guard can outlive the borrow —
41 /// it is moved into the spawned task, which outlives the request handler
42 /// that calls `begin`.
43 pub fn begin(self: &Arc<Self>, key: String) -> InFlightGuard {
44 if let Ok(mut keys) = self.0.lock() {
45 *keys.entry(key.clone()).or_insert(0) += 1;
46 }
47 InFlightGuard {
48 keys: self.clone(),
49 key,
50 }
51 }
52
53 /// Whether a write for `key` is in flight right now.
54 ///
55 /// A lock-poisoned map (a prior holder panicked mid-mutation) answers
56 /// `false`: the safe direction is a 404 telling the client to re-upload,
57 /// not a 202 that polls a key nothing can ever finish writing.
58 pub fn contains(&self, key: &str) -> bool {
59 self.0
60 .lock()
61 .map(|keys| keys.contains_key(key))
62 .unwrap_or(false)
63 }
64}
65
66/// RAII marker returned by [`InFlightKeys::begin`]. Decrements its key's
67/// count on drop -- on task completion, on early return (e.g. admission
68/// saturated), and on panic unwind alike -- removing the key only once the
69/// count reaches zero, so one of two concurrent fills for the same key ending
70/// can never un-mark the other's still-running fill.
71pub struct InFlightGuard {
72 keys: Arc<InFlightKeys>,
73 key: String,
74}
75
76impl Drop for InFlightGuard {
77 fn drop(&mut self) {
78 if let Ok(mut keys) = self.keys.0.lock() {
79 if let Some(count) = keys.get_mut(&self.key) {
80 *count -= 1;
81 if *count == 0 {
82 keys.remove(&self.key);
83 }
84 }
85 }
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 /// `contains` is true only between `begin` and the guard's drop.
94 #[test]
95 fn contains_true_only_while_guard_is_alive() {
96 let keys = Arc::new(InFlightKeys::default());
97 assert!(!keys.contains("k"));
98 let guard = keys.begin("k".to_string());
99 assert!(keys.contains("k"));
100 drop(guard);
101 assert!(!keys.contains("k"));
102 }
103
104 /// Two different keys don't interfere with each other.
105 #[test]
106 fn keys_are_independent() {
107 let keys = Arc::new(InFlightKeys::default());
108 let guard_a = keys.begin("a".to_string());
109 assert!(keys.contains("a"));
110 assert!(!keys.contains("b"));
111 let guard_b = keys.begin("b".to_string());
112 assert!(keys.contains("b"));
113 drop(guard_a);
114 assert!(!keys.contains("a"));
115 assert!(keys.contains("b"));
116 drop(guard_b);
117 assert!(!keys.contains("b"));
118 }
119
120 /// Two concurrent fills for the SAME key (issue #5134 review: two
121 /// uploads of identical content each spawning their own data-model
122 /// task): dropping one guard must not un-mark the other's still-running
123 /// fill. Only the last drop clears the key.
124 #[test]
125 fn two_guards_for_the_same_key_require_both_to_drop() {
126 let keys = Arc::new(InFlightKeys::default());
127 let first = keys.begin("k".to_string());
128 let second = keys.begin("k".to_string());
129 assert!(keys.contains("k"));
130
131 drop(first);
132 assert!(
133 keys.contains("k"),
134 "the second fill is still running; the key must still read as in-flight"
135 );
136
137 drop(second);
138 assert!(!keys.contains("k"), "the last guard's drop must clear the key");
139 }
140}