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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +71 -0
  4. data/Cargo.lock +117 -140
  5. data/README.md +100 -125
  6. data/ROADMAP.md +23 -22
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +31 -0
  9. data/crates/kobako-runtime/Cargo.toml +2 -2
  10. data/crates/kobako-runtime/README.md +4 -3
  11. data/crates/kobako-runtime/src/error.rs +8 -9
  12. data/crates/kobako-runtime/src/lib.rs +1 -1
  13. data/crates/kobako-runtime/src/profile.rs +1 -1
  14. data/crates/kobako-runtime/src/runtime.rs +31 -17
  15. data/crates/kobako-runtime/src/snapshot.rs +13 -10
  16. data/crates/kobako-runtime/src/yielder.rs +9 -6
  17. data/crates/kobako-transport/CHANGELOG.md +28 -0
  18. data/crates/kobako-transport/Cargo.toml +1 -1
  19. data/crates/kobako-transport/README.md +21 -12
  20. data/crates/kobako-transport/src/abi.rs +11 -14
  21. data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
  22. data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
  23. data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
  24. data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
  25. data/crates/kobako-transport/src/envelope/reply.rs +77 -53
  26. data/crates/kobako-transport/src/envelope/request.rs +295 -0
  27. data/crates/kobako-transport/src/envelope/run.rs +1 -2
  28. data/crates/kobako-transport/src/envelope/tag.rs +49 -0
  29. data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
  30. data/crates/kobako-transport/src/envelope.rs +19 -12
  31. data/crates/kobako-transport/src/lib.rs +0 -2
  32. data/crates/kobako-wasmtime/CHANGELOG.md +44 -0
  33. data/crates/kobako-wasmtime/Cargo.toml +7 -5
  34. data/crates/kobako-wasmtime/README.md +2 -2
  35. data/crates/kobako-wasmtime/src/abi.rs +5 -8
  36. data/crates/kobako-wasmtime/src/ambient.rs +2 -2
  37. data/crates/kobako-wasmtime/src/cache.rs +25 -59
  38. data/crates/kobako-wasmtime/src/capture.rs +4 -11
  39. data/crates/kobako-wasmtime/src/config.rs +11 -14
  40. data/crates/kobako-wasmtime/src/dispatch.rs +70 -80
  41. data/crates/kobako-wasmtime/src/driver.rs +133 -86
  42. data/crates/kobako-wasmtime/src/exports.rs +88 -34
  43. data/crates/kobako-wasmtime/src/guest_mem.rs +145 -138
  44. data/crates/kobako-wasmtime/src/instance_pre.rs +11 -22
  45. data/crates/kobako-wasmtime/src/invocation.rs +55 -372
  46. data/crates/kobako-wasmtime/src/io.rs +286 -0
  47. data/crates/kobako-wasmtime/src/lib.rs +3 -24
  48. data/crates/kobako-wasmtime/src/limiter.rs +253 -0
  49. data/crates/kobako-wasmtime/src/trap.rs +32 -45
  50. data/crates/kobako-wasmtime/tests/preamble_flags.rs +69 -0
  51. data/crates/kobako-wasmtime/tests/snippet_compile.rs +94 -0
  52. data/crates/kobako-wasmtime/tests/support/mod.rs +34 -0
  53. data/crates/kobako-wasmtime/tests/unsupported_call.rs +50 -0
  54. data/data/kobako.wasm +0 -0
  55. data/ext/kobako/Cargo.toml +2 -2
  56. data/ext/kobako/src/runtime/bridge.rs +38 -47
  57. data/ext/kobako/src/runtime/errors.rs +26 -32
  58. data/ext/kobako/src/runtime/gvl.rs +4 -12
  59. data/ext/kobako/src/runtime.rs +169 -212
  60. data/lib/kobako/capture.rb +1 -4
  61. data/lib/kobako/catalog/extensions.rb +21 -59
  62. data/lib/kobako/catalog/flags.rb +41 -0
  63. data/lib/kobako/catalog/handles.rb +7 -54
  64. data/lib/kobako/catalog/services.rb +7 -61
  65. data/lib/kobako/catalog/snippets.rb +42 -66
  66. data/lib/kobako/catalog.rb +2 -10
  67. data/lib/kobako/codec/decoder.rb +9 -28
  68. data/lib/kobako/codec/encoder.rb +3 -22
  69. data/lib/kobako/codec/error.rb +6 -27
  70. data/lib/kobako/codec/ext_types.rb +72 -94
  71. data/lib/kobako/codec/handle_walk.rb +65 -130
  72. data/lib/kobako/codec/nesting.rb +3 -8
  73. data/lib/kobako/codec/state.rb +4 -10
  74. data/lib/kobako/codec/utils.rb +6 -34
  75. data/lib/kobako/codec.rb +3 -22
  76. data/lib/kobako/constant_name.rb +8 -0
  77. data/lib/kobako/context.rb +51 -112
  78. data/lib/kobako/errors.rb +51 -127
  79. data/lib/kobako/execution.rb +16 -21
  80. data/lib/kobako/extension.rb +23 -40
  81. data/lib/kobako/handle.rb +7 -12
  82. data/lib/kobako/outcome.rb +51 -80
  83. data/lib/kobako/payload/arguments.rb +1 -16
  84. data/lib/kobako/payload.rb +4 -12
  85. data/lib/kobako/pool.rb +23 -53
  86. data/lib/kobako/runtime.rb +7 -21
  87. data/lib/kobako/sandbox.rb +96 -133
  88. data/lib/kobako/sandbox_options.rb +38 -84
  89. data/lib/kobako/snippet/binary.rb +1 -16
  90. data/lib/kobako/snippet/source.rb +1 -16
  91. data/lib/kobako/snippet.rb +1 -9
  92. data/lib/kobako/transport/call.rb +1 -14
  93. data/lib/kobako/transport/dispatcher.rb +112 -195
  94. data/lib/kobako/transport/error.rb +6 -20
  95. data/lib/kobako/transport/exposure.rb +5 -10
  96. data/lib/kobako/transport/reflection.rb +39 -50
  97. data/lib/kobako/transport/run.rb +17 -71
  98. data/lib/kobako/transport/yielder.rb +21 -73
  99. data/lib/kobako/transport.rb +3 -15
  100. data/lib/kobako/unresolved.rb +4 -11
  101. data/lib/kobako/usage.rb +2 -6
  102. data/lib/kobako/version.rb +1 -1
  103. data/sig/kobako/catalog/extensions.rbs +2 -2
  104. data/sig/kobako/catalog/flags.rbs +17 -0
  105. data/sig/kobako/catalog/services.rbs +0 -6
  106. data/sig/kobako/catalog/snippets.rbs +4 -4
  107. data/sig/kobako/codec/ext_types.rbs +10 -8
  108. data/sig/kobako/codec/handle_walk.rbs +9 -7
  109. data/sig/kobako/codec/utils.rbs +2 -2
  110. data/sig/kobako/codec.rbs +2 -2
  111. data/sig/kobako/constant_name.rbs +3 -0
  112. data/sig/kobako/context.rbs +2 -3
  113. data/sig/kobako/errors.rbs +6 -0
  114. data/sig/kobako/outcome.rbs +6 -8
  115. data/sig/kobako/runtime.rbs +12 -8
  116. data/sig/kobako/sandbox.rbs +4 -0
  117. data/sig/kobako/sandbox_options.rbs +2 -6
  118. data/sig/kobako/transport/dispatcher.rbs +14 -18
  119. data/sig/kobako/transport/reflection.rbs +5 -3
  120. data/sig/kobako/transport/run.rbs +1 -3
  121. data/sig/kobako/transport/yielder.rbs +3 -3
  122. data/sig/kobako/transport.rbs +7 -6
  123. metadata +16 -4
  124. data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
  125. data/crates/kobako-wasmtime/src/frames.rs +0 -273
  126. data/rust-toolchain.toml +0 -15
