kobako 0.9.2 → 0.11.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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +32 -0
- data/Cargo.lock +3 -1
- data/README.md +47 -19
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +12 -2
- data/ext/kobako/src/runtime/ambient.rs +1 -1
- data/ext/kobako/src/runtime/cache.rs +170 -6
- data/ext/kobako/src/runtime/capture.rs +1 -1
- data/ext/kobako/src/runtime/config.rs +3 -4
- data/ext/kobako/src/runtime/dispatch.rs +8 -8
- data/ext/kobako/src/runtime/exports.rs +32 -21
- data/ext/kobako/src/runtime/instance_pre.rs +97 -0
- data/ext/kobako/src/runtime/invocation.rs +36 -93
- data/ext/kobako/src/runtime/trap.rs +5 -5
- data/ext/kobako/src/runtime.rs +389 -403
- data/ext/kobako/src/snapshot.rs +2 -2
- data/lib/kobako/capture.rb +5 -7
- data/lib/kobako/catalog/handles.rb +28 -39
- data/lib/kobako/catalog/namespaces.rb +31 -20
- data/lib/kobako/catalog/snippets.rb +18 -16
- data/lib/kobako/codec/decoder.rb +5 -1
- data/lib/kobako/codec/utils.rb +6 -9
- data/lib/kobako/errors.rb +40 -36
- data/lib/kobako/handle.rb +2 -3
- data/lib/kobako/namespace.rb +17 -6
- data/lib/kobako/outcome.rb +12 -14
- data/lib/kobako/pool.rb +176 -0
- data/lib/kobako/sandbox.rb +68 -88
- data/lib/kobako/sandbox_options.rb +5 -9
- data/lib/kobako/snapshot.rb +2 -4
- data/lib/kobako/snippet/binary.rb +1 -3
- data/lib/kobako/snippet/source.rb +1 -2
- data/lib/kobako/snippet.rb +1 -2
- data/lib/kobako/transport/dispatcher.rb +39 -38
- data/lib/kobako/transport/request.rb +1 -1
- data/lib/kobako/transport/run.rb +23 -28
- data/lib/kobako/transport/yielder.rb +11 -17
- data/lib/kobako/transport.rb +2 -3
- data/lib/kobako/usage.rb +10 -13
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/release-please-config.json +16 -1
- data/sig/kobako/catalog/handles.rbs +0 -2
- data/sig/kobako/errors.rbs +3 -0
- data/sig/kobako/namespace.rbs +2 -0
- data/sig/kobako/pool.rbs +44 -0
- data/sig/kobako/sandbox.rbs +2 -2
- data/sig/kobako/transport/dispatcher.rbs +2 -0
- metadata +4 -1
|
@@ -1,41 +1,48 @@
|
|
|
1
|
-
//!
|
|
1
|
+
//! Per-invocation wasmtime export handles for the host-driven ABI
|
|
2
|
+
//! surface.
|
|
2
3
|
//!
|
|
3
|
-
//! `Runtime::
|
|
4
|
-
//!
|
|
5
|
-
//! `
|
|
6
|
-
//!
|
|
7
|
-
//!
|
|
8
|
-
//!
|
|
9
|
-
//!
|
|
4
|
+
//! `Runtime::instantiate` resolves the ABI exports the run path drives
|
|
5
|
+
//! (`__kobako_eval` / `__kobako_run` / `__kobako_take_outcome` /
|
|
6
|
+
//! `__kobako_alloc`) plus the `memory` export against each fresh
|
|
7
|
+
//! per-invocation instance and bundles their
|
|
8
|
+
//! typed handles here, so the invocation body passes one struct around
|
|
9
|
+
//! rather than re-resolving exports by name at every step. Distinct
|
|
10
|
+
//! from `super::cache` (the process-wide Engine / Module cache): this
|
|
11
|
+
//! carries *which guest function to call*, per invocation.
|
|
10
12
|
//!
|
|
11
|
-
//! `
|
|
12
|
-
//!
|
|
13
|
+
//! `super::dispatch` does not reach this struct — a host import runs
|
|
14
|
+
//! against a `Caller`, so the dispatch path resolves `__kobako_alloc`
|
|
15
|
+
//! and `memory` through `Caller::get_export` instead.
|
|
13
16
|
|
|
14
|
-
use wasmtime::{AsContextMut, Instance as WtInstance, TypedFunc};
|
|
17
|
+
use wasmtime::{AsContextMut, Instance as WtInstance, Memory, TypedFunc};
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
/// The cached host-driven export handles. Each is `Option` because test
|
|
19
|
+
/// The resolved host-driven export handles. Each is `Option` because test
|
|
19
20
|
/// fixtures (a minimal "ping" module) need not provide them; real
|
|
20
21
|
/// `kobako.wasm` always does, and the run-path methods raise a Ruby
|
|
21
|
-
/// `Kobako::TrapError` (via `require_export`) when a
|
|
22
|
+
/// `Kobako::TrapError` (via `require_export` / `require_memory`) when a
|
|
23
|
+
/// handle is `None`.
|
|
24
|
+
///
|
|
25
|
+
/// The handles are indices into the owning Store, not borrows of the
|
|
26
|
+
/// `Instance` — they stay valid for the Store's lifetime, which is why
|
|
27
|
+
/// no `Instance` field is kept.
|
|
22
28
|
pub(crate) struct Exports {
|
|
23
29
|
pub(crate) eval: Option<TypedFunc<(), ()>>,
|
|
24
30
|
pub(crate) run: Option<TypedFunc<(i32, i32), ()>>,
|
|
25
31
|
pub(crate) take_outcome: Option<TypedFunc<(), u64>>,
|
|
32
|
+
pub(crate) alloc: Option<TypedFunc<u32, u32>>,
|
|
33
|
+
pub(crate) memory: Option<Memory>,
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
impl Exports {
|
|
29
|
-
/// Best-effort lookup of the
|
|
30
|
-
///
|
|
37
|
+
/// Best-effort lookup of the host-driven exports against a freshly
|
|
38
|
+
/// instantiated module. Missing exports are not an error here
|
|
31
39
|
/// (the test fixture is a bare module); the host enforces presence at
|
|
32
40
|
/// invocation time. Only the SPEC ABI shapes are accepted —
|
|
33
41
|
/// `__kobako_eval` is `() -> ()`, `__kobako_run` is
|
|
34
|
-
/// `(env_ptr, env_len) -> ()`, `__kobako_take_outcome` is `() -> u64
|
|
42
|
+
/// `(env_ptr, env_len) -> ()`, `__kobako_take_outcome` is `() -> u64`,
|
|
43
|
+
/// `__kobako_alloc` is `(len) -> ptr`
|
|
35
44
|
/// (docs/wire-codec.md § ABI Signatures).
|
|
36
|
-
pub(crate) fn resolve(instance: &WtInstance,
|
|
37
|
-
let mut store_ref = store.borrow_mut();
|
|
38
|
-
let mut ctx = store_ref.as_context_mut();
|
|
45
|
+
pub(crate) fn resolve(instance: &WtInstance, mut ctx: impl AsContextMut) -> Self {
|
|
39
46
|
Self {
|
|
40
47
|
eval: instance
|
|
41
48
|
.get_typed_func::<(), ()>(&mut ctx, "__kobako_eval")
|
|
@@ -46,6 +53,10 @@ impl Exports {
|
|
|
46
53
|
take_outcome: instance
|
|
47
54
|
.get_typed_func::<(), u64>(&mut ctx, "__kobako_take_outcome")
|
|
48
55
|
.ok(),
|
|
56
|
+
alloc: instance
|
|
57
|
+
.get_typed_func::<u32, u32>(&mut ctx, "__kobako_alloc")
|
|
58
|
+
.ok(),
|
|
59
|
+
memory: instance.get_memory(&mut ctx, "memory"),
|
|
49
60
|
}
|
|
50
61
|
}
|
|
51
62
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
//! Per-path cache of pre-instantiated wasmtime artifacts.
|
|
2
|
+
//!
|
|
3
|
+
//! The `Linker` wiring (the WASI preview1 import set plus the
|
|
4
|
+
//! `__kobako_dispatch` host import) and its type-check against the
|
|
5
|
+
//! compiled Module are identical for every `Kobako::Runtime` on the
|
|
6
|
+
//! same Guest Binary — both host closures read all their state from
|
|
7
|
+
//! the `Invocation` inside the calling Store, never from the Runtime.
|
|
8
|
+
//! Caching the resolved `InstancePre` per path leaves only the
|
|
9
|
+
//! `instantiate` call itself on the `Runtime.from_path` hot path.
|
|
10
|
+
//!
|
|
11
|
+
//! Concurrency: see `super::cache` — under Ruby's GVL the Mutex serves
|
|
12
|
+
//! `Sync` bounds rather than real contention.
|
|
13
|
+
|
|
14
|
+
use std::collections::HashMap;
|
|
15
|
+
use std::path::{Path, PathBuf};
|
|
16
|
+
use std::sync::{Mutex, OnceLock};
|
|
17
|
+
|
|
18
|
+
use magnus::{Error as MagnusError, Ruby};
|
|
19
|
+
use wasmtime::{Caller, InstancePre, Linker};
|
|
20
|
+
use wasmtime_wasi::p1;
|
|
21
|
+
|
|
22
|
+
use super::cache::{cached_module, shared_engine};
|
|
23
|
+
use super::invocation::Invocation;
|
|
24
|
+
use super::{dispatch, setup_err, trap};
|
|
25
|
+
|
|
26
|
+
static INSTANCE_PRE_CACHE: OnceLock<Mutex<HashMap<PathBuf, InstancePre<Invocation>>>> =
|
|
27
|
+
OnceLock::new();
|
|
28
|
+
|
|
29
|
+
/// Look up `path` in the per-path `InstancePre` cache, wiring the
|
|
30
|
+
/// Linker and resolving the Module's imports on a miss. Compilation
|
|
31
|
+
/// faults surface through `cached_module`; import-resolution faults
|
|
32
|
+
/// raise `Kobako::SetupError`.
|
|
33
|
+
pub(crate) fn cached_instance_pre(path: &Path) -> Result<InstancePre<Invocation>, MagnusError> {
|
|
34
|
+
let cache = INSTANCE_PRE_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
|
|
35
|
+
|
|
36
|
+
if let Some(pre) = cache
|
|
37
|
+
.lock()
|
|
38
|
+
.expect("instance_pre cache mutex poisoned")
|
|
39
|
+
.get(path)
|
|
40
|
+
.cloned()
|
|
41
|
+
{
|
|
42
|
+
return Ok(pre);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let module = cached_module(path)?;
|
|
46
|
+
let linker = build_linker()?;
|
|
47
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
48
|
+
let pre = linker
|
|
49
|
+
.instantiate_pre(&module)
|
|
50
|
+
.map_err(|e| trap::instantiate_err(&ruby, e))?;
|
|
51
|
+
cache
|
|
52
|
+
.lock()
|
|
53
|
+
.expect("instance_pre cache mutex poisoned")
|
|
54
|
+
.insert(path.to_path_buf(), pre.clone());
|
|
55
|
+
Ok(pre)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Build the host-import `Linker` every Guest Binary instantiates
|
|
59
|
+
/// against.
|
|
60
|
+
fn build_linker() -> Result<Linker<Invocation>, MagnusError> {
|
|
61
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
62
|
+
let mut linker: Linker<Invocation> = Linker::new(shared_engine()?);
|
|
63
|
+
|
|
64
|
+
// Wire the wasmtime-wasi preview1 WASI imports. Routes guest fd 1/2
|
|
65
|
+
// to the MemoryOutputPipes set up before each run via
|
|
66
|
+
// `Runtime::eval`. The closure pulls a `&mut WasiP1Ctx` out of
|
|
67
|
+
// Invocation; the panic semantics live inside `Invocation::wasi_mut`
|
|
68
|
+
// so the wiring stays honest about its precondition.
|
|
69
|
+
p1::add_to_linker_sync(&mut linker, |state: &mut Invocation| state.wasi_mut())
|
|
70
|
+
.map_err(|e| setup_err(&ruby, format!("failed to set up the WASI runtime: {}", e)))?;
|
|
71
|
+
|
|
72
|
+
// `__kobako_dispatch` host import. Signature per docs/wire-codec.md
|
|
73
|
+
// § ABI Signatures:
|
|
74
|
+
// (req_ptr: i32, req_len: i32) -> i64
|
|
75
|
+
// Decodes the Request bytes, dispatches via the Ruby-side
|
|
76
|
+
// dispatch Proc (bound per-Sandbox through `Runtime#on_dispatch=`),
|
|
77
|
+
// allocates a guest buffer through `__kobako_alloc`, writes
|
|
78
|
+
// the Response bytes there, and returns the packed
|
|
79
|
+
// `(ptr<<32)|len`. The dispatcher returns 0 on any wire-layer
|
|
80
|
+
// fault (including no Proc bound); see `dispatch::handle`.
|
|
81
|
+
linker
|
|
82
|
+
.func_wrap(
|
|
83
|
+
"env",
|
|
84
|
+
"__kobako_dispatch",
|
|
85
|
+
|mut caller: Caller<'_, Invocation>, req_ptr: i32, req_len: i32| -> i64 {
|
|
86
|
+
dispatch::handle(&mut caller, req_ptr, req_len)
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
.map_err(|e| {
|
|
90
|
+
setup_err(
|
|
91
|
+
&ruby,
|
|
92
|
+
format!("failed to set up the host callback bridge: {}", e),
|
|
93
|
+
)
|
|
94
|
+
})?;
|
|
95
|
+
|
|
96
|
+
Ok(linker)
|
|
97
|
+
}
|
|
@@ -2,29 +2,27 @@
|
|
|
2
2
|
//! [SPEC.md Single-Invocation Slot] (one `Invocation` per OS thread
|
|
3
3
|
//! for the lifetime of one `Runtime::eval` / `Runtime::run` call).
|
|
4
4
|
//!
|
|
5
|
-
//! Owned
|
|
6
|
-
//! and threaded through every host import —
|
|
7
|
-
//! dispatcher reads the bound dispatch Proc,
|
|
8
|
-
//! on `crate::runtime::Runtime` install
|
|
9
|
-
//!
|
|
5
|
+
//! Owned as the data of each per-invocation `wasmtime::Store`
|
|
6
|
+
//! and threaded through every host import —
|
|
7
|
+
//! the `__kobako_dispatch` dispatcher reads the bound dispatch Proc,
|
|
8
|
+
//! while the run-path methods on `crate::runtime::Runtime` install the
|
|
9
|
+
//! invocation's WASI context + pipes at Store creation.
|
|
10
10
|
//!
|
|
11
11
|
//! The slot also carries the per-invocation wall-clock deadline
|
|
12
|
-
//!
|
|
13
|
-
//! delta cap `MemoryLimiter
|
|
12
|
+
//! and the per-invocation linear-memory
|
|
13
|
+
//! delta cap `MemoryLimiter`. Both are
|
|
14
14
|
//! read from the wasmtime `epoch_deadline_callback` / `ResourceLimiter`
|
|
15
|
-
//! callbacks installed in `crate::runtime::Runtime::
|
|
15
|
+
//! callbacks installed in `crate::runtime::Runtime::new_store`. The
|
|
16
16
|
//! memory cap measures only the `memory.grow` delta past the linear-
|
|
17
|
-
//! memory size captured at invocation entry — the
|
|
18
|
-
//!
|
|
19
|
-
//! budget.
|
|
17
|
+
//! memory size captured at invocation entry — the image's initial
|
|
18
|
+
//! allocation is outside the budget.
|
|
20
19
|
//!
|
|
21
20
|
//! [SPEC.md Single-Invocation Slot]: ../../../../SPEC.md
|
|
22
21
|
|
|
23
|
-
use std::cell::{Ref, RefCell, RefMut};
|
|
24
22
|
use std::time::{Duration, Instant};
|
|
25
23
|
|
|
26
24
|
use magnus::{value::Opaque, Value};
|
|
27
|
-
use wasmtime::
|
|
25
|
+
use wasmtime::ResourceLimiter;
|
|
28
26
|
use wasmtime_wasi::p1::WasiP1Ctx;
|
|
29
27
|
use wasmtime_wasi::p2::pipe::MemoryOutputPipe;
|
|
30
28
|
|
|
@@ -53,7 +51,7 @@ impl Invocation {
|
|
|
53
51
|
/// Build a fresh per-Store host state. `memory_limit` carries the
|
|
54
52
|
/// `Sandbox#memory_limit` cap in bytes (or `None` to disable the cap);
|
|
55
53
|
/// it is read from the wasmtime `ResourceLimiter` callback every
|
|
56
|
-
/// time the guest grows linear memory
|
|
54
|
+
/// time the guest grows linear memory.
|
|
57
55
|
pub(super) fn new(memory_limit: Option<usize>) -> Self {
|
|
58
56
|
Self {
|
|
59
57
|
wasi: None,
|
|
@@ -70,7 +68,7 @@ impl Invocation {
|
|
|
70
68
|
/// Install a freshly-built WASI context plus the matching stdout/stderr
|
|
71
69
|
/// pipe clones. Called from `crate::runtime::Runtime::eval` /
|
|
72
70
|
/// `crate::runtime::Runtime::run` at the top of every guest
|
|
73
|
-
/// invocation
|
|
71
|
+
/// invocation.
|
|
74
72
|
pub(super) fn install_wasi(
|
|
75
73
|
&mut self,
|
|
76
74
|
wasi: WasiP1Ctx,
|
|
@@ -128,20 +126,20 @@ impl Invocation {
|
|
|
128
126
|
|
|
129
127
|
/// Replace the per-run wall-clock deadline. `Some(at)` makes the
|
|
130
128
|
/// epoch-deadline callback trap once `Instant::now() >= at`; `None`
|
|
131
|
-
/// disables the cap. Called at the top of every `#run
|
|
129
|
+
/// disables the cap. Called at the top of every `#run`.
|
|
132
130
|
pub(super) fn set_deadline(&mut self, deadline: Option<Instant>) {
|
|
133
131
|
self.deadline = deadline;
|
|
134
132
|
}
|
|
135
133
|
|
|
136
134
|
/// Return the current per-run deadline. Read from the epoch-deadline
|
|
137
|
-
/// callback installed by `crate::runtime::Runtime::
|
|
135
|
+
/// callback installed by `crate::runtime::Runtime::new_store`.
|
|
138
136
|
pub(super) fn deadline(&self) -> Option<Instant> {
|
|
139
137
|
self.deadline
|
|
140
138
|
}
|
|
141
139
|
|
|
142
140
|
/// Mutable handle to the embedded `MemoryLimiter`. Required by
|
|
143
141
|
/// the wasmtime `ResourceLimiter` callback wiring in
|
|
144
|
-
/// `crate::runtime::Runtime::
|
|
142
|
+
/// `crate::runtime::Runtime::new_store`
|
|
145
143
|
/// (`store.limiter(|state| state.limiter_mut())`); kept private to
|
|
146
144
|
/// the wasm submodule so the only public surface for arming the
|
|
147
145
|
/// cap goes through `Invocation::arm_memory_cap` /
|
|
@@ -150,7 +148,7 @@ impl Invocation {
|
|
|
150
148
|
&mut self.limiter
|
|
151
149
|
}
|
|
152
150
|
|
|
153
|
-
/// Arm the
|
|
151
|
+
/// Arm the memory cap for one guest run with
|
|
154
152
|
/// the current linear-memory size as the baseline. The limiter
|
|
155
153
|
/// charges only the `memory.grow` delta past `baseline` against
|
|
156
154
|
/// the cap, so the mruby image's initial allocation and the
|
|
@@ -163,23 +161,23 @@ impl Invocation {
|
|
|
163
161
|
self.limiter.activate(baseline);
|
|
164
162
|
}
|
|
165
163
|
|
|
166
|
-
/// Disarm the
|
|
164
|
+
/// Disarm the memory cap. See
|
|
167
165
|
/// `Invocation::arm_memory_cap`.
|
|
168
166
|
pub(super) fn disarm_memory_cap(&mut self) {
|
|
169
167
|
self.limiter.deactivate();
|
|
170
168
|
}
|
|
171
169
|
|
|
172
|
-
/// Stamp the wall-clock entry instant for the
|
|
173
|
-
///
|
|
170
|
+
/// Stamp the wall-clock entry instant for the `wall_time`
|
|
171
|
+
/// measurement. Called at the top of every
|
|
174
172
|
/// invocation immediately before the guest export call so the
|
|
175
|
-
/// bracket matches the `timeout` deadline accounting
|
|
173
|
+
/// bracket matches the `timeout` deadline accounting and
|
|
176
174
|
/// excludes post-run host bookkeeping such as `OUTCOME_BUFFER`
|
|
177
175
|
/// decoding.
|
|
178
176
|
pub(super) fn start_wall_clock(&mut self) {
|
|
179
177
|
self.wall_entry = Some(Instant::now());
|
|
180
178
|
}
|
|
181
179
|
|
|
182
|
-
/// Close the
|
|
180
|
+
/// Close the `wall_time` measurement
|
|
183
181
|
/// started by `Invocation::start_wall_clock`. Idempotent — a
|
|
184
182
|
/// stop with no matching start (e.g. if the guest export call
|
|
185
183
|
/// never executed because of a host-side allocation failure)
|
|
@@ -191,13 +189,13 @@ impl Invocation {
|
|
|
191
189
|
}
|
|
192
190
|
|
|
193
191
|
/// Return the wall-clock duration the most recent invocation
|
|
194
|
-
/// spent inside the guest export call
|
|
192
|
+
/// spent inside the guest export call.
|
|
195
193
|
/// Zero before the first invocation.
|
|
196
194
|
pub(super) fn wall_time(&self) -> Duration {
|
|
197
195
|
self.wall_time
|
|
198
196
|
}
|
|
199
197
|
|
|
200
|
-
/// Return the
|
|
198
|
+
/// Return the `memory_peak` — the high-
|
|
201
199
|
/// water mark of the per-invocation `memory.grow` delta past the
|
|
202
200
|
/// linear-memory size captured at invocation entry. Zero before
|
|
203
201
|
/// the first invocation.
|
|
@@ -207,7 +205,7 @@ impl Invocation {
|
|
|
207
205
|
}
|
|
208
206
|
|
|
209
207
|
/// Resource limiter that enforces the per-invocation `memory_limit`
|
|
210
|
-
/// cap
|
|
208
|
+
/// cap.
|
|
211
209
|
///
|
|
212
210
|
/// `max_memory` is the byte cap on per-invocation growth (`None` disables
|
|
213
211
|
/// the cap). `baseline` is the linear-memory size captured at invocation
|
|
@@ -250,9 +248,9 @@ impl MemoryLimiter {
|
|
|
250
248
|
/// the cap is dormant by default — the module's declared initial
|
|
251
249
|
/// memory is allocated during `Linker::instantiate` and the
|
|
252
250
|
/// per-invocation budget excludes anything that existed before
|
|
253
|
-
/// arming
|
|
251
|
+
/// arming. Also clears the
|
|
254
252
|
/// per-invocation `MemoryLimiter::peak` high-water so the
|
|
255
|
-
///
|
|
253
|
+
/// `memory_peak` accounting restarts from
|
|
256
254
|
/// zero for the new invocation.
|
|
257
255
|
fn activate(&mut self, baseline: usize) {
|
|
258
256
|
self.baseline = baseline;
|
|
@@ -271,9 +269,9 @@ impl MemoryLimiter {
|
|
|
271
269
|
/// Return the high-water mark of the per-invocation
|
|
272
270
|
/// `memory.grow` delta past `baseline` observed since the last
|
|
273
271
|
/// `MemoryLimiter::activate`. Read after the guest export
|
|
274
|
-
/// returns to populate `Kobako::Usage#memory_peak
|
|
275
|
-
///
|
|
276
|
-
/// rejected `desired` values that trip the
|
|
272
|
+
/// returns to populate `Kobako::Usage#memory_peak`.
|
|
273
|
+
/// Pinned to the last accepted grow —
|
|
274
|
+
/// rejected `desired` values that trip the memory
|
|
277
275
|
/// cap never update the peak, so the reported value never exceeds
|
|
278
276
|
/// `memory_limit`.
|
|
279
277
|
pub(super) fn peak(&self) -> usize {
|
|
@@ -313,8 +311,9 @@ impl ResourceLimiter for MemoryLimiter {
|
|
|
313
311
|
}
|
|
314
312
|
}
|
|
315
313
|
|
|
316
|
-
/// Marker error returned from `MemoryLimiter::memory_growing`
|
|
317
|
-
///
|
|
314
|
+
/// Marker error returned from `MemoryLimiter::memory_growing` when the
|
|
315
|
+
/// per-invocation memory cap is exceeded. Downcast from the wasmtime
|
|
316
|
+
/// trap error to surface as
|
|
318
317
|
/// `Kobako::MemoryLimitError` on the Ruby side. Callers use the
|
|
319
318
|
/// `Display` impl below — no field is read directly — so the inner
|
|
320
319
|
/// state stays private.
|
|
@@ -348,9 +347,9 @@ impl std::fmt::Display for MemoryLimitTrap {
|
|
|
348
347
|
|
|
349
348
|
impl std::error::Error for MemoryLimitTrap {}
|
|
350
349
|
|
|
351
|
-
/// Marker error returned from the epoch-deadline callback
|
|
352
|
-
///
|
|
353
|
-
/// surface as `Kobako::TimeoutError` on the Ruby side.
|
|
350
|
+
/// Marker error returned from the epoch-deadline callback when the
|
|
351
|
+
/// wall-clock deadline is exceeded. Downcast from the wasmtime trap
|
|
352
|
+
/// error to surface as `Kobako::TimeoutError` on the Ruby side.
|
|
354
353
|
#[derive(Debug)]
|
|
355
354
|
pub(crate) struct TimeoutTrap;
|
|
356
355
|
|
|
@@ -362,62 +361,6 @@ impl std::fmt::Display for TimeoutTrap {
|
|
|
362
361
|
|
|
363
362
|
impl std::error::Error for TimeoutTrap {}
|
|
364
363
|
|
|
365
|
-
/// Interior-mutability wrapper around `wasmtime::Store<Invocation>`.
|
|
366
|
-
///
|
|
367
|
-
/// Magnus requires `Send + Sync` for wrapped types. `wasmtime::Store` is not
|
|
368
|
-
/// `Sync`, so we wrap it in a `RefCell`. `RefCell` alone is sufficient
|
|
369
|
-
/// because magnus enforces single-threaded GVL access from Ruby; `Send` and
|
|
370
|
-
/// `Sync` are asserted via the unsafe impls below.
|
|
371
|
-
pub(super) struct StoreCell {
|
|
372
|
-
inner: RefCell<WtStore<Invocation>>,
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
impl StoreCell {
|
|
376
|
-
/// Wrap a freshly-built `wasmtime::Store<Invocation>` so it can be owned
|
|
377
|
-
/// by the magnus-wrapped `Runtime`.
|
|
378
|
-
pub(super) fn new(store: WtStore<Invocation>) -> Self {
|
|
379
|
-
Self {
|
|
380
|
-
inner: RefCell::new(store),
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/// Immutable borrow of the wrapped Store. Panics if a `borrow_mut()`
|
|
385
|
-
/// is currently live — matches `RefCell::borrow` semantics.
|
|
386
|
-
pub(super) fn borrow(&self) -> Ref<'_, WtStore<Invocation>> {
|
|
387
|
-
self.inner.borrow()
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/// Mutable borrow of the wrapped Store. Panics if any other borrow is
|
|
391
|
-
/// currently live — matches `RefCell::borrow_mut` semantics.
|
|
392
|
-
pub(super) fn borrow_mut(&self) -> RefMut<'_, WtStore<Invocation>> {
|
|
393
|
-
self.inner.borrow_mut()
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// SAFETY: magnus requires `Send + Sync` on `#[magnus::wrap]` types. Both
|
|
398
|
-
// claims hold under the GVL invariant:
|
|
399
|
-
//
|
|
400
|
-
// * Send — `wasmtime::Store<Invocation>` is itself `Send` (verified
|
|
401
|
-
// upstream by wasmtime; see `wasmtime::Store`'s trait impls).
|
|
402
|
-
// `RefCell<T>: Send` whenever `T: Send`. The remaining stored state
|
|
403
|
-
// (`Invocation`) holds `Opaque<Value>` for the Ruby Server handle —
|
|
404
|
-
// `Opaque<Value>` is documented as `Send` by magnus precisely so
|
|
405
|
-
// wrapped objects can satisfy this bound.
|
|
406
|
-
//
|
|
407
|
-
// * Sync — `RefCell` is *not* `Sync` in the general Rust sense
|
|
408
|
-
// (concurrent `borrow_mut` is UB). We assert `Sync` here because the
|
|
409
|
-
// GVL serialises every call into Ruby C and every entry into magnus-
|
|
410
|
-
// wrapped methods onto a single OS thread at a time: by the time the
|
|
411
|
-
// `Sync` bound matters, magnus has already established that only one
|
|
412
|
-
// thread can be inside the wrapper. Cross-thread mutation cannot
|
|
413
|
-
// occur. If a future magnus release adopts a thread model that
|
|
414
|
-
// permits concurrent access to wrapped objects, this assertion would
|
|
415
|
-
// have to revert and `StoreCell` would need to switch to
|
|
416
|
-
// `Mutex<wasmtime::Store<…>>` — but as of magnus 0.8 the contract
|
|
417
|
-
// holds.
|
|
418
|
-
unsafe impl Send for StoreCell {}
|
|
419
|
-
unsafe impl Sync for StoreCell {}
|
|
420
|
-
|
|
421
364
|
#[cfg(test)]
|
|
422
365
|
mod tests {
|
|
423
366
|
//! Unit tests for `MemoryLimiter` — the per-invocation memory
|
|
@@ -17,7 +17,7 @@ use super::invocation::{Invocation, MemoryLimitTrap, TimeoutTrap};
|
|
|
17
17
|
use super::{memory_limit_err, setup_err, timeout_err, trap_err};
|
|
18
18
|
|
|
19
19
|
/// Epoch-deadline callback installed on every Store. Read the per-run
|
|
20
|
-
/// wall-clock deadline from `Invocation`
|
|
20
|
+
/// wall-clock deadline from `Invocation` and trap with
|
|
21
21
|
/// `TimeoutTrap` once the deadline has passed; otherwise extend the
|
|
22
22
|
/// next check by one tick of the process-wide epoch ticker. When the
|
|
23
23
|
/// deadline is `None` the callback should not fire under normal
|
|
@@ -41,9 +41,9 @@ pub(super) fn epoch_deadline_callback(
|
|
|
41
41
|
/// without the magnus surface.
|
|
42
42
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
43
43
|
enum TrapClass {
|
|
44
|
-
///
|
|
44
|
+
/// Wall-clock cap path.
|
|
45
45
|
Timeout,
|
|
46
|
-
///
|
|
46
|
+
/// Linear-memory cap path.
|
|
47
47
|
MemoryLimit,
|
|
48
48
|
/// Any other wasmtime error — surfaces as the base
|
|
49
49
|
/// `Kobako::TrapError`.
|
|
@@ -117,8 +117,8 @@ fn other_trap_message(err: &wasmtime::Error) -> String {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/// Map an instantiation error to `Kobako::SetupError`. Instantiation runs
|
|
120
|
-
/// during `from_path` construction, before any invocation —
|
|
121
|
-
///
|
|
120
|
+
/// during `from_path` construction, before any invocation — every such
|
|
121
|
+
/// failure is a construction setup fault, not a
|
|
122
122
|
/// per-invocation cap outcome. The memory cap is dormant during
|
|
123
123
|
/// instantiation (see `Invocation::arm_memory_cap` /
|
|
124
124
|
/// `Invocation::disarm_memory_cap`) and the epoch deadline is not yet
|