kino 0.6.0 → 0.7.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.
data/lib/kino/server.rb CHANGED
@@ -68,6 +68,16 @@ module Kino
68
68
  @bind = settings[:bind]
69
69
  @requested_port = settings[:port]
70
70
  @workers = Integer(settings[:workers])
71
+ # The pool ceiling; equal to the floor for a fixed pool.
72
+ @max_workers = settings[:max_workers].nil? ? @workers : Integer(settings[:max_workers])
73
+ if @max_workers < @workers
74
+ raise ArgumentError, "max_workers (#{@max_workers}) must be at least workers (#{@workers})"
75
+ end
76
+ @scale_down_after = settings[:scale_down_after].nil? ? 30.0 : Float(settings[:scale_down_after])
77
+ raise ArgumentError, "scale_down_after must be positive" unless @scale_down_after.positive?
78
+ if !settings[:scale_down_after].nil? && !elastic?
79
+ Log.warn("scale_down_after has no effect unless max_workers is above workers")
80
+ end
71
81
  @on_error = validate_hook(settings[:on_error], :on_error)
72
82
  @after_worker_boot = validate_hook(settings[:after_worker_boot], :after_worker_boot)
73
83
  @after_request_complete = validate_hook(settings[:after_request_complete], :after_request_complete)
@@ -81,8 +91,9 @@ module Kino
81
91
  # The access log's GC and allocation figures come from the VM's
82
92
  # process-wide counters, so they are measured only where one
83
93
  # request at a time can own them: the GVL serializes :threaded
84
- # mode, and a single ractor has nothing to race.
85
- access_timing: !!settings[:log_requests] && (@mode == :threaded || @workers == 1)
94
+ # mode, and a single ractor (a pool that can never grow past one)
95
+ # has nothing to race.
96
+ access_timing: !!settings[:log_requests] && (@mode == :threaded || @max_workers == 1)
86
97
  )
87
98
  # Default threads per mode: 1 in :ractor (threads inside a ractor
88
99
  # share its lock; a measured +17% on fast handlers; raise `workers`
@@ -126,10 +137,10 @@ module Kino
126
137
  else
127
138
  @workers * @threads
128
139
  end
129
- @worker_threads = []
130
- @worker_threads_lock = Mutex.new
131
140
  @supervisor = nil
141
+ @threaded_pool = nil
132
142
  @quarantine_monitor = nil
143
+ @pool_scaler = nil
133
144
  @started = false
134
145
  end
135
146
 
@@ -158,7 +169,8 @@ module Kino
158
169
  tls_cert: @tls&.fetch(:cert), tls_key: @tls&.fetch(:key),
159
170
  http2: @http2,
160
171
  lanes: @lanes, log_requests: @log_requests,
161
- mode: @mode.to_s, workers: @workers, threads: @threads, batch: @batch,
172
+ mode: @mode.to_s, workers: @workers, max_workers: @max_workers,
173
+ threads: @threads, batch: @batch,
162
174
  control_bind: @control_bind, control_token: @control_token
163
175
  )
164
176
  booted = true
@@ -169,12 +181,15 @@ module Kino
169
181
  # lifetime so in-flight buffers survive even a worker ractor crash.
170
182
  @pin_keeper = Native.pin_keeper(@id)
171
183
  if @mode == :ractor
184
+ warn_scheduler_cap
172
185
  @supervisor = RactorSupervisor.new(@id, @app, workers: @workers, threads: @threads,
173
186
  batch: @batch, hooks: @worker_hooks, on_worker_exit: @on_worker_exit).start
174
187
  else
175
- @worker_threads = (@workers * @threads).times.map { spawn_worker_thread }
188
+ @threaded_pool = ThreadedPool.new(@id, @app, threads: @threads, batch: @batch,
189
+ hooks: @worker_hooks, on_worker_exit: @on_worker_exit).start(@workers)
176
190
  end
177
191
  start_quarantine_monitor if @quarantine_timeout_ms
192
+ start_pool_scaler if elastic?
178
193
  Native.control_ready(@id)
179
194
  HookFire.fire(@after_boot, "after_boot")
180
195
  @started = true
@@ -192,6 +207,7 @@ module Kino
192
207
  def shutdown(timeout: nil)
193
208
  return unless @started
194
209
 
210
+ @pool_scaler&.stop
195
211
  @quarantine_monitor&.stop
