aft/hashline/integration/
binding.rs1use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12use std::sync::{Arc, Condvar, Mutex, MutexGuard, Weak};
13
14use crate::hashline::apply::RegisterStore;
15use crate::hashline::snapshot::SnapshotStore;
16
17#[derive(Clone, Debug, Eq, Hash, PartialEq)]
19pub struct SessionKey {
20 pub root: PathBuf,
21 pub session_id: String,
22}
23
24impl SessionKey {
25 pub fn new(root: impl Into<PathBuf>, session_id: impl Into<String>) -> Self {
26 Self {
27 root: root.into(),
28 session_id: session_id.into(),
29 }
30 }
31}
32
33#[derive(Clone, Debug, Eq, PartialEq)]
36pub struct DowngradeWarning {
37 pub code: &'static str,
38 pub reason: &'static str,
39}
40
41impl DowngradeWarning {
42 pub const EDIT_NOT_REGISTERED: Self = Self {
43 code: "hashline_downgraded",
44 reason: "edit_not_registered",
45 };
46
47 pub fn to_json(&self) -> serde_json::Value {
49 serde_json::json!({
50 "code": self.code,
51 "reason": self.reason,
52 })
53 }
54}
55
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58pub struct RegistrationRequest {
59 pub configured_enabled: bool,
61 pub edit_slot_survives: bool,
65}
66
67impl RegistrationRequest {
68 pub const fn effective(self) -> bool {
69 self.configured_enabled && self.edit_slot_survives
70 }
71
72 pub const fn should_downgrade(self) -> bool {
73 self.configured_enabled && !self.edit_slot_survives
74 }
75}
76
77#[derive(Clone, Debug, Eq, PartialEq)]
79pub struct RegistrationOutcome {
80 pub configured_enabled: bool,
81 pub edit_slot_survives: bool,
82 pub effective: bool,
83 pub downgrade: Option<DowngradeWarning>,
85 pub stores_cleared: bool,
87 pub stores_preserved: bool,
89}
90
91#[derive(Debug)]
93pub struct HashlineBinding {
94 key: SessionKey,
95 configured_enabled: bool,
96 edit_slot_survives: bool,
97 effective: bool,
98 snapshots: SnapshotStore,
99 registers: RegisterStore,
100 in_flight: usize,
102}
103
104impl HashlineBinding {
105 fn new(key: SessionKey, request: RegistrationRequest) -> Self {
106 Self {
107 key,
108 configured_enabled: request.configured_enabled,
109 edit_slot_survives: request.edit_slot_survives,
110 effective: request.effective(),
111 snapshots: SnapshotStore::new(),
112 registers: RegisterStore::new(),
113 in_flight: 0,
114 }
115 }
116
117 pub fn key(&self) -> &SessionKey {
118 &self.key
119 }
120
121 pub fn configured_enabled(&self) -> bool {
122 self.configured_enabled
123 }
124
125 pub fn edit_slot_survives(&self) -> bool {
126 self.edit_slot_survives
127 }
128
129 pub fn effective(&self) -> bool {
130 self.effective
131 }
132
133 pub fn snapshots(&self) -> &SnapshotStore {
134 &self.snapshots
135 }
136
137 pub fn snapshots_mut(&mut self) -> &mut SnapshotStore {
138 &mut self.snapshots
139 }
140
141 pub fn registers(&self) -> &RegisterStore {
142 &self.registers
143 }
144
145 pub fn registers_mut(&mut self) -> &mut RegisterStore {
146 &mut self.registers
147 }
148
149 pub fn stores_mut(&mut self) -> (&mut SnapshotStore, &mut RegisterStore) {
151 (&mut self.snapshots, &mut self.registers)
152 }
153
154 pub fn in_flight(&self) -> usize {
155 self.in_flight
156 }
157
158 fn clear_stores(&mut self) {
159 self.snapshots.clear();
160 *self.registers_mut() = RegisterStore::new();
161 }
162}
163
164#[derive(Clone, Debug)]
166pub struct BindingHandle {
167 inner: Arc<Mutex<HashlineBinding>>,
168}
169
170impl BindingHandle {
171 pub fn with_binding<R>(&self, f: impl FnOnce(&HashlineBinding) -> R) -> R {
172 let guard = self.inner.lock().unwrap_or_else(|p| p.into_inner());
173 f(&guard)
174 }
175
176 pub fn with_binding_mut<R>(&self, f: impl FnOnce(&mut HashlineBinding) -> R) -> R {
177 let mut guard = self.inner.lock().unwrap_or_else(|p| p.into_inner());
178 f(&mut guard)
179 }
180
181 pub fn effective(&self) -> bool {
182 self.with_binding(|b| b.effective())
183 }
184
185 pub fn session_key(&self) -> SessionKey {
186 self.with_binding(|b| b.key().clone())
187 }
188}
189
190pub struct BindingGuard {
193 handle: BindingHandle,
194 registry: Weak<BindingRegistryInner>,
195}
196
197impl BindingGuard {
198 pub fn handle(&self) -> &BindingHandle {
199 &self.handle
200 }
201
202 pub fn effective(&self) -> bool {
203 self.handle.effective()
204 }
205
206 pub fn with_binding<R>(&self, f: impl FnOnce(&HashlineBinding) -> R) -> R {
207 self.handle.with_binding(f)
208 }
209
210 pub fn with_binding_mut<R>(&self, f: impl FnOnce(&mut HashlineBinding) -> R) -> R {
211 self.handle.with_binding_mut(f)
212 }
213}
214
215impl Drop for BindingGuard {
216 fn drop(&mut self) {
217 if let Some(registry) = self.registry.upgrade() {
218 registry.release_guard(&self.handle);
219 } else {
220 let mut binding = self.handle.inner.lock().unwrap_or_else(|p| p.into_inner());
223 binding.in_flight = binding.in_flight.saturating_sub(1);
224 }
225 }
226}
227
228struct BindingRegistryInner {
229 state: Mutex<RegistryState>,
230 drain: Condvar,
231}
232
233#[derive(Default)]
234struct RegistryState {
235 bindings: HashMap<SessionKey, Arc<Mutex<HashlineBinding>>>,
236}
237
238pub struct BindingRegistry {
240 inner: Arc<BindingRegistryInner>,
241}
242
243impl Default for BindingRegistry {
244 fn default() -> Self {
245 Self::new()
246 }
247}
248
249impl BindingRegistry {
250 pub fn new() -> Self {
251 Self {
252 inner: Arc::new(BindingRegistryInner {
253 state: Mutex::new(RegistryState::default()),
254 drain: Condvar::new(),
255 }),
256 }
257 }
258
259 fn lock(&self) -> MutexGuard<'_, RegistryState> {
260 self.inner
261 .state
262 .lock()
263 .unwrap_or_else(|poisoned| poisoned.into_inner())
264 }
265
266 pub fn register(
273 &self,
274 root: impl AsRef<Path>,
275 session_id: impl Into<String>,
276 request: RegistrationRequest,
277 ) -> RegistrationOutcome {
278 let key = SessionKey::new(root.as_ref().to_path_buf(), session_id.into());
279 self.register_key(key, request, || {})
280 }
281
282 fn register_key(
283 &self,
284 key: SessionKey,
285 request: RegistrationRequest,
286 after_existing_read: impl FnOnce(),
287 ) -> RegistrationOutcome {
288 let effective = request.effective();
289 let downgrade = request
290 .should_downgrade()
291 .then_some(DowngradeWarning::EDIT_NOT_REGISTERED);
292
293 let mut state = self.lock();
297 let existing = state.bindings.get(&key).cloned();
298 let previous_effective = existing.as_ref().map(|binding| {
299 binding
300 .lock()
301 .unwrap_or_else(|p| p.into_inner())
302 .effective()
303 });
304 after_existing_read();
305
306 let (stores_cleared, stores_preserved) = if let Some(existing) = existing {
307 if previous_effective != Some(effective) {
308 self.drain_in_flight(&existing);
309 {
310 let mut binding = existing.lock().unwrap_or_else(|p| p.into_inner());
311 binding.configured_enabled = request.configured_enabled;
312 binding.edit_slot_survives = request.edit_slot_survives;
313 binding.effective = effective;
314 binding.clear_stores();
315 }
316 state.bindings.insert(key, existing);
317 (true, false)
318 } else {
319 {
320 let mut binding = existing.lock().unwrap_or_else(|p| p.into_inner());
321 binding.configured_enabled = request.configured_enabled;
322 binding.edit_slot_survives = request.edit_slot_survives;
323 }
325 state.bindings.insert(key, existing);
326 (false, true)
327 }
328 } else {
329 let binding = Arc::new(Mutex::new(HashlineBinding::new(key.clone(), request)));
330 state.bindings.insert(key, binding);
331 (false, false)
332 };
333
334 RegistrationOutcome {
335 configured_enabled: request.configured_enabled,
336 edit_slot_survives: request.edit_slot_survives,
337 effective,
338 downgrade,
339 stores_cleared,
340 stores_preserved,
341 }
342 }
343
344 pub fn capture(
347 &self,
348 root: impl AsRef<Path>,
349 session_id: impl Into<String>,
350 ) -> Option<BindingGuard> {
351 let key = SessionKey::new(root.as_ref().to_path_buf(), session_id.into());
352 let handle = {
353 let state = self.lock();
354 let arc = state.bindings.get(&key)?.clone();
355 {
356 let mut binding = arc.lock().unwrap_or_else(|p| p.into_inner());
357 binding.in_flight = binding.in_flight.saturating_add(1);
358 }
359 BindingHandle { inner: arc }
360 };
361 Some(BindingGuard {
362 handle,
363 registry: Arc::downgrade(&self.inner),
364 })
365 }
366
367 pub fn peek(
369 &self,
370 root: impl AsRef<Path>,
371 session_id: impl Into<String>,
372 ) -> Option<BindingHandle> {
373 let key = SessionKey::new(root.as_ref().to_path_buf(), session_id.into());
374 let state = self.lock();
375 state
376 .bindings
377 .get(&key)
378 .map(|arc| BindingHandle { inner: arc.clone() })
379 }
380
381 pub fn teardown(&self, root: impl AsRef<Path>, session_id: impl Into<String>) -> bool {
383 let key = SessionKey::new(root.as_ref().to_path_buf(), session_id.into());
384 let existing = {
385 let state = self.lock();
386 state.bindings.get(&key).cloned()
387 };
388 if let Some(existing) = existing {
389 self.drain_in_flight(&existing);
390 }
391 let mut state = self.lock();
392 state.bindings.remove(&key).is_some()
393 }
394
395 pub fn len(&self) -> usize {
397 self.lock().bindings.len()
398 }
399
400 pub fn is_empty(&self) -> bool {
401 self.len() == 0
402 }
403
404 fn drain_in_flight(&self, binding: &Arc<Mutex<HashlineBinding>>) {
405 let mut guard = binding.lock().unwrap_or_else(|p| p.into_inner());
406 while guard.in_flight > 0 {
407 guard = self
408 .inner
409 .drain
410 .wait(guard)
411 .unwrap_or_else(|p| p.into_inner());
412 }
413 }
414}
415
416impl BindingRegistryInner {
417 fn release_guard(&self, handle: &BindingHandle) {
418 {
419 let mut binding = handle.inner.lock().unwrap_or_else(|p| p.into_inner());
420 binding.in_flight = binding.in_flight.saturating_sub(1);
421 }
422 self.drain.notify_all();
423 }
424}
425
426pub fn effective_for_capture(guard: Option<&BindingGuard>) -> bool {
428 guard.map(|g| g.effective()).unwrap_or(false)
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434 use crate::hashline::scan::scan_bytes;
435 use std::sync::mpsc::{self, RecvTimeoutError};
436 use std::thread;
437 use std::time::Duration;
438
439 #[test]
440 fn concurrent_same_session_registration_serializes_read_compare_write() {
441 let registry = Arc::new(BindingRegistry::new());
442 let key = SessionKey::new("/tmp/hashline-register-race", "shared-session");
443 registry.register(
444 &key.root,
445 key.session_id.clone(),
446 RegistrationRequest {
447 configured_enabled: true,
448 edit_slot_survives: true,
449 },
450 );
451 registry
452 .peek(&key.root, key.session_id.clone())
453 .expect("initial binding")
454 .with_binding_mut(|binding| {
455 binding
456 .snapshots_mut()
457 .publish("race.rs", scan_bytes(b"before race\n"));
458 });
459
460 let (first_read_tx, first_read_rx) = mpsc::channel();
461 let (release_first_tx, release_first_rx) = mpsc::channel();
462 let first_registry = Arc::clone(®istry);
463 let first_key = key.clone();
464 let first = thread::spawn(move || {
465 first_registry.register_key(
466 first_key,
467 RegistrationRequest {
468 configured_enabled: false,
469 edit_slot_survives: true,
470 },
471 || {
472 first_read_tx.send(()).expect("signal first read");
473 release_first_rx.recv().expect("release first registration");
474 },
475 )
476 });
477
478 first_read_rx
479 .recv()
480 .expect("first registration read existing binding");
481 let (second_started_tx, second_started_rx) = mpsc::channel();
482 let (second_read_tx, second_read_rx) = mpsc::channel();
483 let (second_done_tx, second_done_rx) = mpsc::channel();
484 let second_registry = Arc::clone(®istry);
485 let second_key = key.clone();
486 let second = thread::spawn(move || {
487 second_started_tx.send(()).expect("signal second start");
488 let outcome = second_registry.register_key(
489 second_key,
490 RegistrationRequest {
491 configured_enabled: true,
492 edit_slot_survives: false,
493 },
494 || second_read_tx.send(()).expect("signal second read"),
495 );
496 second_done_tx.send(outcome).expect("send second outcome");
497 });
498
499 second_started_rx
500 .recv()
501 .expect("second registration started");
502 assert!(matches!(
503 second_read_rx.recv_timeout(Duration::from_secs(1)),
504 Err(RecvTimeoutError::Timeout)
505 ));
506 release_first_tx
507 .send(())
508 .expect("release first registration");
509
510 let first_outcome = first.join().expect("first registration");
511 let second_outcome = second_done_rx
512 .recv_timeout(Duration::from_secs(2))
513 .expect("second registration completes after first");
514 second.join().expect("second registration");
515
516 assert!(first_outcome.stores_cleared);
517 assert!(second_outcome.stores_preserved);
518 let final_binding = registry
519 .peek(&key.root, key.session_id)
520 .expect("final binding");
521 final_binding.with_binding(|binding| {
522 assert!(binding.configured_enabled());
523 assert!(!binding.edit_slot_survives());
524 assert!(!binding.effective());
525 assert!(binding.snapshots().is_empty());
526 });
527 }
528}