Class: Kobako::Pool
- Inherits:
-
Object
- Object
- Kobako::Pool
- Defined in:
- lib/kobako/pool.rb,
sig/kobako/pool.rbs
Overview
Kobako::Pool — a bounded set of warm, identically set-up Sandboxes handed out one exclusive holder at a time.
Construction forwards every Kobako::Sandbox.new keyword verbatim
and holds the optional block as the per-Sandbox setup hook; a
checkout prefers an idle Sandbox and constructs a new one only when
none is idle and fewer than slots exist. #with blocks up
to checkout_timeout seconds when every slot is held, applies
the TrapError discard-and-recreate contract at checkin, and
the Pool releases everything with its own reachability — there is no
teardown verb.
Constant Summary collapse
- DEFAULT_CHECKOUT_TIMEOUT_SECONDS =
The
#withwait bound applied whencheckout_timeoutis not given. 5.0
Instance Method Summary collapse
-
#acquire ⇒ Kobako::Sandbox
The idle-first claim loop: an idle Sandbox wins, unclaimed capacity constructs, and a full pool waits for a checkin.
-
#await_slot!(deadline) ⇒ void
Wait for a checkin or freed capacity; raises
Kobako::PoolTimeoutErroroncedeadlinehas passed. -
#checkin(sandbox) ⇒ void
Return a Sandbox to the idle list and wake one waiting checkout.
-
#checkout ⇒ Kobako::Sandbox
Acquire a Sandbox and hand it over in pre-invocation state — empty output buffers and truncation predicates false.
-
#claim_or_wait(deadline) ⇒ [Symbol, Kobako::Sandbox?]
Single locked decision point for one claim attempt.
-
#construct_slot ⇒ Kobako::Sandbox
Construct and set up one pooled Sandbox against the capacity reserved by
claim_or_wait. -
#initialize(slots:, checkout_timeout: DEFAULT_CHECKOUT_TIMEOUT_SECONDS, **sandbox_options, &setup) ⇒ Pool
constructor
Build a Pool of up to
slotsSandboxes. -
#monotonic_now ⇒ Float
The wait deadline runs on the monotonic clock so a wall-clock jump cannot stretch or cut the checkout wait.
-
#normalize_checkout_timeout(checkout_timeout) ⇒ Float?
Coerce
checkout_timeoutinto the Float seconds the wait loop consumes, ornilto wait indefinitely — the same normalisation idiomSandboxOptionsapplies totimeout. -
#release_capacity! ⇒ void
Give back reserved-but-unfilled capacity — a failed construction or a discarded Sandbox — and wake one waiting checkout to claim it.
-
#validate_slots!(slots) ⇒ void
Pre-flight for
slots— no coercion, a positive Integer is the only accepted shape. -
#with ⇒ void
Yield one exclusively-held Sandbox to the block and return the block's value.
Constructor Details
#initialize(slots:, checkout_timeout: DEFAULT_CHECKOUT_TIMEOUT_SECONDS, **sandbox_options, &setup) ⇒ Pool
Build a Pool of up to slots Sandboxes. slots is
a positive Integer; checkout_timeout bounds the #with wait in
seconds (+nil+ waits indefinitely); every other keyword is
forwarded verbatim to Kobako::Sandbox.new. The optional block
runs exactly once per constructed Sandbox — it is the setup window
for #define / #preload before that Sandbox's first checkout.
No Sandbox is constructed here. Raises ArgumentError for an
invalid slots / checkout_timeout.
30 31 32 33 34 35 36 37 38 39 40 |
# File 'lib/kobako/pool.rb', line 30 def initialize(slots:, checkout_timeout: DEFAULT_CHECKOUT_TIMEOUT_SECONDS, **, &setup) validate_slots!(slots) @slots = slots @checkout_timeout = normalize_checkout_timeout(checkout_timeout) @sandbox_options = @setup = setup @idle = [] # : Array[Kobako::Sandbox] @constructed = 0 @mutex = Mutex.new @slot_freed = ConditionVariable.new end |
Instance Method Details
#acquire ⇒ Kobako::Sandbox
The idle-first claim loop: an idle Sandbox wins, unclaimed capacity constructs, and a full pool waits for a checkin.
72 73 74 75 76 77 78 79 80 |
# File 'lib/kobako/pool.rb', line 72 def acquire timeout = @checkout_timeout deadline = timeout && (monotonic_now + timeout) loop do action, sandbox = claim_or_wait(deadline) return sandbox if action == :idle && sandbox return construct_slot if action == :build end end |
#await_slot!(deadline) ⇒ void
This method returns an undefined value.
Wait for a checkin or freed capacity; raises
Kobako::PoolTimeoutError once deadline has passed. Must
run while holding @mutex.
104 105 106 107 108 109 110 111 112 |
# File 'lib/kobako/pool.rb', line 104 def await_slot!(deadline) remaining = deadline && (deadline - monotonic_now) if remaining && remaining <= 0 raise PoolTimeoutError, "no Sandbox returned within #{@checkout_timeout}s: all #{@slots} slots are held" end @slot_freed.wait(@mutex, remaining) end |
#checkin(sandbox) ⇒ void
This method returns an undefined value.
Return a Sandbox to the idle list and wake one waiting checkout.
129 130 131 132 133 134 |
# File 'lib/kobako/pool.rb', line 129 def checkin(sandbox) @mutex.synchronize do @idle.push(sandbox) @slot_freed.signal end end |
#checkout ⇒ Kobako::Sandbox
Acquire a Sandbox and hand it over in pre-invocation state — empty output buffers and truncation predicates false.
66 67 68 |
# File 'lib/kobako/pool.rb', line 66 def checkout acquire.tap(&:reset_invocation_state!) end |
#claim_or_wait(deadline) ⇒ [Symbol, Kobako::Sandbox?]
Single locked decision point for one claim attempt. Waiting
happens inside the lock (so a checkin can wake it); construction
happens outside (so a slow setup block never holds the lock) —
capacity is reserved here and released by construct_slot on
failure.
87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/kobako/pool.rb', line 87 def claim_or_wait(deadline) @mutex.synchronize do return [:idle, @idle.pop] unless @idle.empty? if @constructed < @slots @constructed += 1 return [:build, nil] end await_slot!(deadline) [:retry, nil] end end |
#construct_slot ⇒ Kobako::Sandbox
Construct and set up one pooled Sandbox against the capacity
reserved by claim_or_wait. Construction and setup-block errors
propagate to the checkout caller unchanged; the reserved
capacity is released so a later checkout can retry.
118 119 120 121 122 123 124 125 126 |
# File 'lib/kobako/pool.rb', line 118 def construct_slot done = false sandbox = Sandbox.new(**@sandbox_options) @setup&.call(sandbox) done = true sandbox ensure release_capacity! unless done end |
#monotonic_now ⇒ Float
The wait deadline runs on the monotonic clock so a wall-clock jump cannot stretch or cut the checkout wait.
147 148 149 |
# File 'lib/kobako/pool.rb', line 147 def monotonic_now Process.clock_gettime(Process::CLOCK_MONOTONIC) end |
#normalize_checkout_timeout(checkout_timeout) ⇒ Float?
Coerce checkout_timeout into the Float seconds the wait loop
consumes, or nil to wait indefinitely — the same normalisation
idiom SandboxOptions applies to timeout.
162 163 164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/kobako/pool.rb', line 162 def normalize_checkout_timeout(checkout_timeout) return nil if checkout_timeout.nil? unless checkout_timeout.is_a?(Numeric) raise ArgumentError, "checkout_timeout must be Numeric or nil, got #{checkout_timeout.inspect}" end seconds = checkout_timeout.to_f unless seconds.positive? && seconds.finite? raise ArgumentError, "checkout_timeout must be > 0 and finite (got #{checkout_timeout})" end seconds end |
#release_capacity! ⇒ void
This method returns an undefined value.
Give back reserved-but-unfilled capacity — a failed construction or a discarded Sandbox — and wake one waiting checkout to claim it.
138 139 140 141 142 143 |
# File 'lib/kobako/pool.rb', line 138 def release_capacity! @mutex.synchronize do @constructed -= 1 @slot_freed.signal end end |
#validate_slots!(slots) ⇒ void
This method returns an undefined value.
Pre-flight for slots — no coercion, a positive Integer is
the only accepted shape.
153 154 155 156 157 |
# File 'lib/kobako/pool.rb', line 153 def validate_slots!(slots) return if slots.is_a?(Integer) && slots.positive? raise ArgumentError, "slots must be a positive Integer, got #{slots.inspect}" end |
#with ⇒ void
This method returns an undefined value.
Yield one exclusively-held Sandbox to the block and return the
block's value. Blocks while every slot is held; raises
Kobako::PoolTimeoutError once the wait exceeds checkout_timeout.
The Sandbox returns to the pool at block exit — unless the block raised
Kobako::TrapError, in which case the unrecoverable Sandbox is
discarded and its slot refills by a fresh construction on next
demand.
49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/kobako/pool.rb', line 49 def with sandbox = checkout begin yield sandbox rescue TrapError release_capacity! sandbox = nil raise ensure checkin(sandbox) if sandbox end end |