Class: Kobako::Context

Inherits:
Object
  • Object
show all
Defined in:
lib/kobako/context.rb,
sig/kobako/context.rbs

Overview

Kobako::Context — the per-invocation driver behind a single Sandbox#eval / #run. Each Context owns a fresh Catalog::Handles table and drives one guest invocation: it builds the dispatch handler, runs the guest, records the run's captures and usage, and decodes the outcome. One Context per invocation owns all per-invocation state, so the reusable Sandbox holds none and overlapping evals never touch each other's state.

Instance Method Summary collapse

Constructor Details

#initialize(runtime:, services:, snippets:, extensions:) ⇒ Context

Build a Context over the Sandbox-owned config — the Runtime, the sealed Catalog::Services / Catalog::Snippets registries, and the Catalog::Extensions whose callable backends this Context resolves for its own run. The Catalog::Handles table is this invocation's own, so guest→host dispatch and host→guest auto-wrap share one allocator scoped to the run; the resolved provider map is likewise per-invocation, so concurrent invocations never share mutable state.



28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/kobako/context.rb', line 28

def initialize(runtime:, services:, snippets:, extensions:)
  @runtime = runtime
  @services = services
  @snippets = snippets
  @extensions = extensions
  @resolved = {} # : Hash[String, untyped]
  @overrides = {} # : Hash[String, untyped]
  @spent = false
  @handler = Catalog::Handles.new
  @stdout_capture = @stderr_capture = Capture::EMPTY
  @usage = Usage::EMPTY
end

Instance Method Details

#bind(path, object) ⇒ self

Override the object bound at an already-declared path for this invocation only — the per-eval hook the #eval / #run block uses to fill a fillable or shadow any static / per-invocation binding. path must name a declared (Frame 1) binding; overriding an undeclared path raises ArgumentError so the Frame 1 key set stays fixed. Valid only while the block runs — the Context is spent once the block returns, so a captured ctx used afterward raises ArgumentError too, putting both misuses on one rescuable channel. Returns self.

Parameters:

  • path (Symbol, String)
  • object (Object)

Returns:

  • (self)

Raises:

  • (ArgumentError)


49
50
51
52
53
54
55
56
57
# File 'lib/kobako/context.rb', line 49

def bind(path, object)
  raise ArgumentError, "Kobako::Context is spent; ctx.bind is only valid inside the #eval / #run block" if @spent

  key = path.to_s
  raise ArgumentError, "cannot override undeclared path #{key.inspect}" unless @services.bound?(key)

  @overrides[key] = object
  self
end

#build_execution(value, failed:) ⇒ Kobako::Execution

Freeze this run's observables plus value (+nil+ on a failed run) into the read-only Execution the caller receives or the error carries. failed records the two apart so a nil value from a successful run stays distinct from a failed one.

Parameters:

  • value (Object)
  • failed: (Boolean)

Returns:



165
166
167
# File 'lib/kobako/context.rb', line 165

def build_execution(value, failed:)
  Execution.new(value: value, usage: @usage, stdout: @stdout_capture, stderr: @stderr_capture, failed: failed)
end

#collect_overrides {|arg0| ... } ⇒ void

This method returns an undefined value.

Run the per-eval override block, handing it this Context so it can call ctx.bind, then spend the Context so a captured ctx used after the block raises. A block that raises propagates before the guest drives, so the guest never runs and no Execution is produced.

Yields:

Yield Parameters:

Yield Returns:

  • (void)


103
104
105
106
107
# File 'lib/kobako/context.rb', line 103

def collect_overrides
  yield self
ensure
  @spent = true
end

#dispatch_handlerKobako::Transport::dispatch_proc

Build this invocation's guest→host dispatch handler — a Proc routing each guest→host call through the stateless Transport::Dispatcher, capturing this Context as the path resolver (its #lookup layers the per-invocation providers over the static bindings) plus @handler. Handed to Runtime#eval / #run as a call argument, so the Runtime holds no dispatch state and the Proc stays GC-rooted as a live argument for the synchronous call. The ext hands the Proc a per-dispatch guest_yielder — a String → String callable that re-enters the in-flight guest to run a yielded block — which the Dispatcher forwards to the Transport::Yielder it builds for the call.

Returns:

  • (Kobako::Transport::dispatch_proc)


119
120
121
122
123
124
125
# File 'lib/kobako/context.rb', line 119

def dispatch_handler
  lambda do |target, method_name, block_given, payload, guest_yielder|
    call = Transport::Call.new(target: target, method_name: method_name,
                               block_given: block_given, payload: payload)
    Transport::Dispatcher.dispatch(call, self, @handler, guest_yielder)
  end
end

#eval(code, &block) ⇒ void

This method returns an undefined value.

Execute a guest mruby source string in a fresh mrb_state and return the decoded last expression. A given block runs first, receiving this Context to collect ctx.bind overrides before the guest drives.

Parameters:

  • code (String)


79
80
81
82
83
84
# File 'lib/kobako/context.rb', line 79

def eval(code, &block)
  collect_overrides(&block) if block
  invoke!(:eval) do
    @runtime.eval(dispatch_handler, @services.paths, code.b, @snippets.entries)
  end
end

#invoke!(verb, entrypoint: nil) { ... } ⇒ Kobako::Execution

Drive one invocation and settle it into a frozen Execution. verb tags the TrapError message so the failing export is identifiable. This invocation's callable Extension backends are resolved first — before the guest runs — so a provider that raises propagates unwrapped and leaves the guest unrun. Usage and captures are recorded before the trap check, so a trapped Snapshot's error carries them just like a completed run's return value. A could-not-start fault ran no invocation at all, so it carries no Execution and gains only the verb prefix.

