castrum 0.9.3 → 0.9.6

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.
Files changed (206) hide show
  1. package/Cargo.lock +2242 -0
  2. package/Cargo.toml +155 -0
  3. package/README.md +14 -6
  4. package/build.rs +5 -0
  5. package/castrum.darwin-arm64.node +0 -0
  6. package/castrum.darwin-x64.node +0 -0
  7. package/castrum.linux-arm64-gnu.node +0 -0
  8. package/castrum.linux-arm64-musl.node +0 -0
  9. package/castrum.linux-x64-gnu.node +0 -0
  10. package/castrum.linux-x64-musl.node +0 -0
  11. package/castrum.linux-x64-v3-gnu.node +0 -0
  12. package/castrum.win32-x64-msvc.node +0 -0
  13. package/dist/index.d.ts +1508 -1291
  14. package/dist/index.js +3391 -2069
  15. package/docs/API.md +2 -1
  16. package/docs/ARCHITECTURE.md +3 -2
  17. package/docs/CASE_STUDY.md +5 -3
  18. package/docs/ENVIRONMENT.md +25 -2
  19. package/docs/FFI_BUN_GUIDE.md +242 -14
  20. package/docs/HOW_TO_ADD_AN_OP.md +10 -0
  21. package/docs/REPO_MAP.md +17 -6
  22. package/docs/RND-CONCURRENCY.md +772 -0
  23. package/docs/bun-builtins-decision-matrix.md +4 -1
  24. package/index.ts +24 -25
  25. package/package.json +173 -156
  26. package/rust/crypto/aead.rs +378 -0
  27. package/rust/crypto/argon2.rs +265 -0
  28. package/rust/crypto/base64.rs +420 -0
  29. package/rust/crypto/bcrypt.rs +89 -0
  30. package/rust/crypto/cookie_sign.rs +169 -0
  31. package/rust/crypto/csrf.rs +106 -0
  32. package/rust/crypto/ed25519.rs +215 -0
  33. package/rust/crypto/hashing.rs +140 -0
  34. package/rust/crypto/hmac_sha256.rs +88 -0
  35. package/rust/crypto/jwt/api.rs +286 -0
  36. package/rust/crypto/jwt/mod.rs +22 -0
  37. package/rust/crypto/jwt/tests.rs +312 -0
  38. package/rust/crypto/jwt/token.rs +360 -0
  39. package/rust/crypto/mod.rs +27 -0
  40. package/rust/crypto/pbkdf2.rs +105 -0
  41. package/rust/crypto/random_token.rs +85 -0
  42. package/rust/crypto/session.rs +222 -0
  43. package/rust/ffi/crypto.rs +708 -0
  44. package/rust/ffi/hashing.rs +285 -0
  45. package/rust/ffi/http.rs +752 -0
  46. package/rust/ffi/ingress.rs +303 -0
  47. package/rust/ffi/json.rs +517 -0
  48. package/rust/ffi/jwt.rs +419 -0
  49. package/rust/ffi/metrics.rs +373 -0
  50. package/rust/ffi/mod.rs +69 -0
  51. package/rust/ffi/payload.rs +275 -0
  52. package/rust/ffi/probe.rs +60 -0
  53. package/rust/ffi/rate_limit.rs +77 -0
  54. package/rust/ffi/route.rs +85 -0
  55. package/rust/ffi/task.rs +172 -0
  56. package/rust/ffi/tests.rs +2333 -0
  57. package/rust/ffi/util.rs +152 -0
  58. package/rust/ffi/validators.rs +123 -0
  59. package/rust/http/accept.rs +606 -0
  60. package/rust/http/cookie_parser.rs +140 -0
  61. package/rust/http/etag.rs +289 -0
  62. package/rust/http/form.rs +136 -0
  63. package/rust/http/headers.rs +267 -0
  64. package/rust/http/http_date.rs +286 -0
  65. package/rust/http/http_parser.rs +252 -0
  66. package/rust/http/media_type.rs +355 -0
  67. package/rust/http/method.rs +200 -0
  68. package/rust/http/mime_lookup.rs +130 -0
  69. package/rust/http/mod.rs +32 -0
  70. package/rust/http/multipart.rs +685 -0
  71. package/rust/http/query_parser.rs +211 -0
  72. package/rust/http/url_codec.rs +296 -0
  73. package/rust/http/url_join.rs +324 -0
  74. package/rust/ingress/api.rs +355 -0
  75. package/rust/ingress/cors.rs +403 -0
  76. package/rust/ingress/ingress_constants.rs +209 -0
  77. package/rust/ingress/ip_trust.rs +312 -0
  78. package/rust/ingress/mod.rs +48 -0
  79. package/rust/ingress/native_route.rs +770 -0
  80. package/rust/ingress/options.rs +176 -0
  81. package/rust/ingress/output.rs +406 -0
  82. package/rust/ingress/packed.rs +191 -0
  83. package/rust/ingress/pipeline.rs +534 -0
  84. package/rust/ingress/proxy.rs +138 -0
  85. package/rust/ingress/rate_limit.rs +538 -0
  86. package/rust/ingress/terminal.rs +114 -0
  87. package/rust/ingress/tests.rs +567 -0
  88. package/rust/ingress/time.rs +70 -0
  89. package/rust/json/fast_schema/capture.rs +336 -0
  90. package/rust/json/fast_schema/compile.rs +556 -0
  91. package/rust/json/fast_schema/cursor.rs +368 -0
  92. package/rust/json/fast_schema/email.rs +282 -0
  93. package/rust/json/fast_schema/errors.rs +247 -0
  94. package/rust/json/fast_schema/mod.rs +57 -0
  95. package/rust/json/fast_schema/tests.rs +1143 -0
  96. package/rust/json/fast_schema/types.rs +140 -0
  97. package/rust/json/fast_schema/validate.rs +931 -0
  98. package/rust/json/json_ops.rs +121 -0
  99. package/rust/json/json_schema.rs +837 -0
  100. package/rust/json/json_ser.rs +883 -0
  101. package/rust/json/mod.rs +19 -0
  102. package/rust/json/napi_marshal.rs +318 -0
  103. package/rust/json/patch/api.rs +60 -0
  104. package/rust/json/patch/engine.rs +153 -0
  105. package/rust/json/patch/mod.rs +47 -0
  106. package/rust/json/patch/ops.rs +270 -0
  107. package/rust/json/patch/pointer.rs +49 -0
  108. package/rust/json/patch/tests.rs +499 -0
  109. package/rust/lib.rs +73 -0
  110. package/rust/metrics/api.rs +148 -0
  111. package/rust/metrics/mod.rs +21 -0
  112. package/rust/metrics/registry.rs +961 -0
  113. package/rust/panic_safety.rs +40 -0
  114. package/rust/payload/compress.rs +512 -0
  115. package/rust/payload/mod.rs +13 -0
  116. package/rust/payload/sse.rs +214 -0
  117. package/rust/payload/template.rs +253 -0
  118. package/rust/payload/websocket.rs +69 -0
  119. package/rust/payload/ws_frames.rs +354 -0
  120. package/rust/proptest_suite.rs +139 -0
  121. package/rust/selection.rs +95 -0
  122. package/rust/src/bin/native-bench.rs +495 -0
  123. package/rust/task/completion.rs +119 -0
  124. package/rust/task/mod.rs +65 -0
  125. package/rust/task/ops.rs +503 -0
  126. package/rust/task/runtime.rs +220 -0
  127. package/rust/task/tests.rs +231 -0
  128. package/rust/test_support.rs +71 -0
  129. package/rust/util/batch/api.rs +266 -0
  130. package/rust/util/batch/core.rs +473 -0
  131. package/rust/util/batch/mod.rs +23 -0
  132. package/rust/util/batch/tests.rs +445 -0
  133. package/rust/util/batch_core.rs +184 -0
  134. package/rust/util/bytes.rs +594 -0
  135. package/rust/util/mod.rs +34 -0
  136. package/rust/util/packed.rs +473 -0
  137. package/rust/util/text.rs +130 -0
  138. package/rust/util/threadpool.rs +128 -0
  139. package/rust/util/validation.rs +419 -0
  140. package/rust-toolchain.toml +17 -0
  141. package/scripts/postinstall.mjs +199 -0
  142. package/src/ingress/constants.ts +1 -1
  143. package/src/ingress/context.ts +2 -2
  144. package/src/ingress/decode/baked-result.ts +13 -13
  145. package/src/ingress/decode/fast-result.ts +17 -17
  146. package/src/ingress/errors.ts +7 -7
  147. package/src/ingress/headers/baked-templates.ts +1 -1
  148. package/src/ingress/headers/fast-templates.ts +3 -3
  149. package/src/ingress/health.ts +1 -2
  150. package/src/ingress/index.ts +1 -1
  151. package/src/ingress/native-route.ts +9 -5
  152. package/src/ingress/options.ts +1 -1
  153. package/src/ingress/packing/gather-raw-headers.ts +1 -1
  154. package/src/ingress/packing/route-wire.ts +1 -5
  155. package/src/ingress/response/error-bodies.ts +5 -5
  156. package/src/ingress/response/terminal.ts +1 -1
  157. package/src/ingress/routes/echo.ts +4 -4
  158. package/src/ingress/routes/fallback.ts +2 -2
  159. package/src/ingress/routes/head.ts +2 -2
  160. package/src/ingress/routes/native.ts +2 -1
  161. package/src/ingress/routes/options.ts +1 -1
  162. package/src/ingress/routes/read.ts +2 -2
  163. package/src/ingress/server.ts +10 -6
  164. package/src/ingress/status.ts +7 -7
  165. package/src/integration/batch.ts +1 -1
  166. package/src/integration/index.ts +9 -9
  167. package/src/integration/pipeline.ts +6 -6
  168. package/src/integration/websocket.ts +2 -2
  169. package/src/loader/index.ts +1 -5
  170. package/src/loader/ops.ts +1 -1
  171. package/src/native/ffi/build/codecs.ts +130 -12
  172. package/src/native/ffi/build/compress.ts +22 -10
  173. package/src/native/ffi/build/instances.ts +46 -4
  174. package/src/native/ffi/build/metrics.ts +247 -0
  175. package/src/native/ffi/build/parse.ts +59 -23
  176. package/src/native/ffi/build/task.ts +79 -0
  177. package/src/native/ffi/build/util.ts +32 -1
  178. package/src/native/ffi/build.ts +4 -0
  179. package/src/native/ffi/selftest.ts +12 -3
  180. package/src/native/ffi/types.ts +151 -9
  181. package/src/native/ffi.ts +160 -15
  182. package/src/native/index.ts +1 -1
  183. package/src/native/loader.ts +55 -5
  184. package/src/native/types/instances.ts +34 -0
  185. package/src/native/types.ts +11 -0
  186. package/src/runtime/codec.ts +53 -9
  187. package/src/runtime/types.ts +7 -0
  188. package/src/rust-ffi/batch/build.ts +16 -9
  189. package/src/rust-ffi/index.ts +7 -6
  190. package/src/rust-ffi/options.ts +1 -3
  191. package/src/rust-ffi/proven.ts +3 -3
  192. package/src/rust-ffi/scalar/crypto.ts +1 -1
  193. package/src/rust-ffi/scalar/factories.ts +73 -0
  194. package/src/rust-ffi/scalar/hashing.ts +1 -1
  195. package/src/rust-ffi/scalar/http.ts +71 -27
  196. package/src/rust-ffi/scalar/index.ts +2 -0
  197. package/src/rust-ffi/scalar/interface.ts +20 -0
  198. package/src/rust-ffi/scalar/payload.ts +11 -3
  199. package/src/rust-ffi/scalar/validation.ts +121 -0
  200. package/src/rust-ffi/text.ts +25 -3
  201. package/src/selection.ts +1 -1
  202. package/src/shared/bytes.ts +24 -4
  203. package/src/shared/codec.ts +18 -3
  204. package/src/task/index.ts +24 -0
  205. package/src/task/op.ts +171 -0
  206. package/src/task/runtime.ts +568 -0
