ngx/sync.rs
1//! Synchronization primitives over shared memory.
2//!
3//! This module provides an alternative implementation for the `ngx_atomic_t` type,
4//! `ngx_atomic_*`/`ngx_rwlock_*` family of functions and related usage patterns from nginx.
5//!
6//! `<ngx_atomic.h>` contains a wide variety of implementation variants for different platforms and
7//! build configurations. It's not feasible to properly expose all of these to the Rust code, and we
8//! are not going to. The implementation here uses similar logic on the foundation of the
9//! [core::sync::atomic] types and is intentionally _not interoperable_ with the nginx atomics.
10//! Thus, it's only suitable for use for new shared memory structures instead of, for example,
11//! interacting with the upstream zones.
12//!
13//! One potential pitfall here is that atomics in Rust are specified in terms of threads, and we use
14//! the types in this module for interprocess synchronization. This should not be an issue though,
15//! as Rust refers to the C/C++11 memory model for atomics, and there's a following note in
16//! [atomics.lockfree]:
17//!
18//! > [Note: Operations that are lock-free should also be address-free. That is, atomic operations
19//! > on the same memory location via two different addresses will communicate atomically. The
20//! > implementation should not depend on any per-process state. This restriction enables
21//! > communication via memory that is mapped into a process more than once and by memory that is
22//! > shared between two processes. — end note]
23//!
24//! In practice, this recommendation is applied in all the implementations that matter to us.
25use core::sync::atomic::{self, Ordering};
26
27use nginx_sys::ngx_sched_yield;
28
29const NGX_RWLOCK_SPIN: usize = 2048;
30const NGX_RWLOCK_WLOCK: usize = usize::MAX;
31
32type NgxAtomic = atomic::AtomicUsize;
33
34/// Raw lock type.
35///
36pub struct RawSpinlock(NgxAtomic);
37
38/// Reader-writer lock over an atomic variable, based on the nginx rwlock implementation.
39pub type RwLock<T> = lock_api::RwLock<RawSpinlock, T>;
40
41/// RAII structure used to release the shared read access of a lock when dropped.
42pub type RwLockReadGuard<'a, T> = lock_api::RwLockReadGuard<'a, RawSpinlock, T>;
43
44/// RAII structure used to release the exclusive write access of a lock when dropped.
45pub type RwLockWriteGuard<'a, T> = lock_api::RwLockWriteGuard<'a, RawSpinlock, T>;
46
47unsafe impl lock_api::RawRwLock for RawSpinlock {
48 // Only used for initialization, will not be mutated
49 #[allow(clippy::declare_interior_mutable_const)]
50 const INIT: RawSpinlock = RawSpinlock(NgxAtomic::new(0));
51
52 type GuardMarker = lock_api::GuardNoSend;
53
54 fn lock_shared(&self) {
55 loop {
56 if self.try_lock_shared() {
57 return;
58 }
59
60 if unsafe { nginx_sys::ngx_ncpu > 1 } {
61 for n in 0..NGX_RWLOCK_SPIN {
62 for _ in 0..n {
63 core::hint::spin_loop()
64 }
65
66 if self.try_lock_shared() {
67 return;
68 }
69 }
70 }
71
72 ngx_sched_yield()
73 }
74 }
75
76 fn try_lock_shared(&self) -> bool {
77 let value = self.0.load(Ordering::Acquire);
78
79 if value == NGX_RWLOCK_WLOCK {
80 return false;
81 }
82
83 self.0
84 .compare_exchange(value, value + 1, Ordering::Acquire, Ordering::Relaxed)
85 .is_ok()
86 }
87
88 unsafe fn unlock_shared(&self) {
89 self.0.fetch_sub(1, Ordering::Release);
90 }
91
92 fn lock_exclusive(&self) {
93 loop {
94 if self.try_lock_exclusive() {
95 return;
96 }
97
98 if unsafe { nginx_sys::ngx_ncpu > 1 } {
99 for n in 0..NGX_RWLOCK_SPIN {
100 for _ in 0..n {
101 core::hint::spin_loop()
102 }
103
104 if self.try_lock_exclusive() {
105 return;
106 }
107 }
108 }
109
110 ngx_sched_yield()
111 }
112 }
113
114 fn try_lock_exclusive(&self) -> bool {
115 self.0
116 .compare_exchange(0, NGX_RWLOCK_WLOCK, Ordering::Acquire, Ordering::Relaxed)
117 .is_ok()
118 }
119
120 unsafe fn unlock_exclusive(&self) {
121 self.0.store(0, Ordering::Release)
122 }
123}