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 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(Debug)]
168struct BindingSlot {
169 binding: Mutex<HashlineBinding>,
170 drain: Condvar,
171}
172
173impl BindingSlot {
174 fn new(binding: HashlineBinding) -> Self {
175 Self {
176 binding: Mutex::new(binding),
177 drain: Condvar::new(),
178 }
179 }
180
181 fn lock(&self) -> MutexGuard<'_, HashlineBinding> {
182 self.binding
183 .lock()
184 .unwrap_or_else(std::sync::PoisonError::into_inner)
185 }
186
187 fn drain_in_flight(&self) {
188 let mut binding = self.lock();
189 while binding.in_flight > 0 {
190 binding = self
191 .drain
192 .wait(binding)
193 .unwrap_or_else(std::sync::PoisonError::into_inner);
194 }
195 }
196
197 fn release_guard(&self) {
198 {
199 let mut binding = self.lock();
200 binding.in_flight = binding.in_flight.saturating_sub(1);
201 }
202 self.drain.notify_all();
203 }
204}
205
206#[derive(Clone, Debug)]
208pub struct BindingHandle {
209 inner: Arc<BindingSlot>,
210}
211
212impl BindingHandle {
213 pub fn with_binding<R>(&self, f: impl FnOnce(&HashlineBinding) -> R) -> R {
214 let guard = self.inner.lock();
215 f(&guard)
216 }
217
218 pub fn with_binding_mut<R>(&self, f: impl FnOnce(&mut HashlineBinding) -> R) -> R {
219 let mut guard = self.inner.lock();
220 f(&mut guard)
221 }
222
223 pub fn effective(&self) -> bool {
224 self.with_binding(|b| b.effective())
225 }
226
227 pub fn session_key(&self) -> SessionKey {
228 self.with_binding(|b| b.key().clone())
229 }
230}
231
232pub struct BindingGuard {
235 handle: BindingHandle,
236}
237
238impl BindingGuard {
239 pub fn handle(&self) -> &BindingHandle {
240 &self.handle
241 }
242
243 pub fn effective(&self) -> bool {
244 self.handle.effective()
245 }
246
247 pub fn with_binding<R>(&self, f: impl FnOnce(&HashlineBinding) -> R) -> R {
248 self.handle.with_binding(f)
249 }
250
251 pub fn with_binding_mut<R>(&self, f: impl FnOnce(&mut HashlineBinding) -> R) -> R {
252 self.handle.with_binding_mut(f)
253 }
254}
255
256impl Drop for BindingGuard {
257 fn drop(&mut self) {
258 self.handle.inner.release_guard();
259 }
260}
261
262struct BindingRegistryInner {
263 state: Mutex<RegistryState>,
264}
265
266#[derive(Default)]
267struct RegistryState {
268 bindings: HashMap<SessionKey, Arc<BindingSlot>>,
269}
270
271pub struct BindingRegistry {
273 inner: Arc<BindingRegistryInner>,
274}
275
276impl Default for BindingRegistry {
277 fn default() -> Self {
278 Self::new()
279 }
280}
281
282impl BindingRegistry {
283 pub fn new() -> Self {
284 Self {
285 inner: Arc::new(BindingRegistryInner {
286 state: Mutex::new(RegistryState::default()),
287 }),
288 }
289 }
290
291 fn lock(&self) -> MutexGuard<'_, RegistryState> {
292 self.inner
293 .state
294 .lock()
295 .unwrap_or_else(|poisoned| poisoned.into_inner())
296 }
297
298 pub fn register(
305 &self,
306 root: impl AsRef<Path>,
307 session_id: impl Into<String>,
308 request: RegistrationRequest,
309 ) -> RegistrationOutcome {
310 let key = SessionKey::new(root.as_ref().to_path_buf(), session_id.into());
311 self.register_key(key, request, || {})
312 }
313
314 fn register_key(
315 &self,
316 key: SessionKey,
317 request: RegistrationRequest,
318 after_existing_read: impl FnOnce(),
319 ) -> RegistrationOutcome {
320 let effective = request.effective();
321 let downgrade = request
322 .should_downgrade()
323 .then_some(DowngradeWarning::EDIT_NOT_REGISTERED);
324
325 let mut state = self.lock();
329 let existing = state.bindings.get(&key).cloned();
330 let previous_effective = existing.as_ref().map(|binding| binding.lock().effective());
331 after_existing_read();
332
333 let (stores_cleared, stores_preserved) = if let Some(existing) = existing {
334 if previous_effective != Some(effective) {
335 self.drain_in_flight(&existing);
336 {
337 let mut binding = existing.lock();
338 binding.configured_enabled = request.configured_enabled;
339 binding.edit_slot_survives = request.edit_slot_survives;
340 binding.effective = effective;
341 binding.clear_stores();
342 }
343 state.bindings.insert(key, existing);
344 (true, false)
345 } else {
346 {
347 let mut binding = existing.lock();
348 binding.configured_enabled = request.configured_enabled;
349 binding.edit_slot_survives = request.edit_slot_survives;
350 }
352 state.bindings.insert(key, existing);
353 (false, true)
354 }
355 } else {
356 let binding = Arc::new(BindingSlot::new(HashlineBinding::new(key.clone(), request)));
357 state.bindings.insert(key, binding);
358 (false, false)
359 };
360
361 RegistrationOutcome {
362 configured_enabled: request.configured_enabled,
363 edit_slot_survives: request.edit_slot_survives,
364 effective,
365 downgrade,
366 stores_cleared,
367 stores_preserved,
368 }
369 }
370
371 pub fn capture(
374 &self,
375 root: impl AsRef<Path>,
376 session_id: impl Into<String>,
377 ) -> Option<BindingGuard> {
378 let key = SessionKey::new(root.as_ref().to_path_buf(), session_id.into());
379 let handle = {
380 let state = self.lock();
381 let arc = state.bindings.get(&key)?.clone();
382 {
383 let mut binding = arc.lock();
384 binding.in_flight = binding.in_flight.saturating_add(1);
385 }
386 BindingHandle { inner: arc }
387 };
388 Some(BindingGuard { handle })
389 }
390
391 pub fn peek(
393 &self,
394 root: impl AsRef<Path>,
395 session_id: impl Into<String>,
396 ) -> Option<BindingHandle> {
397 let key = SessionKey::new(root.as_ref().to_path_buf(), session_id.into());
398 let state = self.lock();
399 state
400 .bindings
401 .get(&key)
402 .map(|arc| BindingHandle { inner: arc.clone() })
403 }
404
405 pub fn teardown(&self, root: impl AsRef<Path>, session_id: impl Into<String>) -> bool {
407 let key = SessionKey::new(root.as_ref().to_path_buf(), session_id.into());
408 let existing = {
409 let mut state = self.lock();
410 state.bindings.remove(&key)
411 };
412 let Some(existing) = existing else {
413 return false;
414 };
415
416 self.drain_in_flight(&existing);
420 true
421 }
422
423 pub fn len(&self) -> usize {
425 self.lock().bindings.len()
426 }
427
428 pub fn is_empty(&self) -> bool {
429 self.len() == 0
430 }
431
432 fn drain_in_flight(&self, binding: &Arc<BindingSlot>) {
433 binding.drain_in_flight();
434 }
435}
436
437pub fn effective_for_capture(guard: Option<&BindingGuard>) -> bool {
439 guard.map(|g| g.effective()).unwrap_or(false)
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445 use crate::hashline::scan::scan_bytes;
446 use std::sync::mpsc::{self, RecvTimeoutError};
447 use std::thread;
448 use std::time::Duration;
449
450 #[test]
451 fn draining_two_sessions_uses_each_sessions_mutex_partner() {
452 let registry = BindingRegistry::new();
453 let root = Path::new("/tmp/hashline-condvar-partners");
454 registry.register(
455 root,
456 "first",
457 RegistrationRequest {
458 configured_enabled: true,
459 edit_slot_survives: true,
460 },
461 );
462 registry.register(
463 root,
464 "second",
465 RegistrationRequest {
466 configured_enabled: true,
467 edit_slot_survives: true,
468 },
469 );
470 let first = registry.peek(root, "first").expect("first binding");
471 let second = registry.peek(root, "second").expect("second binding");
472
473 for handle in [&first, &second] {
474 let guard = handle.inner.lock();
475 let (_guard, timeout) = handle
476 .inner
477 .drain
478 .wait_timeout(guard, Duration::from_millis(1))
479 .unwrap_or_else(std::sync::PoisonError::into_inner);
480 assert!(timeout.timed_out());
481 }
482 }
483
484 #[test]
485 fn teardown_without_a_binding_returns_immediately() {
486 let registry = BindingRegistry::new();
487 assert!(!registry.teardown("/tmp/hashline-no-binding", "missing"));
488 }
489
490 #[test]
491 fn teardown_notifies_each_session_through_its_own_slot() {
492 let registry = Arc::new(BindingRegistry::new());
493 let root = Path::new("/tmp/hashline-session-drains");
494 for session in ["first", "second"] {
495 registry.register(
496 root,
497 session,
498 RegistrationRequest {
499 configured_enabled: true,
500 edit_slot_survives: true,
501 },
502 );
503 }
504 let first_guard = registry.capture(root, "first").expect("first guard");
505 let second_guard = registry.capture(root, "second").expect("second guard");
506
507 let (done_tx, done_rx) = mpsc::channel();
508 let first_registry = Arc::clone(®istry);
509 let first_done_tx = done_tx.clone();
510 let first_root = root.to_path_buf();
511 let first_teardown = thread::spawn(move || {
512 assert!(first_registry.teardown(&first_root, "first"));
513 first_done_tx.send("first").expect("signal first teardown");
514 });
515 let second_registry = Arc::clone(®istry);
516 let second_root = root.to_path_buf();
517 let second_teardown = thread::spawn(move || {
518 assert!(second_registry.teardown(&second_root, "second"));
519 done_tx.send("second").expect("signal second teardown");
520 });
521
522 let deadline = std::time::Instant::now() + Duration::from_secs(2);
523 while (registry.peek(root, "first").is_some() || registry.peek(root, "second").is_some())
524 && std::time::Instant::now() < deadline
525 {
526 thread::yield_now();
527 }
528 assert!(registry.peek(root, "first").is_none());
529 assert!(registry.peek(root, "second").is_none());
530
531 drop(second_guard);
532 assert_eq!(
533 done_rx
534 .recv_timeout(Duration::from_secs(2))
535 .expect("second teardown completes"),
536 "second"
537 );
538 assert!(matches!(
539 done_rx.recv_timeout(Duration::from_millis(50)),
540 Err(RecvTimeoutError::Timeout)
541 ));
542
543 drop(first_guard);
544 assert_eq!(
545 done_rx
546 .recv_timeout(Duration::from_secs(2))
547 .expect("first teardown completes"),
548 "first"
549 );
550 first_teardown.join().expect("first teardown thread");
551 second_teardown.join().expect("second teardown thread");
552 }
553
554 #[test]
555 fn concurrent_same_session_registration_serializes_read_compare_write() {
556 let registry = Arc::new(BindingRegistry::new());
557 let key = SessionKey::new("/tmp/hashline-register-race", "shared-session");
558 registry.register(
559 &key.root,
560 key.session_id.clone(),
561 RegistrationRequest {
562 configured_enabled: true,
563 edit_slot_survives: true,
564 },
565 );
566 registry
567 .peek(&key.root, key.session_id.clone())
568 .expect("initial binding")
569 .with_binding_mut(|binding| {
570 binding
571 .snapshots_mut()
572 .publish("race.rs", scan_bytes(b"before race\n"));
573 });
574
575 let (first_read_tx, first_read_rx) = mpsc::channel();
576 let (release_first_tx, release_first_rx) = mpsc::channel();
577 let first_registry = Arc::clone(®istry);
578 let first_key = key.clone();
579 let first = thread::spawn(move || {
580 first_registry.register_key(
581 first_key,
582 RegistrationRequest {
583 configured_enabled: false,
584 edit_slot_survives: true,
585 },
586 || {
587 first_read_tx.send(()).expect("signal first read");
588 release_first_rx.recv().expect("release first registration");
589 },
590 )
591 });
592
593 first_read_rx
594 .recv()
595 .expect("first registration read existing binding");
596 let (second_started_tx, second_started_rx) = mpsc::channel();
597 let (second_read_tx, second_read_rx) = mpsc::channel();
598 let (second_done_tx, second_done_rx) = mpsc::channel();
599 let second_registry = Arc::clone(®istry);
600 let second_key = key.clone();
601 let second = thread::spawn(move || {
602 second_started_tx.send(()).expect("signal second start");
603 let outcome = second_registry.register_key(
604 second_key,
605 RegistrationRequest {
606 configured_enabled: true,
607 edit_slot_survives: false,
608 },
609 || second_read_tx.send(()).expect("signal second read"),
610 );
611 second_done_tx.send(outcome).expect("send second outcome");
612 });
613
614 second_started_rx
615 .recv()
616 .expect("second registration started");
617 assert!(matches!(
618 second_read_rx.recv_timeout(Duration::from_secs(1)),
619 Err(RecvTimeoutError::Timeout)
620 ));
621 release_first_tx
622 .send(())
623 .expect("release first registration");
624
625 let first_outcome = first.join().expect("first registration");
626 let second_outcome = second_done_rx
627 .recv_timeout(Duration::from_secs(2))
628 .expect("second registration completes after first");
629 second.join().expect("second registration");
630
631 assert!(first_outcome.stores_cleared);
632 assert!(second_outcome.stores_preserved);
633 let final_binding = registry
634 .peek(&key.root, key.session_id)
635 .expect("final binding");
636 final_binding.with_binding(|binding| {
637 assert!(binding.configured_enabled());
638 assert!(!binding.edit_slot_survives());
639 assert!(!binding.effective());
640 assert!(binding.snapshots().is_empty());
641 });
642 }
643}