kobako 0.9.2-aarch64-linux → 0.11.0-aarch64-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.
data/lib/kobako/errors.rb CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  # Top-level Kobako namespace.
4
4
  module Kobako
5
- # Error taxonomy (docs/behavior.md § Error Scenarios).
5
+ # Error taxonomy.
6
6
  #
7
7
  # Every `Kobako::Sandbox` invocation (`#eval` or `#run`) either returns a value or raises
8
8
  # exactly one of three invocation-outcome classes. Attribution is decided after the
9
- # guest binary returns control to the host (docs/behavior.md
10
- # "Step 1 — Wasm trap" then "Step 2 — Outcome envelope tag").
9
+ # guest binary returns control to the host: first the Wasm-trap layer, then
10
+ # the outcome-envelope tag.
11
11
  #
12
12
  # Three invocation-outcome branches:
13
13
  #
@@ -21,20 +21,21 @@ module Kobako
21
21
  # call that failed and was not rescued inside the
22
22
  # script).
23
23
  #
24
- # A fourth branch sits outside the invocation taxonomy:
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
28
- # configured +wasm_path+ before any invocation runs
29
- # ({docs/behavior.md E-40 / E-41}[link:../../docs/behavior.md]).
28
+ # configured +wasm_path+ before any invocation runs.
30
29
  # Not an invocation outcome, so it never passes
31
30
  # through the two-step attribution decision.
31
+ # * {PoolTimeoutError} — pool checkout layer. Raised by `Kobako::Pool#with`
32
+ # when the checkout wait exceeds +checkout_timeout+.
32
33
  #
33
- # Subclasses pinned by docs/behavior.md Error Classes:
34
+ # Named subclasses:
34
35
  #
35
36
  # * {ModuleNotBuiltError} < {SetupError} — Guest Binary artifact absent
36
- # at +wasm_path+ (E-40).
37
- # * {HandlerExhaustedError} < {SandboxError} — Handle id cap hit (B-21).
37
+ # at +wasm_path+.
38
+ # * {HandlerExhaustedError} < {SandboxError} — Handle id cap hit.
38
39
 
39
40
  # Base for all kobako-raised errors so callers that want to ignore the
40
41
  # taxonomy can rescue a single class.
@@ -43,13 +44,13 @@ module Kobako
43
44
  # Wasm engine layer. Raised when the Wasm execution engine crashed
44
45
  # (trap, OOM, unreachable) or when the wire layer detected a structural
45
46
  # violation that signals a corrupted guest execution environment
46
- # (zero-length OUTCOME_BUFFER, unknown outcome tag — SPEC E-02 / E-03).
47
+ # (zero-length OUTCOME_BUFFER, unknown outcome tag).
47
48
  #
48
- # Two named subclasses cover the configured per-invocation caps from B-01:
49
+ # Two named subclasses cover the configured per-invocation caps:
49
50
  #
50
- # * {TimeoutError} — wall-clock +timeout+ exceeded (E-19).
51
+ # * {TimeoutError} — wall-clock +timeout+ exceeded.
51
52
  # * {MemoryLimitError} — guest +memory.grow+ would exceed
52
- # +memory_limit+ (E-20).
53
+ # +memory_limit+.
53
54
  #
54
55
  # Host Apps that only care about "guest is unrecoverable, discard the
55
56
  # Sandbox" can rescue +TrapError+ and ignore the subclass; Host Apps that
@@ -57,24 +58,23 @@ module Kobako
57
58
  # first.
58
59
  class TrapError < Error; end
59
60
 
60
- # Wall-clock timeout cap exhausted. {docs/behavior.md E-19}[link:../../docs/behavior.md]:
61
- # the absolute deadline +entry_time + timeout+ passed and the next guest
62
- # wasm safepoint trapped. The Sandbox is unrecoverable after this point;
63
- # discard and recreate before another execution.
61
+ # Wall-clock timeout cap exhausted: the absolute deadline
62
+ # +entry_time + timeout+ passed and the next guest wasm safepoint
63
+ # trapped. The Sandbox is unrecoverable after this point; discard and
64
+ # recreate before another execution.
64
65
  class TimeoutError < TrapError; end
65
66
 
66
- # Linear-memory cap exhausted. {docs/behavior.md E-20}[link:../../docs/behavior.md]:
67
- # a guest +memory.grow+ would have pushed linear memory past the
68
- # configured +memory_limit+. The Sandbox is unrecoverable after this
69
- # point; discard and recreate before another execution.
67
+ # Linear-memory cap exhausted: a guest +memory.grow+ would have pushed
68
+ # linear memory past the configured +memory_limit+. The Sandbox is
69
+ # unrecoverable after this point; discard and recreate before another
70
+ # execution.
70
71
  class MemoryLimitError < TrapError; end
