kobako 0.9.2-x86_64-linux → 0.10.0-x86_64-linux
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 +25 -0
- data/README.md +35 -14
- data/data/kobako.wasm +0 -0
- data/lib/kobako/3.3/kobako.so +0 -0
- data/lib/kobako/3.4/kobako.so +0 -0
- data/lib/kobako/4.0/kobako.so +0 -0
- data/lib/kobako/catalog/handles.rb +9 -13
- data/lib/kobako/catalog/namespaces.rb +22 -3
- data/lib/kobako/catalog/snippets.rb +9 -1
- data/lib/kobako/codec/decoder.rb +5 -1
- data/lib/kobako/errors.rb +11 -1
- data/lib/kobako/namespace.rb +16 -2
- data/lib/kobako/pool.rb +182 -0
- data/lib/kobako/sandbox.rb +16 -14
- 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
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a2f9fdb62d68dfbb45d5cc88705b056e869a1f8ea527444a74daae4e2c24a7a
|
|
4
|
+
data.tar.gz: f6fd31dfb518022be1ca9619f5343fdf5a671caeffe451514eb0cde039371c6e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7c61b7745938ff9f51dbf821d2f317ffaf11631651f6bfad695e07824c03fd7013e2d39bac95d31a951fa2b50138575907ce83a35364f7948a6464ed37bac710
|
|
7
|
+
data.tar.gz: c05f84febfd37914fd4fa80d48aea10b7041bdf756d50da749f85f9697e2365a1853e80579399a0d564244514107a7bd62907c8261c978d2143a2b0b55a67141
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{".":"0.
|
|
1
|
+
{".":"0.10.0","wasm/kobako-core":"0.5.0","wasm/kobako":"0.5.0","wasm/kobako-io":"0.5.0","wasm/kobako-regexp":"0.5.0","wasm/kobako-baker":"0.5.0"}
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.0](https://github.com/elct9620/kobako/compare/v0.9.2...v0.10.0) (2026-06-12)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **catalog:** reject member binding after the seal (E-45) ([5193ed6](https://github.com/elct9620/kobako/commit/5193ed64100fa8ac05a2ba18cfa00634b4f40e6f))
|
|
9
|
+
* **guest:** bake the canonical boot state and instantiate per invocation (B-49) ([ee9ae6e](https://github.com/elct9620/kobako/commit/ee9ae6e09eab30f54dba0eeec00a5a2c80da819f))
|
|
10
|
+
* **pool:** add Kobako::Pool warm-Sandbox checkout (B-46..B-48) ([abf9bf8](https://github.com/elct9620/kobako/commit/abf9bf8d3c725c0ca0b8f2ab8b2ddd6f71ee6de4))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **ext:** give the ABI probe a WASI context ([18e21ea](https://github.com/elct9620/kobako/commit/18e21eac8b160ade2724578aeacd86170403ee2c))
|
|
16
|
+
* **ext:** trust the artifact disk cache only in an exclusively writable directory ([17679cc](https://github.com/elct9620/kobako/commit/17679cc0d38d2a1b605e2faaeed762477a718c18))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Performance Improvements
|
|
20
|
+
|
|
21
|
+
* **bench:** re-bless the anchor onto the post-0.9.2 performance round ([195224d](https://github.com/elct9620/kobako/commit/195224d5d48e53dc2be17752e6d6af6382a0c1ec))
|
|
22
|
+
* **catalog:** drop the alloc-path block iteration from the gadget refusal ([542fe59](https://github.com/elct9620/kobako/commit/542fe59464bde57283d8e91984b82e82592bc3ab))
|
|
23
|
+
* **ext:** amortise module compilation across processes via .cwasm cache ([2e688bc](https://github.com/elct9620/kobako/commit/2e688bc4a1cdf1d0d4d5a0bce2efb314a5b8d1f7))
|
|
24
|
+
* **ext:** bound and harden the compiled-artifact cache ([949f222](https://github.com/elct9620/kobako/commit/949f2227af7cdf7d1913dcae58df683912a7dbd5))
|
|
25
|
+
* **ext:** cache ABI export handles and per-path InstancePre ([47573d0](https://github.com/elct9620/kobako/commit/47573d022233c788ce94413d1a2901ee9d62fc2e))
|
|
26
|
+
* **lib:** cache sealed frame encodings and cut decode-walk allocations ([e599573](https://github.com/elct9620/kobako/commit/e599573e37531b363baca83a1aa5833930100320))
|
|
27
|
+
|
|
3
28
|
## [0.9.2](https://github.com/elct9620/kobako/compare/v0.9.1...v0.9.2) (2026-06-11)
|
|
4
29
|
|
|
5
30
|
|
data/README.md
CHANGED
|
@@ -179,7 +179,8 @@ One Sandbox serves many invocations. Service bindings and preloaded snippets per
|
|
|
179
179
|
|
|
180
180
|
──────────────── invocation N ───────────────────
|
|
181
181
|
|
|
182
|
-
1.
|
|
182
|
+
1. start from the canonical boot state
|
|
183
|
+
(mruby pre-initialized into the artifact at build time)
|
|
183
184
|
|
|
184
185
|
2. replay snippets (in insertion order):
|
|
185
186
|
:Adder → defines Adder
|
|
@@ -189,7 +190,7 @@ One Sandbox serves many invocations. Service bindings and preloaded snippets per
|
|
|
189
190
|
|
|
190
191
|
4. return value to host
|
|
191
192
|
|
|
192
|
-
5. discard
|
|
193
|
+
5. discard the instance; reset per-invocation state:
|
|
193
194
|
· Handles invalidated
|
|
194
195
|
· stdout / stderr buffers cleared
|
|
195
196
|
· memory delta zeroed
|
|
@@ -199,6 +200,25 @@ One Sandbox serves many invocations. Service bindings and preloaded snippets per
|
|
|
199
200
|
|
|
200
201
|
For workloads that must be isolated from each other (one Sandbox per tenant, per student submission, per agent session), construct a fresh `Kobako::Sandbox` per scope — wasmtime's Engine and the compiled Module are cached at process scope, so additional Sandboxes amortize cold-start cost automatically.
|
|
201
202
|
|
|
203
|
+
### Pooling
|
|
204
|
+
|
|
205
|
+
For hosts that serve many short invocations, `Kobako::Pool` keeps a bounded set of warm, identically set-up Sandboxes and hands each one to a single exclusive holder at a time ([`docs/behavior.md`](docs/behavior.md) B-46..B-48). Construction forwards every `Sandbox.new` keyword verbatim; the optional block is the per-Sandbox setup window and runs exactly once per constructed Sandbox.
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
pool = Kobako::Pool.new(slots: 4) do |sandbox|
|
|
209
|
+
sandbox.define(:KV).bind(:Lookup, ->(key) { redis.get(key) })
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
pool.with { |sandbox| sandbox.eval(%(KV::Lookup.call("user_42"))) }
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
| Option | Meaning | Default |
|
|
216
|
+
|--------|---------|---------|
|
|
217
|
+
| `slots:` | Upper bound on constructed Sandboxes | required |
|
|
218
|
+
| `checkout_timeout:` | Seconds `#with` waits for a free Sandbox; `nil` waits indefinitely | 5.0 |
|
|
219
|
+
|
|
220
|
+
Sandboxes construct lazily on first demand. `#with` yields a Sandbox with empty output buffers and returns the block's value; at block exit the Sandbox returns to the pool, except a block that raises `Kobako::TrapError` discards its Sandbox and the slot refills by a fresh construction on next demand. A checkout that waits past `checkout_timeout` raises `Kobako::PoolTimeoutError`. There is no teardown verb — a Pool releases everything with its own reachability.
|
|
221
|
+
|
|
202
222
|
### Service Blocks
|
|
203
223
|
|
|
204
224
|
A Service method can accept a guest-supplied block via `&blk` and `yield` into it. The block body runs inside the Wasm guest; `break` / `next` / exceptions follow normal Ruby semantics, scoped to the single dispatch. See [`docs/behavior.md`](docs/behavior.md) B-23..B-30.
|
|
@@ -248,7 +268,7 @@ This is deliberate, not a leak. Handle IDs run to 2³¹ − 1 per invocation and
|
|
|
248
268
|
|
|
249
269
|
### Snippets & Entrypoints
|
|
250
270
|
|
|
251
|
-
`Sandbox#preload` registers named mruby snippets that replay
|
|
271
|
+
`Sandbox#preload` registers named mruby snippets that replay into every invocation's canonical boot state; `Sandbox#run(:Target, *args, **kwargs)` dispatches into a top-level `Object` constant defined by those snippets ([`docs/behavior.md`](docs/behavior.md) B-31..B-33).
|
|
252
272
|
|
|
253
273
|
```ruby
|
|
254
274
|
sandbox = Kobako::Sandbox.new
|
|
@@ -262,7 +282,7 @@ sandbox.run(:Greeter, name: "world") # => "hello, world"
|
|
|
262
282
|
```
|
|
263
283
|
per-invocation replay (every #eval / #run, snippets in insertion order):
|
|
264
284
|
|
|
265
|
-
|
|
285
|
+
canonical boot state
|
|
266
286
|
│
|
|
267
287
|
├──▶ replay :Adder (defines Adder)
|
|
268
288
|
│
|
|
@@ -271,7 +291,7 @@ sandbox.run(:Greeter, name: "world") # => "hello, world"
|
|
|
271
291
|
└──▶ eval(source) -or- run(:Target, *args, **kwargs)
|
|
272
292
|
│
|
|
273
293
|
▼
|
|
274
|
-
return value, then
|
|
294
|
+
return value, then instance discarded
|
|
275
295
|
```
|
|
276
296
|
|
|
277
297
|
`#preload` accepts two payload forms:
|
|
@@ -312,15 +332,16 @@ Order-of-magnitude figures on macOS arm64, Ruby 3.4.7, YJIT off. Absolute values
|
|
|
312
332
|
|
|
313
333
|
| Phase | Cost |
|
|
314
334
|
|--------------------------------------------------------------|-----------------------|
|
|
315
|
-
| First `Sandbox.new`
|
|
316
|
-
|
|
|
317
|
-
|
|
|
318
|
-
| Warm `#
|
|
319
|
-
|
|
|
320
|
-
|
|
|
321
|
-
|
|
|
322
|
-
|
|
323
|
-
|
|
335
|
+
| First `Sandbox.new` ever for a Guest Binary (Module JIT, then disk-cached) | ~500 ms once per machine |
|
|
336
|
+
| First `Sandbox.new` in a fresh process (`.cwasm` cache warm) | ~5 ms one-time |
|
|
337
|
+
| Subsequent `Sandbox.new` (caches warm) | ~30 µs |
|
|
338
|
+
| Warm `#eval("nil")` on a reused Sandbox | ~73 µs |
|
|
339
|
+
| Warm `#run(:Entrypoint, ...)` dispatch | ~104 µs |
|
|
340
|
+
| Service call amortized inside one invocation | ~6.8 µs |
|
|
341
|
+
| Snippet replay per invocation | ~8 µs each |
|
|
342
|
+
| Per additional idle Sandbox (RSS) | ~1 KB |
|
|
343
|
+
|
|
344
|
+
The Cranelift JIT runs once per machine and gem version — the compiled artifact persists in a `.cwasm` disk cache, so later processes deserialize in milliseconds. An idle Sandbox holds no wasm instance (the canonical boot state is baked into the artifact and instantiated per invocation), which is why a thousand idle tenants cost ~32 MB total. `ext/` does not release the GVL during wasmtime execution, so wasm work is GVL-serialized: aggregate throughput stays around 16k `#eval`/s regardless of Thread count, though Ruby-side `#eval` setup still overlaps. A +10% regression on any of the six SPEC-mandated benchmarks blocks release.
|
|
324
345
|
|
|
325
346
|
Regexp is an opt-in capability gem, excluded from the default binary and the gated set; its throughput is tracked in a separate non-gated characterization (`#10` in [`benchmark/README.md`](benchmark/README.md)). There `=~` (~5 µs/match) costs about 4× `match?` (~1.2 µs), because `=~` eagerly builds the `MatchData` and match globals — prefer `match?` for boolean tests.
|
|
326
347
|
|
data/data/kobako.wasm
CHANGED
|
Binary file
|
data/lib/kobako/3.3/kobako.so
CHANGED
|
Binary file
|
data/lib/kobako/3.4/kobako.so
CHANGED
|
Binary file
|
data/lib/kobako/4.0/kobako.so
CHANGED
|
Binary file
|
|
@@ -30,13 +30,6 @@ module Kobako
|
|
|
30
30
|
# +0x7fff_ffff+ (2³¹ − 1). Allocation beyond the cap raises
|
|
31
31
|
# immediately — no silent truncation, no wrap, no ID reuse.
|
|
32
32
|
class Handles
|
|
33
|
-
# Reflective gadget types that are never minted as a Capability Handle
|
|
34
|
-
# ({docs/behavior.md B-43}[link:../../../docs/behavior.md]): wrapping a
|
|
35
|
-
# +Binding+ / +Method+ / +UnboundMethod+ would hand the guest a callable
|
|
36
|
-
# proxy onto host reflection (a returned +Binding+ reaches +Binding#eval+).
|
|
37
|
-
UNWRAPPABLE_TYPES = [Binding, Method, UnboundMethod].freeze
|
|
38
|
-
private_constant :UNWRAPPABLE_TYPES
|
|
39
|
-
|
|
40
33
|
# Build a fresh, empty table. +next_id+ is an internal seam that
|
|
41
34
|
# sets the starting value of the monotonic counter (defaults to 1 per
|
|
42
35
|
# B-15); tests pass a value near +Kobako::Handle::MAX_ID+ to exercise
|
|
@@ -97,13 +90,16 @@ module Kobako
|
|
|
97
90
|
private
|
|
98
91
|
|
|
99
92
|
# Refuse to mint a Capability Handle for a reflective gadget
|
|
100
|
-
# ({
|
|
101
|
-
#
|
|
102
|
-
#
|
|
93
|
+
# ({docs/behavior.md B-43}[link:../../../docs/behavior.md]): a +Binding+ /
|
|
94
|
+
# +Method+ / +UnboundMethod+ would hand the guest a callable proxy onto
|
|
95
|
+
# host reflection (a returned +Binding+ reaches +Binding#eval+). Raising
|
|
96
|
+
# here keeps the rule at the single mint point, so it holds on both the
|
|
97
|
+
# Service-return (B-14) and the +#run+ host→guest auto-wrap (B-34) paths.
|
|
103
98
|
def reject_unwrappable!(object)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
99
|
+
case object
|
|
100
|
+
when Binding, Method, UnboundMethod
|
|
101
|
+
raise SandboxError, "a #{object.class} cannot cross as a Capability Handle"
|
|
102
|
+
end
|
|
107
103
|
end
|
|
108
104
|
|
|
109
105
|
# Guard {#alloc} against issuing an ID past the B-21 cap. Returns +nil+
|
|
@@ -37,6 +37,7 @@ module Kobako
|
|
|
37
37
|
@namespaces = {} # : Hash[String, Kobako::Namespace]
|
|
38
38
|
@handler = handler
|
|
39
39
|
@sealed = false
|
|
40
|
+
@encoded = nil # : String?
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
# Declare or retrieve the Namespace named +name+ (idempotent —
|
|
@@ -80,14 +81,32 @@ module Kobako
|
|
|
80
81
|
# Arrays, so none of the kobako ext types actually fire. Structure:
|
|
81
82
|
# +[["Namespace", ["MemberA", "MemberB"]], ...]+. Returns a binary
|
|
82
83
|
# +String+ of msgpack bytes.
|
|
84
|
+
#
|
|
85
|
+
# Once sealed, the bytes are computed once and reused for every
|
|
86
|
+
# subsequent invocation: B-33 seals Service registration (B-07 /
|
|
87
|
+
# B-08) at the first invocation, so the preamble is exactly the
|
|
88
|
+
# bindings that existed at that moment — a bind reaching a
|
|
89
|
+
# +Kobako::Namespace+ after the seal raises +ArgumentError+ (E-45)
|
|
90
|
+
# and never alters Frame 1.
|
|
83
91
|
def encode
|
|
84
|
-
|
|
92
|
+
return @encoded if @encoded
|
|
93
|
+
|
|
94
|
+
bytes = Codec::Encoder.encode(@namespaces.values.map(&:to_preamble)).freeze
|
|
95
|
+
@encoded = bytes if @sealed
|
|
96
|
+
bytes
|
|
85
97
|
end
|
|
86
98
|
|
|
87
|
-
# Mark the registry as sealed
|
|
88
|
-
#
|
|
99
|
+
# Mark the registry as sealed and propagate the seal to every
|
|
100
|
+
# declared +Kobako::Namespace+
|
|
101
|
+
# ({docs/behavior.md B-33}[link:../../../docs/behavior.md]). Called
|
|
102
|
+
# by +Sandbox+ on the first invocation. After sealing, #define
|
|
103
|
+
# raises ArgumentError (E-18) and +Namespace#bind+ raises
|
|
104
|
+
# ArgumentError (E-45). Idempotent.
|
|
89
105
|
def seal!
|
|
106
|
+
return self if @sealed
|
|
107
|
+
|
|
90
108
|
@sealed = true
|
|
109
|
+
@namespaces.each_value(&:seal!)
|
|
91
110
|
self
|
|
92
111
|
end
|
|
93
112
|
|
|
@@ -31,6 +31,7 @@ module Kobako
|
|
|
31
31
|
|
|
32
32
|
def initialize
|
|
33
33
|
@entries = [] # : Array[Kobako::Snippet::Source | Kobako::Snippet::Binary]
|
|
34
|
+
@encoded = nil # : String?
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
# Serialize the registered snippets to wire bytes. Each entry
|
|
@@ -42,8 +43,14 @@ module Kobako
|
|
|
42
43
|
# carriers — this collection-tier method reads their attributes
|
|
43
44
|
# externally via +entry_payload+ rather than asking each entry to
|
|
44
45
|
# self-encode.
|
|
46
|
+
#
|
|
47
|
+
# The bytes are memoized — the table is replayed verbatim on every
|
|
48
|
+
# invocation after B-33 seals it, so Frame 3 never changes between
|
|
49
|
+
# encodes; {#register} drops the memo while the table is still open.
|
|
45
50
|
def encode
|
|
46
|
-
|
|
51
|
+
return @encoded if @encoded
|
|
52
|
+
|
|
53
|
+
@encoded = Codec::Encoder.encode(@entries.map { |entry| entry_payload(entry) }).freeze
|
|
47
54
|
end
|
|
48
55
|
|
|
49
56
|
# Register one preloaded snippet in either of two forms
|
|
@@ -68,6 +75,7 @@ module Kobako
|
|
|
68
75
|
# missing keywords, wrong types, malformed +name+ (E-34), or
|
|
69
76
|
# duplicate +code:+ +name+ (E-33).
|
|
70
77
|
def register(code: nil, name: nil, binary: nil)
|
|
78
|
+
@encoded = nil
|
|
71
79
|
if binary
|
|
72
80
|
raise ArgumentError, "cannot combine binary: with code: / name:" if code || name
|
|
73
81
|
|
data/lib/kobako/codec/decoder.rb
CHANGED
|
@@ -64,7 +64,11 @@ module Kobako
|
|
|
64
64
|
case value
|
|
65
65
|
when String then Utils.assert_utf8!(value, "str payload") if value.encoding == Encoding::UTF_8
|
|
66
66
|
when Array then value.each { |v| validate_utf8!(v) }
|
|
67
|
-
when Hash
|
|
67
|
+
when Hash
|
|
68
|
+
value.each do |key, val|
|
|
69
|
+
validate_utf8!(key)
|
|
70
|
+
validate_utf8!(val)
|
|
71
|
+
end
|
|
68
72
|
end
|
|
69
73
|
end
|
|
70
74
|
end
|
data/lib/kobako/errors.rb
CHANGED
|
@@ -21,7 +21,7 @@ module Kobako
|
|
|
21
21
|
# call that failed and was not rescued inside the
|
|
22
22
|
# script).
|
|
23
23
|
#
|
|
24
|
-
#
|
|
24
|
+
# Two further branches sit outside the invocation taxonomy:
|
|
25
25
|
#
|
|
26
26
|
# * {SetupError} — construction layer. Raised by `Kobako::Sandbox.new`
|
|
27
27
|
# when the wasm runtime cannot be built from the
|
|
@@ -29,6 +29,9 @@ module Kobako
|
|
|
29
29
|
# ({docs/behavior.md E-40 / E-41}[link:../../docs/behavior.md]).
|
|
30
30
|
# Not an invocation outcome, so it never passes
|
|
31
31
|
# through the two-step attribution decision.
|
|
32
|
+
# * {PoolTimeoutError} — pool checkout layer. Raised by `Kobako::Pool#with`
|
|
33
|
+
# when the checkout wait exceeds +checkout_timeout+
|
|
34
|
+
# ({docs/behavior.md E-46}[link:../../docs/behavior.md]).
|
|
32
35
|
#
|
|
33
36
|
# Subclasses pinned by docs/behavior.md Error Classes:
|
|
34
37
|
#
|
|
@@ -137,4 +140,11 @@ module Kobako
|
|
|
137
140
|
# snippet failures while callers wanting bytecode-specific handling
|
|
138
141
|
# can `rescue Kobako::BytecodeError` directly.
|
|
139
142
|
class BytecodeError < SandboxError; end
|
|
143
|
+
|
|
144
|
+
# Pool checkout layer. Raised by +Kobako::Pool#with+ when the checkout
|
|
145
|
+
# wait exceeded the configured +checkout_timeout+ while every slot was
|
|
146
|
+
# held ({docs/behavior.md E-46}[link:../../docs/behavior.md]). No
|
|
147
|
+
# Sandbox state is touched — retrying succeeds as soon as a holder
|
|
148
|
+
# returns its Sandbox.
|
|
149
|
+
class PoolTimeoutError < Error; end
|
|
140
150
|
end
|
data/lib/kobako/namespace.rb
CHANGED
|
@@ -18,15 +18,20 @@ module Kobako
|
|
|
18
18
|
def initialize(name)
|
|
19
19
|
@name = name
|
|
20
20
|
@members = {} # : Hash[String, untyped]
|
|
21
|
+
@sealed = false
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
# Bind +object+ under +member+ inside this Namespace. +member+ is a
|
|
24
25
|
# constant-form name as a +Symbol+ or +String+. +object+ is any Ruby
|
|
25
26
|
# object that responds to the methods guest code will invoke. Returns
|
|
26
27
|
# +self+ for chaining. Raises +ArgumentError+ when +member+ does not
|
|
27
|
-
# match the constant pattern,
|
|
28
|
-
# bound ({docs/behavior.md B-11}[link:../../docs/behavior.md])
|
|
28
|
+
# match the constant pattern, when a Member of the same name is
|
|
29
|
+
# already bound ({docs/behavior.md B-11}[link:../../docs/behavior.md]),
|
|
30
|
+
# or when the owning Sandbox's first invocation has sealed Service
|
|
31
|
+
# registration ({docs/behavior.md E-45}[link:../../docs/behavior.md]).
|
|
29
32
|
def bind(member, object)
|
|
33
|
+
raise ArgumentError, "cannot bind after first Sandbox invocation" if @sealed
|
|
34
|
+
|
|
30
35
|
member_str = validate_member_name!(member)
|
|
31
36
|
raise ArgumentError, "Member #{@name}::#{member_str} is already bound" if @members.key?(member_str)
|
|
32
37
|
|
|
@@ -34,6 +39,15 @@ module Kobako
|
|
|
34
39
|
self
|
|
35
40
|
end
|
|
36
41
|
|
|
42
|
+
# Mark this Namespace as sealed ({docs/behavior.md B-33}[link:../../docs/behavior.md]).
|
|
43
|
+
# Called by +Kobako::Catalog::Namespaces#seal!+ on the owning
|
|
44
|
+
# Sandbox's first invocation; afterwards {#bind} raises
|
|
45
|
+
# +ArgumentError+ (E-45). Idempotent; returns +self+.
|
|
46
|
+
def seal!
|
|
47
|
+
@sealed = true
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
37
51
|
# Member lookup; raises +KeyError+ when no Member is registered
|
|
38
52
|
# under +member+.
|
|
39
53
|
def fetch(member)
|
data/lib/kobako/pool.rb
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "sandbox"
|
|
5
|
+
|
|
6
|
+
module Kobako
|
|
7
|
+
# Kobako::Pool — a bounded set of warm, identically set-up Sandboxes
|
|
8
|
+
# handed out one exclusive holder at a time
|
|
9
|
+
# ({docs/behavior.md B-46..B-48}[link:../../docs/behavior.md]).
|
|
10
|
+
#
|
|
11
|
+
# Construction forwards every +Kobako::Sandbox.new+ keyword verbatim
|
|
12
|
+
# and holds the optional block as the per-Sandbox setup hook; a
|
|
13
|
+
# checkout prefers an idle Sandbox and constructs a new one only when
|
|
14
|
+
# none is idle and fewer than +slots+ exist (B-46). +#with+ blocks up
|
|
15
|
+
# to +checkout_timeout+ seconds when every slot is held (E-46), applies
|
|
16
|
+
# the +TrapError+ discard-and-recreate contract at checkin (B-47), and
|
|
17
|
+
# the Pool releases everything with its own reachability — there is no
|
|
18
|
+
# teardown verb (B-48).
|
|
19
|
+
class Pool
|
|
20
|
+
# The +#with+ wait bound applied when +checkout_timeout+ is not given
|
|
21
|
+
# ({docs/behavior.md B-46}[link:../../docs/behavior.md]).
|
|
22
|
+
DEFAULT_CHECKOUT_TIMEOUT_SECONDS = 5.0
|
|
23
|
+
|
|
24
|
+
# Build a Pool of up to +slots+ Sandboxes
|
|
25
|
+
# ({docs/behavior.md B-46}[link:../../docs/behavior.md]). +slots+ is
|
|
26
|
+
# a positive Integer; +checkout_timeout+ bounds the +#with+ wait in
|
|
27
|
+
# seconds (+nil+ waits indefinitely); every other keyword is
|
|
28
|
+
# forwarded verbatim to +Kobako::Sandbox.new+. The optional block
|
|
29
|
+
# runs exactly once per constructed Sandbox — it is the setup window
|
|
30
|
+
# for +#define+ / +#preload+ before that Sandbox's first checkout.
|
|
31
|
+
# No Sandbox is constructed here. Raises +ArgumentError+ for an
|
|
32
|
+
# invalid +slots+ / +checkout_timeout+
|
|
33
|
+
# ({docs/behavior.md E-47}[link:../../docs/behavior.md]).
|
|
34
|
+
def initialize(slots:, checkout_timeout: DEFAULT_CHECKOUT_TIMEOUT_SECONDS, **sandbox_options, &setup)
|
|
35
|
+
validate_slots!(slots)
|
|
36
|
+
@slots = slots
|
|
37
|
+
@checkout_timeout = normalize_checkout_timeout(checkout_timeout)
|
|
38
|
+
@sandbox_options = sandbox_options
|
|
39
|
+
@setup = setup
|
|
40
|
+
@idle = [] # : Array[Kobako::Sandbox]
|
|
41
|
+
@constructed = 0
|
|
42
|
+
@mutex = Mutex.new
|
|
43
|
+
@slot_freed = ConditionVariable.new
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Yield one exclusively-held Sandbox to the block and return the
|
|
47
|
+
# block's value ({docs/behavior.md B-47}[link:../../docs/behavior.md]).
|
|
48
|
+
# Blocks while every slot is held; raises +Kobako::PoolTimeoutError+
|
|
49
|
+
# once the wait exceeds +checkout_timeout+
|
|
50
|
+
# ({docs/behavior.md E-46}[link:../../docs/behavior.md]). The Sandbox
|
|
51
|
+
# returns to the pool at block exit — unless the block raised
|
|
52
|
+
# +Kobako::TrapError+, in which case the unrecoverable Sandbox is
|
|
53
|
+
# discarded and its slot refills by a fresh construction on next
|
|
54
|
+
# demand.
|
|
55
|
+
def with
|
|
56
|
+
sandbox = checkout
|
|
57
|
+
begin
|
|
58
|
+
yield sandbox
|
|
59
|
+
rescue TrapError
|
|
60
|
+
release_capacity!
|
|
61
|
+
sandbox = nil
|
|
62
|
+
raise
|
|
63
|
+
ensure
|
|
64
|
+
checkin(sandbox) if sandbox
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Acquire a Sandbox and hand it over in pre-invocation state — empty
|
|
71
|
+
# output buffers and truncation predicates false (B-47).
|
|
72
|
+
def checkout
|
|
73
|
+
acquire.tap(&:reset_invocation_state!)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The idle-first claim loop (B-46): an idle Sandbox wins, unclaimed
|
|
77
|
+
# capacity constructs, and a full pool waits for a checkin.
|
|
78
|
+
def acquire
|
|
79
|
+
timeout = @checkout_timeout
|
|
80
|
+
deadline = timeout && (monotonic_now + timeout)
|
|
81
|
+
loop do
|
|
82
|
+
action, sandbox = claim_or_wait(deadline)
|
|
83
|
+
return sandbox if action == :idle && sandbox
|
|
84
|
+
return construct_slot if action == :build
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Single locked decision point for one claim attempt. Waiting
|
|
89
|
+
# happens inside the lock (so a checkin can wake it); construction
|
|
90
|
+
# happens outside (so a slow setup block never holds the lock) —
|
|
91
|
+
# capacity is reserved here and released by +construct_slot+ on
|
|
92
|
+
# failure.
|
|
93
|
+
def claim_or_wait(deadline)
|
|
94
|
+
@mutex.synchronize do
|
|
95
|
+
return [:idle, @idle.pop] unless @idle.empty?
|
|
96
|
+
|
|
97
|
+
if @constructed < @slots
|
|
98
|
+
@constructed += 1
|
|
99
|
+
return [:build, nil]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
await_slot!(deadline)
|
|
103
|
+
[:retry, nil]
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Wait for a checkin or freed capacity; raises
|
|
108
|
+
# +Kobako::PoolTimeoutError+ once +deadline+ has passed (E-46). Must
|
|
109
|
+
# run while holding +@mutex+.
|
|
110
|
+
def await_slot!(deadline)
|
|
111
|
+
remaining = deadline && (deadline - monotonic_now)
|
|
112
|
+
if remaining && remaining <= 0
|
|
113
|
+
raise PoolTimeoutError,
|
|
114
|
+
"no Sandbox returned within #{@checkout_timeout}s: all #{@slots} slots are held"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
@slot_freed.wait(@mutex, remaining)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Construct and set up one pooled Sandbox against the capacity
|
|
121
|
+
# reserved by +claim_or_wait+. Construction and setup-block errors
|
|
122
|
+
# propagate to the checkout caller unchanged (B-46); the reserved
|
|
123
|
+
# capacity is released so a later checkout can retry.
|
|
124
|
+
def construct_slot
|
|
125
|
+
done = false
|
|
126
|
+
sandbox = Sandbox.new(**@sandbox_options)
|
|
127
|
+
@setup&.call(sandbox)
|
|
128
|
+
done = true
|
|
129
|
+
sandbox
|
|
130
|
+
ensure
|
|
131
|
+
release_capacity! unless done
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Return a Sandbox to the idle list and wake one waiting checkout.
|
|
135
|
+
def checkin(sandbox)
|
|
136
|
+
@mutex.synchronize do
|
|
137
|
+
@idle.push(sandbox)
|
|
138
|
+
@slot_freed.signal
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Give back reserved-but-unfilled capacity — a failed construction or
|
|
143
|
+
# a discarded Sandbox — and wake one waiting checkout to claim it.
|
|
144
|
+
def release_capacity!
|
|
145
|
+
@mutex.synchronize do
|
|
146
|
+
@constructed -= 1
|
|
147
|
+
@slot_freed.signal
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# The wait deadline runs on the monotonic clock so a wall-clock jump
|
|
152
|
+
# cannot stretch or cut the checkout wait.
|
|
153
|
+
def monotonic_now
|
|
154
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# E-47 pre-flight for +slots+ — no coercion, a positive Integer is
|
|
158
|
+
# the only accepted shape.
|
|
159
|
+
def validate_slots!(slots)
|
|
160
|
+
return if slots.is_a?(Integer) && slots.positive?
|
|
161
|
+
|
|
162
|
+
raise ArgumentError, "slots must be a positive Integer, got #{slots.inspect}"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Coerce +checkout_timeout+ into the Float seconds the wait loop
|
|
166
|
+
# consumes, or +nil+ to wait indefinitely — the E-39 normalisation
|
|
167
|
+
# idiom applied to E-47.
|
|
168
|
+
def normalize_checkout_timeout(checkout_timeout)
|
|
169
|
+
return nil if checkout_timeout.nil?
|
|
170
|
+
unless checkout_timeout.is_a?(Numeric)
|
|
171
|
+
raise ArgumentError, "checkout_timeout must be Numeric or nil, got #{checkout_timeout.inspect}"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
seconds = checkout_timeout.to_f
|
|
175
|
+
unless seconds.positive? && seconds.finite?
|
|
176
|
+
raise ArgumentError, "checkout_timeout must be > 0 and finite (got #{checkout_timeout})"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
seconds
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
data/lib/kobako/sandbox.rb
CHANGED
|
@@ -206,6 +206,22 @@ module Kobako
|
|
|
206
206
|
end
|
|
207
207
|
end
|
|
208
208
|
|
|
209
|
+
# Reset all per-invocation observable state to its pre-invocation
|
|
210
|
+
# sentinels — both per-channel captures
|
|
211
|
+
# ({docs/behavior.md B-05}[link:../../docs/behavior.md]) and the
|
|
212
|
+
# per-last-invocation usage record
|
|
213
|
+
# ({docs/behavior.md B-35}[link:../../docs/behavior.md]). Shared by
|
|
214
|
+
# +#initialize+ (first-time setup) and +#begin_invocation!+
|
|
215
|
+
# (between-invocation reset) so both paths agree on what
|
|
216
|
+
# "pre-invocation state" means; +Kobako::Pool+ calls it at checkout
|
|
217
|
+
# so a pooled Sandbox hands over empty output buffers
|
|
218
|
+
# ({docs/behavior.md B-47}[link:../../docs/behavior.md]).
|
|
219
|
+
def reset_invocation_state!
|
|
220
|
+
@stdout_capture = Capture::EMPTY
|
|
221
|
+
@stderr_capture = Capture::EMPTY
|
|
222
|
+
@usage = Usage::EMPTY
|
|
223
|
+
end
|
|
224
|
+
|
|
209
225
|
private
|
|
210
226
|
|
|
211
227
|
# Configure the +Runtime+'s host↔guest dispatch wiring
|
|
@@ -237,20 +253,6 @@ module Kobako
|
|
|
237
253
|
reset_invocation_state!
|
|
238
254
|
end
|
|
239
255
|
|
|
240
|
-
# Reset all per-invocation observable state to its pre-invocation
|
|
241
|
-
# sentinels — both per-channel captures
|
|
242
|
-
# ({docs/behavior.md B-05}[link:../../docs/behavior.md]) and the
|
|
243
|
-
# per-last-invocation usage record
|
|
244
|
-
# ({docs/behavior.md B-35}[link:../../docs/behavior.md]). Shared by
|
|
245
|
-
# +#initialize+ (first-time setup) and +#begin_invocation!+
|
|
246
|
-
# (between-invocation reset) so both paths agree on what
|
|
247
|
-
# "pre-invocation state" means.
|
|
248
|
-
def reset_invocation_state!
|
|
249
|
-
@stdout_capture = Capture::EMPTY
|
|
250
|
-
@stderr_capture = Capture::EMPTY
|
|
251
|
-
@usage = Usage::EMPTY
|
|
252
|
-
end
|
|
253
|
-
|
|
254
256
|
# Read the per-last-invocation +wall_time+ and +memory_peak+ from
|
|
255
257
|
# the ext and wrap them as a +Kobako::Usage+ value object
|
|
256
258
|
# ({docs/behavior.md B-35}[link:../../docs/behavior.md]). Runs in
|
data/lib/kobako/version.rb
CHANGED
data/lib/kobako.rb
CHANGED
data/release-please-config.json
CHANGED
|
@@ -73,13 +73,28 @@
|
|
|
73
73
|
"path": "/wasm/kobako-regexp/README.md"
|
|
74
74
|
}
|
|
75
75
|
]
|
|
76
|
+
},
|
|
77
|
+
"wasm/kobako-baker": {
|
|
78
|
+
"component": "kobako-baker",
|
|
79
|
+
"release-type": "rust",
|
|
80
|
+
"extra-files": [
|
|
81
|
+
{
|
|
82
|
+
"type": "toml",
|
|
83
|
+
"path": "/wasm/kobako-baker/Cargo.lock",
|
|
84
|
+
"jsonpath": "$.package[?(@.name=='kobako-baker')].version"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"type": "generic",
|
|
88
|
+
"path": "/wasm/kobako-baker/README.md"
|
|
89
|
+
}
|
|
90
|
+
]
|
|
76
91
|
}
|
|
77
92
|
},
|
|
78
93
|
"plugins": [
|
|
79
94
|
{
|
|
80
95
|
"type": "linked-versions",
|
|
81
96
|
"groupName": "kobako guest crates",
|
|
82
|
-
"components": ["kobako-core", "kobako-rs", "kobako-io", "kobako-regexp"]
|
|
97
|
+
"components": ["kobako-core", "kobako-rs", "kobako-io", "kobako-regexp", "kobako-baker"]
|
|
83
98
|
}
|
|
84
99
|
],
|
|
85
100
|
"extra-files": [
|
data/sig/kobako/errors.rbs
CHANGED
data/sig/kobako/namespace.rbs
CHANGED
data/sig/kobako/pool.rbs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Kobako
|
|
2
|
+
class Pool
|
|
3
|
+
DEFAULT_CHECKOUT_TIMEOUT_SECONDS: Float
|
|
4
|
+
|
|
5
|
+
@slots: Integer
|
|
6
|
+
@checkout_timeout: Float?
|
|
7
|
+
@sandbox_options: Hash[Symbol, untyped]
|
|
8
|
+
@setup: ^(Kobako::Sandbox) -> void | nil
|
|
9
|
+
@idle: Array[Kobako::Sandbox]
|
|
10
|
+
@constructed: Integer
|
|
11
|
+
@mutex: Thread::Mutex
|
|
12
|
+
@slot_freed: Thread::ConditionVariable
|
|
13
|
+
|
|
14
|
+
def initialize: (
|
|
15
|
+
slots: Integer,
|
|
16
|
+
?checkout_timeout: (Float | Integer)?,
|
|
17
|
+
**untyped sandbox_options
|
|
18
|
+
) ?{ (Kobako::Sandbox) -> void } -> void
|
|
19
|
+
|
|
20
|
+
def with: [T] () { (Kobako::Sandbox) -> T } -> T
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def checkout: () -> Kobako::Sandbox
|
|
25
|
+
|
|
26
|
+
def acquire: () -> Kobako::Sandbox
|
|
27
|
+
|
|
28
|
+
def claim_or_wait: (Float? deadline) -> [Symbol, Kobako::Sandbox?]
|
|
29
|
+
|
|
30
|
+
def await_slot!: (Float? deadline) -> void
|
|
31
|
+
|
|
32
|
+
def construct_slot: () -> Kobako::Sandbox
|
|
33
|
+
|
|
34
|
+
def checkin: (Kobako::Sandbox sandbox) -> void
|
|
35
|
+
|
|
36
|
+
def release_capacity!: () -> void
|
|
37
|
+
|
|
38
|
+
def monotonic_now: () -> Float
|
|
39
|
+
|
|
40
|
+
def validate_slots!: (untyped slots) -> void
|
|
41
|
+
|
|
42
|
+
def normalize_checkout_timeout: ((Float | Integer)? checkout_timeout) -> Float?
|
|
43
|
+
end
|
|
44
|
+
end
|
data/sig/kobako/sandbox.rbs
CHANGED
|
@@ -38,12 +38,12 @@ module Kobako
|
|
|
38
38
|
|
|
39
39
|
def eval: (String code) -> untyped
|
|
40
40
|
|
|
41
|
+
def reset_invocation_state!: () -> void
|
|
42
|
+
|
|
41
43
|
private
|
|
42
44
|
|
|
43
45
|
def install_dispatch_proc!: () -> void
|
|
44
46
|
|
|
45
|
-
def reset_invocation_state!: () -> void
|
|
46
|
-
|
|
47
47
|
def begin_invocation!: () -> void
|
|
48
48
|
|
|
49
49
|
def read_usage!: () -> void
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kobako
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: x86_64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Aotokitsuruya
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: msgpack
|
|
@@ -59,6 +59,7 @@ files:
|
|
|
59
59
|
- lib/kobako/namespace.rb
|
|
60
60
|
- lib/kobako/outcome.rb
|
|
61
61
|
- lib/kobako/outcome/panic.rb
|
|
62
|
+
- lib/kobako/pool.rb
|
|
62
63
|
- lib/kobako/runtime.rb
|
|
63
64
|
- lib/kobako/sandbox.rb
|
|
64
65
|
- lib/kobako/sandbox_options.rb
|
|
@@ -96,6 +97,7 @@ files:
|
|
|
96
97
|
- sig/kobako/namespace.rbs
|
|
97
98
|
- sig/kobako/outcome.rbs
|
|
98
99
|
- sig/kobako/outcome/panic.rbs
|
|
100
|
+
- sig/kobako/pool.rbs
|
|
99
101
|
- sig/kobako/runtime.rbs
|
|
100
102
|
- sig/kobako/sandbox.rbs
|
|
101
103
|
- sig/kobako/sandbox_options.rbs
|