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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +32 -0
- data/README.md +47 -19
- 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/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 -2
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
|
|
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
|
|
10
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
34
|
+
# Named subclasses:
|
|
34
35
|
#
|
|
35
36
|
# * {ModuleNotBuiltError} < {SetupError} — Guest Binary artifact absent
|
|
36
|
-
# at +wasm_path
|
|
37
|
-
# * {HandlerExhaustedError} < {SandboxError} — Handle id cap hit
|
|
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
|
|
47
|
+
# (zero-length OUTCOME_BUFFER, unknown outcome tag).
|
|
47
48
|
#
|
|
48
|
-
# Two named subclasses cover the configured per-invocation caps
|
|
49
|
+
# Two named subclasses cover the configured per-invocation caps:
|
|
49
50
|
#
|
|
50
|
-
# * {TimeoutError} — wall-clock +timeout+ exceeded
|
|
51
|
+
# * {TimeoutError} — wall-clock +timeout+ exceeded.
|
|
51
52
|
# * {MemoryLimitError} — guest +memory.grow+ would exceed
|
|
52
|
-
# +memory_limit
|
|
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
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
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
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
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
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
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
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
#
|
|
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
|
|
7
|
-
#
|
|
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]
|
data/lib/kobako/namespace.rb
CHANGED
|
@@ -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,
|
|
28
|
-
# bound
|
|
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)
|
data/lib/kobako/outcome.rb
CHANGED
|
@@ -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
|
|
21
|
-
# * tag 0x02, origin="service" → ServiceError
|
|
22
|
-
# * tag 0x02, origin="sandbox"/missing → SandboxError
|
|
23
|
-
# * tag 0x02, decode fails → SandboxError
|
|
24
|
-
# * unknown tag → TrapError
|
|
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
|
|
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
|
|
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
|
-
#
|
|
134
|
-
#
|
|
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
|
|
139
|
-
#
|
|
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
|
data/lib/kobako/pool.rb
ADDED
|
@@ -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
|