71
72
 
72
73
  # Construction-layer error raised by +Kobako::Sandbox.new+ /
73
74
  # +Kobako::Runtime.from_path+ when the wasm runtime cannot be built
74
75
  # from the configured +wasm_path+ before any invocation runs —
75
76
  # an unreadable artifact, bytes that are not a valid Wasm module, or
76
- # engine / linker / instantiation setup failure
77
- # ({docs/behavior.md E-41}[link:../../docs/behavior.md]). Construction
77
+ # engine / linker / instantiation setup failure. Construction
78
78
  # is not an invocation, so +SetupError+ sits beside the invocation
79
79
  # taxonomy under +Kobako::Error+ rather than under +TrapError+: no
80
80
  # Sandbox is produced, so the +TrapError+ "discard and recreate"
@@ -83,8 +83,7 @@ module Kobako
83
83
 
84
84
  # The named +SetupError+ subclass for the common, actionable case:
85
85
  # the Guest Binary artifact is absent at +wasm_path+ — the pre-build
86
- # state on a fresh clone before +bundle exec rake compile+
87
- # ({docs/behavior.md E-40}[link:../../docs/behavior.md]). Host Apps
86
+ # state on a fresh clone before +bundle exec rake compile+. Host Apps
88
87
  # that only need "the Sandbox could not be set up" rescue +SetupError+;
89
88
  # those wanting to special-case the unbuilt-artifact state rescue
90
89
  # +ModuleNotBuiltError+ first.
@@ -120,21 +119,26 @@ module Kobako
120
119
  end
121
120
  end
122
121
 
123
- # docs/behavior.md Error Classes: HandlerExhaustedError is the canonical
124
- # SandboxError subclass for the id-cap-hit path (B-21). Raised when the
125
- # per-invocation Handle ID counter in Catalog::Handles reaches
126
- # +0x7fff_ffff+ (2³¹ − 1) and further allocation would exceed the cap.
122
+ # HandlerExhaustedError is the canonical SandboxError subclass for the
123
+ # id-cap-hit path. Raised when the per-invocation Handle ID counter in
124
+ # Catalog::Handles reaches +0x7fff_ffff+ (2³¹ 1) and further
125
+ # allocation would exceed the cap.
127
126
  class HandlerExhaustedError < SandboxError; end
128
127
 
129
- # docs/behavior.md Error Classes: BytecodeError is the SandboxError
130
- # subclass raised when a `#preload(binary:)` snippet fails structural
131
- # validation during the first invocation's snippet replay against a
132
- # fresh `mrb_state` (E-37 RITE version mismatch, E-38 corrupt body).
133
- # Bytecode that loads cleanly and then raises at top level is E-36
134
- # and surfaces as plain `SandboxError` with the natural mruby class
135
- # preserved. Inherits from SandboxError so a single
128
+ # BytecodeError is the SandboxError subclass raised when a
129
+ # `#preload(binary:)` snippet fails structural validation during the
130
+ # first invocation's snippet replay against a fresh `mrb_state` (RITE
131
+ # version mismatch or corrupt body). Bytecode that loads cleanly and
132
+ # then raises at top level surfaces as plain `SandboxError` with the
133
+ # natural mruby class preserved. Inherits from SandboxError so a single
136
134
  # `rescue Kobako::SandboxError` covers both source and bytecode
137
135
  # snippet failures while callers wanting bytecode-specific handling
138
136
  # can `rescue Kobako::BytecodeError` directly.
139
137
  class BytecodeError < SandboxError; end
138
+
139
+ # Pool checkout layer. Raised by +Kobako::Pool#with+ when the checkout
140
+ # wait exceeded the configured +checkout_timeout+ while every slot was
141
+ # held. No Sandbox state is touched — retrying succeeds as soon as a holder
142
+ # returns its Sandbox.
143
+ class PoolTimeoutError < Error; end
140
144
  end
data/lib/kobako/handle.rb CHANGED
@@ -3,9 +3,8 @@
3
3
  module Kobako
4
4
  # Wire-level value object for an ext-0x01 Capability Handle, used in both
5
5
  # directions across the Sandbox boundary: as a Service method's return
6
- # value (guest→host return path; {docs/behavior.md B-14}[link:../../docs/behavior.md])
7
- # and as a +#run+ argument auto-wrapped by the host
8
- # ({docs/behavior.md B-34}[link:../../docs/behavior.md]).
6
+ # value (guest→host return path) and as a +#run+ argument auto-wrapped
7
+ # by the host.
9
8
  #
