kobako 0.3.0 → 0.5.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 -0
- data/CHANGELOG.md +29 -0
- data/Cargo.lock +1 -1
- data/README.md +85 -6
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/lib.rs +4 -2
- data/ext/kobako/src/{wasm → runtime}/cache.rs +22 -18
- data/ext/kobako/src/runtime/capture.rs +91 -0
- data/ext/kobako/src/runtime/config.rs +26 -0
- data/ext/kobako/src/runtime/dispatch.rs +211 -0
- data/ext/kobako/src/runtime/exports.rs +51 -0
- data/ext/kobako/src/runtime/guest_mem.rs +228 -0
- data/ext/kobako/src/{wasm/host_state.rs → runtime/invocation.rs} +195 -81
- data/ext/kobako/src/runtime/trap.rs +134 -0
- data/ext/kobako/src/runtime.rs +782 -0
- data/ext/kobako/src/snapshot.rs +110 -0
- data/lib/kobako/capture.rb +11 -16
- data/lib/kobako/catalog/handles.rb +107 -0
- data/lib/kobako/catalog/namespaces.rb +99 -0
- data/lib/kobako/{snippet/table.rb → catalog/snippets.rb} +37 -62
- data/lib/kobako/catalog.rb +18 -0
- data/lib/kobako/codec/decoder.rb +13 -7
- data/lib/kobako/codec/factory.rb +21 -18
- data/lib/kobako/codec/utils.rb +118 -29
- data/lib/kobako/codec.rb +6 -3
- data/lib/kobako/errors.rb +45 -28
- data/lib/kobako/fault.rb +40 -0
- data/lib/kobako/handle.rb +60 -0
- data/lib/kobako/namespace.rb +67 -0
- data/lib/kobako/outcome.rb +55 -29
- data/lib/kobako/runtime.rb +30 -0
- data/lib/kobako/sandbox.rb +131 -67
- data/lib/kobako/sandbox_options.rb +6 -9
- data/lib/kobako/snapshot.rb +40 -0
- data/lib/kobako/snippet/binary.rb +6 -7
- data/lib/kobako/snippet/source.rb +8 -8
- data/lib/kobako/snippet.rb +7 -9
- data/lib/kobako/transport/dispatcher.rb +195 -0
- data/lib/kobako/transport/error.rb +24 -0
- data/lib/kobako/transport/request.rb +78 -0
- data/lib/kobako/transport/response.rb +69 -0
- data/lib/kobako/transport/run.rb +141 -0
- data/lib/kobako/transport/yield.rb +91 -0
- data/lib/kobako/transport/yielder.rb +89 -0
- data/lib/kobako/transport.rb +24 -0
- data/lib/kobako/usage.rb +41 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +4 -3
- data/release-please-config.json +24 -0
- data/sig/kobako/capture.rbs +0 -2
- data/sig/kobako/{rpc/handle_table.rbs → catalog/handles.rbs} +3 -9
- data/sig/kobako/catalog/namespaces.rbs +17 -0
- data/sig/kobako/{snippet/table.rbs → catalog/snippets.rbs} +2 -11
- data/sig/kobako/{rpc.rbs → catalog.rbs} +1 -1
- data/sig/kobako/codec/decoder.rbs +2 -1
- data/sig/kobako/codec/factory.rbs +3 -3
- data/sig/kobako/codec/utils.rbs +11 -1
- data/sig/kobako/errors.rbs +7 -7
- data/sig/kobako/fault.rbs +19 -0
- data/sig/kobako/handle.rbs +18 -0
- data/sig/kobako/namespace.rbs +19 -0
- data/sig/kobako/outcome.rbs +2 -2
- data/sig/kobako/runtime.rbs +23 -0
- data/sig/kobako/sandbox.rbs +10 -7
- data/sig/kobako/snapshot.rbs +15 -0
- data/sig/kobako/transport/dispatcher.rbs +34 -0
- data/sig/kobako/transport/error.rbs +6 -0
- data/sig/kobako/transport/request.rbs +32 -0
- data/sig/kobako/transport/response.rbs +30 -0
- data/sig/kobako/transport/run.rbs +27 -0
- data/sig/kobako/transport/yield.rbs +34 -0
- data/sig/kobako/transport/yielder.rbs +21 -0
- data/sig/kobako/transport.rbs +4 -0
- data/sig/kobako/usage.rbs +11 -0
- metadata +52 -30
- data/ext/kobako/src/wasm/dispatch.rs +0 -161
- data/ext/kobako/src/wasm/instance.rs +0 -771
- data/ext/kobako/src/wasm.rs +0 -125
- data/lib/kobako/invocation.rb +0 -112
- data/lib/kobako/rpc/dispatcher.rb +0 -169
- data/lib/kobako/rpc/envelope.rb +0 -118
- data/lib/kobako/rpc/fault.rb +0 -41
- data/lib/kobako/rpc/handle.rb +0 -39
- data/lib/kobako/rpc/handle_table.rb +0 -107
- data/lib/kobako/rpc/namespace.rb +0 -74
- data/lib/kobako/rpc/server.rb +0 -158
- data/lib/kobako/rpc.rb +0 -11
- data/lib/kobako/wasm.rb +0 -25
- data/sig/kobako/invocation.rbs +0 -23
- data/sig/kobako/rpc/dispatcher.rbs +0 -33
- data/sig/kobako/rpc/envelope.rbs +0 -51
- data/sig/kobako/rpc/fault.rbs +0 -20
- data/sig/kobako/rpc/handle.rbs +0 -19
- data/sig/kobako/rpc/namespace.rbs +0 -24
- data/sig/kobako/rpc/server.rbs +0 -37
- data/sig/kobako/wasm.rbs +0 -39
|
@@ -1,65 +1,75 @@
|
|
|
1
|
-
//! Per-
|
|
1
|
+
//! Per-invocation host state — the materialised
|
|
2
|
+
//! [SPEC.md Single-Invocation Slot] (one `Invocation` per OS thread
|
|
3
|
+
//! for the lifetime of one `Runtime::eval` / `Runtime::run` call).
|
|
2
4
|
//!
|
|
3
|
-
//! Owned by
|
|
5
|
+
//! Owned by `StoreCell` (a `RefCell` shim wrapping `wasmtime::Store`)
|
|
4
6
|
//! and threaded through every host import — the `__kobako_dispatch`
|
|
5
|
-
//! dispatcher reads the
|
|
6
|
-
//!
|
|
7
|
-
//! every
|
|
7
|
+
//! dispatcher reads the bound dispatch Proc, while the run-path methods
|
|
8
|
+
//! on `crate::runtime::Runtime` install fresh WASI context + pipes
|
|
9
|
+
//! before every invocation (docs/behavior.md B-03 / B-04).
|
|
8
10
|
//!
|
|
9
|
-
//! The
|
|
11
|
+
//! The slot also carries the per-invocation wall-clock deadline
|
|
10
12
|
//! (docs/behavior.md B-01, E-19) and the per-invocation linear-memory
|
|
11
|
-
//! delta cap
|
|
13
|
+
//! delta cap `MemoryLimiter` (docs/behavior.md B-01, E-20). Both are
|
|
12
14
|
//! read from the wasmtime `epoch_deadline_callback` / `ResourceLimiter`
|
|
13
|
-
//! callbacks installed in
|
|
15
|
+
//! callbacks installed in `crate::runtime::Runtime::from_path`. The
|
|
14
16
|
//! memory cap measures only the `memory.grow` delta past the linear-
|
|
15
17
|
//! memory size captured at invocation entry — the mruby image's
|
|
16
18
|
//! initial allocation and prior invocations' watermark are outside the
|
|
17
19
|
//! budget.
|
|
20
|
+
//!
|
|
21
|
+
//! [SPEC.md Single-Invocation Slot]: ../../../SPEC.md
|
|
18
22
|
|
|
19
23
|
use std::cell::{Ref, RefCell, RefMut};
|
|
20
|
-
use std::time::Instant;
|
|
24
|
+
use std::time::{Duration, Instant};
|
|
21
25
|
|
|
22
26
|
use magnus::{value::Opaque, Value};
|
|
23
27
|
use wasmtime::{ResourceLimiter, Store as WtStore};
|
|
24
28
|
use wasmtime_wasi::p1::WasiP1Ctx;
|
|
25
29
|
use wasmtime_wasi::p2::pipe::MemoryOutputPipe;
|
|
26
30
|
|
|
27
|
-
/// Per-
|
|
31
|
+
/// Per-invocation host state — the data half of the Single-Invocation
|
|
32
|
+
/// Slot. Threaded through every host import callback.
|
|
28
33
|
///
|
|
29
|
-
/// All field access is mediated by methods on this type — the WASI ctx
|
|
30
|
-
/// rebuilt fresh before each
|
|
31
|
-
/// Ruby
|
|
32
|
-
/// captured stdout/stderr bytes
|
|
33
|
-
///
|
|
34
|
-
/// are private so the mutation
|
|
35
|
-
|
|
34
|
+
/// All field access is mediated by methods on this type — the WASI ctx
|
|
35
|
+
/// is rebuilt fresh before each invocation via
|
|
36
|
+
/// `Invocation::install_wasi`, the Ruby dispatch Proc is set once via
|
|
37
|
+
/// `Invocation::bind_on_dispatch`, and captured stdout/stderr bytes
|
|
38
|
+
/// are read after the invocation via `Invocation::stdout_bytes` /
|
|
39
|
+
/// `Invocation::stderr_bytes`. The fields are private so the mutation
|
|
40
|
+
/// surface stays narrow.
|
|
41
|
+
pub(super) struct Invocation {
|
|
36
42
|
wasi: Option<WasiP1Ctx>,
|
|
37
43
|
stdout_pipe: Option<MemoryOutputPipe>,
|
|
38
44
|
stderr_pipe: Option<MemoryOutputPipe>,
|
|
39
|
-
|
|
45
|
+
on_dispatch: Option<Opaque<Value>>,
|
|
40
46
|
deadline: Option<Instant>,
|
|
41
|
-
limiter:
|
|
47
|
+
limiter: MemoryLimiter,
|
|
48
|
+
wall_entry: Option<Instant>,
|
|
49
|
+
wall_time: Duration,
|
|
42
50
|
}
|
|
43
51
|
|
|
44
|
-
impl
|
|
52
|
+
impl Invocation {
|
|
45
53
|
/// Build a fresh per-Store host state. `memory_limit` carries the
|
|
46
54
|
/// `Sandbox#memory_limit` cap in bytes (or `None` to disable the cap);
|
|
47
|
-
/// it is read from the wasmtime
|
|
55
|
+
/// it is read from the wasmtime `ResourceLimiter` callback every
|
|
48
56
|
/// time the guest grows linear memory (docs/behavior.md B-01, E-20).
|
|
49
57
|
pub(super) fn new(memory_limit: Option<usize>) -> Self {
|
|
50
58
|
Self {
|
|
51
59
|
wasi: None,
|
|
52
60
|
stdout_pipe: None,
|
|
53
61
|
stderr_pipe: None,
|
|
54
|
-
|
|
62
|
+
on_dispatch: None,
|
|
55
63
|
deadline: None,
|
|
56
|
-
limiter:
|
|
64
|
+
limiter: MemoryLimiter::new(memory_limit),
|
|
65
|
+
wall_entry: None,
|
|
66
|
+
wall_time: Duration::ZERO,
|
|
57
67
|
}
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
/// Install a freshly-built WASI context plus the matching stdout/stderr
|
|
61
|
-
/// pipe clones. Called from
|
|
62
|
-
///
|
|
71
|
+
/// pipe clones. Called from `crate::runtime::Runtime::eval` /
|
|
72
|
+
/// `crate::runtime::Runtime::run` at the top of every guest
|
|
63
73
|
/// invocation (docs/behavior.md B-03 / B-04).
|
|
64
74
|
pub(super) fn install_wasi(
|
|
65
75
|
&mut self,
|
|
@@ -72,10 +82,11 @@ impl HostState {
|
|
|
72
82
|
self.stderr_pipe = Some(stderr);
|
|
73
83
|
}
|
|
74
84
|
|
|
75
|
-
///
|
|
76
|
-
///
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
/// Register the Ruby-side dispatch +Proc+. From this point on, every
|
|
86
|
+
/// `__kobako_dispatch` host import invocation calls the Proc with the
|
|
87
|
+
/// request bytes and expects encoded Response bytes back.
|
|
88
|
+
pub(super) fn bind_on_dispatch(&mut self, proc_value: Opaque<Value>) {
|
|
89
|
+
self.on_dispatch = Some(proc_value);
|
|
79
90
|
}
|
|
80
91
|
|
|
81
92
|
/// Snapshot the bytes captured on guest fd 1 during the most recent
|
|
@@ -96,21 +107,22 @@ impl HostState {
|
|
|
96
107
|
.unwrap_or_default()
|
|
97
108
|
}
|
|
98
109
|
|
|
99
|
-
/// Return the bound
|
|
100
|
-
/// handle is returned by value rather than by reference. None
|
|
101
|
-
///
|
|
102
|
-
|
|
103
|
-
|
|
110
|
+
/// Return the bound dispatch Proc handle. `Opaque<Value>` is `Copy`,
|
|
111
|
+
/// so the handle is returned by value rather than by reference. None
|
|
112
|
+
/// means no Proc has been bound yet via
|
|
113
|
+
/// `Invocation::bind_on_dispatch`.
|
|
114
|
+
pub(super) fn on_dispatch(&self) -> Option<Opaque<Value>> {
|
|
115
|
+
self.on_dispatch
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
/// Mutable handle to the live WASI context. Panics if no context has
|
|
107
119
|
/// been installed yet — every call site is downstream of
|
|
108
|
-
///
|
|
109
|
-
/// `
|
|
120
|
+
/// `Invocation::install_wasi` running at the top of
|
|
121
|
+
/// `Runtime::eval` / `Runtime::run`, so reaching this branch with
|
|
110
122
|
/// `None` signals a host-side wiring bug.
|
|
111
123
|
pub(super) fn wasi_mut(&mut self) -> &mut WasiP1Ctx {
|
|
112
124
|
self.wasi.as_mut().expect(
|
|
113
|
-
"WASI context not initialised — call
|
|
125
|
+
"WASI context not initialised — call Runtime#eval / Runtime#run before any WASI use",
|
|
114
126
|
)
|
|
115
127
|
}
|
|
116
128
|
|
|
@@ -122,19 +134,19 @@ impl HostState {
|
|
|
122
134
|
}
|
|
123
135
|
|
|
124
136
|
/// Return the current per-run deadline. Read from the epoch-deadline
|
|
125
|
-
/// callback installed by
|
|
137
|
+
/// callback installed by `crate::runtime::Runtime::from_path`.
|
|
126
138
|
pub(super) fn deadline(&self) -> Option<Instant> {
|
|
127
139
|
self.deadline
|
|
128
140
|
}
|
|
129
141
|
|
|
130
|
-
/// Mutable handle to the embedded
|
|
131
|
-
/// the wasmtime
|
|
132
|
-
///
|
|
142
|
+
/// Mutable handle to the embedded `MemoryLimiter`. Required by
|
|
143
|
+
/// the wasmtime `ResourceLimiter` callback wiring in
|
|
144
|
+
/// `crate::runtime::Runtime::from_path`
|
|
133
145
|
/// (`store.limiter(|state| state.limiter_mut())`); kept private to
|
|
134
146
|
/// the wasm submodule so the only public surface for arming the
|
|
135
|
-
/// cap goes through
|
|
136
|
-
///
|
|
137
|
-
pub(super) fn limiter_mut(&mut self) -> &mut
|
|
147
|
+
/// cap goes through `Invocation::arm_memory_cap` /
|
|
148
|
+
/// `Invocation::disarm_memory_cap`.
|
|
149
|
+
pub(super) fn limiter_mut(&mut self) -> &mut MemoryLimiter {
|
|
138
150
|
&mut self.limiter
|
|
139
151
|
}
|
|
140
152
|
|
|
@@ -143,7 +155,7 @@ impl HostState {
|
|
|
143
155
|
/// charges only the `memory.grow` delta past `baseline` against
|
|
144
156
|
/// the cap, so the mruby image's initial allocation and the
|
|
145
157
|
/// high-water mark left by prior invocations do not consume the
|
|
146
|
-
/// budget. Paired with
|
|
158
|
+
/// budget. Paired with `Invocation::disarm_memory_cap` around the
|
|
147
159
|
/// call to the corresponding `__kobako_*` export so post-run host
|
|
148
160
|
/// bookkeeping (e.g. fetching the OUTCOME_BUFFER) is not
|
|
149
161
|
/// attributed to the user script.
|
|
@@ -152,10 +164,46 @@ impl HostState {
|
|
|
152
164
|
}
|
|
153
165
|
|
|
154
166
|
/// Disarm the docs/behavior.md E-20 memory cap. See
|
|
155
|
-
///
|
|
167
|
+
/// `Invocation::arm_memory_cap`.
|
|
156
168
|
pub(super) fn disarm_memory_cap(&mut self) {
|
|
157
169
|
self.limiter.deactivate();
|
|
158
170
|
}
|
|
171
|
+
|
|
172
|
+
/// Stamp the wall-clock entry instant for the docs/behavior.md
|
|
173
|
+
/// B-35 `wall_time` measurement. Called at the top of every
|
|
174
|
+
/// invocation immediately before the guest export call so the
|
|
175
|
+
/// bracket matches the `timeout` deadline accounting (B-01) and
|
|
176
|
+
/// excludes post-run host bookkeeping such as `OUTCOME_BUFFER`
|
|
177
|
+
/// decoding.
|
|
178
|
+
pub(super) fn start_wall_clock(&mut self) {
|
|
179
|
+
self.wall_entry = Some(Instant::now());
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/// Close the docs/behavior.md B-35 `wall_time` measurement
|
|
183
|
+
/// started by `Invocation::start_wall_clock`. Idempotent — a
|
|
184
|
+
/// stop with no matching start (e.g. if the guest export call
|
|
185
|
+
/// never executed because of a host-side allocation failure)
|
|
186
|
+
/// leaves the previously-recorded value untouched.
|
|
187
|
+
pub(super) fn stop_wall_clock(&mut self) {
|
|
188
|
+
if let Some(entry) = self.wall_entry.take() {
|
|
189
|
+
self.wall_time = entry.elapsed();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// Return the wall-clock duration the most recent invocation
|
|
194
|
+
/// spent inside the guest export call (docs/behavior.md B-35).
|
|
195
|
+
/// Zero before the first invocation.
|
|
196
|
+
pub(super) fn wall_time(&self) -> Duration {
|
|
197
|
+
self.wall_time
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/// Return the docs/behavior.md B-35 `memory_peak` — the high-
|
|
201
|
+
/// water mark of the per-invocation `memory.grow` delta past the
|
|
202
|
+
/// linear-memory size captured at invocation entry. Zero before
|
|
203
|
+
/// the first invocation.
|
|
204
|
+
pub(super) fn memory_peak(&self) -> usize {
|
|
205
|
+
self.limiter.peak()
|
|
206
|
+
}
|
|
159
207
|
}
|
|
160
208
|
|
|
161
209
|
/// Resource limiter that enforces the per-invocation `memory_limit`
|
|
@@ -163,59 +211,77 @@ impl HostState {
|
|
|
163
211
|
///
|
|
164
212
|
/// `max_memory` is the byte cap on per-invocation growth (`None` disables
|
|
165
213
|
/// the cap). `baseline` is the linear-memory size captured at invocation
|
|
166
|
-
/// entry by
|
|
214
|
+
/// entry by `MemoryLimiter::activate`; the limiter charges only the
|
|
167
215
|
/// `memory.grow` delta past `baseline` against `max_memory`, so the
|
|
168
216
|
/// mruby image's initial allocation and any high-water mark left by
|
|
169
217
|
/// prior invocations on the same Sandbox do not consume the budget.
|
|
170
218
|
/// `cap_active` gates whether the cap is enforced — wasmtime's
|
|
171
219
|
/// `ResourceLimiter` also fires for the module's declared initial
|
|
172
220
|
/// allocation at instantiation time, but the cap stays dormant until
|
|
173
|
-
///
|
|
174
|
-
/// `
|
|
221
|
+
/// `MemoryLimiter::activate` flips the flag for one
|
|
222
|
+
/// `Runtime::eval` / `Runtime::run` call. When `cap_active` is
|
|
175
223
|
/// `false`, the limiter always allows growth.
|
|
176
224
|
///
|
|
177
225
|
/// When `memory.grow` would push the per-invocation delta past
|
|
178
|
-
/// `max_memory`, the limiter returns
|
|
226
|
+
/// `max_memory`, the limiter returns `MemoryLimitTrap` from
|
|
179
227
|
/// `memory_growing`; wasmtime turns that into the trap surfaced to the
|
|
180
228
|
/// host as a guest invocation failure.
|
|
181
229
|
#[derive(Debug, Clone, Copy)]
|
|
182
|
-
pub(super) struct
|
|
230
|
+
pub(super) struct MemoryLimiter {
|
|
183
231
|
max_memory: Option<usize>,
|
|
184
232
|
baseline: usize,
|
|
185
233
|
cap_active: bool,
|
|
234
|
+
peak: usize,
|
|
186
235
|
}
|
|
187
236
|
|
|
188
|
-
impl
|
|
237
|
+
impl MemoryLimiter {
|
|
189
238
|
fn new(max_memory: Option<usize>) -> Self {
|
|
190
239
|
Self {
|
|
191
240
|
max_memory,
|
|
192
241
|
baseline: 0,
|
|
193
242
|
cap_active: false,
|
|
243
|
+
peak: 0,
|
|
194
244
|
}
|
|
195
245
|
}
|
|
196
246
|
|
|
197
247
|
/// Arm the cap so subsequent `memory.grow` calls are charged
|
|
198
248
|
/// against `max_memory` starting from `baseline` bytes. Called via
|
|
199
|
-
///
|
|
249
|
+
/// `Invocation::arm_memory_cap` at the top of every invocation;
|
|
200
250
|
/// the cap is dormant by default — the module's declared initial
|
|
201
251
|
/// memory is allocated during `Linker::instantiate` and the
|
|
202
252
|
/// per-invocation budget excludes anything that existed before
|
|
203
|
-
/// arming (docs/behavior.md B-01 Notes, E-20).
|
|
253
|
+
/// arming (docs/behavior.md B-01 Notes, E-20). Also clears the
|
|
254
|
+
/// per-invocation `MemoryLimiter::peak` high-water so the
|
|
255
|
+
/// docs/behavior.md B-35 `memory_peak` accounting restarts from
|
|
256
|
+
/// zero for the new invocation.
|
|
204
257
|
fn activate(&mut self, baseline: usize) {
|
|
205
258
|
self.baseline = baseline;
|
|
206
259
|
self.cap_active = true;
|
|
260
|
+
self.peak = 0;
|
|
207
261
|
}
|
|
208
262
|
|
|
209
263
|
/// Disarm the cap so post-run host bookkeeping (e.g. fetching the
|
|
210
264
|
/// OUTCOME_BUFFER, which can grow guest memory transiently) is
|
|
211
265
|
/// not attributed to the user script. Paired with
|
|
212
|
-
///
|
|
266
|
+
/// `MemoryLimiter::activate`.
|
|
213
267
|
fn deactivate(&mut self) {
|
|
214
268
|
self.cap_active = false;
|
|
215
269
|
}
|
|
270
|
+
|
|
271
|
+
/// Return the high-water mark of the per-invocation
|
|
272
|
+
/// `memory.grow` delta past `baseline` observed since the last
|
|
273
|
+
/// `MemoryLimiter::activate`. Read after the guest export
|
|
274
|
+
/// returns to populate `Kobako::Usage#memory_peak`
|
|
275
|
+
/// (docs/behavior.md B-35). Pinned to the last accepted grow —
|
|
276
|
+
/// rejected `desired` values that trip the docs/behavior.md E-20
|
|
277
|
+
/// cap never update the peak, so the reported value never exceeds
|
|
278
|
+
/// `memory_limit`.
|
|
279
|
+
pub(super) fn peak(&self) -> usize {
|
|
280
|
+
self.peak
|
|
281
|
+
}
|
|
216
282
|
}
|
|
217
283
|
|
|
218
|
-
impl ResourceLimiter for
|
|
284
|
+
impl ResourceLimiter for MemoryLimiter {
|
|
219
285
|
fn memory_growing(
|
|
220
286
|
&mut self,
|
|
221
287
|
_current: usize,
|
|
@@ -225,12 +291,15 @@ impl ResourceLimiter for KobakoLimiter {
|
|
|
225
291
|
if !self.cap_active {
|
|
226
292
|
return Ok(true);
|
|
227
293
|
}
|
|
294
|
+
let delta = desired.saturating_sub(self.baseline);
|
|
228
295
|
if let Some(limit) = self.max_memory {
|
|
229
|
-
let delta = desired.saturating_sub(self.baseline);
|
|
230
296
|
if delta > limit {
|
|
231
297
|
return Err(wasmtime::Error::new(MemoryLimitTrap { desired, limit }));
|
|
232
298
|
}
|
|
233
299
|
}
|
|
300
|
+
if delta > self.peak {
|
|
301
|
+
self.peak = delta;
|
|
302
|
+
}
|
|
234
303
|
Ok(true)
|
|
235
304
|
}
|
|
236
305
|
|
|
@@ -244,9 +313,9 @@ impl ResourceLimiter for KobakoLimiter {
|
|
|
244
313
|
}
|
|
245
314
|
}
|
|
246
315
|
|
|
247
|
-
/// Marker error returned from
|
|
316
|
+
/// Marker error returned from `MemoryLimiter::memory_growing` on
|
|
248
317
|
/// docs/behavior.md E-20. Downcast from the wasmtime trap error to surface as
|
|
249
|
-
/// `Kobako::
|
|
318
|
+
/// `Kobako::MemoryLimitError` on the Ruby side. Callers use the
|
|
250
319
|
/// `Display` impl below — no field is read directly — so the inner
|
|
251
320
|
/// state stays private.
|
|
252
321
|
#[derive(Debug)]
|
|
@@ -257,7 +326,7 @@ pub(crate) struct MemoryLimitTrap {
|
|
|
257
326
|
|
|
258
327
|
impl MemoryLimitTrap {
|
|
259
328
|
/// Construct a trap with the given +desired+ / +limit+ pair. Used
|
|
260
|
-
/// internally by
|
|
329
|
+
/// internally by `MemoryLimiter::memory_growing` in production and
|
|
261
330
|
/// by the sibling-module +classify_trap+ unit tests to materialise
|
|
262
331
|
/// a representative error for downcast routing.
|
|
263
332
|
#[cfg(test)]
|
|
@@ -270,7 +339,8 @@ impl std::fmt::Display for MemoryLimitTrap {
|
|
|
270
339
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
271
340
|
write!(
|
|
272
341
|
f,
|
|
273
|
-
"
|
|
342
|
+
"memory usage exceeded memory_limit: \
|
|
343
|
+
requested={} bytes, limit={} bytes",
|
|
274
344
|
self.desired, self.limit
|
|
275
345
|
)
|
|
276
346
|
}
|
|
@@ -280,32 +350,32 @@ impl std::error::Error for MemoryLimitTrap {}
|
|
|
280
350
|
|
|
281
351
|
/// Marker error returned from the epoch-deadline callback on
|
|
282
352
|
/// docs/behavior.md E-19. Downcast from the wasmtime trap error to
|
|
283
|
-
/// surface as `Kobako::
|
|
353
|
+
/// surface as `Kobako::TimeoutError` on the Ruby side.
|
|
284
354
|
#[derive(Debug)]
|
|
285
355
|
pub(crate) struct TimeoutTrap;
|
|
286
356
|
|
|
287
357
|
impl std::fmt::Display for TimeoutTrap {
|
|
288
358
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
289
|
-
write!(f, "
|
|
359
|
+
write!(f, "wall-clock deadline exceeded")
|
|
290
360
|
}
|
|
291
361
|
}
|
|
292
362
|
|
|
293
363
|
impl std::error::Error for TimeoutTrap {}
|
|
294
364
|
|
|
295
|
-
/// Interior-mutability wrapper around `wasmtime::Store<
|
|
365
|
+
/// Interior-mutability wrapper around `wasmtime::Store<Invocation>`.
|
|
296
366
|
///
|
|
297
367
|
/// Magnus requires `Send + Sync` for wrapped types. `wasmtime::Store` is not
|
|
298
368
|
/// `Sync`, so we wrap it in a `RefCell`. `RefCell` alone is sufficient
|
|
299
369
|
/// because magnus enforces single-threaded GVL access from Ruby; `Send` and
|
|
300
370
|
/// `Sync` are asserted via the unsafe impls below.
|
|
301
371
|
pub(super) struct StoreCell {
|
|
302
|
-
inner: RefCell<WtStore<
|
|
372
|
+
inner: RefCell<WtStore<Invocation>>,
|
|
303
373
|
}
|
|
304
374
|
|
|
305
375
|
impl StoreCell {
|
|
306
|
-
/// Wrap a freshly-built `wasmtime::Store<
|
|
307
|
-
/// by the magnus-wrapped `
|
|
308
|
-
pub(super) fn new(store: WtStore<
|
|
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 {
|
|
309
379
|
Self {
|
|
310
380
|
inner: RefCell::new(store),
|
|
311
381
|
}
|
|
@@ -313,13 +383,13 @@ impl StoreCell {
|
|
|
313
383
|
|
|
314
384
|
/// Immutable borrow of the wrapped Store. Panics if a `borrow_mut()`
|
|
315
385
|
/// is currently live — matches `RefCell::borrow` semantics.
|
|
316
|
-
pub(super) fn borrow(&self) -> Ref<'_, WtStore<
|
|
386
|
+
pub(super) fn borrow(&self) -> Ref<'_, WtStore<Invocation>> {
|
|
317
387
|
self.inner.borrow()
|
|
318
388
|
}
|
|
319
389
|
|
|
320
390
|
/// Mutable borrow of the wrapped Store. Panics if any other borrow is
|
|
321
391
|
/// currently live — matches `RefCell::borrow_mut` semantics.
|
|
322
|
-
pub(super) fn borrow_mut(&self) -> RefMut<'_, WtStore<
|
|
392
|
+
pub(super) fn borrow_mut(&self) -> RefMut<'_, WtStore<Invocation>> {
|
|
323
393
|
self.inner.borrow_mut()
|
|
324
394
|
}
|
|
325
395
|
}
|
|
@@ -327,10 +397,10 @@ impl StoreCell {
|
|
|
327
397
|
// SAFETY: magnus requires `Send + Sync` on `#[magnus::wrap]` types. Both
|
|
328
398
|
// claims hold under the GVL invariant:
|
|
329
399
|
//
|
|
330
|
-
// * Send — `wasmtime::Store<
|
|
400
|
+
// * Send — `wasmtime::Store<Invocation>` is itself `Send` (verified
|
|
331
401
|
// upstream by wasmtime; see `wasmtime::Store`'s trait impls).
|
|
332
402
|
// `RefCell<T>: Send` whenever `T: Send`. The remaining stored state
|
|
333
|
-
// (`
|
|
403
|
+
// (`Invocation`) holds `Opaque<Value>` for the Ruby Server handle —
|
|
334
404
|
// `Opaque<Value>` is documented as `Send` by magnus precisely so
|
|
335
405
|
// wrapped objects can satisfy this bound.
|
|
336
406
|
//
|
|
@@ -350,24 +420,24 @@ unsafe impl Sync for StoreCell {}
|
|
|
350
420
|
|
|
351
421
|
#[cfg(test)]
|
|
352
422
|
mod tests {
|
|
353
|
-
//! Unit tests for
|
|
423
|
+
//! Unit tests for `MemoryLimiter` — the per-invocation memory
|
|
354
424
|
//! delta cap. The Ruby-facing E2E suite exercises the full path
|
|
355
425
|
//! through wasmtime; these tests pin the pure delta arithmetic so
|
|
356
426
|
//! a regression that breaks the baseline accounting (e.g. dropping
|
|
357
427
|
//! the `baseline` subtraction, or letting `activate` carry stale
|
|
358
428
|
//! state across invocations) is caught without spinning up a
|
|
359
429
|
//! Store.
|
|
360
|
-
use super::{
|
|
430
|
+
use super::{MemoryLimitTrap, MemoryLimiter};
|
|
361
431
|
use wasmtime::ResourceLimiter;
|
|
362
432
|
|
|
363
|
-
fn assert_growing(limiter: &mut
|
|
433
|
+
fn assert_growing(limiter: &mut MemoryLimiter, desired: usize) {
|
|
364
434
|
assert!(
|
|
365
435
|
limiter.memory_growing(0, desired, None).unwrap(),
|
|
366
436
|
"expected memory_growing({desired}) to allow growth"
|
|
367
437
|
);
|
|
368
438
|
}
|
|
369
439
|
|
|
370
|
-
fn assert_trapping(limiter: &mut
|
|
440
|
+
fn assert_trapping(limiter: &mut MemoryLimiter, desired: usize) {
|
|
371
441
|
let err = limiter
|
|
372
442
|
.memory_growing(0, desired, None)
|
|
373
443
|
.expect_err("expected memory_growing to trap");
|
|
@@ -379,7 +449,7 @@ mod tests {
|
|
|
379
449
|
|
|
380
450
|
#[test]
|
|
381
451
|
fn dormant_limiter_allows_any_growth() {
|
|
382
|
-
let mut limiter =
|
|
452
|
+
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
383
453
|
// Without `activate`, the cap is dormant — the module's
|
|
384
454
|
// declared initial allocation must pass through unconditionally.
|
|
385
455
|
assert_growing(&mut limiter, 100 << 20);
|
|
@@ -387,7 +457,7 @@ mod tests {
|
|
|
387
457
|
|
|
388
458
|
#[test]
|
|
389
459
|
fn delta_below_cap_passes_after_activate() {
|
|
390
|
-
let mut limiter =
|
|
460
|
+
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
391
461
|
limiter.activate(2 << 20);
|
|
392
462
|
// baseline=2 MiB, desired=2.5 MiB → delta=0.5 MiB ≤ 1 MiB cap.
|
|
393
463
|
assert_growing(&mut limiter, (2 << 20) + (1 << 19));
|
|
@@ -395,7 +465,7 @@ mod tests {
|
|
|
395
465
|
|
|
396
466
|
#[test]
|
|
397
467
|
fn delta_past_cap_traps_with_memory_limit_trap() {
|
|
398
|
-
let mut limiter =
|
|
468
|
+
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
399
469
|
limiter.activate(2 << 20);
|
|
400
470
|
// baseline=2 MiB, desired=4 MiB → delta=2 MiB > 1 MiB cap.
|
|
401
471
|
assert_trapping(&mut limiter, 4 << 20);
|
|
@@ -403,7 +473,7 @@ mod tests {
|
|
|
403
473
|
|
|
404
474
|
#[test]
|
|
405
475
|
fn activate_resets_baseline_on_each_invocation() {
|
|
406
|
-
let mut limiter =
|
|
476
|
+
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
407
477
|
limiter.activate(2 << 20);
|
|
408
478
|
assert_growing(&mut limiter, (2 << 20) + (1 << 20));
|
|
409
479
|
// Second invocation: linear memory has grown to 3 MiB. Re-arming
|
|
@@ -416,8 +486,52 @@ mod tests {
|
|
|
416
486
|
|
|
417
487
|
#[test]
|
|
418
488
|
fn disabled_cap_ignores_delta_size() {
|
|
419
|
-
let mut limiter =
|
|
489
|
+
let mut limiter = MemoryLimiter::new(None);
|
|
420
490
|
limiter.activate(0);
|
|
421
491
|
assert_growing(&mut limiter, 100 << 20);
|
|
422
492
|
}
|
|
493
|
+
|
|
494
|
+
#[test]
|
|
495
|
+
fn peak_starts_at_zero_before_any_grow() {
|
|
496
|
+
let limiter = MemoryLimiter::new(Some(1 << 20));
|
|
497
|
+
assert_eq!(limiter.peak(), 0);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
#[test]
|
|
501
|
+
fn peak_tracks_high_water_of_delta_past_baseline() {
|
|
502
|
+
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
503
|
+
limiter.activate(2 << 20);
|
|
504
|
+
assert_growing(&mut limiter, (2 << 20) + (1 << 18)); // delta=256 KiB
|
|
505
|
+
assert_growing(&mut limiter, (2 << 20) + (1 << 19)); // delta=512 KiB (new peak)
|
|
506
|
+
assert_growing(&mut limiter, (2 << 20) + (1 << 17)); // delta=128 KiB (below peak)
|
|
507
|
+
assert_eq!(limiter.peak(), 1 << 19);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
#[test]
|
|
511
|
+
fn trap_does_not_update_peak() {
|
|
512
|
+
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
513
|
+
limiter.activate(2 << 20);
|
|
514
|
+
assert_growing(&mut limiter, (2 << 20) + (1 << 19)); // delta=512 KiB
|
|
515
|
+
assert_trapping(&mut limiter, (2 << 20) + (2 << 20)); // would be 2 MiB > 1 MiB cap
|
|
516
|
+
// Peak reflects the last accepted grow, not the rejected desired.
|
|
517
|
+
assert_eq!(limiter.peak(), 1 << 19);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
#[test]
|
|
521
|
+
fn activate_resets_peak_for_new_invocation() {
|
|
522
|
+
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
523
|
+
limiter.activate(2 << 20);
|
|
524
|
+
assert_growing(&mut limiter, (2 << 20) + (1 << 19));
|
|
525
|
+
assert_eq!(limiter.peak(), 1 << 19);
|
|
526
|
+
limiter.activate(3 << 20);
|
|
527
|
+
assert_eq!(limiter.peak(), 0);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
#[test]
|
|
531
|
+
fn disabled_cap_still_tracks_peak() {
|
|
532
|
+
let mut limiter = MemoryLimiter::new(None);
|
|
533
|
+
limiter.activate(1 << 20);
|
|
534
|
+
assert_growing(&mut limiter, (1 << 20) + (4 << 20));
|
|
535
|
+
assert_eq!(limiter.peak(), 4 << 20);
|
|
536
|
+
}
|
|
423
537
|
}
|