1use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12use std::sync::{Arc, Condvar, Mutex, MutexGuard};
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 const TAGGED_READ_UNAVAILABLE: Self = Self {
50 code: "hashline_downgraded",
51 reason: "tagged_read_unavailable",
52 };
53
54 pub fn to_json(&self) -> serde_json::Value {
56 serde_json::json!({
57 "code": self.code,
58 "reason": self.reason,
59 })
60 }
61}
62
63#[derive(Clone, Copy, Debug, Eq, PartialEq)]
65pub struct RegistrationRequest {
66 pub configured_enabled: bool,
68 pub edit_slot_survives: bool,
72 pub read_slot_survives: bool,
76}
77
78impl RegistrationRequest {
79 pub const fn effective(self) -> bool {
80 self.configured_enabled && self.edit_slot_survives && self.read_slot_survives
81 }
82
83 pub const fn should_downgrade(self) -> bool {
84 self.configured_enabled && !(self.edit_slot_survives && self.read_slot_survives)
85 }
86
87 pub const fn downgrade_warning(self) -> Option<DowngradeWarning> {
94 if !self.should_downgrade() {
95 return None;
96 }
97 if self.read_slot_survives {
98 Some(DowngradeWarning::EDIT_NOT_REGISTERED)
99 } else {
100 Some(DowngradeWarning::TAGGED_READ_UNAVAILABLE)
101 }
102 }
103}
104
105#[derive(Clone, Debug, Eq, PartialEq)]
107pub struct RegistrationOutcome {
108 pub configured_enabled: bool,
109 pub edit_slot_survives: bool,
110 pub read_slot_survives: bool,
111 pub effective: bool,
112 pub downgrade: Option<DowngradeWarning>,
114 pub stores_cleared: bool,
116 pub stores_preserved: bool,
118}
119
120#[derive(Debug)]
122pub struct HashlineBinding {
123 key: SessionKey,
124 configured_enabled: bool,
125 edit_slot_survives: bool,
126 read_slot_survives: bool,
127 effective: bool,
128 snapshots: SnapshotStore,
129 registers: RegisterStore,
130 in_flight: usize,
132}
133
134impl HashlineBinding {
135 fn new(key: SessionKey, request: RegistrationRequest) -> Self {
136 Self {
137 key,
138 configured_enabled: request.configured_enabled,
139 edit_slot_survives: request.edit_slot_survives,
140 read_slot_survives: request.read_slot_survives,
141 effective: request.effective(),
142 snapshots: SnapshotStore::new(),
143 registers: RegisterStore::new(),
144 in_flight: 0,
145 }
146 }
147
148 pub fn key(&self) -> &SessionKey {
149 &self.key
150 }
151
152 pub fn configured_enabled(&self) -> bool {
153 self.configured_enabled
154 }
155
156 pub fn edit_slot_survives(&self) -> bool {
157 self.edit_slot_survives
158 }
159
160 pub fn read_slot_survives(&self) -> bool {
161 self.read_slot_survives
162 }
163
164 pub fn effective(&self) -> bool {
165 self.effective
166 }
167
168 pub fn snapshots(&self) -> &SnapshotStore {
169 &self.snapshots
170 }
171
172 pub fn snapshots_mut(&mut self) -> &mut SnapshotStore {
173 &mut self.snapshots
174 }
175
176 pub fn registers(&self) -> &RegisterStore {
177 &self.registers
178 }
179
180 pub fn registers_mut(&mut self) -> &mut RegisterStore {
181 &mut self.registers
182 }
183
184 pub fn stores_mut(&mut self) -> (&mut SnapshotStore, &mut RegisterStore) {
186 (&mut self.snapshots, &mut self.registers)
187 }
188
189 pub fn in_flight(&self) -> usize {
190 self.in_flight
191 }
192
193 fn clear_stores(&mut self) {
194 self.snapshots.clear();
195 *self.registers_mut() = RegisterStore::new();
196 }
197}
198
199#[derive(Debug)]
203struct BindingSlot {
204 binding: Mutex<HashlineBinding>,
205 drain: Condvar,
206}
207
208impl BindingSlot {
209 fn new(binding: HashlineBinding) -> Self {
210 Self {
211 binding: Mutex::new(binding),
212 drain: Condvar::new(),
213 }
214 }
215
216 fn lock(&self) -> MutexGuard<'_, HashlineBinding> {
217 self.binding
218 .lock()
219 .unwrap_or_else(std::sync::PoisonError::into_inner)
220 }
221
222 fn drain_in_flight(&self) {
223 let mut binding = self.lock();
224 while binding.in_flight > 0 {
225 binding = self
226 .drain
227 .wait(binding)
228 .unwrap_or_else(std::sync::PoisonError::into_inner);
229 }
230 }
231
232 fn release_guard(&self) {
233 {
234 let mut binding = self.lock();
235 binding.in_flight = binding.in_flight.saturating_sub(1);
236 }
237 self.drain.notify_all();
238 }
239}
240
241#[derive(Clone, Debug)]
243pub struct BindingHandle {
244 inner: Arc<BindingSlot>,
245}
246
247impl BindingHandle {
248 pub fn with_binding<R>(&self, f: impl FnOnce(&HashlineBinding) -> R) -> R {
249 let guard = self.inner.lock();
250 f(&guard)
251 }
252
253 pub fn with_binding_mut<R>(&self, f: impl FnOnce(&mut HashlineBinding) -> R) -> R {
254 let mut guard = self.inner.lock();
255 f(&mut guard)
256 }
257
258 pub fn effective(&self) -> bool {
259 self.with_binding(|b| b.effective())
260 }
261
262 pub fn session_key(&self) -> SessionKey {
263 self.with_binding(|b| b.key().clone())
264 }
265}
266
267pub struct BindingGuard {
270 handle: BindingHandle,
271}
272
273impl BindingGuard {
274 pub fn handle(&self) -> &BindingHandle {
275 &self.handle
276 }
277
278 pub fn effective(&self) -> bool {
279 self.handle.effective()
280 }
281
282 pub fn with_binding<R>(&self, f: impl FnOnce(&HashlineBinding) -> R) -> R {
283 self.handle.with_binding(f)
284 }
285
286 pub fn with_binding_mut<R>(&self, f: impl FnOnce(&mut HashlineBinding) -> R) -> R {
287 self.handle.with_binding_mut(f)
288 }
289}
290
291impl Drop for BindingGuard {
292 fn drop(&mut self) {
293 self.handle.inner.release_guard();
294 }
295}
296
297struct BindingRegistryInner {
298 state: Mutex<RegistryState>,
299}
300
301#[derive(Default)]
302struct RegistryState {
303 bindings: HashMap<SessionKey, Arc<BindingSlot>>,
304}
305
306pub struct BindingRegistry {
308 inner: Arc<BindingRegistryInner>,
309}
310
311impl Default for BindingRegistry {
312 fn default() -> Self {
313 Self::new()
314 }
315}
316
317impl BindingRegistry {
318 pub fn new() -> Self {
319 Self {
320 inner: Arc::new(BindingRegistryInner {
321 state: Mutex::new(RegistryState::default()),
322 }),
323 }
324 }
325
326 fn lock(&self) -> MutexGuard<'_, RegistryState> {
327 self.inner
328 .state
329 .lock()
330 .unwrap_or_else(|poisoned| poisoned.into_inner())
331 }
332
333 pub fn register(
340 &self,
341 root: impl AsRef<Path>,
342 session_id: impl Into<String>,
343 request: RegistrationRequest,
344 ) -> RegistrationOutcome {
345 let key = SessionKey::new(root.as_ref().to_path_buf(), session_id.into());
346 self.register_key(key, request, || {})
347 }
348
349 fn register_key(
350 &self,
351 key: SessionKey,
352 request: RegistrationRequest,
353 after_existing_read: impl FnOnce(),
354 ) -> RegistrationOutcome {
355 let effective = request.effective();
356 let downgrade = request.downgrade_warning();
357
358 let mut state = self.lock();
362 let existing = state.bindings.get(&key).cloned();
363 let previous_effective = existing.as_ref().map(|binding| binding.lock().effective());
364 after_existing_read();
365
366 let (stores_cleared, stores_preserved) = if let Some(existing) = existing {
367 if previous_effective != Some(effective) {
368 self.drain_in_flight(&existing);
369 {
370 let mut binding = existing.lock();
371 binding.configured_enabled = request.configured_enabled;
372 binding.edit_slot_survives = request.edit_slot_survives;
373 binding.read_slot_survives = request.read_slot_survives;
374 binding.effective = effective;
375 binding.clear_stores();
376 }
377 state.bindings.insert(key, existing);
378 (true, false)
379 } else {
380 {
381 let mut binding = existing.lock();
382 binding.configured_enabled = request.configured_enabled;
383 binding.edit_slot_survives = request.edit_slot_survives;
384 binding.read_slot_survives = request.read_slot_survives;
385 }
387 state.bindings.insert(key, existing);
388 (false, true)
389 }
390 } else {
391 let binding = Arc::new(BindingSlot::new(HashlineBinding::new(key.clone(), request)));
392 state.bindings.insert(key, binding);
393 (false, false)
394 };
395
396 RegistrationOutcome {
397 configured_enabled: request.configured_enabled,
398 edit_slot_survives: request.edit_slot_survives,
399 read_slot_survives: request.read_slot_survives,
400 effective,
401 downgrade,
402 stores_cleared,
403 stores_preserved,
404 }
405 }
406
407 pub fn capture(
410 &self,
411 root: impl AsRef<Path>,
412 session_id: impl Into<String>,
413 ) -> Option<BindingGuard> {
414 let key = SessionKey::new(root.as_ref().to_path_buf(), session_id.into());
415 let handle = {
416 let state = self.lock();
417 let arc = state.bindings.get(&key)?.clone();
418 {
419 let mut binding = arc.lock();
420 binding.in_flight = binding.in_flight.saturating_add(1);
421 }
422 BindingHandle { inner: arc }
423 };
424 Some(BindingGuard { handle })
425 }
426
427 pub fn peek(
429 &self,
430 root: impl AsRef<Path>,
431 session_id: impl Into<String>,
432 ) -> Option<BindingHandle> {
433 let key = SessionKey::new(root.as_ref().to_path_buf(), session_id.into());
434 let state = self.lock();
435 state
436 .bindings
437 .get(&key)
438 .map(|arc| BindingHandle { inner: arc.clone() })
439 }
440
441 pub fn teardown(&self, root: impl AsRef<Path>, session_id: impl Into<String>) -> bool {
443 let key = SessionKey::new(root.as_ref().to_path_buf(), session_id.into());
444 let existing = {
445 let mut state = self.lock();
446 state.bindings.remove(&key)
447 };
448 let Some(existing) = existing else {
449 return false;
450 };
451
452 self.drain_in_flight(&existing);
456 true
457 }
458
459 pub fn len(&self) -> usize {
461 self.lock().bindings.len()
462 }
463
464 pub fn is_empty(&self) -> bool {
465 self.len() == 0
466 }
467
468 fn drain_in_flight(&self, binding: &Arc<BindingSlot>) {
469 binding.drain_in_flight();
470 }
471}
472
473pub fn effective_for_capture(guard: Option<&BindingGuard>) -> bool {
475 guard.map(|g| g.effective()).unwrap_or(false)
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481 use crate::hashline::scan::scan_bytes;
482 use std::sync::mpsc::{self, RecvTimeoutError};
483 use std::thread;
484 use std::time::Duration;
485
486 #[test]
487 fn draining_two_sessions_uses_each_sessions_mutex_partner() {
488 let registry = BindingRegistry::new();
489 let root = Path::new("/tmp/hashline-condvar-partners");
490 registry.register(
491 root,
492 "first",
493 RegistrationRequest {
494 configured_enabled: true,
495 edit_slot_survives: true,
496 read_slot_survives: true,
497 },
498 );
499 registry.register(
500 root,
501 "second",
502 RegistrationRequest {
503 configured_enabled: true,
504 edit_slot_survives: true,
505 read_slot_survives: true,
506 },
507 );
508 let first = registry.peek(root, "first").expect("first binding");
509 let second = registry.peek(root, "second").expect("second binding");
510
511 for handle in [&first, &second] {
512 let guard = handle.inner.lock();
513 let (_guard, timeout) = handle
514 .inner
515 .drain
516 .wait_timeout(guard, Duration::from_millis(1))
517 .unwrap_or_else(std::sync::PoisonError::into_inner);
518 assert!(timeout.timed_out());
519 }
520 }
521
522 #[test]
523 fn teardown_without_a_binding_returns_immediately() {
524 let registry = BindingRegistry::new();
525 assert!(!registry.teardown("/tmp/hashline-no-binding", "missing"));
526 }
527
528 #[test]
529 fn teardown_notifies_each_session_through_its_own_slot() {
530 let registry = Arc::new(BindingRegistry::new());
531 let root = Path::new("/tmp/hashline-session-drains");
532 for session in ["first", "second"] {
533 registry.register(
534 root,
535 session,
536 RegistrationRequest {
537 configured_enabled: true,
538 edit_slot_survives: true,
539 read_slot_survives: true,
540 },
541 );
542 }
543 let first_guard = registry.capture(root, "first").expect("first guard");
544 let second_guard = registry.capture(root, "second").expect("second guard");
545
546 let (done_tx, done_rx) = mpsc::channel();
547 let first_registry = Arc::clone(®istry);
548 let first_done_tx = done_tx.clone();
549 let first_root = root.to_path_buf();
550 let first_teardown = thread::spawn(move || {
551 assert!(first_registry.teardown(&first_root, "first"));
552 first_done_tx.send("first").expect("signal first teardown");
553 });
554 let second_registry = Arc::clone(®istry);
555 let second_root = root.to_path_buf();
556 let second_teardown = thread::spawn(move || {
557 assert!(second_registry.teardown(&second_root, "second"));
558 done_tx.send("second").expect("signal second teardown");
559 });
560
561 let deadline = std::time::Instant::now() + Duration::from_secs(2);
562 while (registry.peek(root, "first").is_some() || registry.peek(root, "second").is_some())
563 && std::time::Instant::now() < deadline
564 {
565 thread::yield_now();
566 }
567 assert!(registry.peek(root, "first").is_none());
568 assert!(registry.peek(root, "second").is_none());
569
570 drop(second_guard);
571 assert_eq!(
572 done_rx
573 .recv_timeout(Duration::from_secs(2))
574 .expect("second teardown completes"),
575 "second"
576 );
577 assert!(matches!(
578 done_rx.recv_timeout(Duration::from_millis(50)),
579 Err(RecvTimeoutError::Timeout)
580 ));
581
582 drop(first_guard);
583 assert_eq!(
584 done_rx
585 .recv_timeout(Duration::from_secs(2))
586 .expect("first teardown completes"),
587 "first"
588 );
589 first_teardown.join().expect("first teardown thread");
590 second_teardown.join().expect("second teardown thread");
591 }
592
593 #[test]
594 fn concurrent_same_session_registration_serializes_read_compare_write() {
595 let registry = Arc::new(BindingRegistry::new());
596 let key = SessionKey::new("/tmp/hashline-register-race", "shared-session");
597 registry.register(
598 &key.root,
599 key.session_id.clone(),
600 RegistrationRequest {
601 configured_enabled: true,
602 edit_slot_survives: true,
603 read_slot_survives: true,
604 },
605 );
606 registry
607 .peek(&key.root, key.session_id.clone())
608 .expect("initial binding")
609 .with_binding_mut(|binding| {
610 binding
611 .snapshots_mut()
612 .publish("race.rs", scan_bytes(b"before race\n"));
613 });
614
615 let (first_read_tx, first_read_rx) = mpsc::channel();
616 let (release_first_tx, release_first_rx) = mpsc::channel();
617 let first_registry = Arc::clone(®istry);
618 let first_key = key.clone();
619 let first = thread::spawn(move || {
620 first_registry.register_key(
621 first_key,
622 RegistrationRequest {
623 configured_enabled: false,
624 edit_slot_survives: true,
625 read_slot_survives: true,
626 },
627 || {
628 first_read_tx.send(()).expect("signal first read");
629 release_first_rx.recv().expect("release first registration");
630 },
631 )
632 });
633
634 first_read_rx
635 .recv()
636 .expect("first registration read existing binding");
637 let (second_started_tx, second_started_rx) = mpsc::channel();
638 let (second_read_tx, second_read_rx) = mpsc::channel();
639 let (second_done_tx, second_done_rx) = mpsc::channel();
640 let second_registry = Arc::clone(®istry);
641 let second_key = key.clone();
642 let second = thread::spawn(move || {
643 second_started_tx.send(()).expect("signal second start");
644 let outcome = second_registry.register_key(
645 second_key,
646 RegistrationRequest {
647 configured_enabled: true,
648 edit_slot_survives: false,
649 read_slot_survives: true,
650 },
651 || second_read_tx.send(()).expect("signal second read"),
652 );
653 second_done_tx.send(outcome).expect("send second outcome");
654 });
655
656 second_started_rx
657 .recv()
658 .expect("second registration started");
659 assert!(matches!(
660 second_read_rx.recv_timeout(Duration::from_secs(1)),
661 Err(RecvTimeoutError::Timeout)
662 ));
663 release_first_tx
664 .send(())
665 .expect("release first registration");
666
667 let first_outcome = first.join().expect("first registration");
668 let second_outcome = second_done_rx
669 .recv_timeout(Duration::from_secs(2))
670 .expect("second registration completes after first");
671 second.join().expect("second registration");
672
673 assert!(first_outcome.stores_cleared);
674 assert!(second_outcome.stores_preserved);
675 let final_binding = registry
676 .peek(&key.root, key.session_id)
677 .expect("final binding");
678 final_binding.with_binding(|binding| {
679 assert!(binding.configured_enabled());
680 assert!(!binding.edit_slot_survives());
681 assert!(!binding.effective());
682 assert!(binding.snapshots().is_empty());
683 });
684 }
685}