reactor-effect-native 0.2.0 → 0.3.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +4 -2
- package/README.md +90 -15
- package/dist/_internal/bridge.d.ts +70 -14
- package/dist/_internal/bridge.d.ts.map +1 -1
- package/dist/_internal/bridge.js +290 -145
- package/dist/_internal/bridge.js.map +1 -1
- package/dist/_internal/isolated/child.d.ts +2 -0
- package/dist/_internal/isolated/child.d.ts.map +1 -0
- package/dist/_internal/isolated/child.js +176 -0
- package/dist/_internal/isolated/child.js.map +1 -0
- package/dist/_internal/isolated/host.d.ts +147 -0
- package/dist/_internal/isolated/host.d.ts.map +1 -0
- package/dist/_internal/isolated/host.js +645 -0
- package/dist/_internal/isolated/host.js.map +1 -0
- package/dist/_internal/isolated/protocol.d.ts +399 -0
- package/dist/_internal/isolated/protocol.d.ts.map +1 -0
- package/dist/_internal/isolated/protocol.js +250 -0
- package/dist/_internal/isolated/protocol.js.map +1 -0
- package/dist/_internal/peer.d.ts +68 -13
- package/dist/_internal/peer.d.ts.map +1 -1
- package/dist/_internal/peer.js +233 -157
- package/dist/_internal/peer.js.map +1 -1
- package/dist/index.d.ts +19 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -33
- package/dist/index.js.map +1 -1
- package/dist/isolated.d.ts +25 -0
- package/dist/isolated.d.ts.map +1 -0
- package/dist/isolated.js +39 -0
- package/dist/isolated.js.map +1 -0
- package/lib/darwin-arm64/libreactor_effect_native.dylib +0 -0
- package/lib/darwin-arm64/native-identity.json +4 -3
- package/lib/linux-x64/libreactor_effect_native.so +0 -0
- package/lib/linux-x64/native-identity.json +4 -3
- package/package.json +7 -3
- package/rust/Cargo.toml +108 -2
- package/rust/build.rs +253 -114
- package/rust/clippy.toml +7 -0
- package/rust/include/reactor_effect_native.h +101 -42
- package/rust/src/abi.rs +258 -0
- package/rust/src/error.rs +149 -0
- package/rust/src/ffi/memory.rs +256 -0
- package/rust/src/ffi/tests.rs +719 -0
- package/rust/src/ffi.rs +474 -0
- package/rust/src/lib.rs +36 -2341
- package/rust/src/peer/callbacks.rs +145 -0
- package/rust/src/peer/media.rs +221 -0
- package/rust/src/peer/owner/tests.rs +188 -0
- package/rust/src/peer/owner.rs +353 -0
- package/rust/src/peer/shared.rs +323 -0
- package/rust/src/peer/tests.rs +513 -0
- package/rust/src/peer.rs +189 -0
- package/rust/src/protocol/event.rs +238 -0
- package/rust/src/protocol/request.rs +347 -0
- package/rust/src/protocol/stats.rs +356 -0
- package/rust/src/protocol.rs +71 -0
- package/rust/src/sync/gate.rs +159 -0
- package/rust/src/sync/notifier.rs +136 -0
- package/rust/src/sync/queue.rs +384 -0
- package/rust/src/sync.rs +20 -0
- package/rust/src/test_support.rs +99 -0
- package/rust-toolchain.toml +7 -0
- package/scripts/stage.mjs +41 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
//! The readiness hand-off to the host's notify callback.
|
|
2
|
+
|
|
3
|
+
use super::lock;
|
|
4
|
+
use crate::abi::Ready;
|
|
5
|
+
use std::mem;
|
|
6
|
+
use std::sync::{Condvar, Mutex, PoisonError};
|
|
7
|
+
|
|
8
|
+
/// Readiness waiting for the host. libwebrtc threads only set bits here; a
|
|
9
|
+
/// peer's notifier thread is the one thread that ever waits on the host.
|
|
10
|
+
#[derive(Default)]
|
|
11
|
+
pub(crate) struct Notifier {
|
|
12
|
+
state: Mutex<State>,
|
|
13
|
+
wake: Condvar,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
#[derive(Default)]
|
|
17
|
+
struct State {
|
|
18
|
+
/// Readiness bits the host has not been passed yet.
|
|
19
|
+
ready: u32,
|
|
20
|
+
closed: bool,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
impl Notifier {
|
|
24
|
+
/// Mark a queue ready, waking the notifier thread if nothing was pending.
|
|
25
|
+
/// Never waits for the host.
|
|
26
|
+
pub(crate) fn signal(&self, ready: Ready) {
|
|
27
|
+
let mut state = lock(&self.state);
|
|
28
|
+
if state.closed || state.ready & ready.bit() != 0 {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
let was_idle = state.ready == 0;
|
|
32
|
+
state.ready |= ready.bit();
|
|
33
|
+
if was_idle {
|
|
34
|
+
self.wake.notify_one();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Stop the notifier thread. Later signals are ignored.
|
|
39
|
+
pub(crate) fn close(&self) {
|
|
40
|
+
lock(&self.state).closed = true;
|
|
41
|
+
self.wake.notify_all();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// The notifier thread's loop: pass pending readiness bits to `notify`
|
|
45
|
+
/// until closed. Signals raised while `notify` runs coalesce into its next
|
|
46
|
+
/// call.
|
|
47
|
+
pub(crate) fn run(&self, mut notify: impl FnMut(u32)) {
|
|
48
|
+
loop {
|
|
49
|
+
let ready = {
|
|
50
|
+
let mut state = self
|
|
51
|
+
.wake
|
|
52
|
+
.wait_while(lock(&self.state), |state| state.ready == 0 && !state.closed)
|
|
53
|
+
.unwrap_or_else(PoisonError::into_inner);
|
|
54
|
+
if state.closed {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
mem::take(&mut state.ready)
|
|
58
|
+
};
|
|
59
|
+
notify(ready);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#[cfg(test)]
|
|
65
|
+
mod tests {
|
|
66
|
+
use super::*;
|
|
67
|
+
use crate::test_support::Defer;
|
|
68
|
+
use std::sync::mpsc;
|
|
69
|
+
use std::thread;
|
|
70
|
+
use std::time::Duration;
|
|
71
|
+
|
|
72
|
+
const TIMEOUT: Duration = Duration::from_secs(5);
|
|
73
|
+
|
|
74
|
+
#[test]
|
|
75
|
+
fn readiness_raised_before_the_host_runs_arrives_as_one_call() {
|
|
76
|
+
let notifier = Notifier::default();
|
|
77
|
+
notifier.signal(Ready::Video);
|
|
78
|
+
notifier.signal(Ready::Video);
|
|
79
|
+
notifier.signal(Ready::Audio);
|
|
80
|
+
let (calls, received) = mpsc::channel();
|
|
81
|
+
thread::scope(|scope| {
|
|
82
|
+
let _stop_runner = Defer(|| notifier.close());
|
|
83
|
+
let runner = scope.spawn(|| notifier.run(|ready| calls.send(ready).unwrap()));
|
|
84
|
+
assert_eq!(
|
|
85
|
+
received.recv_timeout(TIMEOUT),
|
|
86
|
+
Ok(Ready::Video.bit() | Ready::Audio.bit())
|
|
87
|
+
);
|
|
88
|
+
notifier.signal(Ready::Events);
|
|
89
|
+
assert_eq!(received.recv_timeout(TIMEOUT), Ok(Ready::Events.bit()));
|
|
90
|
+
notifier.close();
|
|
91
|
+
runner.join().unwrap();
|
|
92
|
+
});
|
|
93
|
+
notifier.signal(Ready::Events);
|
|
94
|
+
assert!(
|
|
95
|
+
received.try_recv().is_err(),
|
|
96
|
+
"a signal after close must not reach the host"
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[test]
|
|
101
|
+
fn signals_never_wait_for_a_busy_host_and_coalesce_behind_it() {
|
|
102
|
+
let notifier = Notifier::default();
|
|
103
|
+
let (entered, host_entered) = mpsc::channel();
|
|
104
|
+
let (release, host_released) = mpsc::channel::<()>();
|
|
105
|
+
let (calls, received) = mpsc::channel();
|
|
106
|
+
let notifier = ¬ifier;
|
|
107
|
+
thread::scope(|scope| {
|
|
108
|
+
let _stop_runner = Defer(|| notifier.close());
|
|
109
|
+
let runner = scope.spawn(move || {
|
|
110
|
+
notifier.run(|ready| {
|
|
111
|
+
calls.send(ready).unwrap();
|
|
112
|
+
if ready == Ready::Video.bit() {
|
|
113
|
+
entered.send(()).unwrap();
|
|
114
|
+
host_released.recv_timeout(TIMEOUT).unwrap();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
notifier.signal(Ready::Video);
|
|
119
|
+
host_entered.recv_timeout(TIMEOUT).unwrap();
|
|
120
|
+
|
|
121
|
+
// The host is still inside its callback: these must not block.
|
|
122
|
+
notifier.signal(Ready::Audio);
|
|
123
|
+
notifier.signal(Ready::Events);
|
|
124
|
+
notifier.signal(Ready::Audio);
|
|
125
|
+
release.send(()).unwrap();
|
|
126
|
+
|
|
127
|
+
assert_eq!(received.recv_timeout(TIMEOUT), Ok(Ready::Video.bit()));
|
|
128
|
+
assert_eq!(
|
|
129
|
+
received.recv_timeout(TIMEOUT),
|
|
130
|
+
Ok(Ready::Audio.bit() | Ready::Events.bit())
|
|
131
|
+
);
|
|
132
|
+
notifier.close();
|
|
133
|
+
runner.join().unwrap();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
//! Bounded FIFO queues between libwebrtc producers and one host reader.
|
|
2
|
+
|
|
3
|
+
use super::lock;
|
|
4
|
+
use std::collections::VecDeque;
|
|
5
|
+
use std::sync::Mutex;
|
|
6
|
+
|
|
7
|
+
/// An item whose size counts against its queue's byte bound.
|
|
8
|
+
pub(crate) trait QueueItem {
|
|
9
|
+
/// The bytes this item holds.
|
|
10
|
+
fn byte_len(&self) -> usize;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
impl QueueItem for Vec<u8> {
|
|
14
|
+
fn byte_len(&self) -> usize {
|
|
15
|
+
self.len()
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// What [`Queue::try_push`] did with an item.
|
|
20
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
21
|
+
pub(crate) enum Push {
|
|
22
|
+
Accepted,
|
|
23
|
+
/// The queue is closed; the item was discarded without being counted.
|
|
24
|
+
Closed,
|
|
25
|
+
/// The queue is full; the item was refused and counted as dropped.
|
|
26
|
+
Overflow,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// What [`Queue::take`] found at the front of the queue.
|
|
30
|
+
#[derive(Debug, PartialEq, Eq)]
|
|
31
|
+
pub(crate) enum Taken<T> {
|
|
32
|
+
Item(T),
|
|
33
|
+
/// The reader cannot hold the front item, which stays queued.
|
|
34
|
+
TooSmall,
|
|
35
|
+
Empty,
|
|
36
|
+
Closed,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// A queue's accounting. Every item pushed while the queue is open is
|
|
40
|
+
/// eventually counted exactly once: dropped, taken or still queued.
|
|
41
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
42
|
+
pub(crate) struct Counts {
|
|
43
|
+
pub(crate) dropped: u64,
|
|
44
|
+
pub(crate) taken: u64,
|
|
45
|
+
pub(crate) queued: usize,
|
|
46
|
+
pub(crate) bytes: usize,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
struct State<T> {
|
|
50
|
+
items: VecDeque<T>,
|
|
51
|
+
bytes: usize,
|
|
52
|
+
dropped: u64,
|
|
53
|
+
taken: u64,
|
|
54
|
+
closed: bool,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// A bounded FIFO between libwebrtc producers and one host reader, limited to
|
|
58
|
+
/// `max_items` items holding at most `max_bytes` bytes together.
|
|
59
|
+
pub(crate) struct Queue<T> {
|
|
60
|
+
state: Mutex<State<T>>,
|
|
61
|
+
max_items: usize,
|
|
62
|
+
max_bytes: usize,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
impl<T: QueueItem> Queue<T> {
|
|
66
|
+
pub(crate) fn new(max_items: usize, max_bytes: usize) -> Self {
|
|
67
|
+
Self {
|
|
68
|
+
state: Mutex::new(State {
|
|
69
|
+
items: VecDeque::new(),
|
|
70
|
+
bytes: 0,
|
|
71
|
+
dropped: 0,
|
|
72
|
+
taken: 0,
|
|
73
|
+
closed: false,
|
|
74
|
+
}),
|
|
75
|
+
max_items,
|
|
76
|
+
max_bytes,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Queue media, where the newest item always wins: evict the oldest items
|
|
81
|
+
/// until `item` fits. Returns whether it was queued. An item larger than
|
|
82
|
+
/// the whole byte bound is dropped at once.
|
|
83
|
+
pub(crate) fn push_drop_oldest(&self, item: T) -> bool {
|
|
84
|
+
let size = item.byte_len();
|
|
85
|
+
let mut state = lock(&self.state);
|
|
86
|
+
if state.closed {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if size > self.max_bytes {
|
|
90
|
+
state.dropped += 1;
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
while state.items.len() >= self.max_items || state.bytes + size > self.max_bytes {
|
|
94
|
+
let Some(evicted) = state.items.pop_front() else {
|
|
95
|
+
break;
|
|
96
|
+
};
|
|
97
|
+
state.bytes -= evicted.byte_len();
|
|
98
|
+
state.dropped += 1;
|
|
99
|
+
}
|
|
100
|
+
state.bytes += size;
|
|
101
|
+
state.items.push_back(item);
|
|
102
|
+
true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Queue a transport event, which is never evicted: a full queue refuses
|
|
106
|
+
/// it and its caller retires the connection.
|
|
107
|
+
pub(crate) fn try_push(&self, item: T) -> Push {
|
|
108
|
+
let size = item.byte_len();
|
|
109
|
+
let mut state = lock(&self.state);
|
|
110
|
+
if state.closed {
|
|
111
|
+
return Push::Closed;
|
|
112
|
+
}
|
|
113
|
+
if state.items.len() >= self.max_items || state.bytes + size > self.max_bytes {
|
|
114
|
+
state.dropped += 1;
|
|
115
|
+
return Push::Overflow;
|
|
116
|
+
}
|
|
117
|
+
state.bytes += size;
|
|
118
|
+
state.items.push_back(item);
|
|
119
|
+
Push::Accepted
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// Discard the backlog, counting it as dropped, and queue only `item`.
|
|
123
|
+
pub(crate) fn replace(&self, item: T) {
|
|
124
|
+
let mut state = lock(&self.state);
|
|
125
|
+
if state.closed {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
state.dropped += state.items.len() as u64;
|
|
129
|
+
state.items.clear();
|
|
130
|
+
state.bytes = item.byte_len();
|
|
131
|
+
state.items.push_back(item);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// Remove the front item if `fits` accepts it.
|
|
135
|
+
///
|
|
136
|
+
/// `fits` sees the item under the queue's lock so it can report the item's
|
|
137
|
+
/// sizes. The caller copies the item after the lock is released, so a
|
|
138
|
+
/// producer never waits for that copy.
|
|
139
|
+
pub(crate) fn take(&self, fits: impl FnOnce(&T) -> bool) -> Taken<T> {
|
|
140
|
+
let mut state = lock(&self.state);
|
|
141
|
+
let Some(item) = state.items.pop_front() else {
|
|
142
|
+
return if state.closed {
|
|
143
|
+
Taken::Closed
|
|
144
|
+
} else {
|
|
145
|
+
Taken::Empty
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
if !fits(&item) {
|
|
149
|
+
state.items.push_front(item);
|
|
150
|
+
return Taken::TooSmall;
|
|
151
|
+
}
|
|
152
|
+
state.bytes -= item.byte_len();
|
|
153
|
+
state.taken += 1;
|
|
154
|
+
Taken::Item(item)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// Refuse later items and discard queued ones, counting them as dropped.
|
|
158
|
+
pub(crate) fn close(&self) {
|
|
159
|
+
let mut state = lock(&self.state);
|
|
160
|
+
state.dropped += state.items.len() as u64;
|
|
161
|
+
state.items.clear();
|
|
162
|
+
state.bytes = 0;
|
|
163
|
+
state.closed = true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
pub(crate) fn counts(&self) -> Counts {
|
|
167
|
+
let state = lock(&self.state);
|
|
168
|
+
Counts {
|
|
169
|
+
dropped: state.dropped,
|
|
170
|
+
taken: state.taken,
|
|
171
|
+
queued: state.items.len(),
|
|
172
|
+
bytes: state.bytes,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[cfg(test)]
|
|
178
|
+
mod tests {
|
|
179
|
+
use super::*;
|
|
180
|
+
use crate::test_support::Rng;
|
|
181
|
+
|
|
182
|
+
/// A queue item carrying its push order, with an arbitrary size.
|
|
183
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
184
|
+
struct Item {
|
|
185
|
+
sequence: u64,
|
|
186
|
+
size: usize,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
impl QueueItem for Item {
|
|
190
|
+
fn byte_len(&self) -> usize {
|
|
191
|
+
self.size
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fn take_any<T: QueueItem>(queue: &Queue<T>) -> Option<T> {
|
|
196
|
+
match queue.take(|_| true) {
|
|
197
|
+
Taken::Item(item) => Some(item),
|
|
198
|
+
Taken::TooSmall => panic!("take_any accepts every item"),
|
|
199
|
+
Taken::Empty | Taken::Closed => None,
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#[test]
|
|
204
|
+
fn media_evicts_the_oldest_items_and_counts_each_one() {
|
|
205
|
+
let queue = Queue::new(2, 32);
|
|
206
|
+
for fill in 1..=3u8 {
|
|
207
|
+
assert!(queue.push_drop_oldest(vec![fill; 8]));
|
|
208
|
+
}
|
|
209
|
+
assert_eq!(take_any(&queue), Some(vec![2; 8]));
|
|
210
|
+
assert_eq!(take_any(&queue), Some(vec![3; 8]));
|
|
211
|
+
assert_eq!(take_any(&queue), None);
|
|
212
|
+
|
|
213
|
+
// The byte bound evicts too, and an item above it is dropped unqueued.
|
|
214
|
+
assert!(queue.push_drop_oldest(vec![4; 20]));
|
|
215
|
+
assert!(queue.push_drop_oldest(vec![5; 20]));
|
|
216
|
+
assert!(!queue.push_drop_oldest(vec![6; 33]));
|
|
217
|
+
assert_eq!(
|
|
218
|
+
queue.counts(),
|
|
219
|
+
Counts {
|
|
220
|
+
dropped: 3,
|
|
221
|
+
taken: 2,
|
|
222
|
+
queued: 1,
|
|
223
|
+
bytes: 20
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#[test]
|
|
229
|
+
fn events_are_refused_rather_than_evicted_when_full() {
|
|
230
|
+
let queue = Queue::new(2, 32);
|
|
231
|
+
assert_eq!(queue.try_push(vec![1; 8]), Push::Accepted);
|
|
232
|
+
assert_eq!(queue.try_push(vec![2; 30]), Push::Overflow, "byte bound");
|
|
233
|
+
assert_eq!(queue.try_push(vec![3; 8]), Push::Accepted);
|
|
234
|
+
assert_eq!(queue.try_push(vec![4; 1]), Push::Overflow, "item bound");
|
|
235
|
+
assert_eq!(take_any(&queue), Some(vec![1; 8]));
|
|
236
|
+
assert_eq!(take_any(&queue), Some(vec![3; 8]));
|
|
237
|
+
assert_eq!(queue.counts().dropped, 2);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#[test]
|
|
241
|
+
fn replace_keeps_only_the_new_item_and_drops_the_backlog() {
|
|
242
|
+
let queue = Queue::new(4, 64);
|
|
243
|
+
assert_eq!(queue.try_push(vec![1; 8]), Push::Accepted);
|
|
244
|
+
assert_eq!(queue.try_push(vec![2; 8]), Push::Accepted);
|
|
245
|
+
queue.replace(vec![9; 3]);
|
|
246
|
+
assert_eq!(
|
|
247
|
+
queue.counts(),
|
|
248
|
+
Counts {
|
|
249
|
+
dropped: 2,
|
|
250
|
+
taken: 0,
|
|
251
|
+
queued: 1,
|
|
252
|
+
bytes: 3
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
assert_eq!(take_any(&queue), Some(vec![9; 3]));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
#[test]
|
|
259
|
+
fn take_keeps_an_item_the_reader_cannot_hold() {
|
|
260
|
+
let queue = Queue::new(4, 64);
|
|
261
|
+
assert!(queue.push_drop_oldest(vec![7; 12]));
|
|
262
|
+
let mut seen = 0;
|
|
263
|
+
let taken = queue.take(|item| {
|
|
264
|
+
seen = item.len();
|
|
265
|
+
false
|
|
266
|
+
});
|
|
267
|
+
assert_eq!(taken, Taken::TooSmall);
|
|
268
|
+
assert_eq!(seen, 12);
|
|
269
|
+
assert_eq!(queue.counts().queued, 1);
|
|
270
|
+
assert_eq!(take_any(&queue), Some(vec![7; 12]));
|
|
271
|
+
assert_eq!(queue.counts().taken, 1);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
#[test]
|
|
275
|
+
fn close_drops_what_is_queued_and_refuses_later_items() {
|
|
276
|
+
let queue = Queue::new(4, 64);
|
|
277
|
+
assert!(queue.push_drop_oldest(vec![1; 4]));
|
|
278
|
+
assert!(queue.push_drop_oldest(vec![2; 4]));
|
|
279
|
+
queue.close();
|
|
280
|
+
assert_eq!(queue.take(|_| true), Taken::Closed);
|
|
281
|
+
assert!(!queue.push_drop_oldest(vec![3; 4]));
|
|
282
|
+
assert_eq!(queue.try_push(vec![4; 4]), Push::Closed);
|
|
283
|
+
queue.replace(vec![5; 4]);
|
|
284
|
+
assert_eq!(
|
|
285
|
+
queue.counts(),
|
|
286
|
+
Counts {
|
|
287
|
+
dropped: 2,
|
|
288
|
+
taken: 0,
|
|
289
|
+
queued: 0,
|
|
290
|
+
bytes: 0
|
|
291
|
+
},
|
|
292
|
+
"a closed queue counts nothing it refused"
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/// Drive a fresh queue with seeded random pushes, takes and a rare close,
|
|
297
|
+
/// checking after every operation the properties both queue kinds share:
|
|
298
|
+
/// its bounds, FIFO order, and that each item pushed while it was open is
|
|
299
|
+
/// counted exactly once. `push` checks the property of one queue kind; it
|
|
300
|
+
/// is told whether the queue is open.
|
|
301
|
+
fn exercise(seed: u64, push: impl Fn(&Queue<Item>, Item, Bounds, bool)) {
|
|
302
|
+
let mut rng = Rng::new(seed);
|
|
303
|
+
for _ in 0..200 {
|
|
304
|
+
let bounds = Bounds {
|
|
305
|
+
items: 1 + rng.below(8),
|
|
306
|
+
bytes: 1 + rng.below(64),
|
|
307
|
+
};
|
|
308
|
+
let queue = Queue::new(bounds.items, bounds.bytes);
|
|
309
|
+
let (mut open, mut observed, mut last_taken) = (true, 0, None);
|
|
310
|
+
for sequence in 0..400 {
|
|
311
|
+
match rng.below(100) {
|
|
312
|
+
0..=54 => {
|
|
313
|
+
// Some items exceed the whole byte bound.
|
|
314
|
+
let size = rng.below(bounds.bytes + bounds.bytes / 4 + 1);
|
|
315
|
+
push(&queue, Item { sequence, size }, bounds, open);
|
|
316
|
+
observed += u64::from(open);
|
|
317
|
+
}
|
|
318
|
+
55..=98 => {
|
|
319
|
+
if let Some(item) = take_any(&queue) {
|
|
320
|
+
assert!(
|
|
321
|
+
last_taken.is_none_or(|last| item.sequence > last),
|
|
322
|
+
"items must leave in the order they were pushed"
|
|
323
|
+
);
|
|
324
|
+
last_taken = Some(item.sequence);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
_ => {
|
|
328
|
+
queue.close();
|
|
329
|
+
open = false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
let counts = queue.counts();
|
|
333
|
+
assert!(
|
|
334
|
+
counts.queued <= bounds.items,
|
|
335
|
+
"{counts:?} breaks {bounds:?}"
|
|
336
|
+
);
|
|
337
|
+
assert!(counts.bytes <= bounds.bytes, "{counts:?} breaks {bounds:?}");
|
|
338
|
+
assert_eq!(
|
|
339
|
+
counts.dropped + counts.taken + counts.queued as u64,
|
|
340
|
+
observed,
|
|
341
|
+
"every item pushed while open is dropped, taken or queued, once"
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
while take_any(&queue).is_some() {}
|
|
345
|
+
assert_eq!(queue.counts().bytes, 0, "a drained queue holds no bytes");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
#[derive(Debug, Clone, Copy)]
|
|
350
|
+
struct Bounds {
|
|
351
|
+
items: usize,
|
|
352
|
+
bytes: usize,
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
#[test]
|
|
356
|
+
fn media_accounting_holds_under_random_operations() {
|
|
357
|
+
exercise(0x5eed_0001, |queue, item, bounds, open| {
|
|
358
|
+
let fits = item.size <= bounds.bytes;
|
|
359
|
+
assert_eq!(
|
|
360
|
+
queue.push_drop_oldest(item),
|
|
361
|
+
open && fits,
|
|
362
|
+
"while open, the newest media is queued exactly when it fits the byte bound"
|
|
363
|
+
);
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
#[test]
|
|
368
|
+
fn event_accounting_holds_under_random_operations() {
|
|
369
|
+
exercise(0x5eed_0002, |queue, item, bounds, open| {
|
|
370
|
+
let before = queue.counts();
|
|
371
|
+
let room = before.queued < bounds.items && before.bytes + item.size <= bounds.bytes;
|
|
372
|
+
let expected = match (open, room) {
|
|
373
|
+
(false, _) => Push::Closed,
|
|
374
|
+
(true, true) => Push::Accepted,
|
|
375
|
+
(true, false) => Push::Overflow,
|
|
376
|
+
};
|
|
377
|
+
assert_eq!(queue.try_push(item), expected);
|
|
378
|
+
assert!(
|
|
379
|
+
queue.counts().queued >= before.queued,
|
|
380
|
+
"events must never be evicted"
|
|
381
|
+
);
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
package/rust/src/sync.rs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//! The hand-off between libwebrtc callbacks, a peer's threads and its host.
|
|
2
|
+
|
|
3
|
+
mod gate;
|
|
4
|
+
mod notifier;
|
|
5
|
+
mod queue;
|
|
6
|
+
|
|
7
|
+
pub(crate) use gate::CallbackGate;
|
|
8
|
+
pub(crate) use notifier::Notifier;
|
|
9
|
+
pub(crate) use queue::{Push, Queue, QueueItem, Taken};
|
|
10
|
+
|
|
11
|
+
use std::sync::{Mutex, MutexGuard, PoisonError};
|
|
12
|
+
|
|
13
|
+
/// Lock `mutex`, recovering its data if a panic poisoned it.
|
|
14
|
+
///
|
|
15
|
+
/// No critical section in this crate can panic after it starts mutating, so a
|
|
16
|
+
/// poisoned lock still guards consistent data, and a peer keeps serving its
|
|
17
|
+
/// host after an entry point caught a panic.
|
|
18
|
+
pub(crate) fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
|
|
19
|
+
mutex.lock().unwrap_or_else(PoisonError::into_inner)
|
|
20
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
//! Helpers shared by this crate's tests.
|
|
2
|
+
|
|
3
|
+
use reactor_webrtc::IceCandidate;
|
|
4
|
+
use serde_json::Value;
|
|
5
|
+
use std::thread;
|
|
6
|
+
use std::time::{Duration, Instant};
|
|
7
|
+
|
|
8
|
+
/// Poll `done` until it holds, failing the test after `timeout`.
|
|
9
|
+
pub(crate) fn wait_until(what: &str, timeout: Duration, mut done: impl FnMut() -> bool) {
|
|
10
|
+
let deadline = Instant::now() + timeout;
|
|
11
|
+
while !done() {
|
|
12
|
+
assert!(
|
|
13
|
+
Instant::now() < deadline,
|
|
14
|
+
"timed out after {timeout:?} waiting for {what}"
|
|
15
|
+
);
|
|
16
|
+
thread::sleep(Duration::from_millis(2));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Runs its closure when dropped, including while a failed assertion
|
|
21
|
+
/// unwinds: a test releases the threads its scope joins with one, so a failure
|
|
22
|
+
/// fails instead of hanging.
|
|
23
|
+
pub(crate) struct Defer<F: FnMut()>(pub(crate) F);
|
|
24
|
+
|
|
25
|
+
impl<F: FnMut()> Drop for Defer<F> {
|
|
26
|
+
fn drop(&mut self) {
|
|
27
|
+
(self.0)();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Split an event packet into its JSON header and its payload.
|
|
32
|
+
pub(crate) fn parse_packet(packet: &[u8]) -> (Value, &[u8]) {
|
|
33
|
+
let (length, rest) = packet
|
|
34
|
+
.split_first_chunk::<4>()
|
|
35
|
+
.expect("a packet starts with its header length");
|
|
36
|
+
let (header, payload) = rest
|
|
37
|
+
.split_at_checked(u32::from_le_bytes(*length) as usize)
|
|
38
|
+
.expect("the header fits in the packet");
|
|
39
|
+
(
|
|
40
|
+
serde_json::from_slice(header).expect("the header is JSON"),
|
|
41
|
+
payload,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// A deterministic xorshift generator: a failing sequence reproduces from
|
|
46
|
+
/// its seed.
|
|
47
|
+
pub(crate) struct Rng(u64);
|
|
48
|
+
|
|
49
|
+
impl Rng {
|
|
50
|
+
pub(crate) fn new(seed: u64) -> Self {
|
|
51
|
+
Self(seed.max(1))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// A value in `0..bound`.
|
|
55
|
+
pub(crate) fn below(&mut self, bound: usize) -> usize {
|
|
56
|
+
self.0 ^= self.0 << 13;
|
|
57
|
+
self.0 ^= self.0 >> 7;
|
|
58
|
+
self.0 ^= self.0 << 17;
|
|
59
|
+
usize::try_from(self.0 % bound as u64).unwrap()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Add gathered candidates to their m-sections of `sdp` and end each
|
|
64
|
+
/// section's candidates, as an answer from Reactor arrives: the bridge's C ABI
|
|
65
|
+
/// has no call for remote candidates. The far peer example has its own copy,
|
|
66
|
+
/// since an example cannot use a library's test code.
|
|
67
|
+
pub(crate) fn with_candidates(sdp: &str, candidates: &[IceCandidate]) -> String {
|
|
68
|
+
// The session section, then one section per m-line.
|
|
69
|
+
let mut session = Vec::new();
|
|
70
|
+
let mut media: Vec<Vec<String>> = Vec::new();
|
|
71
|
+
for line in sdp.split("\r\n").filter(|line| !line.is_empty()) {
|
|
72
|
+
if line.starts_with("m=") {
|
|
73
|
+
media.push(Vec::new());
|
|
74
|
+
}
|
|
75
|
+
media
|
|
76
|
+
.last_mut()
|
|
77
|
+
.unwrap_or(&mut session)
|
|
78
|
+
.push(line.to_owned());
|
|
79
|
+
}
|
|
80
|
+
for candidate in candidates {
|
|
81
|
+
let section = candidate
|
|
82
|
+
.sdp_mline_index
|
|
83
|
+
.and_then(|index| media.get_mut(usize::from(index)));
|
|
84
|
+
if let Some(section) = section {
|
|
85
|
+
let attribute = candidate.candidate.trim_start_matches("a=");
|
|
86
|
+
section.push(format!("a={attribute}"));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for section in &mut media {
|
|
90
|
+
section.push("a=end-of-candidates".to_owned());
|
|
91
|
+
}
|
|
92
|
+
let mut sdp = session
|
|
93
|
+
.into_iter()
|
|
94
|
+
.chain(media.into_iter().flatten())
|
|
95
|
+
.collect::<Vec<_>>()
|
|
96
|
+
.join("\r\n");
|
|
97
|
+
sdp.push_str("\r\n");
|
|
98
|
+
sdp
|
|
99
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# The toolchain the native scripts and CI use, so local lints match CI's.
|
|
2
|
+
# Keep it in step with RUST_VERSION in .github/workflows/ci.yml, the
|
|
3
|
+
# Dockerfile's base image and rust-version in rust/Cargo.toml.
|
|
4
|
+
[toolchain]
|
|
5
|
+
channel = "1.90.0"
|
|
6
|
+
components = ["clippy", "rustfmt"]
|
|
7
|
+
profile = "minimal"
|