196
212
  deadline = monotonic_now + (timeout || @shutdown_timeout)
197
213
  Native.stop_accepting(@id)
@@ -224,7 +240,6 @@ module Kino
224
240
  # The runtime is gone, so hyper has dropped every pinned buffer;
225
241
  # the keeper (and the strings it marked) may now be collected.
226
242
  @pin_keeper = nil
227
- @worker_threads.clear
228
243
  @started = false
229
244
  remove_pidfile if @pidfile
230
245
  nil
@@ -233,7 +248,7 @@ module Kino
233
248
  # Block until every worker has exited (i.e. until shutdown).
234
249
  # @return [void]
235
250
  def wait
236
- @supervisor ? @supervisor.join : @worker_threads.each(&:join)
251
+ pool.join
237
252
  end
238
253
 
239
254
  # Production entry point: build the server and {#run} it. The `kino`
@@ -298,19 +313,22 @@ module Kino
298
313
  # lanes mode) once started
299
314
  def stats
300
315
  base = {
301
- mode: @mode, lanes: @lanes, workers: @workers, threads: @threads,
302
- batch: @batch, respawns: 0
316
+ mode: @mode, lanes: @lanes, workers: @workers, max_workers: @max_workers,
317
+ threads: @threads, batch: @batch, respawns: 0,
318
+ active_workers: @workers, scale_ups: 0, scale_downs: 0
303
319
  }
304
320
  return base unless @started
305
321
 
306
322
  queued, in_flight, served, rejected, timeouts, respawns, lane_depths = Native.server_stats(@id)
307
323
  base.merge!(queued:, in_flight:, served:, rejected:, timeouts:, respawns:)
308
324
  base[:lane_depths] = lane_depths if lane_depths
325
+ active_workers, _max_workers, scale_ups, scale_downs = Native.pool_stats(@id)
326
+ base.merge!(active_workers:, scale_ups:, scale_downs:)
309
327
  rows = Native.worker_stats(@id)
310
- base[:worker_status] = rows.map do |index, served, in_flight, busy_ms, quarantined|
311
- {index:, served:, in_flight:, busy_ms:, quarantined:}
328
+ base[:worker_status] = rows.map do |index, served, in_flight, busy_ms, quarantined, retired|
329
+ {index:, served:, in_flight:, busy_ms:, quarantined:, retired:}
312
330
  end
313
- base[:quarantined] = rows.count { |_index, _served, _in_flight, _busy_ms, quarantined| quarantined }
331
+ base[:quarantined] = rows.count { |row| row[4] }
314
332
  count, sum_seconds = Native.queue_time(@id)
315
333
  base[:queue_time] = {count:, sum_seconds:}
316
334
  base
@@ -318,63 +336,38 @@ module Kino
318
336
 
319
337
  private
320
338
 
321
- # Register a fresh dispatch slot and run a worker thread on it; returns
322
- # the thread. Used at boot and by the quarantine replacer.
323
- def spawn_worker_thread
324
- worker_id = Native.register_worker(@id)
325
- Thread.new do
326
- # Named so log lines from inside say which worker spoke.
327
- Thread.current.name = "worker-#{worker_id}"
328
- error = nil
329
- begin
330
- Worker.run(@id, worker_id, @app, @batch, @worker_hooks)
331
- rescue Exception => e # rubocop:disable Lint/RescueException -- a hard crash in a threaded worker thread
332
- error = e
333
- raise
334
- ensure
335
- HookFire.fire(@on_worker_exit, "on_worker_exit", worker_id, error)
336
- end
337
- end
339
+ # The worker pool this mode runs: the ractor supervisor, or the
340
+ # threaded pool. Both spawn, retire, replace and join workers behind
341
+ # the same methods.
342
+ def pool
343
+ @supervisor || @threaded_pool
338
344
  end
339
345
 
340
- # Track a replacement thread spawned outside the initial pool assignment
341
- # (the quarantine replacer) so shutdown's join/done?/kill sweeps see it.
342
- def track_replacement_thread(thread)
343
- @worker_threads_lock.synchronize { @worker_threads << thread }
346
+ # Both pools are quarantine replacers: replace(worker_id) spawns a
347
+ # replacement worker, then quarantines the wedged slot.
348
+ def start_quarantine_monitor
349
+ @quarantine_monitor = QuarantineMonitor.new(
350
+ server_id: @id, timeout_ms: @quarantine_timeout_ms,
351
+ max: @quarantine_max, replacer: pool
352
+ ).start
344
353
  end