@@ -0,0 +1,220 @@
1
+ // rust/task/runtime.rs — process-wide off-thread task pool.
2
+ //
3
+ // A small, dependency-free, work-sharing pool: one condvar-gated queue and N
4
+ // worker threads. It is deliberately SEPARATE from the rayon global pool
5
+ // (`util/threadpool.rs`), which stays the batch `par_iter` executor — mixing
6
+ // long blocking offloads into rayon would starve batch parallelism.
7
+ //
8
+ // Lifecycle is first-init-wins (`OnceLock` + a `start()` CAS), so the FFI
9
+ // entry, a direct `submit`, and tests all converge on ONE pool per process.
10
+
11
+ use parking_lot::{Condvar, Mutex};
12
+ use std::collections::VecDeque;
13
+ use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
14
+ use std::sync::OnceLock;
15
+ use std::time::Duration;
16
+
17
+ /// Jobs a worker takes per queue-lock acquisition. Amortizes the lock when many
18
+ /// tasks arrive at once (e.g. `Promise.all` over 2 000 items) instead of
19
+ /// re-locking per task.
20
+ const BATCH_POP: usize = 16;
21
+ /// Bounded spin before parking. A job that lands while a worker is between
22
+ /// "queue empty" and "park" is picked up WITHOUT a futex syscall — the common
23
+ /// case for a burst, and pure kernel-time saved.
24
+ const SPIN_ROUNDS: u32 = 64;
25
+ /// Idle park timeout. A bounded wait (instead of an indefinite one) means a
26
+ /// missed wakeup costs at most this much latency and can never wedge a worker.
27
+ const PARK_TIMEOUT: Duration = Duration::from_micros(200);
28
+
29
+ /// A unit of offloaded work. Owns its input bytes (`'static`, `Send`).
30
+ pub type Job = Box<dyn FnOnce() + Send + 'static>;
31
+
32
+ /// The process-wide task pool.
33
+ pub struct TaskPool {
34
+ queue: Mutex<VecDeque<Job>>,
35
+ cv: Condvar,
36
+ running: AtomicBool,
37
+ threads: AtomicU32,
38
+ }
39
+
40
+ static POOL: OnceLock<TaskPool> = OnceLock::new();
41
+
42
+ impl TaskPool {
43
+ fn new() -> Self {
44
+ Self {
45
+ queue: Mutex::new(VecDeque::new()),
46
+ cv: Condvar::new(),
47
+ running: AtomicBool::new(false),
48
+ threads: AtomicU32::new(0),
49
+ }
50
+ }
51
+
52
+ /// Spawn `n` workers. Idempotent: the `running` CAS means only the first
53
+ /// caller actually spawns, so a concurrent `init` + implicit `submit` race
54
+ /// cannot double the pool.
55
+ fn start(&'static self, n: usize) {
56
+ if self.running.swap(true, Ordering::AcqRel) {
57
+ return;
58
+ }
59
+ let n = n.max(1);
60
+ self.threads.store(n as u32, Ordering::Release);
61
+ for i in 0..n {
62
+ let _ = std::thread::Builder::new()
63
+ .name(format!("castrum-task-{i}"))
64
+ .stack_size(512 * 1024)
65
+ .spawn(move || {
66
+ pin_worker(i);
67
+ self.worker();
68
+ });
69
+ }
70
+ }
71
+
72
+ fn worker(&self) {
73
+ // Reused across iterations so the batch path adds no per-task allocation.
74
+ let mut batch: Vec<Job> = Vec::with_capacity(BATCH_POP);
75
+ let mut spin = 0u32;
76
+ loop {
77
+ batch.clear();
78
+ {
79
+ let mut q = self.queue.lock();
80
+ if let Some(first) = q.pop_front() {
81
+ batch.push(first);
82
+ // Only keep pulling while there is genuinely plenty of work
83
+ // left, so one greedy worker cannot starve its idle peers
84
+ // (e.g. 8 tasks on an 11-thread pool must fan out 1:1).
85
+ if q.len() >= self.threads.load(Ordering::Relaxed) as usize {
86
+ for _ in 1..BATCH_POP {
87
+ match q.pop_front() {
88
+ Some(job) => batch.push(job),
89
+ None => break,
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ if !batch.is_empty() {
96
+ spin = 0;
97
+ for job in batch.drain(..) {
98
+ job();
99
+ }
100
+ continue;
101
+ }
102
+ if !self.running.load(Ordering::Acquire) {
103
+ // Exit only once the queue is genuinely empty (a submit that
104
+ // raced the shutdown flag still runs).
105
+ if self.queue.lock().is_empty() {
106
+ break;
107
+ }
108
+ continue;
109
+ }
110
+ // Empty and running: spin briefly, then park with a short timeout.
111
+ if spin < SPIN_ROUNDS {
112
+ spin += 1;
113
+ std::hint::spin_loop();
114
+ continue;
115
+ }
116
+ spin = 0;
117
+ let mut q = self.queue.lock();
118
+ if q.is_empty() && self.running.load(Ordering::Acquire) {
119
+ self.cv.wait_for(&mut q, PARK_TIMEOUT);
120
+ }
121
+ }
122
+ }
123
+
124
+ fn enqueue(&'static self, job: Job) {
125
+ self.queue.lock().push_back(job);
126
+ self.cv.notify_one();
127
+ }
128
+
129
+ fn shutdown(&self) {
130
+ self.running.store(false, Ordering::Release);
131
+ self.cv.notify_all();
132
+ }
133
+ }
134
+
135
+ #[cfg(target_os = "linux")]
136
+ static TASK_CORE_IDS: OnceLock<Option<Vec<core_affinity::CoreId>>> = OnceLock::new();
137
+
138
+ /// Pin a worker to a dedicated core when `CASTRUM_TASK_PIN_CORES` (or the
139
+ /// shared `CASTRUM_PIN_CORES`) is set. Same rationale as the rayon pool's
140
+ /// pinning: stable cache locality and less scheduler migration, at the cost of
141
+ /// flexibility. Core 0 is skipped so the Bun event-loop thread keeps it.
142
+ #[cfg(target_os = "linux")]
143
+ fn pin_worker(id: usize) {
144
+ if std::env::var("CASTRUM_TASK_PIN_CORES").is_err()
145
+ && std::env::var("CASTRUM_PIN_CORES").is_err()
146
+ {
147
+ return;
148
+ }
149
+ let ids = TASK_CORE_IDS.get_or_init(core_affinity::get_core_ids);
150
+ if let Some(ids) = ids {
151
+ if ids.len() > 1 {
152
+ let idx = 1 + (id % (ids.len() - 1));
153
+ let _ = core_affinity::set_for_current(ids[idx]);
154
+ }
155
+ }
156
+ }
157
+
158
+ #[cfg(not(target_os = "linux"))]
159
+ fn pin_worker(_id: usize) {}
160
+
161
+ /// Default worker count: all cores but one (headroom for the Bun event loop),
162
+ /// overridable with `CASTRUM_TASK_THREADS`.
163
+ pub fn default_threads() -> usize {
164
+ std::env::var("CASTRUM_TASK_THREADS")
165
+ .ok()
166
+ .and_then(|v| v.parse::<usize>().ok())
167
+ .filter(|&n| n > 0)
168
+ .unwrap_or_else(|| {
169
+ std::thread::available_parallelism()
170
+ .map(|n| n.get().saturating_sub(1))
171
+ .unwrap_or(1)
172
+ .max(1)
173
+ })
174
+ }
175
+
176
+ /// Start the pool with `threads` workers (`None`/`0` → {@link default_threads}).
177
+ ///
178
+ /// Returns `0` when this call started the pool and `1` when it was already
179
+ /// running (both are success; the codebase keeps non-zero = error).
180
+ pub fn init(threads: Option<u32>) -> u32 {
181
+ let p = POOL.get_or_init(TaskPool::new);
182
+ if p.running.load(Ordering::Acquire) {
183
+ return 1;
184
+ }
185
+ let n = threads
186
+ .filter(|&t| t > 0)
187
+ .map(|t| t as usize)
188
+ .unwrap_or_else(default_threads);
189
+ p.start(n);
190
+ 0
191
+ }
192
+
193
+ /// Enqueue `job`, auto-starting the pool on first use. Always accepted in v1
194
+ /// (the queue is unbounded; admission/backpressure is a later milestone).
195
+ pub fn submit<F>(job: F) -> bool
196
+ where
197
+ F: FnOnce() + Send + 'static,
198
+ {
199
+ let p = POOL.get_or_init(TaskPool::new);
200
+ if !p.running.load(Ordering::Acquire) {
201
+ let n = default_threads();
202
+ p.start(n);
203
+ }
204
+ p.enqueue(Box::new(job));
205
+ true
206
+ }
207
+
208
+ /// Configured worker count (0 before the pool starts).
209
+ pub fn threads() -> u32 {
210
+ POOL.get_or_init(TaskPool::new)
211
+ .threads
212
+ .load(Ordering::Acquire)
213
+ }
214
+
215
+ /// Stop accepting and let idle workers exit. In-flight jobs still complete.
216
+ pub fn shutdown() {
217
+ if let Some(p) = POOL.get() {
218
+ p.shutdown();
219
+ }
220
+ }
@@ -0,0 +1,231 @@
1
+ // rust/task/tests.rs — pool / ring / cancellation / panic / doorbell tests.
2
+ //
3
+ // These touch PROCESS-WIDE state (one pool + one ring per process), so they are
4
+ // serialized by a test mutex rather than relying on the default parallel test
5
+ // threads.
6
+
7
+ use parking_lot::Mutex;
8
+ use std::sync::atomic::{AtomicU32, Ordering};
9
+
10
+ use super::completion::{self, Completion};
11
+ use super::ops::{
12
+ self, OP_GZIP_COMPRESS, OP_GZIP_DECOMPRESS, OP_TEST_PANIC, STATUS_CANCELLED, STATUS_ERROR,
13
+ STATUS_OK,
14
+ };
15
+ use super::runtime;
16
+
17
+ /// Serializes tests that share the global ring/pool/cancel set.
18
+ static LOCK: Mutex<()> = Mutex::new(());
19
+
20
+ fn parse(buf: &[u8], written: usize) -> Vec<(u64, u32, Vec<u8>)> {
21
+ let mut out = Vec::new();
22
+ if written < 4 {
23
+ return out;
24
+ }
25
+ let count = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
26
+ let mut off = 4usize;
27
+ for _ in 0..count {
28
+ if off + 16 > written {
29
+ break;
30
+ }
31
+ let id = u64::from_le_bytes(buf[off..off + 8].try_into().unwrap());
32
+ let status = u32::from_le_bytes(buf[off + 8..off + 12].try_into().unwrap());
33
+ let len = u32::from_le_bytes(buf[off + 12..off + 16].try_into().unwrap()) as usize;
34
+ off += 16;
35
+ if off + len > written {
36
+ break;
37
+ }
38
+ out.push((id, status, buf[off..off + len].to_vec()));
39
+ off += len;
40
+ }
41
+ out
42
+ }
43
+
44
+ /// Drain everything pending into a right-sized buffer (one grow retry).
45
+ fn drain_all() -> Vec<(u64, u32, Vec<u8>)> {
46
+ let mut buf = vec![0u8; 1 << 16];
47
+ loop {
48
+ let w = completion::drain(&mut buf);
49
+ if w == 0 {
50
+ return Vec::new();
51
+ }
52
+ if w > buf.len() {
53
+ buf = vec![0u8; w];
54
+ continue;
55
+ }
56
+ return parse(&buf, w);
57
+ }
58
+ }
59
+
60
+ /// Poll until `id` completes (10 s guard) and return `(status, body)`.
61
+ fn wait_for(id: u64) -> (u32, Vec<u8>) {
62
+ let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
63
+ loop {
64
+ for (rid, status, body) in drain_all() {
65
+ if rid == id {
66
+ return (status, body);
67
+ }
68
+ }
69
+ assert!(
70
+ std::time::Instant::now() < deadline,
71
+ "task {id} never completed"
72
+ );
73
+ std::thread::sleep(std::time::Duration::from_millis(2));
74
+ }
75
+ }
76
+
77
+ fn gzip_args(payload: &[u8]) -> Vec<u8> {
78
+ let compressed = crate::payload::compress::gzip_compress_bytes(payload, 6).unwrap();
79
+ let mut args = Vec::with_capacity(4 + compressed.len());
80
+ args.extend_from_slice(&0u32.to_le_bytes());
81
+ args.extend_from_slice(&compressed);
82
+ args
83
+ }
84
+
85
+ #[test]
86
+ fn submit_runs_off_thread_and_drains() {
87
+ let _g = LOCK.lock();
88
+ completion::clear();
89
+ let payload = b"hello castrum tasks".repeat(64);
90
+ assert!(super::submit_op(
91
+ OP_GZIP_DECOMPRESS,
92
+ gzip_args(&payload),
93
+ 0x1001
94
+ ));
95
+ let (status, body) = wait_for(0x1001);
96
+ assert_eq!(status, STATUS_OK);
97
+ assert_eq!(body, payload);
98
+ }
99
+
100
+ #[test]
101
+ fn a_slice_submit_reads_the_payload_in_place() {
102
+ let _g = LOCK.lock();
103
+ completion::clear();
104
+ // Zero-copy INPUT: only the 4-byte level header crosses the boundary; the
105
+ // payload pointer is read on the worker. The buffer must outlive the task,
106
+ // which is exactly the contract JS upholds by holding `Deferred.keep`.
107
+ let payload = b"zero-copy input payload".repeat(256);
108
+ let hdr = 6u32.to_le_bytes().to_vec();
109
+ assert!(super::submit_slice_op(
110
+ OP_GZIP_COMPRESS,
111
+ hdr,
112
+ payload.as_ptr() as usize,
113
+ payload.len(),
114
+ 0x1009,
115
+ ));
116
+ let (status, body) = wait_for(0x1009);
117
+ assert_eq!(status, STATUS_OK);
118
+
119
+ let restored =
120
+ crate::payload::compress::gzip_decompress_bytes(&body, payload.len() + 1).unwrap();
121
+ assert_eq!(restored, payload);
122
+
123
+ // A null payload with a zero length is an empty input, not a crash.
124
+ completion::clear();
125
+ assert!(super::submit_slice_op(
126
+ OP_GZIP_COMPRESS,
127
+ 6u32.to_le_bytes().to_vec(),
128
+ 0,
129
+ 0,
130
+ 0x100A
131
+ ));
132
+ assert_eq!(wait_for(0x100A).0, STATUS_OK);
133
+ }
134
+
135
+ #[test]
136
+ fn error_status_carries_a_message() {
137
+ let _g = LOCK.lock();
138
+ completion::clear();
139
+ // Too-short args → the op returns an error string, not a panic.
140
+ assert!(super::submit_op(OP_GZIP_DECOMPRESS, vec![], 0x1002));
141
+ let (status, body) = wait_for(0x1002);
142
+ assert_eq!(status, STATUS_ERROR);
143
+ assert!(String::from_utf8_lossy(&body).contains("too short"));
144
+ }
145
+
146
+ #[test]
147
+ fn cancellation_is_acknowledged() {
148
+ let _g = LOCK.lock();
149
+ completion::clear();
150
+ let id = 0x1003;
151
+ assert!(ops::mark_cancelled(id));
152
+ ops::execute_guarded(OP_GZIP_DECOMPRESS, &gzip_args(b"x"), id);
153
+ let (status, _) = wait_for(id);
154
+ assert_eq!(status, STATUS_CANCELLED);
155
+ }
156
+
157
+ #[test]
158
+ fn a_panicking_task_is_contained() {
159
+ let _g = LOCK.lock();
160
+ completion::clear();
161
+ assert!(super::submit_op(OP_TEST_PANIC, vec![], 0x1004));
162
+ let (status, body) = wait_for(0x1004);
163
+ assert_eq!(status, STATUS_ERROR);
164
+ assert!(String::from_utf8_lossy(&body).contains("panicked"));
165
+ }
166
+
167
+ #[test]
168
+ fn drain_uses_the_needed_size_convention() {
169
+ let _g = LOCK.lock();
170
+ completion::clear();
171
+ completion::set_doorbell(0);
172
+ completion::push(Completion {
173
+ id: 7,
174
+ status: STATUS_OK,
175
+ body: vec![1, 2, 3],
176
+ });
177
+ let mut tiny = [0u8; 1];
178
+ let needed = completion::drain(&mut tiny);
179
+ assert_eq!(needed, 4 + 16 + 3);
180
+ // Nothing was consumed, so a right-sized retry still gets it.
181
+ let mut ok = vec![0u8; needed];
182
+ let written = completion::drain(&mut ok);
183
+ assert_eq!(written, needed);
184
+ let parsed = parse(&ok, written);
185
+ assert_eq!(parsed, vec![(7u64, STATUS_OK, vec![1u8, 2, 3])]);
186
+ }
187
+
188
+ static BELLS: AtomicU32 = AtomicU32::new(0);
189
+
190
+ extern "C" fn bell() {
191
+ BELLS.fetch_add(1, Ordering::SeqCst);
192
+ }
193
+
194
+ #[test]
195
+ fn doorbell_coalesces_a_batch() {
196
+ let _g = LOCK.lock();
197
+ completion::clear();
198
+ completion::set_doorbell(bell as *const () as usize);
199
+ BELLS.store(0, Ordering::SeqCst);
200
+ completion::push(Completion {
201
+ id: 1,
202
+ status: STATUS_OK,
203
+ body: vec![0],
204
+ });
205
+ completion::push(Completion {
206
+ id: 2,
207
+ status: STATUS_OK,
208
+ body: vec![0],
209
+ });
210
+ // Both pushes land inside one armed window → exactly one crossing.
211
+ assert_eq!(BELLS.load(Ordering::SeqCst), 1);
212
+ let mut buf = vec![0u8; 4096];
213
+ assert!(completion::drain(&mut buf) > 0);
214
+ // A push after the drain re-arms the bell.
215
+ completion::push(Completion {
216
+ id: 3,
217
+ status: STATUS_OK,
218
+ body: vec![0],
219
+ });
220
+ assert_eq!(BELLS.load(Ordering::SeqCst), 2);
221
+ completion::drain(&mut buf);
222
+ completion::set_doorbell(0);
223
+ completion::clear();
224
+ }
225
+
226
+ #[test]
227
+ fn pool_starts_with_a_nonzero_thread_count() {
228
+ let _g = LOCK.lock();
229
+ runtime::init(None);
230
+ assert!(runtime::threads() >= 1);
231
+ }
@@ -0,0 +1,71 @@
1
+ // rust/test_support.rs — Shared helpers for Rust unit tests (`cargo test`).
2
+ //
3
+ // Only compiled when running tests. Each module's `#[cfg(test)] mod tests`
4
+ // imports whatever it needs from here instead of redefining it.
5
+
6
+ /// Build a packed header buffer:
7
+ /// `[u16 count] repeated { [u16 name_len][name][u32 val_len][val] }`.
8
+ pub(crate) fn pack_headers<'a, I>(pairs: I) -> Vec<u8>
9
+ where
10
+ I: IntoIterator<Item = (&'a str, &'a str)>,
11
+ {
12
+ let pairs: Vec<(&str, &str)> = pairs.into_iter().collect();
13
+ let mut out = Vec::new();
14
+ out.extend_from_slice(&(pairs.len() as u16).to_le_bytes());
15
+ for (name, value) in pairs {
16
+ out.extend_from_slice(&(name.len() as u16).to_le_bytes());
17
+ out.extend_from_slice(name.as_bytes());
18
+ out.extend_from_slice(&(value.len() as u32).to_le_bytes());
19
+ out.extend_from_slice(value.as_bytes());
20
+ }
21
+ out
22
+ }
23
+
24
+ /// Decode a packed pairs buffer:
25
+ /// `[u32 count] repeated { [u32 key_len][key][u32 val_len][val] }`.
26
+ pub(crate) fn decode_packed_pairs(packed: &[u8]) -> Vec<(Vec<u8>, Vec<u8>)> {
27
+ assert!(packed.len() >= 4, "packed buffer too short");
28
+ let count = u32::from_le_bytes([packed[0], packed[1], packed[2], packed[3]]) as usize;
29
+ let mut out = Vec::with_capacity(count);
30
+ let mut pos = 4usize;
31
+ for _ in 0..count {
32
+ let key_len = u32::from_le_bytes([
33
+ packed[pos],
34
+ packed[pos + 1],
35
+ packed[pos + 2],
36
+ packed[pos + 3],
37
+ ]) as usize;
38
+ pos += 4;
39
+ let key = packed[pos..pos + key_len].to_vec();
40
+ pos += key_len;
41
+ let val_len = u32::from_le_bytes([
42
+ packed[pos],
43
+ packed[pos + 1],
44
+ packed[pos + 2],
45
+ packed[pos + 3],
46
+ ]) as usize;
47
+ pos += 4;
48
+ let val = packed[pos..pos + val_len].to_vec();
49
+ pos += val_len;
50
+ out.push((key, val));
51
+ }
52
+ out
53
+ }
54
+
55
+ /// Deterministic xorshift PRNG so fuzz-style tests are reproducible.
56
+ pub(crate) struct Rng(pub u64);
57
+
58
+ impl Rng {
59
+ pub fn next(&mut self) -> u64 {
60
+ let mut x = self.0;
61
+ x ^= x << 13;
62
+ x ^= x >> 7;
63
+ x ^= x << 17;
64
+ self.0 = x;
65
+ x
66
+ }
67
+
68
+ pub fn bytes(&mut self, len: usize) -> Vec<u8> {
69
+ (0..len).map(|_| self.next() as u8).collect()
70
+ }
71
+ }