Parameters:

  • verb (Symbol)
  • entrypoint: (Symbol, nil) (defaults to: nil)

Yields:

Yield Returns:

Returns:



195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/kobako/context.rb', line 195

def invoke!(verb, entrypoint: nil)
  @resolved = @extensions.resolve
  begin
    snapshot = yield
  rescue Kobako::TrapError => e
    raise trap_class_for(e), "Sandbox##{verb} failed: #{e.message}"
  end
  populate_observability!(snapshot)
  return settle_outcome(snapshot, verb, entrypoint) unless snapshot.trapped?

  raise trap_error_for(snapshot, verb).with_execution(build_execution(nil, failed: true))
end

#lookup(path) ⇒ Object

Resolve a Service path to the object backing it this invocation, layering the per-eval ctx.bind overrides over this Context's per-invocation provider results over the Sandbox's static base bindings. An unbound path raises KeyError; a fillable left unfilled resolves to Kobako::Unresolved and is reported the same way, so an unresolved capability fails closed as an undefined target rather than dispatching to the sentinel. The Dispatcher maps either KeyError to an undefined-target wire fault. Internal — the per-invocation dispatch handler is the sole caller.

Parameters:

  • path (Symbol, String)

Returns:

  • (Object)

Raises:

  • (KeyError)


68
69
70
71
72
73
74
# File 'lib/kobako/context.rb', line 68

def lookup(path)
  key = path.to_s
  object = @overrides.fetch(key) { @resolved.fetch(key) { @services.lookup(path) } }
  raise KeyError, "service #{path} is declared but unresolved this invocation" if Unresolved.equal?(object)

  object
end

#populate_observability!(snapshot) ⇒ void

This method returns an undefined value.

Record this invocation's usage and both output captures from the ext Snapshot. Every Snapshot carries them — value return or trap alike — so #usage / #stdout / #stderr stay readable after a rescued trap.

Parameters:



130
131
132
133
134
# File 'lib/kobako/context.rb', line 130

def populate_observability!(snapshot)
  @usage = Usage.new(wall_time: snapshot.wall_time, memory_peak: snapshot.memory_peak)
  @stdout_capture = Capture.new(bytes: snapshot.stdout, truncated: snapshot.stdout_truncated?)
  @stderr_capture = Capture.new(bytes: snapshot.stderr, truncated: snapshot.stderr_truncated?)
end

#run(request, &block) ⇒ void

This method returns an undefined value.

Dispatch a Transport::Run into a preloaded entrypoint and return the decoded result. A given block runs first, receiving this Context to collect ctx.bind overrides before the guest drives.

Parameters:



89
90
91
92
93
94
95
# File 'lib/kobako/context.rb', line 89

def run(request, &block)
  collect_overrides(&block) if block
  invoke!(:run, entrypoint: request.entrypoint) do
    @runtime.run(dispatch_handler, @services.paths, @snippets.entries,
                 request.entrypoint.to_s, request.payload(@handler))
  end
end

#settle_outcome(snapshot, verb, entrypoint) ⇒ Kobako::Execution

Settle a completed run's outcome into its Execution. A Capability Handle in the result is restored to its host object first. The settle sits in the rescue so a wire-violation trap or a Panic both attach this run's Execution, just like a guest-call trap does. entrypoint is the name #run asked for, which the host knows and the wire never carries, so an unresolved one names itself on the error it raises.

Parameters:

Returns:



175
176
177
178
179
180
181
182
183
184
185
# File 'lib/kobako/context.rb', line 175

def settle_outcome(snapshot, verb, entrypoint)
  kind, payload, panic = snapshot.outcome
  value, carried = Codec.track_handles { Outcome.reify(kind, payload, panic, entrypoint: entrypoint) }
  value = Codec::HandleWalk.deep_restore(value, @handler) if carried
  build_execution(value, failed: false)
rescue Kobako::TrapError => e
  raise trap_class_for(e).new("Sandbox##{verb} failed: #{e.message}").with_execution(build_execution(nil,
                                                                                                     failed: true))
rescue Kobako::SandboxError, Kobako::ServiceError => e
  raise e.with_execution(build_execution(nil, failed: true))
end

#trap_class_for(err) ⇒ singleton(Kobako::TrapError)

Pick the TrapError subclass to re-raise based on +err+'s actual class. Cap-trap subclasses (+TimeoutError+ / MemoryLimitError) preserve their named identity; everything else collapses to the base Kobako::TrapError, so #invoke! can add the verb prefix without erasing the named subclass.

Parameters:

Returns:



140
141
142
143
144
145
146
# File 'lib/kobako/context.rb', line 140

def trap_class_for(err)
  case err
  when TimeoutError     then TimeoutError
  when MemoryLimitError then MemoryLimitError
  else TrapError
  end
end

#trap_error_for(snapshot, verb) ⇒ Kobako::TrapError

Build the +TrapError+-family exception for a trapped Snapshot from its neutral trap kind, tagged with the verb — the cap subclasses (+TimeoutError+ / MemoryLimitError) keep their identity, every other engine fault is the base TrapError.

Parameters:

Returns:



152
153
154
155
156
157
158
159
# File 'lib/kobako/context.rb', line 152

def trap_error_for(snapshot, verb)
  klass = case snapshot.trap_kind
          when :timeout      then TimeoutError
          when :memory_limit then MemoryLimitError
          else TrapError
          end
  klass.new("Sandbox##{verb} failed: #{snapshot.trap_message}")
end