345
354
 
346
- # @private
347
- # The :threaded-mode quarantine replacer: spawns a replacement worker
348
- # thread, quarantines the wedged slot, then tracks the new thread so
349
- # shutdown's join/done?/kill sweeps see it. Built from bound Method
350
- # objects instead of a server reference, so it drives the server
351
- # through those methods without send or instance_variable_get.
352
- class ThreadedReplacer
353
- def initialize(server_id:, spawner:, tracker:)
354
- @server_id = server_id
355
- @spawner = spawner
356
- @tracker = tracker
357
- end
355
+ # Ruby's M:N scheduler runs non-main ractors' Ruby code on at most
356
+ # RUBY_MAX_CPU native threads (default 8). Workers past that cap share
357
+ # timeslices instead of adding parallelism, and a fresh ractor can wait
358
+ # seconds for its first one while the others are CPU-bound.
359
+ def warn_scheduler_cap
360
+ cap = Integer(ENV.fetch("RUBY_MAX_CPU", "8"), exception: false) || 8
361
+ return if @max_workers <= cap
358
362
 
359
- def replace(worker_id)
360
- thread = @spawner.call # spawn FIRST (may raise ThreadError)
361
- Native.quarantine_slot(@server_id, worker_id) # quarantine after success
362
- @tracker.call(thread)
363
- true
364
- end
363
+ Log.warn("#{@max_workers} ractor workers exceed RUBY_MAX_CPU=#{cap}: only #{cap} can run " \
364
+ "Ruby code at once; set RUBY_MAX_CPU=#{@max_workers} for CPU-bound apps")
365
365
  end
366
- private_constant :ThreadedReplacer
367
366
 