10
9
  # SPEC pins the binary layout to fixext 4 with a 4-byte big-endian u32
11
10
  # payload ({docs/wire-codec.md}[link:../../docs/wire-codec.md]
@@ -1,13 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kobako
4
- # A named grouping of Members for one Sandbox
5
- # ({docs/behavior.md B-07..B-11}[link:../../docs/behavior.md]).
4
+ # A named grouping of Members for one Sandbox.
6
5
  # Returned by +Sandbox#define+. Each instance owns a flat name→object
7
6
  # table of Members; member binding is validated against {NAME_PATTERN}.
8
7
  class Namespace
9
- # Ruby constant-name pattern shared by Namespace and Member names
10
- # ({docs/behavior.md B-07/B-08 Notes}[link:../../docs/behavior.md]).
8
+ # Ruby constant-name pattern shared by Namespace and Member names.
11
9
  NAME_PATTERN = /\A[A-Z]\w*\z/
12
10
 
13
11
  attr_reader :name
@@ -18,15 +16,19 @@ module Kobako
18
16
  def initialize(name)
19
17
  @name = name
20
18
  @members = {} # : Hash[String, untyped]
19
+ @sealed = false
21
20
  end
22
21
 
23
22
  # Bind +object+ under +member+ inside this Namespace. +member+ is a
24
23
  # constant-form name as a +Symbol+ or +String+. +object+ is any Ruby
25
24
  # object that responds to the methods guest code will invoke. Returns
26
25
  # +self+ for chaining. Raises +ArgumentError+ when +member+ does not
27
- # match the constant pattern, or a Member of the same name is already
28
- # bound ({docs/behavior.md B-11}[link:../../docs/behavior.md]).
26
+ # match the constant pattern, when a Member of the same name is
27
+ # already bound, or when the owning Sandbox's first invocation has
28
+ # sealed Service registration.
29
29
  def bind(member, object)
30
+ raise ArgumentError, "cannot bind after first Sandbox invocation" if @sealed
31
+
30
32
  member_str = validate_member_name!(member)
31
33
  raise ArgumentError, "Member #{@name}::#{member_str} is already bound" if @members.key?(member_str)
32
34
 
@@ -34,6 +36,15 @@ module Kobako
34
36
  self
35
37
  end
36
38
 
39
+ # Mark this Namespace as sealed. Called by
40
+ # +Kobako::Catalog::Namespaces#seal!+ on the owning Sandbox's first
41
+ # invocation; afterwards {#bind} raises +ArgumentError+. Idempotent;
42
+ # returns +self+.
43
+ def seal!
44
+ @sealed = true
45
+ self
46
+ end
47
+
37
48
  # Member lookup; raises +KeyError+ when no Member is registered
38
49
  # under +member+.
39
50
  def fetch(member)
@@ -7,8 +7,7 @@ module Kobako
7
7
  # Host-facing boundary for the OUTCOME_BUFFER produced by
8
8
  # +__kobako_eval+. Takes raw outcome bytes — a one-byte tag followed by
9
9
  # the msgpack-encoded body — and maps them to either the unwrapped
10
- # mruby return value or a raised three-layer
11
- # ({docs/behavior.md Error Scenarios}[link:../../docs/behavior.md]) exception.
10
+ # mruby return value or a raised three-layer exception.
12
11
  #
13
12
  # Self-contained: this module owns the wire framing (tag bytes,
14
13
  # body decoding), and the +Panic+ wire record lives at
@@ -17,11 +16,11 @@ module Kobako
17
16
  # nothing in +Transport+ participates.
18
17
  #
19
18
  # * tag 0x01, decode OK → return decoded value
20
- # * tag 0x01, decode fails → SandboxError (E-09)
21
- # * tag 0x02, origin="service" → ServiceError (E-13)
22
- # * tag 0x02, origin="sandbox"/missing → SandboxError (E-04..E-07)
23
- # * tag 0x02, decode fails → SandboxError (E-08)
24
- # * unknown tag → TrapError (E-03)
19
+ # * tag 0x01, decode fails → SandboxError
20
+ # * tag 0x02, origin="service" → ServiceError
21
+ # * tag 0x02, origin="sandbox"/missing → SandboxError
22
+ # * tag 0x02, decode fails → SandboxError
23
+ # * unknown tag → TrapError
25
24
  module Outcome
26
25
  # First byte of the OUTCOME_BUFFER for the success branch — body is
27
26
  # the bare msgpack encoding of the returned value
