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.
- package/Cargo.lock +2242 -0
- package/Cargo.toml +155 -0
- package/README.md +14 -6
- package/build.rs +5 -0
- package/castrum.darwin-arm64.node +0 -0
- package/castrum.darwin-x64.node +0 -0
- package/castrum.linux-arm64-gnu.node +0 -0
- package/castrum.linux-arm64-musl.node +0 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/castrum.linux-x64-musl.node +0 -0
- package/castrum.linux-x64-v3-gnu.node +0 -0
- package/castrum.win32-x64-msvc.node +0 -0
- package/dist/index.d.ts +1508 -1291
- package/dist/index.js +3391 -2069
- package/docs/API.md +2 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CASE_STUDY.md +5 -3
- package/docs/ENVIRONMENT.md +25 -2
- package/docs/FFI_BUN_GUIDE.md +242 -14
- package/docs/HOW_TO_ADD_AN_OP.md +10 -0
- package/docs/REPO_MAP.md +17 -6
- package/docs/RND-CONCURRENCY.md +772 -0
- package/docs/bun-builtins-decision-matrix.md +4 -1
- package/index.ts +24 -25
- package/package.json +173 -156
- package/rust/crypto/aead.rs +378 -0
- package/rust/crypto/argon2.rs +265 -0
- package/rust/crypto/base64.rs +420 -0
- package/rust/crypto/bcrypt.rs +89 -0
- package/rust/crypto/cookie_sign.rs +169 -0
- package/rust/crypto/csrf.rs +106 -0
- package/rust/crypto/ed25519.rs +215 -0
- package/rust/crypto/hashing.rs +140 -0
- package/rust/crypto/hmac_sha256.rs +88 -0
- package/rust/crypto/jwt/api.rs +286 -0
- package/rust/crypto/jwt/mod.rs +22 -0
- package/rust/crypto/jwt/tests.rs +312 -0
- package/rust/crypto/jwt/token.rs +360 -0
- package/rust/crypto/mod.rs +27 -0
- package/rust/crypto/pbkdf2.rs +105 -0
- package/rust/crypto/random_token.rs +85 -0
- package/rust/crypto/session.rs +222 -0
- package/rust/ffi/crypto.rs +708 -0
- package/rust/ffi/hashing.rs +285 -0
- package/rust/ffi/http.rs +752 -0
- package/rust/ffi/ingress.rs +303 -0
- package/rust/ffi/json.rs +517 -0
- package/rust/ffi/jwt.rs +419 -0
- package/rust/ffi/metrics.rs +373 -0
- package/rust/ffi/mod.rs +69 -0
- package/rust/ffi/payload.rs +275 -0
- package/rust/ffi/probe.rs +60 -0
- package/rust/ffi/rate_limit.rs +77 -0
- package/rust/ffi/route.rs +85 -0
- package/rust/ffi/task.rs +172 -0
- package/rust/ffi/tests.rs +2333 -0
- package/rust/ffi/util.rs +152 -0
- package/rust/ffi/validators.rs +123 -0
- package/rust/http/accept.rs +606 -0
- package/rust/http/cookie_parser.rs +140 -0
- package/rust/http/etag.rs +289 -0
- package/rust/http/form.rs +136 -0
- package/rust/http/headers.rs +267 -0
- package/rust/http/http_date.rs +286 -0
- package/rust/http/http_parser.rs +252 -0
- package/rust/http/media_type.rs +355 -0
- package/rust/http/method.rs +200 -0
- package/rust/http/mime_lookup.rs +130 -0
- package/rust/http/mod.rs +32 -0
- package/rust/http/multipart.rs +685 -0
- package/rust/http/query_parser.rs +211 -0
- package/rust/http/url_codec.rs +296 -0
- package/rust/http/url_join.rs +324 -0
- package/rust/ingress/api.rs +355 -0
- package/rust/ingress/cors.rs +403 -0
- package/rust/ingress/ingress_constants.rs +209 -0
- package/rust/ingress/ip_trust.rs +312 -0
- package/rust/ingress/mod.rs +48 -0
- package/rust/ingress/native_route.rs +770 -0
- package/rust/ingress/options.rs +176 -0
- package/rust/ingress/output.rs +406 -0
- package/rust/ingress/packed.rs +191 -0
- package/rust/ingress/pipeline.rs +534 -0
- package/rust/ingress/proxy.rs +138 -0
- package/rust/ingress/rate_limit.rs +538 -0
- package/rust/ingress/terminal.rs +114 -0
- package/rust/ingress/tests.rs +567 -0
- package/rust/ingress/time.rs +70 -0
- package/rust/json/fast_schema/capture.rs +336 -0
- package/rust/json/fast_schema/compile.rs +556 -0
- package/rust/json/fast_schema/cursor.rs +368 -0
- package/rust/json/fast_schema/email.rs +282 -0
- package/rust/json/fast_schema/errors.rs +247 -0
- package/rust/json/fast_schema/mod.rs +57 -0
- package/rust/json/fast_schema/tests.rs +1143 -0
- package/rust/json/fast_schema/types.rs +140 -0
- package/rust/json/fast_schema/validate.rs +931 -0
- package/rust/json/json_ops.rs +121 -0
- package/rust/json/json_schema.rs +837 -0
- package/rust/json/json_ser.rs +883 -0
- package/rust/json/mod.rs +19 -0
- package/rust/json/napi_marshal.rs +318 -0
- package/rust/json/patch/api.rs +60 -0
- package/rust/json/patch/engine.rs +153 -0
- package/rust/json/patch/mod.rs +47 -0
- package/rust/json/patch/ops.rs +270 -0
- package/rust/json/patch/pointer.rs +49 -0
- package/rust/json/patch/tests.rs +499 -0
- package/rust/lib.rs +73 -0
- package/rust/metrics/api.rs +148 -0
- package/rust/metrics/mod.rs +21 -0
- package/rust/metrics/registry.rs +961 -0
- package/rust/panic_safety.rs +40 -0
- package/rust/payload/compress.rs +512 -0
- package/rust/payload/mod.rs +13 -0
- package/rust/payload/sse.rs +214 -0
- package/rust/payload/template.rs +253 -0
- package/rust/payload/websocket.rs +69 -0
- package/rust/payload/ws_frames.rs +354 -0
- package/rust/proptest_suite.rs +139 -0
- package/rust/selection.rs +95 -0
- package/rust/src/bin/native-bench.rs +495 -0
- package/rust/task/completion.rs +119 -0
- package/rust/task/mod.rs +65 -0
- package/rust/task/ops.rs +503 -0
- package/rust/task/runtime.rs +220 -0
- package/rust/task/tests.rs +231 -0
- package/rust/test_support.rs +71 -0
- package/rust/util/batch/api.rs +266 -0
- package/rust/util/batch/core.rs +473 -0
- package/rust/util/batch/mod.rs +23 -0
- package/rust/util/batch/tests.rs +445 -0
- package/rust/util/batch_core.rs +184 -0
- package/rust/util/bytes.rs +594 -0
- package/rust/util/mod.rs +34 -0
- package/rust/util/packed.rs +473 -0
- package/rust/util/text.rs +130 -0
- package/rust/util/threadpool.rs +128 -0
- package/rust/util/validation.rs +419 -0
- package/rust-toolchain.toml +17 -0
- package/scripts/postinstall.mjs +199 -0
- package/src/ingress/constants.ts +1 -1
- package/src/ingress/context.ts +2 -2
- package/src/ingress/decode/baked-result.ts +13 -13
- package/src/ingress/decode/fast-result.ts +17 -17
- package/src/ingress/errors.ts +7 -7
- package/src/ingress/headers/baked-templates.ts +1 -1
- package/src/ingress/headers/fast-templates.ts +3 -3
- package/src/ingress/health.ts +1 -2
- package/src/ingress/index.ts +1 -1
- package/src/ingress/native-route.ts +9 -5
- package/src/ingress/options.ts +1 -1
- package/src/ingress/packing/gather-raw-headers.ts +1 -1
- package/src/ingress/packing/route-wire.ts +1 -5
- package/src/ingress/response/error-bodies.ts +5 -5
- package/src/ingress/response/terminal.ts +1 -1
- package/src/ingress/routes/echo.ts +4 -4
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +2 -2
- package/src/ingress/routes/native.ts +2 -1
- package/src/ingress/routes/options.ts +1 -1
- package/src/ingress/routes/read.ts +2 -2
- package/src/ingress/server.ts +10 -6
- package/src/ingress/status.ts +7 -7
- package/src/integration/batch.ts +1 -1
- package/src/integration/index.ts +9 -9
- package/src/integration/pipeline.ts +6 -6
- package/src/integration/websocket.ts +2 -2
- package/src/loader/index.ts +1 -5
- package/src/loader/ops.ts +1 -1
- package/src/native/ffi/build/codecs.ts +130 -12
- package/src/native/ffi/build/compress.ts +22 -10
- package/src/native/ffi/build/instances.ts +46 -4
- package/src/native/ffi/build/metrics.ts +247 -0
- package/src/native/ffi/build/parse.ts +59 -23
- package/src/native/ffi/build/task.ts +79 -0
- package/src/native/ffi/build/util.ts +32 -1
- package/src/native/ffi/build.ts +4 -0
- package/src/native/ffi/selftest.ts +12 -3
- package/src/native/ffi/types.ts +151 -9
- package/src/native/ffi.ts +160 -15
- package/src/native/index.ts +1 -1
- package/src/native/loader.ts +55 -5
- package/src/native/types/instances.ts +34 -0
- package/src/native/types.ts +11 -0
- package/src/runtime/codec.ts +53 -9
- package/src/runtime/types.ts +7 -0
- package/src/rust-ffi/batch/build.ts +16 -9
- package/src/rust-ffi/index.ts +7 -6
- package/src/rust-ffi/options.ts +1 -3
- package/src/rust-ffi/proven.ts +3 -3
- package/src/rust-ffi/scalar/crypto.ts +1 -1
- package/src/rust-ffi/scalar/factories.ts +73 -0
- package/src/rust-ffi/scalar/hashing.ts +1 -1
- package/src/rust-ffi/scalar/http.ts +71 -27
- package/src/rust-ffi/scalar/index.ts +2 -0
- package/src/rust-ffi/scalar/interface.ts +20 -0
- package/src/rust-ffi/scalar/payload.ts +11 -3
- package/src/rust-ffi/scalar/validation.ts +121 -0
- package/src/rust-ffi/text.ts +25 -3
- package/src/selection.ts +1 -1
- package/src/shared/bytes.ts +24 -4
- package/src/shared/codec.ts +18 -3
- package/src/task/index.ts +24 -0
- package/src/task/op.ts +171 -0
- 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
|
+
}
|