368
- # A replacer.replace(worker_id) spawns a replacement worker, then
369
- # quarantines the wedged slot, mode-appropriately. In :ractor the
370
- # supervisor is the replacer; in :threaded a small object over
371
- # spawn_worker_thread.
372
- def start_quarantine_monitor
373
- replacer = @supervisor || ThreadedReplacer.new(server_id: @id, spawner: method(:spawn_worker_thread),
374
- tracker: method(:track_replacement_thread))
375
- @quarantine_monitor = QuarantineMonitor.new(
376
- server_id: @id, timeout_ms: @quarantine_timeout_ms,
377
- max: @quarantine_max, replacer: replacer
367
+ def start_pool_scaler
368
+ @pool_scaler = PoolScaler.new(
369
+ server_id: @id, pool: pool, floor: @workers, ceiling: @max_workers,
370
+ scale_down_after: @scale_down_after
378
371
  ).start
379
372
  end
380
373
 
@@ -400,6 +393,11 @@ module Kino
400
393
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
401
394
  end
402
395
 
396
+ # A pool that can grow: the ceiling is above the floor.
397
+ def elastic?
398
+ @max_workers > @workers
399
+ end
400
+
403
401
  # Default connection cap: most of the process open-file limit. A
404
402
  # connection flood's failure mode is descriptor exhaustion, and in
405
403
  # :ractor/:threaded mode the app's own sockets and files share this
@@ -474,34 +472,15 @@ module Kino
474
472
  end
475
473
 
476
474
  def join_workers(deadline)
477
- if @supervisor
478
- @supervisor.shutdown([deadline - monotonic_now, 0].max)
479
- else
480
- threads = @worker_threads_lock.synchronize { @worker_threads.dup }
481
- threads.each do |thread|
482
- thread.join([deadline - monotonic_now, 0.01].max)
483
- end
484
- end
475
+ pool.shutdown([deadline - monotonic_now, 0].max)
485
476
  end
486
477
 
487
478
  def workers_done?
488
- if @supervisor
489
- @supervisor.done?
490
- else
491
- threads = @worker_threads_lock.synchronize { @worker_threads.dup }
492
- threads.none?(&:alive?)
493
- end
479
+ pool.done?
494
480
  end
495
481
 
496
482
  def kill_stragglers
497
- if @supervisor
498
- # Ractors cannot be force-killed; their clients were already freed
499
- # by abort_all_inflight. The stuck ractor leaks until process exit.
500
- Log.error("shutdown deadline passed with stuck ractor workers") unless @supervisor.done?
501
- else
502
- threads = @worker_threads_lock.synchronize { @worker_threads.dup }
503
- threads.each { |thread| thread.kill if thread.alive? }
504
- end
483
+ pool.kill_stragglers
505
484
  end
506
485
 
507
486
  # Policy (mode resolution): when is an app safe for ractor
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kino
4
+ # @private
5
+ # Dispatch slots for a worker pool: fresh ones from the native registry,
6
+ # or ones that retired workers handed back, reset for their next
7
+ # occupant. The native side never removes a slot, so recycling is what
8
+ # keeps the slot table from growing as an elastic pool breathes. Only
9
+ # cleanly exited workers return slots; a crashed worker's slots are
10
+ # abandoned (stale interrupt kicks and dead weak refs go down with
11
+ # them).
12
+ class SlotBank
13
+ def initialize(server_id)
14
+ @server_id = server_id
15
+ @free = []
16
+ @lock = Mutex.new
17
+ end
18
+
19
+ def claim
20
+ id = @lock.synchronize { @free.pop }
21
+ return Native.register_worker(@server_id) unless id
22
+
23
+ Native.reset_slot(@server_id, id)
24
+ id
25
+ end
26
+
27
+ def release(ids)
28
+ @lock.synchronize { @free.concat(ids) }
29
+ end
30
+ end
31
+ end
@@ -40,6 +40,21 @@
40
40
  # `workers` instead.
41
41
  # threads 1
42
42
 
43
+ ## Elastic pool (experimental)
44
+ #
45
+ # Let the pool grow past `workers` under load and shrink back when
46
+ # idle: one worker is added every 100 ms while requests wait in the
47
+ # queue, and a worker above `workers` retires after `scale_down_after`
48
+ # seconds idle. Helps apps that wait on databases or other services;
49
+ # pure CPU work gains nothing past the core count. Unset (the default)
50
+ # keeps the pool fixed. In :ractor mode, Ruby runs at most RUBY_MAX_CPU
51
+ # (default 8) ractors' Ruby code at once; set that variable to match
52
+ # `max_workers` on bigger boxes.
53
+ # max_workers 32
54
+
55
+ # Seconds an extra worker must sit idle before it is retired.
56
+ # scale_down_after 30
57
+
43
58
  ## Dispatch mode
44
59
  #
45
60
  # :auto - picks :ractor when your app supports it, else :threaded.
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kino
4
+ # @private
5
+ # The :threaded-mode worker pool: `workers` groups of `threads` plain
6
+ # Threads, each thread on its own dispatch slot. A group is one ractor's
7
+ # worth of capacity, so `workers` and `max_workers` mean the same thing
8
+ # in both modes. Behind PoolScaler here (`grow`, `retire`, `groups`,
9
+ # `active_count`), the quarantine monitor's replacer (`replace`), and
10
+ # the join/kill sweeps Server#shutdown runs.
11
+ class ThreadedPool
12
+ def initialize(server_id, app, threads:, batch: 1, hooks: nil, on_worker_exit: nil)
13
+ @server_id = server_id
14
+ @app = app
15
+ @threads = threads
16
+ @batch = batch
17
+ @hooks = hooks
18
+ @on_worker_exit = on_worker_exit
19
+ @lock = Mutex.new
20
+ # index => {slots:, threads:}; the groups asked to leave; quarantine
21
+ # replacements (one thread each, standing in for a wedged slot:
22
+ # neither counted nor retired, so the wedged group keeps counting as
23
+ # the capacity it still is). Retired groups hand their slots back to
24
+ # the bank.
25
+ @groups = {}
26
+ @slot_to_group = {}
27
+ @retiring = {}
28
+ @bank = SlotBank.new(server_id)
29
+ @replacements = {}
30
+ @wedged = {}
31
+ @next_index = -1
32
+ end
33
+
34
+ def start(workers)
35
+ workers.times { spawn_group }
36
+ report_active
37
+ self
38
+ end
39
+
40
+ # Groups that are staying: quarantine replacements aside, and minus
41
+ # those already told to leave. A retiring group leaves the count at
42
+ # once, not when its last thread exits, so the scaler never sees a
43
+ # stale surplus and retires past its floor.
44
+ def active_count
45
+ @lock.synchronize do
46
+ @groups.count { |index, _| !@replacements.key?(index) && !@retiring.key?(index) }
47
+ end
48
+ end
49
+
50
+ # Group index => slot ids for every group the scaler may retire: not
51
+ # already leaving, not wedged, not a quarantine replacement.
52
+ def groups
53
+ reap
54
+ @lock.synchronize do
55
+ @groups
56
+ .reject { |index, _| @retiring.key?(index) || @wedged.key?(index) || @replacements.key?(index) }
57
+ .transform_values { |group| group[:slots].dup }
58
+ end
59
+ end
60
+
61
+ # Add one group; returns its index.
62
+ def grow
63
+ reap
64
+ index = spawn_group
65
+ report_active
66
+ index
67
+ end
68
+
69
+ # Send a group home: its slots stop receiving work now, each thread
70
+ # finishes what it holds and leaves at its next idle tick, and the
71
+ # slots come back to the free list once every thread has exited.
72
+ def retire(index)
73
+ slots = @lock.synchronize do
74
+ next nil unless @groups.key?(index) && !@retiring.key?(index)
75
+
76
+ @retiring[index] = true
77
+ @groups[index][:slots]
78
+ end
79
+ return false unless slots
80
+
81
+ slots.each { |id| Native.retire_slot(@server_id, id) }
82
+ true
83
+ end
84
+
85
+ # The quarantine replacer: spawn a replacement thread on a fresh slot
86
+ # FIRST (may raise ThreadError), then quarantine the wedged slot.
87
+ def replace(worker_id)
88
+ spawn_group(slots: 1, replacement: true)
89
+ Native.quarantine_slot(@server_id, worker_id)
90
+ @lock.synchronize do
91
+ wedged = @slot_to_group[worker_id]
92
+ @wedged[wedged] = true if wedged
93
+ end
94
+ true
95
+ end
96
+
97
+ # Join every thread up to the (numeric) deadline.
98
+ def shutdown(timeout)
99
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
100
+ all_threads.each do |thread|
101
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
102
+ thread.join([remaining, 0.01].max)
103
+ end
104
+ end
105
+
106
+ def done?
107
+ all_threads.none?(&:alive?)
108
+ end
109
+
110
+ # Block until every thread exits on its own (drain elsewhere).
111
+ def join
112
+ all_threads.each(&:join)
113
+ end
114
+
115
+ def kill_stragglers
116
+ all_threads.each { |thread| thread.kill if thread.alive? }
117
+ end
118
+
119
+ private
120
+
121
+ def all_threads
122
+ @lock.synchronize { @groups.values.flat_map { |group| group[:threads] } }
123
+ end
124
+
125
+ # The group is in the table before its first thread starts, so a
126
+ # ThreadError partway through leaves nothing untracked for shutdown.
127
+ def spawn_group(slots: @threads, replacement: false)
128
+ group = {slots: [], threads: []}
129
+ index = @lock.synchronize do
130
+ @next_index += 1
131
+ @groups[@next_index] = group
132
+ @replacements[@next_index] = true if replacement
133
+ @next_index
134
+ end
135
+ slots.times do
136
+ id = @bank.claim
137
+ @lock.synchronize do
138
+ group[:slots] << id
139
+ @slot_to_group[id] = index
140
+ end
141
+ thread = spawn_thread(id)
142
+ @lock.synchronize { group[:threads] << thread }
143
+ end
144
+ index
145
+ end
146
+
147
+ def spawn_thread(worker_id)
148
+ Thread.new do
149
+ # Named so log lines from inside say which worker spoke.
150
+ Thread.current.name = "worker-#{worker_id}"
151
+ error = nil
152
+ begin
153
+ Worker.run(@server_id, worker_id, @app, @batch, @hooks)
154
+ rescue Exception => e # rubocop:disable Lint/RescueException -- a hard crash in a threaded worker thread
155
+ error = e
156
+ raise
157
+ ensure
158
+ HookFire.fire(@on_worker_exit, "on_worker_exit", worker_id, error)
159
+ end
160
+ end
161
+ end
162
+
163
+ # Retiring groups whose threads have all exited give their slots back
164
+ # and leave the table. Runs before every pool decision, so the count
165
+ # the control plane sees never lags by more than a scaler tick.
166
+ def reap
167
+ freed = @lock.synchronize do
168
+ done = @retiring.keys.select { |index| @groups[index][:threads].none?(&:alive?) }
169
+ done.flat_map do |index|
170
+ group = @groups.delete(index)
171
+ @retiring.delete(index)
172
+ group[:slots].each { |id| @slot_to_group.delete(id) }
173
+ group[:slots]
174
+ end
175
+ end
176
+ return if freed.empty?
177
+
178
+ @bank.release(freed)
179
+ report_active
180
+ end
181
+
182
+ def report_active
183
+ Native.set_active_workers(@server_id, active_count)
184
+ end
185
+ end
186
+ end
data/lib/kino/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Kino
4
4
  # The gem version (single source of truth; ext/kino/Cargo.toml syncs).
5
- VERSION = "0.6.0"
5
+ VERSION = "0.7.0"
6
6
  end
data/lib/kino.rb CHANGED
@@ -57,8 +57,12 @@ require_relative "kino/configuration"
57
57
  require_relative "kino/hook_fire"
58
58
  require_relative "kino/worker_hooks"
59
59
  require_relative "kino/worker"
60
+ require_relative "kino/slot_bank"
60
61
  require_relative "kino/ractor_supervisor"
62
+ require_relative "kino/threaded_pool"
63
+ require_relative "kino/monitor"
61
64
  require_relative "kino/quarantine_monitor"
65
+ require_relative "kino/pool_scaler"
62
66
  require_relative "kino/server"
63
67
 
64
68
  # Hand the frozen shareable singletons to the native layer: it sets them
@@ -9,13 +9,13 @@ module Rackup
9
9
  module Kino
10
10
  # Host option name => Kino setting plus the coercion it needs: rackup
11
11
  # hands `-O NAME=VALUE` values (and its own -p) over as strings.
12
- OPTION_MAP = {
12
+ OPTION_MAP = Ractor.make_shareable({
13
13
  Host: [:bind, ->(value) { value.to_s }],
14
14
  Port: [:port, ->(value) { Integer(value) }],
15
15
  Workers: [:workers, ->(value) { Integer(value) }],
16
16
  Threads: [:threads, ->(value) { Integer(value) }],
17
17
  Mode: [:mode, ->(value) { value.to_sym }]
18
- }.freeze
18
+ })
19
19
  private_constant :OPTION_MAP
20
20
 
21
21
  # Boot a server for `app` and block until it shuts down, the way the
@@ -27,7 +27,7 @@ module Rackup
27
27
  # want a handle on it
28
28
  # @return [::Kino::Server] the stopped server, after shutdown
29
29
  def self.run(app, **options)
30
- require "kino"
30
+ require "kino" # audition:disable runtime-require
31
31
  server = ::Kino::Server.new(app, **server_options(options))
32
32
  yield server if block_given?
33
33
  server.run
@@ -58,7 +58,7 @@ module Rackup
58
58
  # @param options [Hash{Symbol => Object}]
59
59
  # @return [Hash{Symbol => Object}]
60
60
  def self.server_options(options)
61
- require "kino"
61
+ require "kino" # audition:disable runtime-require
62
62
  options = options.dup
63
63
  host_defaults = {}
64
64
  if (typed = options.delete(:user_supplied_options))
data/sig/kino.rbs CHANGED
@@ -110,6 +110,8 @@ module Kino
110
110
  def bind: (String host) -> untyped
111
111
  def port: (int port) -> untyped
112
112
  def workers: (int count) -> untyped
113
+ def max_workers: (int count) -> untyped
114
+ def scale_down_after: (Numeric seconds) -> untyped
113
115
  def threads: (int count) -> untyped
114
116
  def mode: (Symbol | String mode) -> untyped
115
117
  def queue_depth: (int depth) -> untyped
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kino
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Markin
@@ -219,12 +219,16 @@ files:
219
219
  - lib/kino/input.rb
220
220
  - lib/kino/log.rb
221
221
  - lib/kino/logger.rb
222
+ - lib/kino/monitor.rb
222
223
  - lib/kino/null_input.rb
224
+ - lib/kino/pool_scaler.rb
223
225
  - lib/kino/quarantine_monitor.rb
224
226
  - lib/kino/ractor_supervisor.rb
225
227
  - lib/kino/server.rb
228
+ - lib/kino/slot_bank.rb
226
229
  - lib/kino/stream.rb
227
230
  - lib/kino/templates/kino.rb.tt
231
+ - lib/kino/threaded_pool.rb
228
232
  - lib/kino/version.rb
229
233
  - lib/kino/worker.rb
230
234
  - lib/kino/worker_hooks.rb