Class: SecurityBox::RactorPool
- Inherits:
-
Object
- Object
- SecurityBox::RactorPool
- Defined in:
- lib/security_box/ractor_pool.rb
Overview
Pool of Ractor workers sharing one Engine + compiled Module (stage-3 Q2 "Plan C"): the pool builds a dedicated runtime pair, makes it Ractor-shareable, and N worker Ractors each keep a private Linker. Per eval a worker creates Store + Instance (~0.2ms), applies the request's fuel/epoch/memory limits (safe: the worker owns its store), and invokes.
This is the only supported way to run evaluations in parallel: invoke
holds the GVL for the whole guest lifetime (stage-4 Q1), so threads
serialize (see Pool).
Protocol (Ruby 3.4+/4.0 Ractor port model):
- eval builds a request code, token, config and sends it to the worker with the fewest in-flight requests (worker << request);
- workers run the shared EvalRun core and push worker, result back to the main Ractor (Ractor.main << ...);
- a main-side collector thread receives from the main Ractor port and routes each result to the Queue of the thread waiting for it (results are matched by request id; this thread must be the only main-port receiver in the process);
- every request terminates (epoch + fuel), and a pop deadline converts a dead worker into a :sandbox_error result instead of a hang.
Trade-offs (measured, stage-3 Q2 / stage-4): one module deserialize per pool (~0.5s via the disk cache); each worker's eval still pays the ~240ms guest boot. Ratios at 2/4 Ractors on 6 cores: 0.56 / 0.28 wall vs serial, RSS ~111MB with the shared runtime.
Usage:
pool = SecurityBox::RactorPool.new(:lean, size: 4)
pool.eval("1 + 1") # => Result
pool.shutdown
Constant Summary collapse
- STOP =
:security_box_stop- POP_SLACK_MS =
10_000
Instance Attribute Summary collapse
-
#size ⇒ Object
readonly
Returns the value of attribute size.
Instance Method Summary collapse
- #closed? ⇒ Boolean
-
#eval(code, **overrides) ⇒ Object
Runs
codeon one of the workers and blocks until its Result arrives. -
#initialize(profile = nil, size: 4, **options) ⇒ RactorPool
constructor
A new instance of RactorPool.
-
#metrics ⇒ Object
Snapshot of the counters: evals:, total_ms:, avg_ms:, timeouts:.
-
#shutdown ⇒ Object
Asks all workers to stop.
Constructor Details
#initialize(profile = nil, size: 4, **options) ⇒ RactorPool
Returns a new instance of RactorPool.
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/security_box/ractor_pool.rb', line 43 def initialize(profile = nil, size: 4, **) @size = Integer(size) raise ArgumentError, "size must be >= 1" if @size < 1 @config = resolve_config(profile, ) raise InvalidConfiguration, "rpcs are not supported on RactorPool (handlers cannot cross a Ractor boundary)" unless @config.rpcs.empty? @mutex = Mutex.new @pending = {} # request id => Queue @in_flight = Array.new(@size, 0) @closed = false @next_id = 0 @metrics = { evals: 0, total_ms: 0.0, timeouts: 0 } engine, module_ = Runtime.build_shareable( image_path: @config.image_path, epoch_interval_ms: @config.epoch_interval_ms ) @workers = Array.new(@size) do |index| build_worker(engine, module_, index) end start_collector_thread end |
Instance Attribute Details
#size ⇒ Object (readonly)
Returns the value of attribute size.
41 42 43 |
# File 'lib/security_box/ractor_pool.rb', line 41 def size @size end |
Instance Method Details
#closed? ⇒ Boolean
123 124 125 |
# File 'lib/security_box/ractor_pool.rb', line 123 def closed? @mutex.synchronize { @closed } end |
#eval(code, **overrides) ⇒ Object
Runs code on one of the workers and blocks until its Result arrives.
Per-call overrides follow Sandbox#eval (same Configuration#with rules).
RPC handlers are not supported here (v1): they are host Procs that cannot cross a Ractor boundary. A configuration with rpcs raises InvalidConfiguration at construction.
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/security_box/ractor_pool.rb', line 73 def eval(code, **overrides) raise PoolClosed, "pool is closed" if closed? config = overrides.empty? ? @config : @config.with(**overrides) raise InvalidConfiguration, "rpcs are not supported on RactorPool (handlers cannot cross a Ractor boundary)" unless config.rpcs.empty? request = { id: next_id, code: code, token: SecureRandom.hex(16), config: config } queue = Queue.new worker_index = @mutex.synchronize do @pending[request[:id]] = queue index = @in_flight.each_with_index.min_by { |count, _| count }.last @in_flight[index] += 1 index end begin @workers[worker_index] << request deadline_ms = config.timeout_ms + POP_SLACK_MS result = queue.pop(timeout: deadline_ms / 1000.0) raise ThreadError, "no result" if result.nil? # Queue#pop timeout record(result) result rescue ThreadError, Ractor::ClosedError # Worker died, stalled beyond the deadline, or was shut down mid-flight: # report instead of hang or leak the error. @mutex.synchronize { @metrics[:timeouts] += 1 } Result.worker_unavailable(worker_index) ensure @mutex.synchronize do @in_flight[worker_index] -= 1 if @in_flight[worker_index] > 0 @pending.delete(request[:id]) end end end |
#metrics ⇒ Object
Snapshot of the counters: evals:, total_ms:, avg_ms:, timeouts:.
114 115 116 117 118 119 120 121 |
# File 'lib/security_box/ractor_pool.rb', line 114 def metrics @mutex.synchronize do evals = @metrics[:evals] { size: @size, evals: evals, total_ms: @metrics[:total_ms].round(2), avg_ms: evals.zero? ? 0.0 : (@metrics[:total_ms] / evals).round(2), timeouts: @metrics[:timeouts] } end end |
#shutdown ⇒ Object
Asks all workers to stop. Evals already in flight finish; new evals raise PoolClosed.
129 130 131 132 |
# File 'lib/security_box/ractor_pool.rb', line 129 def shutdown @mutex.synchronize { @closed = true } @workers.each { |worker| worker << STOP } end |