aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-rpc/src/session.rs
blob: 29bd89f39f8e6eed2f0dfccd1b7e8db5d5d81216 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
//! Thread-ID based reentrant session lock.
//!
//! Guarantees at most one logical RPC "call session" is in flight against the shared worker at
//! any time, while allowing the owning thread to recurse into it freely (a handler may itself
//! call back into the other side). A different thread attempting to start a session blocks until
//! the entire outer session (including all of its nested calls) completes — it is never rejected
//! or panicked on, only serialized. This keeps the "exactly one child process" invariant intact
//! even when multiple OS threads use this crate concurrently, which happens today only under
//! `cargo test`'s parallel test harness but is not assumed to be forbidden in the future.

use std::sync::{Condvar, LazyLock, Mutex};

struct SessionLock {
    owner: Mutex<Option<(std::thread::ThreadId, u32)>>,
    cvar: Condvar,
}

impl SessionLock {
    fn acquire(&self) {
        let mut owner = self.owner.lock().unwrap();
        let me = std::thread::current().id();
        loop {
            match *owner {
                None => {
                    *owner = Some((me, 1));
                    return;
                }
                Some((tid, depth)) if tid == me => {
                    *owner = Some((me, depth + 1));
                    return;
                }
                Some(_) => {
                    owner = self.cvar.wait(owner).unwrap();
                }
            }
        }
    }

    fn release(&self) {
        let mut owner = self.owner.lock().unwrap();
        let me = std::thread::current().id();
        match *owner {
            Some((tid, depth)) if tid == me => {
                if depth == 1 {
                    *owner = None;
                    self.cvar.notify_all();
                } else {
                    *owner = Some((me, depth - 1));
                }
            }
            _ => unreachable!("SessionLock::release without a matching acquire on this thread"),
        }
    }
}

static SESSION: LazyLock<SessionLock> = LazyLock::new(|| SessionLock {
    owner: Mutex::new(None),
    cvar: Condvar::new(),
});

/// RAII guard; acquired once at the outermost `rpc_call`, re-entered (depth += 1, no blocking)
/// by nested calls from the same thread.
pub struct SessionGuard;

impl SessionGuard {
    pub fn enter() -> Self {
        SESSION.acquire();
        SessionGuard
    }
}

impl Drop for SessionGuard {
    fn drop(&mut self) {
        SESSION.release();
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn same_thread_reenters_without_blocking() {
        let _outer = SessionGuard::enter();
        let _inner = SessionGuard::enter();
    }

    #[test]
    fn other_threads_are_serialized() {
        use std::sync::Arc;
        use std::sync::atomic::{AtomicU32, Ordering};

        let concurrent = Arc::new(AtomicU32::new(0));
        let mut handles = Vec::new();
        for _ in 0..4 {
            let concurrent = Arc::clone(&concurrent);
            handles.push(std::thread::spawn(move || {
                for _ in 0..50 {
                    let _guard = SessionGuard::enter();
                    let now = concurrent.fetch_add(1, Ordering::SeqCst);
                    assert_eq!(now, 0, "two sessions were in flight at once");
                    concurrent.fetch_sub(1, Ordering::SeqCst);
                }
            }));
        }
        for handle in handles {
            handle.join().unwrap();
        }
    }
}