@@ -71,7 +70,7 @@ module Kobako
71
70
  [tag, body]
72
71
  end
73
72
 
74
- # Decode failure on the success tag is a SandboxError (E-09): the
73
+ # Decode failure on the success tag is a SandboxError: the
75
74
  # framing was fine, but the carried value is unrepresentable. The
76
75
  # specific codec fault is stashed in +details+ rather
77
76
  # than spliced into the message — callers cannot act on the inner
@@ -86,7 +85,7 @@ module Kobako
86
85
  )
87
86
  end
88
87
 
89
- # Decode failure on the panic tag is a SandboxError (E-08). Either
88
+ # Decode failure on the panic tag is a SandboxError. Either
90
89
  # path raises — on success the decoded Panic is mapped to its three-
91
90
  # layer exception via +build_panic_error+ and raised; on wire-decode
92
91
  # failure the rescue path raises the wire-violation +SandboxError+.
@@ -130,13 +129,12 @@ module Kobako
130
129
  )
131
130
  end
132
131
 
133
- # {docs/behavior.md Error Scenarios}[link:../../docs/behavior.md]: map
134
- # the panic +class+ field to the matching Ruby exception subclass so
135
- # callers can rescue specific failure paths. +origin="service"+ →
132
+ # Map the panic +class+ field to the matching Ruby exception subclass
133
+ # so callers can rescue specific failure paths. +origin="service"+
136
134
  # +ServiceError+; +origin="sandbox"+ plus
137
135
  # +class="Kobako::BytecodeError"+ selects the +BytecodeError+
138
- # subclass (E-37 / E-38). Everything else falls back to the base
139
- # class for the origin.
136
+ # subclass. Everything else falls back to the base class for the
137
+ # origin.
140
138
  def panic_target_class(panic)
141
139
  case panic.origin
142
140
  when Panic::ORIGIN_SERVICE
