kobako 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +71 -0
- data/Cargo.lock +117 -140
- data/README.md +100 -125
- data/ROADMAP.md +23 -22
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +31 -0
- data/crates/kobako-runtime/Cargo.toml +2 -2
- data/crates/kobako-runtime/README.md +4 -3
- data/crates/kobako-runtime/src/error.rs +8 -9
- data/crates/kobako-runtime/src/lib.rs +1 -1
- data/crates/kobako-runtime/src/profile.rs +1 -1
- data/crates/kobako-runtime/src/runtime.rs +31 -17
- data/crates/kobako-runtime/src/snapshot.rs +13 -10
- data/crates/kobako-runtime/src/yielder.rs +9 -6
- data/crates/kobako-transport/CHANGELOG.md +28 -0
- data/crates/kobako-transport/Cargo.toml +1 -1
- data/crates/kobako-transport/README.md +21 -12
- data/crates/kobako-transport/src/abi.rs +11 -14
- data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
- data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
- data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
- data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
- data/crates/kobako-transport/src/envelope/reply.rs +77 -53
- data/crates/kobako-transport/src/envelope/request.rs +295 -0
- data/crates/kobako-transport/src/envelope/run.rs +1 -2
- data/crates/kobako-transport/src/envelope/tag.rs +49 -0
- data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
- data/crates/kobako-transport/src/envelope.rs +19 -12
- data/crates/kobako-transport/src/lib.rs +0 -2
- data/crates/kobako-wasmtime/CHANGELOG.md +44 -0
- data/crates/kobako-wasmtime/Cargo.toml +7 -5
- data/crates/kobako-wasmtime/README.md +2 -2
- data/crates/kobako-wasmtime/src/abi.rs +5 -8
- data/crates/kobako-wasmtime/src/ambient.rs +2 -2
- data/crates/kobako-wasmtime/src/cache.rs +25 -59
- data/crates/kobako-wasmtime/src/capture.rs +4 -11
- data/crates/kobako-wasmtime/src/config.rs +11 -14
- data/crates/kobako-wasmtime/src/dispatch.rs +70 -80
- data/crates/kobako-wasmtime/src/driver.rs +133 -86
- data/crates/kobako-wasmtime/src/exports.rs +88 -34
- data/crates/kobako-wasmtime/src/guest_mem.rs +145 -138
- data/crates/kobako-wasmtime/src/instance_pre.rs +11 -22
- data/crates/kobako-wasmtime/src/invocation.rs +55 -372
- data/crates/kobako-wasmtime/src/io.rs +286 -0
- data/crates/kobako-wasmtime/src/lib.rs +3 -24
- data/crates/kobako-wasmtime/src/limiter.rs +253 -0
- data/crates/kobako-wasmtime/src/trap.rs +32 -45
- data/crates/kobako-wasmtime/tests/preamble_flags.rs +69 -0
- data/crates/kobako-wasmtime/tests/snippet_compile.rs +94 -0
- data/crates/kobako-wasmtime/tests/support/mod.rs +34 -0
- data/crates/kobako-wasmtime/tests/unsupported_call.rs +50 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +2 -2
- data/ext/kobako/src/runtime/bridge.rs +38 -47
- data/ext/kobako/src/runtime/errors.rs +26 -32
- data/ext/kobako/src/runtime/gvl.rs +4 -12
- data/ext/kobako/src/runtime.rs +169 -212
- data/lib/kobako/capture.rb +1 -4
- data/lib/kobako/catalog/extensions.rb +21 -59
- data/lib/kobako/catalog/flags.rb +41 -0
- data/lib/kobako/catalog/handles.rb +7 -54
- data/lib/kobako/catalog/services.rb +7 -61
- data/lib/kobako/catalog/snippets.rb +42 -66
- data/lib/kobako/catalog.rb +2 -10
- data/lib/kobako/codec/decoder.rb +9 -28
- data/lib/kobako/codec/encoder.rb +3 -22
- data/lib/kobako/codec/error.rb +6 -27
- data/lib/kobako/codec/ext_types.rb +72 -94
- data/lib/kobako/codec/handle_walk.rb +65 -130
- data/lib/kobako/codec/nesting.rb +3 -8
- data/lib/kobako/codec/state.rb +4 -10
- data/lib/kobako/codec/utils.rb +6 -34
- data/lib/kobako/codec.rb +3 -22
- data/lib/kobako/constant_name.rb +8 -0
- data/lib/kobako/context.rb +51 -112
- data/lib/kobako/errors.rb +51 -127
- data/lib/kobako/execution.rb +16 -21
- data/lib/kobako/extension.rb +23 -40
- data/lib/kobako/handle.rb +7 -12
- data/lib/kobako/outcome.rb +51 -80
- data/lib/kobako/payload/arguments.rb +1 -16
- data/lib/kobako/payload.rb +4 -12
- data/lib/kobako/pool.rb +23 -53
- data/lib/kobako/runtime.rb +7 -21
- data/lib/kobako/sandbox.rb +96 -133
- data/lib/kobako/sandbox_options.rb +38 -84
- data/lib/kobako/snippet/binary.rb +1 -16
- data/lib/kobako/snippet/source.rb +1 -16
- data/lib/kobako/snippet.rb +1 -9
- data/lib/kobako/transport/call.rb +1 -14
- data/lib/kobako/transport/dispatcher.rb +112 -195
- data/lib/kobako/transport/error.rb +6 -20
- data/lib/kobako/transport/exposure.rb +5 -10
- data/lib/kobako/transport/reflection.rb +39 -50
- data/lib/kobako/transport/run.rb +17 -71
- data/lib/kobako/transport/yielder.rb +21 -73
- data/lib/kobako/transport.rb +3 -15
- data/lib/kobako/unresolved.rb +4 -11
- data/lib/kobako/usage.rb +2 -6
- data/lib/kobako/version.rb +1 -1
- data/sig/kobako/catalog/extensions.rbs +2 -2
- data/sig/kobako/catalog/flags.rbs +17 -0
- data/sig/kobako/catalog/services.rbs +0 -6
- data/sig/kobako/catalog/snippets.rbs +4 -4
- data/sig/kobako/codec/ext_types.rbs +10 -8
- data/sig/kobako/codec/handle_walk.rbs +9 -7
- data/sig/kobako/codec/utils.rbs +2 -2
- data/sig/kobako/codec.rbs +2 -2
- data/sig/kobako/constant_name.rbs +3 -0
- data/sig/kobako/context.rbs +2 -3
- data/sig/kobako/errors.rbs +6 -0
- data/sig/kobako/outcome.rbs +6 -8
- data/sig/kobako/runtime.rbs +12 -8
- data/sig/kobako/sandbox.rbs +4 -0
- data/sig/kobako/sandbox_options.rbs +2 -6
- data/sig/kobako/transport/dispatcher.rbs +14 -18
- data/sig/kobako/transport/reflection.rbs +5 -3
- data/sig/kobako/transport/run.rbs +1 -3
- data/sig/kobako/transport/yielder.rbs +3 -3
- data/sig/kobako/transport.rbs +7 -6
- metadata +16 -4
- data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
- data/crates/kobako-wasmtime/src/frames.rs +0 -273
- data/rust-toolchain.toml +0 -15
data/lib/kobako/extension.rb
CHANGED
|
@@ -1,48 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# methods run in-guest and whose privileged methods dispatch to the
|
|
9
|
-
# backend.
|
|
4
|
+
# Guest source paired with an optional host backend, installed with
|
|
5
|
+
# Sandbox#install: the guest sees one constant whose pure methods run in the
|
|
6
|
+
# guest and whose privileged ones call the backend. Any object answering the
|
|
7
|
+
# same four readers installs the same way.
|
|
10
8
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# * +backend+ — an +Extension::Backend+ or +nil+ for a pure-guest
|
|
20
|
-
# Extension.
|
|
21
|
-
# * +depends_on+ — Symbol names of Extensions that must also be
|
|
22
|
-
# installed, checked for presence at the first invocation.
|
|
23
|
-
#
|
|
24
|
-
# +Kobako::Extension+ is the bundled value type; any object exposing the
|
|
25
|
-
# four readers is equally valid, so a Host App or gem may supply its own.
|
|
9
|
+
# [+name+] A constant-form Symbol: the snippet's backtrace name, and
|
|
10
|
+
# what another Extension's +depends_on+ names.
|
|
11
|
+
# [+source+] The mruby source, a String. A host object with no source
|
|
12
|
+
# is bound with Sandbox#bind instead.
|
|
13
|
+
# [+backend+] A Backend, or +nil+ for an Extension that runs entirely in
|
|
14
|
+
# the guest.
|
|
15
|
+
# [+depends_on+] Names of Extensions that must also be installed, checked
|
|
16
|
+
# at the first invocation.
|
|
26
17
|
class Extension < Data.define(:name, :source, :backend, :depends_on)
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
# +"File"+ or nested +"MyApp::Store"+, spelling the guest constant the
|
|
30
|
-
# idiom routes to) with the source of the bound object, declared by
|
|
31
|
-
# explicit keyword:
|
|
18
|
+
# The host object an Extension's constant at +path+ calls, such as
|
|
19
|
+
# <tt>"MyApp::Store"</tt>, given by keyword:
|
|
32
20
|
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
# * neither — a fillable, defaulting to +Kobako::Unresolved+ until the
|
|
40
|
-
# host supplies the invocation's object.
|
|
21
|
+
# [+object:+] One object for the Sandbox's life.
|
|
22
|
+
# [+provider:+] A callable taking no arguments, called once per invocation
|
|
23
|
+
# for that invocation's object. If it raises, the exception
|
|
24
|
+
# reaches the caller and the guest does not run.
|
|
25
|
+
# [neither] A fillable Service, supplied per invocation through
|
|
26
|
+
# Context#bind.
|
|
41
27
|
#
|
|
42
|
-
# The
|
|
43
|
-
#
|
|
44
|
-
# directly with +object:+. Giving both +object:+ and +provider:+ raises
|
|
45
|
-
# +ArgumentError+.
|
|
28
|
+
# The keyword decides the kind, so a callable object meant as itself is
|
|
29
|
+
# given as +object:+. Giving both raises ArgumentError.
|
|
46
30
|
class Backend < Data.define(:path, :object, :provider)
|
|
47
31
|
def initialize(path:, object: nil, provider: nil)
|
|
48
32
|
if !object.nil? && !provider.nil?
|
|
@@ -54,8 +38,7 @@ module Kobako
|
|
|
54
38
|
end
|
|
55
39
|
end
|
|
56
40
|
|
|
57
|
-
# +backend+ and +depends_on+
|
|
58
|
-
# pure-idiom and single-backend shapes stay terse.
|
|
41
|
+
# +backend+ defaults to none, and +depends_on+ to no other Extension.
|
|
59
42
|
def initialize(name:, source:, backend: nil, depends_on: [])
|
|
60
43
|
super
|
|
61
44
|
end
|
data/lib/kobako/handle.rb
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
|
-
# A
|
|
5
|
-
#
|
|
6
|
-
# direction. Host App code cannot construct one: +.new+ is private and
|
|
7
|
-
# +#with+ removed, so no Handle is ever made from a caller-chosen id.
|
|
8
|
-
# The guest's +Kobako::Handle+ shares only the name.
|
|
4
|
+
# A guest-held reference to a host object, named by id. Host code cannot
|
|
5
|
+
# construct one, so no Handle is ever made from a caller-chosen id.
|
|
9
6
|
class Handle < Data.define(:id)
|
|
10
|
-
#
|
|
7
|
+
# The smallest id; 0 never names a Handle.
|
|
11
8
|
MIN_ID = 1
|
|
12
|
-
# The positive half of a signed 32-bit integer
|
|
13
|
-
#
|
|
9
|
+
# The largest id, the positive half of a signed 32-bit integer; an
|
|
10
|
+
# invocation needing more raises HandleExhaustedError.
|
|
14
11
|
MAX_ID = 0x7fff_ffff
|
|
15
12
|
|
|
16
|
-
def initialize(id:)
|
|
13
|
+
def initialize(id:) # :nodoc:
|
|
17
14
|
raise ArgumentError, "Handle id must be Integer" unless id.is_a?(Integer)
|
|
18
15
|
raise ArgumentError, "Handle id #{id} out of range [#{MIN_ID}, #{MAX_ID}]" unless id.between?(MIN_ID, MAX_ID)
|
|
19
16
|
|
|
@@ -23,9 +20,7 @@ module Kobako
|
|
|
23
20
|
private_class_method :new
|
|
24
21
|
undef_method :with
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
# allocator; it runs the same id checks +.new+ would.
|
|
28
|
-
def self.restore(id)
|
|
23
|
+
def self.restore(id) # :nodoc:
|
|
29
24
|
allocate.tap { |handle| handle.send(:initialize, id: id) }
|
|
30
25
|
end
|
|
31
26
|
end
|
data/lib/kobako/outcome.rb
CHANGED
|
@@ -4,15 +4,7 @@ require_relative "codec"
|
|
|
4
4
|
require_relative "transport/error"
|
|
5
5
|
|
|
6
6
|
module Kobako
|
|
7
|
-
|
|
8
|
-
# off the core envelope. Takes the arm it named plus the fields that arm
|
|
9
|
-
# carries, and settles the invocation the way a host does: return the
|
|
10
|
-
# value, or raise the exception the failure attributes to.
|
|
11
|
-
#
|
|
12
|
-
# This is the two-step attribution decision. The wire framing belongs to
|
|
13
|
-
# the native side; the payload codec at +Kobako::Codec+ decodes only
|
|
14
|
-
# the one arm that carries a value.
|
|
15
|
-
module Outcome
|
|
7
|
+
module Outcome # :nodoc:
|
|
16
8
|
# The two +origin+ values a Panic attributes with.
|
|
17
9
|
ORIGIN_SANDBOX = "sandbox"
|
|
18
10
|
ORIGIN_SERVICE = "service"
|
|
@@ -24,91 +16,70 @@ module Kobako
|
|
|
24
16
|
SUBCLASSES = {
|
|
25
17
|
"Kobako::BytecodeError" => BytecodeError,
|
|
26
18
|
"Kobako::UndefinedEntrypointError" => UndefinedEntrypointError,
|
|
19
|
+
"Kobako::UnsupportedFlagError" => UnsupportedFlagError,
|
|
27
20
|
"Kobako::Transport::Error" => Kobako::Transport::Error,
|
|
28
21
|
"Kobako::NoServiceError" => NoServiceError,
|
|
29
22
|
"Kobako::ServiceArgumentError" => ServiceArgumentError
|
|
30
23
|
}.freeze
|
|
24
|
+
private_constant :ORIGIN_SANDBOX, :ORIGIN_SERVICE, :SUBCLASSES
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# present on the panic arm and absent on every other, which is what
|
|
38
|
-
# tells the failure that has a record to attribute from apart from the
|
|
39
|
-
# two that do not. +entrypoint+ is the name this invocation asked for,
|
|
40
|
-
# which the host knows and the wire therefore never carries.
|
|
41
|
-
def reify(kind, payload, panic, entrypoint: nil)
|
|
42
|
-
return decode_value(payload) if kind == :ok
|
|
26
|
+
# +panic+ is present only on the panic arm, which is what tells it from
|
|
27
|
+
# the ok arm's +payload+. +entrypoint+ is known to the host and never
|
|
28
|
+
# carried by the wire.
|
|
29
|
+
def self.reify(payload, panic, entrypoint: nil)
|
|
30
|
+
return decode_value(payload) unless panic
|
|
43
31
|
|
|
44
|
-
raise
|
|
32
|
+
raise panic_error(panic, entrypoint)
|
|
45
33
|
end
|
|
46
34
|
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
selected = error_class(origin, klass)
|
|
53
|
-
attribution = { origin: origin, klass: klass, backtrace_lines: backtrace }
|
|
54
|
-
return selected.new(message, **attribution) unless selected == UndefinedEntrypointError
|
|
35
|
+
# +reify+ is the whole seam: one call settles one invocation, matching
|
|
36
|
+
# the single entry point the Rust frontend's twin exposes. Everything
|
|
37
|
+
# else here is how that decision is reached.
|
|
38
|
+
class << self
|
|
39
|
+
private
|
|
55
40
|
|
|
56
|
-
|
|
57
|
-
|
|
41
|
+
# The fields land on the exception verbatim — it carries the record
|
|
42
|
+
# rather than a translation of one.
|
|
43
|
+
def panic_error(panic, entrypoint)
|
|
44
|
+
origin, klass, message, backtrace, available = panic
|
|
45
|
+
selected = error_class(origin, klass)
|
|
46
|
+
attribution = { origin: origin, klass: klass, backtrace_lines: backtrace }
|
|
47
|
+
return selected.new(message, **attribution) unless selected == UndefinedEntrypointError
|
|
58
48
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# naming a class outside the branch its origin chose is ignored rather
|
|
62
|
-
# than honoured: what a guest calls its exception must not move the
|
|
63
|
-
# failure to a layer the attribution did not put it in.
|
|
64
|
-
def error_class(origin, klass)
|
|
65
|
-
base = origin == ORIGIN_SERVICE ? ServiceError : SandboxError
|
|
66
|
-
selected = SUBCLASSES.fetch(klass, base)
|
|
67
|
-
return base unless selected <= base
|
|
49
|
+
UndefinedEntrypointError.new(message, name: entrypoint, available: available.map(&:to_sym), **attribution)
|
|
50
|
+
end
|
|
68
51
|
|
|
69
|
-
|
|
70
|
-
|
|
52
|
+
# +origin+ picks the branch, and the guest-written class name may
|
|
53
|
+
# narrow within it so callers can rescue one path specifically. A name
|
|
54
|
+
# naming a class outside the branch its origin chose is ignored rather
|
|
55
|
+
# than honoured: what a guest calls its exception must not move the
|
|
56
|
+
# failure to a layer the attribution did not put it in.
|
|
57
|
+
def error_class(origin, klass)
|
|
58
|
+
base = origin == ORIGIN_SERVICE ? ServiceError : SandboxError
|
|
59
|
+
selected = SUBCLASSES.fetch(klass, base)
|
|
60
|
+
return base unless selected <= base
|
|
71
61
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
# to, so both walk the trap path; only the absent-versus-present
|
|
75
|
-
# distinction selects the message.
|
|
76
|
-
def trap_error(kind)
|
|
77
|
-
return TrapError.new("Sandbox exited without producing a result") if kind == :absent
|
|
62
|
+
selected
|
|
63
|
+
end
|
|
78
64
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
65
|
+
# A decode fault means the framing was fine but the carried value is
|
|
66
|
+
# unrepresentable.
|
|
67
|
+
def decode_value(payload)
|
|
68
|
+
Kobako::Codec::Decoder.decode(payload)
|
|
69
|
+
rescue Kobako::Codec::Error => e
|
|
70
|
+
raise wire_error("Sandbox produced an invalid result value", diagnostic: e.message)
|
|
71
|
+
end
|
|
84
72
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
73
|
+
# No guest class stands behind a result kobako cannot decode, so
|
|
74
|
+
# +klass+ names the host class raised.
|
|
75
|
+
def wire_error(message, diagnostic: nil)
|
|
76
|
+
Kobako::Transport::Error.new(
|
|
77
|
+
message,
|
|
78
|
+
origin: ORIGIN_SANDBOX,
|
|
79
|
+
klass: "Kobako::Transport::Error",
|
|
80
|
+
diagnostic: diagnostic
|
|
81
|
+
)
|
|
82
|
+
end
|
|
92
83
|
end
|
|
93
|
-
|
|
94
|
-
# Lift a wire violation the host detected to the real
|
|
95
|
-
# +Kobako::Transport::Error+ class so callers can +rescue+ it
|
|
96
|
-
# specifically instead of pattern-matching on +error.klass+. The
|
|
97
|
-
# +klass+ field is still populated so existing operator-side tooling
|
|
98
|
-
# that greps on the string continues to work.
|
|
99
|
-
def wire_error(message, diagnostic: nil)
|
|
100
|
-
Kobako::Transport::Error.new(
|
|
101
|
-
message,
|
|
102
|
-
origin: ORIGIN_SANDBOX,
|
|
103
|
-
klass: "Kobako::Transport::Error",
|
|
104
|
-
diagnostic: diagnostic
|
|
105
|
-
)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# +reify+ is the whole seam: one call settles one invocation, matching
|
|
109
|
-
# the single entry point the Rust frontend's twin exposes. Everything
|
|
110
|
-
# else here is how that decision is reached.
|
|
111
|
-
private_class_method :panic_error, :error_class, :trap_error, :decode_value, :wire_error
|
|
112
|
-
private_constant :ORIGIN_SANDBOX, :ORIGIN_SERVICE, :SUBCLASSES
|
|
113
84
|
end
|
|
114
85
|
end
|
|
@@ -4,19 +4,7 @@ require_relative "../codec"
|
|
|
4
4
|
|
|
5
5
|
module Kobako
|
|
6
6
|
module Payload
|
|
7
|
-
|
|
8
|
-
# msgpack array, +args+ then +kwargs+. Both elements are always
|
|
9
|
-
# present, so field positions stay stable when either is empty.
|
|
10
|
-
#
|
|
11
|
-
# The positional-versus-keyword split lives here rather than in the
|
|
12
|
-
# core envelope because it is Ruby's call semantics, not the wire's.
|
|
13
|
-
# SPEC pins +kwargs+ keys to Symbols; the invariant is enforced at
|
|
14
|
-
# construction so the value object is the single source of truth.
|
|
15
|
-
#
|
|
16
|
-
# Built on the +class X < Data.define(...)+ subclass form so the class
|
|
17
|
-
# body is fully Steep-visible; see +.rubocop.yml+ for the
|
|
18
|
-
# rationale.
|
|
19
|
-
class Arguments < Data.define(:args, :kwargs)
|
|
7
|
+
class Arguments < Data.define(:args, :kwargs) # :nodoc:
|
|
20
8
|
def initialize(args: [], kwargs: {})
|
|
21
9
|
raise ArgumentError, "payload args must be Array" unless args.is_a?(Array)
|
|
22
10
|
|
|
@@ -30,9 +18,6 @@ module Kobako
|
|
|
30
18
|
Codec::Encoder.encode([args, kwargs])
|
|
31
19
|
end
|
|
32
20
|
|
|
33
|
-
# Decode +bytes+ into an Arguments. Raises +Codec::InvalidTypeError+ when
|
|
34
|
-
# the payload is not the expected 2-element msgpack array, or when the
|
|
35
|
-
# construction invariants reject the decoded fields.
|
|
36
21
|
def self.decode(bytes)
|
|
37
22
|
Codec::Decoder.decode(bytes) do |frame|
|
|
38
23
|
unless frame.is_a?(Array) && frame.length == 2
|
data/lib/kobako/payload.rb
CHANGED
|
@@ -3,17 +3,9 @@
|
|
|
3
3
|
require_relative "payload/arguments"
|
|
4
4
|
|
|
5
5
|
module Kobako
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
#
|
|
11
|
-
# Keeping the two apart is what lets an endpoint with its own schema
|
|
12
|
-
# replace this layer and keep the envelope, so nothing here may reach for
|
|
13
|
-
# a routing field.
|
|
14
|
-
#
|
|
15
|
-
# +Kobako::Payload::Arguments+ is the wire-symmetric peer of
|
|
16
|
-
# +kobako_codec::payload::Arguments+.
|
|
17
|
-
module Payload
|
|
6
|
+
# What the resolved method receives, kept apart from the envelope so an
|
|
7
|
+
# endpoint with its own schema can replace it; nothing here reads a
|
|
8
|
+
# routing field.
|
|
9
|
+
module Payload # :nodoc:
|
|
18
10
|
end
|
|
19
11
|
end
|
data/lib/kobako/pool.rb
CHANGED
|
@@ -4,29 +4,21 @@ require_relative "errors"
|
|
|
4
4
|
require_relative "sandbox"
|
|
5
5
|
|
|
6
6
|
module Kobako
|
|
7
|
-
#
|
|
8
|
-
#
|
|
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.
|
|
7
|
+
# A bounded set of warm, identically set-up Sandboxes, handed out to one
|
|
8
|
+
# holder at a time. Every Sandbox it builds is kept: an invocation leaves
|
|
9
|
+
# nothing behind, whatever ended it.
|
|
18
10
|
class Pool
|
|
19
|
-
# The
|
|
11
|
+
# The default +checkout_timeout+: 5 seconds.
|
|
20
12
|
DEFAULT_CHECKOUT_TIMEOUT_SECONDS = 5.0
|
|
21
13
|
|
|
22
|
-
# Build a Pool of up to +slots+ Sandboxes
|
|
23
|
-
#
|
|
24
|
-
# seconds
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
14
|
+
# Build a Pool of up to +slots+ Sandboxes, built on first demand with the
|
|
15
|
+
# other keywords as Sandbox.new takes them. +checkout_timeout+ bounds how
|
|
16
|
+
# many seconds #with waits, and +nil+ waits indefinitely. The block, if
|
|
17
|
+
# given, sets up each Sandbox once, as with Sandbox#bind and
|
|
18
|
+
# Sandbox#preload.
|
|
19
|
+
#
|
|
20
|
+
# Raises ArgumentError when +slots+ is not a positive Integer or
|
|
21
|
+
# +checkout_timeout+ is not a positive finite number.
|
|
30
22
|
def initialize(slots:, checkout_timeout: DEFAULT_CHECKOUT_TIMEOUT_SECONDS, **sandbox_options, &setup)
|
|
31
23
|
validate_slots!(slots)
|
|
32
24
|
@slots = slots
|
|
@@ -40,29 +32,20 @@ module Kobako
|
|
|
40
32
|
end
|
|
41
33
|
|
|
42
34
|
# Yield one exclusively-held Sandbox to the block and return the
|
|
43
|
-
# block's value
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# +Kobako::TrapError+, in which case the unrecoverable Sandbox is
|
|
47
|
-
# discarded and its slot refills by a fresh construction on next
|
|
48
|
-
# demand.
|
|
35
|
+
# block's value; the Sandbox returns to the pool however the block
|
|
36
|
+
# exits. Raises PoolTimeoutError once every slot has stayed held past
|
|
37
|
+
# +checkout_timeout+.
|
|
49
38
|
def with
|
|
50
39
|
sandbox = acquire
|
|
51
40
|
begin
|
|
52
41
|
yield sandbox
|
|
53
|
-
rescue TrapError
|
|
54
|
-
release_capacity!
|
|
55
|
-
sandbox = nil
|
|
56
|
-
raise
|
|
57
42
|
ensure
|
|
58
|
-
checkin(sandbox)
|
|
43
|
+
checkin(sandbox)
|
|
59
44
|
end
|
|
60
45
|
end
|
|
61
46
|
|
|
62
47
|
private
|
|
63
48
|
|
|
64
|
-
# The idle-first claim loop: an idle Sandbox wins, unclaimed
|
|
65
|
-
# capacity constructs, and a full pool waits for a checkin.
|
|
66
49
|
def acquire
|
|
67
50
|
timeout = @checkout_timeout
|
|
68
51
|
deadline = timeout && (monotonic_now + timeout)
|
|
@@ -73,11 +56,10 @@ module Kobako
|
|
|
73
56
|
end
|
|
74
57
|
end
|
|
75
58
|
|
|
76
|
-
#
|
|
77
|
-
# happens
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
# failure.
|
|
59
|
+
# Waiting happens inside the lock (so a checkin can wake it);
|
|
60
|
+
# construction happens outside (so a slow setup block never holds the
|
|
61
|
+
# lock) — capacity is reserved here and released by +construct_slot+
|
|
62
|
+
# on failure.
|
|
81
63
|
def claim_or_wait(deadline)
|
|
82
64
|
@mutex.synchronize do
|
|
83
65
|
return [:idle, @idle.pop] unless @idle.empty?
|
|
@@ -92,9 +74,7 @@ module Kobako
|
|
|
92
74
|
end
|
|
93
75
|
end
|
|
94
76
|
|
|
95
|
-
#
|
|
96
|
-
# +Kobako::PoolTimeoutError+ once +deadline+ has passed. Must
|
|
97
|
-
# run while holding +@mutex+.
|
|
77
|
+
# Must run while holding +@mutex+.
|
|
98
78
|
def await_slot!(deadline)
|
|
99
79
|
remaining = deadline && (deadline - monotonic_now)
|
|
100
80
|
if remaining && remaining <= 0
|
|
@@ -105,10 +85,8 @@ module Kobako
|
|
|
105
85
|
@slot_freed.wait(@mutex, remaining)
|
|
106
86
|
end
|
|
107
87
|
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
# propagate to the checkout caller unchanged; the reserved
|
|
111
|
-
# capacity is released so a later checkout can retry.
|
|
88
|
+
# A failed construction releases its reserved capacity so a later
|
|
89
|
+
# checkout can retry.
|
|
112
90
|
def construct_slot
|
|
113
91
|
done = false
|
|
114
92
|
sandbox = Sandbox.new(**@sandbox_options)
|
|
@@ -119,7 +97,6 @@ module Kobako
|
|
|
119
97
|
release_capacity! unless done
|
|
120
98
|
end
|
|
121
99
|
|
|
122
|
-
# Return a Sandbox to the idle list and wake one waiting checkout.
|
|
123
100
|
def checkin(sandbox)
|
|
124
101
|
@mutex.synchronize do
|
|
125
102
|
@idle.push(sandbox)
|
|
@@ -127,8 +104,6 @@ module Kobako
|
|
|
127
104
|
end
|
|
128
105
|
end
|
|
129
106
|
|
|
130
|
-
# Give back reserved-but-unfilled capacity — a failed construction or
|
|
131
|
-
# a discarded Sandbox — and wake one waiting checkout to claim it.
|
|
132
107
|
def release_capacity!
|
|
133
108
|
@mutex.synchronize do
|
|
134
109
|
@constructed -= 1
|
|
@@ -142,17 +117,12 @@ module Kobako
|
|
|
142
117
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
143
118
|
end
|
|
144
119
|
|
|
145
|
-
# Pre-flight for +slots+ — no coercion, a positive Integer is
|
|
146
|
-
# the only accepted shape.
|
|
147
120
|
def validate_slots!(slots)
|
|
148
121
|
return if slots.is_a?(Integer) && slots.positive?
|
|
149
122
|
|
|
150
123
|
raise ArgumentError, "slots must be a positive Integer, got #{slots.inspect}"
|
|
151
124
|
end
|
|
152
125
|
|
|
153
|
-
# Coerce +checkout_timeout+ into the Float seconds the wait loop
|
|
154
|
-
# consumes, or +nil+ to wait indefinitely — the same normalisation
|
|
155
|
-
# idiom +SandboxOptions+ applies to +timeout+.
|
|
156
126
|
def normalize_checkout_timeout(checkout_timeout)
|
|
157
127
|
return nil if checkout_timeout.nil?
|
|
158
128
|
unless checkout_timeout.is_a?(Numeric)
|
data/lib/kobako/runtime.rb
CHANGED
|
@@ -1,27 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
# +#eval+ / +#run+, capture and usage readers) is registered from Rust
|
|
12
|
-
# at ext load time.
|
|
13
|
-
class Runtime
|
|
14
|
-
# Absolute path to the gem-bundled +data/kobako.wasm+ artifact. Computed
|
|
15
|
-
# from this file's location so it works for both +bundle exec+ (running
|
|
16
|
-
# from the repo) and an installed gem (running from the gem dir).
|
|
17
|
-
#
|
|
18
|
-
# Returns a String regardless of whether the file currently exists —
|
|
19
|
-
# call sites that need the file to be present should pass this through
|
|
20
|
-
# +Kobako::Runtime.from_path+, which raises
|
|
21
|
-
# +Kobako::ModuleNotBuiltError+ with a clear remediation message.
|
|
22
|
-
# Raises +Kobako::SetupError+ if +__dir__+ is unavailable so that
|
|
23
|
-
# +rescue Kobako::SetupError+ around +Kobako::Sandbox.new+ catches every
|
|
24
|
-
# construction-layer failure uniformly, including this path-resolution one.
|
|
4
|
+
# The native wasmtime runtime, defined by the ext; this file adds only the
|
|
5
|
+
# default Guest Binary path.
|
|
6
|
+
class Runtime # :nodoc:
|
|
7
|
+
# The path is answered whether or not the file exists; from_path is
|
|
8
|
+
# where a missing artifact is refused. A missing +__dir__+ raises
|
|
9
|
+
# SetupError so one rescue around Sandbox.new covers every construction
|
|
10
|
+
# failure.
|
|
25
11
|
def self.default_path
|
|
26
12
|
dir = __dir__ or raise Kobako::SetupError, "Kobako::Runtime.default_path requires __dir__"
|
|
27
13
|
File.expand_path("../../data/kobako.wasm", dir)
|