@@ -1,48 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kobako
4
- # Kobako::Extension — a guest idiom paired with an optional host backend,
5
- # installed on a Sandbox via +Sandbox#install+. It composes the existing
6
- # +#preload+ (the guest +source+) and +#bind+ (the +backend+) verbs into
7
- # one setup unit, so guest code sees a native-style constant whose pure
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
- # The four readers form the contract +#install+ duck-types on:
12
- #
13
- # * +name+ — a Symbol matching +/\A[A-Z]\w*\z/+, the preloaded snippet's
14
- # canonical backtrace name and the +depends_on+ match key. Independent
15
- # of any bound path.
16
- # * +source+ — the mruby idiom as a String; always present, since an
17
- # Extension always carries a guest idiom. A host object with no idiom
18
- # is bound with +#bind+ directly.
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
- # Kobako::Extension::Backend — the host attachment of an Extension,
28
- # pairing +path+ (the constant path the backend binds at, single-segment
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
- # * +object:+ — a static object, fixed for the Sandbox's life.
34
- # * +provider:+ — a no-argument callable invoked once per invocation to
35
- # yield that invocation's object, so a fresh object backs the path
36
- # every invocation. A provider that raises propagates its exception to
37
- # the invocation caller and leaves the guest unrun; the next
38
- # invocation resolves it afresh.
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 kind is chosen by keyword, never inferred from whether the value is
43
- # callable, so a static object that is itself callable is expressed
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+ default to absent so the common
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 Capability Handle as it crosses the wire (ext 0x01, laid out in
5
- # {docs/wire-codec.md}[link:../../docs/wire-codec.md]), in either
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
- # ID 0 is the invalid sentinel and is never allocated.
7
+ # The smallest id; 0 never names a Handle.
11
8
  MIN_ID = 1
12
- # The positive half of a signed 32-bit integer, so an id fits either
13
- # side of the wire unchanged.
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
- # The Host Gem's own constructor, for the wire decoder and the Handle
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
@@ -4,15 +4,7 @@ require_relative "codec"
4
4
  require_relative "transport/error"
5
5
 
6
6
  module Kobako
7
- # Host-facing boundary for the invocation outcome the native side split
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
- module_function
33
-
34
- # Settle one invocation. +kind+ names the arm, +payload+ is the
35
- # codec-encoded value the +:ok+ arm carries, and +panic+ carries
36
- # the Panic's fields +[origin, class, message, backtrace, available]+ —
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 panic ? panic_error(panic, entrypoint) : trap_error(kind)
32
+ raise panic_error(panic, entrypoint)
45
33
  end
46
34
 
47
- # Map a Panic's fields onto the three-layer taxonomy. The fields land
48
- # on the exception verbatim — it carries the record rather than a
49
- # translation of one.
50
- def panic_error(panic, entrypoint)
51
- origin, klass, message, backtrace, available = panic
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
- UndefinedEntrypointError.new(message, name: entrypoint, available: available.map(&:to_sym), **attribution)
57
- end
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
- # +origin+ picks the branch, and the guest-written class name may
60
- # narrow within it so callers can rescue one path specifically. A name
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
- selected
70
- end
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
- # An arm the host cannot settle: the guest wrote nothing, or wrote
73
- # bytes the envelope cannot frame. Either leaves nothing to attribute
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
- TrapError.new(
80
- "Sandbox produced an unrecognised result; the runtime is corrupted, " \
81
- "discard this Sandbox before another invocation"
82
- )
83
- end
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
- # The ok arm's value — the one position a payload codec still
86
- # owns on this path. A decode fault means the framing was fine but the
87
- # carried value is unrepresentable.
88
- def decode_value(payload)
89
- Kobako::Codec::Decoder.decode(payload)
90
- rescue Kobako::Codec::Error => e
91
- raise wire_error("Sandbox produced an invalid result value", diagnostic: e.message)
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
- # The invocation arguments a Call or a Run carries: a 2-element
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
@@ -3,17 +3,9 @@
3
3
  require_relative "payload/arguments"
4
4
 
5
5
  module Kobako
6
- # The payload shapes that ride inside a core envelope's opaque payload
7
- # field. The envelope routes a message and the native side decodes it;
8
- # this layer decides what the resolved method receives, and the byte-level
9
- # encoding of that shape belongs to +Kobako::Codec+ beneath it.
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
- # 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.
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 +#with+ wait bound applied when +checkout_timeout+ is not given.
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. +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 +#bind+ / +#preload+ before that Sandbox's first checkout.
28
- # No Sandbox is constructed here. Raises +ArgumentError+ for an
29
- # invalid +slots+ / +checkout_timeout+.
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. 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.
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) if 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
- # Single locked decision point for one claim attempt. Waiting
77
- # happens inside the lock (so a checkin can wake it); construction
78
- # happens outside (so a slow setup block never holds the lock) —
79
- # capacity is reserved here and released by +construct_slot+ on
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
- # Wait for a checkin or freed capacity; raises
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
- # Construct and set up one pooled Sandbox against the capacity
109
- # reserved by +claim_or_wait+. Construction and setup-block errors
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)
@@ -1,27 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kobako
4
- # Host-side wasmtime runtime, surfaced as a Ruby class by the native ext
5
- # (see ext/kobako/src/runtime.rs). The +Kobako::Runtime+ class wraps the
6
- # wasmtime engine + compiled module + Store; it is the only Ruby-visible
7
- # wasmtime type and the foundational binding layer for +Kobako::Sandbox+.
8
- #
9
- # This file reopens the magnus-defined class only to add the pure-Ruby
10
- # +.default_path+ helper. Every other method (+#from_path+ singleton,
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)