@@ -0,0 +1,176 @@
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
+ #
10
+ # Construction forwards every +Kobako::Sandbox.new+ keyword verbatim
11
+ # and holds the optional block as the per-Sandbox setup hook; a
12
+ # checkout prefers an idle Sandbox and constructs a new one only when
13
+ # none is idle and fewer than +slots+ exist. +#with+ blocks up
14
+ # to +checkout_timeout+ seconds when every slot is held, applies
15
+ # the +TrapError+ discard-and-recreate contract at checkin, and
16
+ # the Pool releases everything with its own reachability — there is no
17
+ # teardown verb.
18
+ class Pool
19
+ # The +#with+ wait bound applied when +checkout_timeout+ is not given.
20
+ DEFAULT_CHECKOUT_TIMEOUT_SECONDS = 5.0
21
+
22
+ # Build a Pool of up to +slots+ Sandboxes. +slots+ is
23
+ # a positive Integer; +checkout_timeout+ bounds the +#with+ wait in
24
+ # seconds (+nil+ waits indefinitely); every other keyword is
25
+ # forwarded verbatim to +Kobako::Sandbox.new+. The optional block
26
+ # runs exactly once per constructed Sandbox — it is the setup window
27
+ # for +#define+ / +#preload+ before that Sandbox's first checkout.
28
+ # No Sandbox is constructed here. Raises +ArgumentError+ for an
29
+ # invalid +slots+ / +checkout_timeout+.
30
+ def initialize(slots:, checkout_timeout: DEFAULT_CHECKOUT_TIMEOUT_SECONDS, **sandbox_options, &setup)
31
+ validate_slots!(slots)
32
+ @slots = slots
33
+ @checkout_timeout = normalize_checkout_timeout(checkout_timeout)
34
+ @sandbox_options = sandbox_options
35
+ @setup = setup
36
+ @idle = [] # : Array[Kobako::Sandbox]
37
+ @constructed = 0
38
+ @mutex = Mutex.new
39
+ @slot_freed = ConditionVariable.new
40
+ end
41
+
42
+ # Yield one exclusively-held Sandbox to the block and return the
43
+ # block's value. Blocks while every slot is held; raises
44
+ # +Kobako::PoolTimeoutError+ once the wait exceeds +checkout_timeout+.
45
+ # The Sandbox returns to the pool at block exit — unless the block raised
46
+ # +Kobako::TrapError+, in which case the unrecoverable Sandbox is
47
+ # discarded and its slot refills by a fresh construction on next
48
+ # demand.
49
+ def with
50
+ sandbox = checkout
51
+ begin
52
+ yield sandbox
53
+ rescue TrapError
54
+ release_capacity!
55
+ sandbox = nil
56
+ raise
57
+ ensure
58
+ checkin(sandbox) if sandbox
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ # Acquire a Sandbox and hand it over in pre-invocation state — empty
65
+ # output buffers and truncation predicates false.
66
+ def checkout
67
+ acquire.tap(&:reset_invocation_state!)
68
+ end
69
+
70
+ # The idle-first claim loop: an idle Sandbox wins, unclaimed
71
+ # capacity constructs, and a full pool waits for a checkin.
72
+ def acquire
73
+ timeout = @checkout_timeout
74
+ deadline = timeout && (monotonic_now + timeout)
75
+ loop do
76
+ action, sandbox = claim_or_wait(deadline)
77
+ return sandbox if action == :idle && sandbox
78
+ return construct_slot if action == :build
79
+ end
80
+ end
81
+
82
+ # Single locked decision point for one claim attempt. Waiting
83
+ # happens inside the lock (so a checkin can wake it); construction
84
+ # happens outside (so a slow setup block never holds the lock) —
85
+ # capacity is reserved here and released by +construct_slot+ on
86
+ # failure.
87
+ def claim_or_wait(deadline)
88
+ @mutex.synchronize do
89
+ return [:idle, @idle.pop] unless @idle.empty?
90
+
91
+ if @constructed < @slots
92
+ @constructed += 1
93
+ return [:build, nil]
94
+ end
95
+
96
+ await_slot!(deadline)
97
+ [:retry, nil]
98
+ end
99
+ end
100
+
101
+ # Wait for a checkin or freed capacity; raises
102
+ # +Kobako::PoolTimeoutError+ once +deadline+ has passed. Must
103
+ # run while holding +@mutex+.
104
+ def await_slot!(deadline)
105
+ remaining = deadline && (deadline - monotonic_now)
106
+ if remaining && remaining <= 0
107
+ raise PoolTimeoutError,
108
+ "no Sandbox returned within #{@checkout_timeout}s: all #{@slots} slots are held"
109
+ end
110
+
111
+ @slot_freed.wait(@mutex, remaining)
112
+ end
113
+
114
+ # Construct and set up one pooled Sandbox against the capacity
115
+ # reserved by +claim_or_wait+. Construction and setup-block errors
116
+ # propagate to the checkout caller unchanged; the reserved
117
+ # capacity is released so a later checkout can retry.
118
+ def construct_slot
119
+ done = false
120
+ sandbox = Sandbox.new(**@sandbox_options)
121
+ @setup&.call(sandbox)
122
+ done = true
123
+ sandbox
124
+ ensure
125
+ release_capacity! unless done
126
+ end
127
+
128
+ # Return a Sandbox to the idle list and wake one waiting checkout.
129
+ def checkin(sandbox)
130
+ @mutex.synchronize do
131
+ @idle.push(sandbox)
132
+ @slot_freed.signal
133
+ end
134
+ end
135
+
136
+ # Give back reserved-but-unfilled capacity — a failed construction or
137
+ # a discarded Sandbox — and wake one waiting checkout to claim it.
138
+ def release_capacity!
139
+ @mutex.synchronize do
140
+ @constructed -= 1
141
+ @slot_freed.signal
142
+ end
143
+ end
144
+
145
+ # The wait deadline runs on the monotonic clock so a wall-clock jump
146
+ # cannot stretch or cut the checkout wait.
147
+ def monotonic_now
148
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
149
+ end
150
+
151
+ # Pre-flight for +slots+ — no coercion, a positive Integer is
152
+ # the only accepted shape.
153
+ def validate_slots!(slots)
154
+ return if slots.is_a?(Integer) && slots.positive?
155
+
156
+ raise ArgumentError, "slots must be a positive Integer, got #{slots.inspect}"
157
+ end
158
+
159
+ # Coerce +checkout_timeout+ into the Float seconds the wait loop
160
+ # consumes, or +nil+ to wait indefinitely — the same normalisation
161
+ # idiom +SandboxOptions+ applies to +timeout+.
162
+ def normalize_checkout_timeout(checkout_timeout)
163
+ return nil if checkout_timeout.nil?
164
+ unless checkout_timeout.is_a?(Numeric)
165
+ raise ArgumentError, "checkout_timeout must be Numeric or nil, got #{checkout_timeout.inspect}"
166
+ end
167
+
168
+ seconds = checkout_timeout.to_f
169
+ unless seconds.positive? && seconds.finite?
170
+ raise ArgumentError, "checkout_timeout must be > 0 and finite (got #{checkout_timeout})"
171
+ end
172
+
173
+ seconds
174
+ end
175
+ end
176
+ end