atomic-ruby 0.15.2 → 0.15.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9c6210195ad6d82fbbb6a84f66bc8ce5faef54d50ed3227c3655ec43a996a9f6
4
- data.tar.gz: eef94abbb8c8bfb1d9a54d36f8f8b0404d49210e6e6a109851fa014a6512aaa5
3
+ metadata.gz: e7c0136cf37fe5133affc1f090fa517636f5bfda0f5f43564a4877eee15bae3f
4
+ data.tar.gz: 0ae3a3aadabcc2bb69621df62188800451289a46032fa57a49c1af9326bcc31e
5
5
  SHA512:
6
- metadata.gz: c89b7c62bda07206115c80c45088d1ae44e5c3aa84288932fae8fad51312d3490bf9510af559178fe6311e5d8ce4ecf87dca0da3420ced47a0113783e63a741d
7
- data.tar.gz: a5c5ecee1ce3f96b5e2c525897c1c979fa3f99b7be6cd971d6882e2a43bf9b735f0de58bcf67dbf1179b0298a006a25ba45e910ee0c104f965c305e4af93f6be
6
+ metadata.gz: 1786faa43a25379cbe68c9de2372c6168a62ab225436c45bbf5049791c39191e3ed75955ab2f5f6385069ddeb86640aa11ba431ce9f2ab54335005a61bd42092
7
+ data.tar.gz: 704c7e7224c9eec351aa0174f597c10c992f4e1594921b8ca91e9ad2df43a6b3eb2c260b9b25ab2d47f8fb0934afa9739ed5312fb00d5d7519d9283c6bf68170
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.15.4] - 2026-09-01
4
+
5
+ - Keep temporary `AtomicThreadPool` workers alive for five seconds
6
+ - Speed up `AtomicThreadPool` scaling for blocking work
7
+
8
+ ## [0.15.3] - 2026-08-31
9
+
10
+ - Prevent enqueues from racing `AtomicThreadPool#shutdown`
11
+ - Remove interrupted waiters from `AtomicConditionVariable`
12
+ - Replace `Thread.pass` busy-wait with `sleep` in `AtomicCountDownLatch#wait`
13
+
3
14
  ## [0.15.2] - 2026-08-30
4
15
 
5
16
  - Avoid thread event hook overhead for fixed thread pools
data/README.md CHANGED
@@ -267,8 +267,8 @@ puts "Atomic Ruby Atomic Bank Account: #{results[2].real.round(6)} seconds"
267
267
  > bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atom_benchmark.rb
268
268
 
269
269
  ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
270
- concurrent-ruby version: 1.3.6
271
- atomic-ruby version: 0.15.2
270
+ concurrent-ruby version: 1.3.8
271
+ atomic-ruby version: 0.15.4
272
272
 
273
273
  Balances:
274
274
  Synchronized Bank Account Balance: 975
@@ -276,9 +276,9 @@ Concurrent Ruby Atomic Bank Account Balance: 975
276
276
  Atomic Ruby Atomic Bank Account Balance: 975
277
277
 
278
278
  Benchmark Results:
279
- Synchronized Bank Account: 5.117072 seconds
280
- Concurrent Ruby Atomic Bank Account: 5.128366 seconds
281
- Atomic Ruby Atomic Bank Account: 5.110595 seconds
279
+ Synchronized Bank Account: 5.104959 seconds
280
+ Concurrent Ruby Atomic Bank Account: 5.120305 seconds
281
+ Atomic Ruby Atomic Bank Account: 5.102732 seconds
282
282
  ```
283
283
 
284
284
  </details>
@@ -358,22 +358,22 @@ end
358
358
  > bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atomic_boolean_benchmark.rb
359
359
 
360
360
  ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
361
- concurrent-ruby version: 1.3.6
362
- atomic-ruby version: 0.15.2
361
+ concurrent-ruby version: 1.3.8
362
+ atomic-ruby version: 0.15.4
363
363
 
364
364
  Warming up --------------------------------------
365
- Synchronized Boolean Toggle 157.000 i/100ms
366
- Concurrent Ruby Atomic Boolean Toggle 134.000 i/100ms
367
- Atomic Ruby Atomic Boolean Toggle 120.000 i/100ms
365
+ Synchronized Boolean Toggle 165.000 i/100ms
366
+ Concurrent Ruby Atomic Boolean Toggle 132.000 i/100ms
367
+ Atomic Ruby Atomic Boolean Toggle 119.000 i/100ms
368
368
  Calculating -------------------------------------
369
- Synchronized Boolean Toggle 1.629k (± 2.0%) i/s (613.87 μs/i) - 8.164k in 5.011636s
370
- Concurrent Ruby Atomic Boolean Toggle 1.325k (± 2.0%) i/s (754.96 μs/i) - 6.700k in 5.058203s
371
- Atomic Ruby Atomic Boolean Toggle 1.190k (± 2.6%) i/s (840.11 μs/i) - 6.000k in 5.040665s
369
+ Synchronized Boolean Toggle 1.647k (± 2.1%) i/s (606.98 μs/i) - 8.250k in 5.007598s
370
+ Concurrent Ruby Atomic Boolean Toggle 1.343k (± 1.9%) i/s (744.55 μs/i) - 6.732k in 5.012307s
371
+ Atomic Ruby Atomic Boolean Toggle 1.207k (± 1.9%) i/s (828.31 μs/i) - 6.069k in 5.027033s
372
372
 
373
373
  Comparison:
374
- Synchronized Boolean Toggle: 1629.0 i/s
375
- Concurrent Ruby Atomic Boolean Toggle: 1324.6 i/s - 1.23x slower
376
- Atomic Ruby Atomic Boolean Toggle: 1190.3 i/s - 1.37x slower
374
+ Synchronized Boolean Toggle: 1647.5 i/s
375
+ Concurrent Ruby Atomic Boolean Toggle: 1343.1 i/s - 1.23x slower
376
+ Atomic Ruby Atomic Boolean Toggle: 1207.3 i/s - 1.36x slower
377
377
  ```
378
378
 
379
379
  </details>
@@ -442,18 +442,18 @@ end
442
442
  > bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atomic_condition_variable_benchmark.rb
443
443
 
444
444
  ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
445
- atomic-ruby version: 0.15.2
445
+ atomic-ruby version: 0.15.4
446
446
 
447
447
  Warming up --------------------------------------
448
- Synchronized Condition Variable Wait/Signal 3.977k i/100ms
449
- Atomic Ruby Atomic Condition Variable Wait/Signal 3.885k i/100ms
448
+ Synchronized Condition Variable Wait/Signal 4.062k i/100ms
449
+ Atomic Ruby Atomic Condition Variable Wait/Signal 4.133k i/100ms
450
450
  Calculating -------------------------------------
451
- Synchronized Condition Variable Wait/Signal 39.458k (± 4.2%) i/s (25.34 μs/i) - 198.850k in 5.039596s
452
- Atomic Ruby Atomic Condition Variable Wait/Signal 39.046k (± 4.2%) i/s (25.61 μs/i) - 198.135k in 5.074411s
451
+ Synchronized Condition Variable Wait/Signal 41.923k (± 3.5%) i/s (23.85 μs/i) - 211.224k in 5.038330s
452
+ Atomic Ruby Atomic Condition Variable Wait/Signal 40.666k (± 3.8%) i/s (24.59 μs/i) - 206.650k in 5.081595s
453
453
 
454
454
  Comparison:
455
- Synchronized Condition Variable Wait/Signal: 39457.5 i/s
456
- Atomic Ruby Atomic Condition Variable Wait/Signal: 39045.9 i/s - same-ish: difference falls within error
455
+ Synchronized Condition Variable Wait/Signal: 41923.4 i/s
456
+ Atomic Ruby Atomic Condition Variable Wait/Signal: 40666.4 i/s - same-ish: difference falls within error
457
457
  ```
458
458
 
459
459
  </details>
@@ -517,18 +517,18 @@ end
517
517
  > bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atomic_queue_benchmark.rb
518
518
 
519
519
  ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
520
- atomic-ruby version: 0.15.2
520
+ atomic-ruby version: 0.15.4
521
521
 
522
522
  Warming up --------------------------------------
523
- Synchronized Queue Push/Pop 181.000 i/100ms
524
- Atomic Ruby Atomic Queue Push/Pop 132.000 i/100ms
523
+ Synchronized Queue Push/Pop 184.000 i/100ms
524
+ Atomic Ruby Atomic Queue Push/Pop 149.000 i/100ms
525
525
  Calculating -------------------------------------
526
- Synchronized Queue Push/Pop 1.845k (± 2.1%) i/s (541.91 μs/i) - 9.231k in 5.002394s
527
- Atomic Ruby Atomic Queue Push/Pop 1.431k (± 6.8%) i/s (698.64 μs/i) - 7.260k in 5.072143s
526
+ Synchronized Queue Push/Pop 1.819k (± 2.0%) i/s (549.73 μs/i) - 9.200k in 5.057550s
527
+ Atomic Ruby Atomic Queue Push/Pop 1.493k (± 1.9%) i/s (669.87 μs/i) - 7.599k in 5.090319s
528
528
 
529
529
  Comparison:
530
- Synchronized Queue Push/Pop: 1845.3 i/s
531
- Atomic Ruby Atomic Queue Push/Pop: 1431.3 i/s - 1.29x slower
530
+ Synchronized Queue Push/Pop: 1819.1 i/s
531
+ Atomic Ruby Atomic Queue Push/Pop: 1492.8 i/s - 1.22x slower
532
532
 
533
533
  ```
534
534
 
@@ -547,7 +547,7 @@ require "benchmark"
547
547
  require "concurrent-ruby"
548
548
  require_relative "../lib/atomic-ruby"
549
549
 
550
- results = []
550
+ fixed_results = []
551
551
 
552
552
  2.times do |idx|
553
553
  result = Benchmark.measure do
@@ -569,6 +569,101 @@ results = []
569
569
  pool.wait_for_termination if idx == 0
570
570
  end
571
571
 
572
+ fixed_results << result
573
+ end
574
+
575
+ adaptive_results = []
576
+
577
+ 2.times do |idx|
578
+ result = Benchmark.measure do
579
+ pool = case idx
580
+ when 0 then Concurrent.new_io_executor
581
+ when 1 then AtomicThreadPool.new(size: 1, max_size: Float::INFINITY)
582
+ end
583
+ latch = case idx
584
+ when 0 then Concurrent::CountDownLatch.new(100)
585
+ when 1 then AtomicCountDownLatch.new(100)
586
+ end
587
+
588
+ 100.times do
589
+ pool << proc do
590
+ sleep(0.2)
591
+ latch.count_down
592
+ end
593
+ end
594
+
595
+ latch.wait
596
+ pool.shutdown
597
+ # concurrent-ruby's #shutdown does not wait for threads to terminate
598
+ pool.wait_for_termination if idx == 0
599
+ end
600
+
601
+ adaptive_results << result
602
+ end
603
+
604
+ puts "\n"
605
+ puts "ruby version: #{RUBY_DESCRIPTION}"
606
+ puts "concurrent-ruby version: #{Concurrent::VERSION}"
607
+ puts "atomic-ruby version: #{AtomicRuby::VERSION}"
608
+ puts "\n"
609
+ puts "Fixed Pool Results:"
610
+ puts "Concurrent Ruby Fixed Thread Pool: #{fixed_results[0].real.round(6)} seconds"
611
+ puts "Atomic Ruby Fixed Thread Pool: #{fixed_results[1].real.round(6)} seconds"
612
+ puts "\n"
613
+ puts "Adaptive Pool Results:"
614
+ puts "Concurrent Ruby IO Thread Pool: #{adaptive_results[0].real.round(6)} seconds"
615
+ puts "Atomic Ruby Adaptive Thread Pool: #{adaptive_results[1].real.round(6)} seconds"
616
+ ```
617
+
618
+ ```
619
+ > bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atomic_thread_pool_benchmark.rb
620
+
621
+ ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
622
+ concurrent-ruby version: 1.3.8
623
+ atomic-ruby version: 0.15.4
624
+
625
+ Fixed Pool Results:
626
+ Concurrent Ruby Fixed Thread Pool: 5.069464 seconds
627
+ Atomic Ruby Fixed Thread Pool: 4.791382 seconds
628
+
629
+ Adaptive Pool Results:
630
+ Concurrent Ruby IO Thread Pool: 0.22309 seconds
631
+ Atomic Ruby Adaptive Thread Pool: 0.61561 seconds
632
+ ```
633
+
634
+ </details>
635
+
636
+ <details>
637
+
638
+ <summary>AtomicCountDownLatch</summary>
639
+
640
+ <br>
641
+
642
+ ```ruby
643
+ # frozen_string_literal: true
644
+
645
+ require "benchmark"
646
+ require "concurrent-ruby"
647
+ require_relative "../lib/atomic-ruby"
648
+
649
+ results = []
650
+
651
+ 2.times do |idx|
652
+ result = Benchmark.measure do
653
+ latch = case idx
654
+ when 0 then Concurrent::CountDownLatch.new(1)
655
+ when 1 then AtomicCountDownLatch.new(1)
656
+ end
657
+
658
+ thread = Thread.new do
659
+ sleep(2)
660
+ latch.count_down
661
+ end
662
+
663
+ latch.wait
664
+ thread.join
665
+ end
666
+
572
667
  results << result
573
668
  end
574
669
 
@@ -578,20 +673,20 @@ puts "concurrent-ruby version: #{Concurrent::VERSION}"
578
673
  puts "atomic-ruby version: #{AtomicRuby::VERSION}"
579
674
  puts "\n"
580
675
  puts "Benchmark Results:"
581
- puts "Concurrent Ruby Thread Pool: #{results[0].real.round(6)} seconds"
582
- puts "Atomic Ruby Atomic Thread Pool: #{results[1].real.round(6)} seconds"
676
+ puts "Concurrent Ruby Count Down Latch: #{results[0].total.round(6)} CPU seconds, #{results[0].real.round(6)} elapsed seconds"
677
+ puts "Atomic Ruby Atomic Count Down Latch: #{results[1].total.round(6)} CPU seconds, #{results[1].real.round(6)} elapsed seconds"
583
678
  ```
584
679
 
585
680
  ```
586
- > bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atomic_thread_pool_benchmark.rb
681
+ > bundle exec rake clobber && bundle exec rake compile && bundle exec ruby examples/atomic_count_down_latch_benchmark.rb
587
682
 
588
683
  ruby version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
589
684
  concurrent-ruby version: 1.3.8
590
- atomic-ruby version: 0.15.2
685
+ atomic-ruby version: 0.15.4
591
686
 
592
687
  Benchmark Results:
593
- Concurrent Ruby Thread Pool: 5.081233 seconds
594
- Atomic Ruby Atomic Thread Pool: 4.737824 seconds
688
+ Concurrent Ruby Count Down Latch: 0.000511 CPU seconds, 2.001465 elapsed seconds
689
+ Atomic Ruby Atomic Count Down Latch: 0.023529 CPU seconds, 2.00364 elapsed seconds
595
690
  ```
596
691
 
597
692
  </details>
@@ -172,6 +172,7 @@ typedef struct {
172
172
  volatile VALUE head;
173
173
  volatile VALUE tail;
174
174
  volatile rb_atomic_t count;
175
+ bool closed;
175
176
  } atomic_ruby_queue_t;
176
177
 
177
178
  static void atomic_ruby_queue_mark(void *ptr) {
@@ -211,6 +212,7 @@ static VALUE rb_cAtomicQueue_allocate(VALUE klass) {
211
212
  RB_OBJ_WRITE(obj, &atomic_ruby_queue->head, Qnil);
212
213
  RB_OBJ_WRITE(obj, &atomic_ruby_queue->tail, Qnil);
213
214
  atomic_ruby_queue->count = 0;
215
+ atomic_ruby_queue->closed = false;
214
216
  return obj;
215
217
  }
216
218
 
@@ -228,6 +230,7 @@ static VALUE rb_cAtomicQueue_initialize(VALUE self) {
228
230
  static VALUE rb_cAtomicQueue_push(VALUE self, VALUE value) {
229
231
  atomic_ruby_queue_t *atomic_ruby_queue;
230
232
  TypedData_Get_Struct(self, atomic_ruby_queue_t, &atomic_ruby_queue_type, atomic_ruby_queue);
233
+ if (atomic_ruby_queue->closed) return Qfalse;
231
234
 
232
235
  VALUE new_node = atomic_ruby_queue_node_new(value, Qnil);
233
236
 
@@ -259,6 +262,13 @@ static VALUE rb_cAtomicQueue_push(VALUE self, VALUE value) {
259
262
  return self;
260
263
  }
261
264
 
265
+ static VALUE rb_cAtomicQueue_close(VALUE self) {
266
+ atomic_ruby_queue_t *atomic_ruby_queue;
267
+ TypedData_Get_Struct(self, atomic_ruby_queue_t, &atomic_ruby_queue_type, atomic_ruby_queue);
268
+ atomic_ruby_queue->closed = true;
269
+ return Qnil;
270
+ }
271
+
262
272
  static VALUE rb_cAtomicQueue_pop(VALUE self) {
263
273
  atomic_ruby_queue_t *atomic_ruby_queue;
264
274
  TypedData_Get_Struct(self, atomic_ruby_queue_t, &atomic_ruby_queue_type, atomic_ruby_queue);
@@ -787,6 +797,7 @@ RUBY_FUNC_EXPORTED void Init_atomic_ruby(void) {
787
797
  rb_define_alloc_func(rb_cAtomicQueue, rb_cAtomicQueue_allocate);
788
798
  rb_define_private_method(rb_cAtomicQueue, "_initialize", rb_cAtomicQueue_initialize, 0);
789
799
  rb_define_private_method(rb_cAtomicQueue, "_push", rb_cAtomicQueue_push, 1);
800
+ rb_define_private_method(rb_cAtomicQueue, "_close", rb_cAtomicQueue_close, 0);
790
801
  rb_define_private_method(rb_cAtomicQueue, "_pop", rb_cAtomicQueue_pop, 0);
791
802
  rb_define_private_method(rb_cAtomicQueue, "_size", rb_cAtomicQueue_size, 0);
792
803
  rb_define_private_method(rb_cAtomicQueue, "_empty_p", rb_cAtomicQueue_empty_p, 0);
@@ -8,19 +8,10 @@ module AtomicRuby
8
8
  #
9
9
  # AtomicConditionVariable lets one or more threads park until another
10
10
  # thread signals them, without the paired `Mutex` that Ruby's
11
- # `ConditionVariable` requires. The set of parked threads is tracked
12
- # in a native doubly-linked list, so registering a waiter, removing a
13
- # waiter, and shifting the head off for a signal are all O(1) and
14
- # allocation-light. Parking still uses `Thread.stop` and
15
- # `Thread#wakeup`, which are the standard kernel-level primitives Ruby
16
- # exposes for sleeping a thread.
11
+ # `ConditionVariable` requires.
17
12
  #
18
- # The lost-wakeup race in a naive `check-then-park` consumer is avoided
19
- # by the {#wait} contract: a waiter registers itself before re-evaluating
20
- # the predicate, so any signal that fires after the producer makes the
21
- # predicate true is guaranteed to see the waiter and wake it. Ruby also
22
- # remembers pending wakeups across `Thread.stop`, so a wakeup that
23
- # arrives between the predicate check and the actual park is not lost.
13
+ # Waiters supply a predicate that is checked again after registration,
14
+ # preventing signals sent after a relevant state change from being lost.
24
15
  #
25
16
  # @example Basic usage
26
17
  # condvar = AtomicConditionVariable.new
@@ -72,10 +63,6 @@ module AtomicRuby
72
63
 
73
64
  # Wakes one parked waiter, or no-ops if none are parked.
74
65
  #
75
- # If a waiter has registered itself but is not yet inside `Thread.stop`,
76
- # Ruby remembers the wakeup and the next `Thread.stop` returns
77
- # immediately.
78
- #
79
66
  # @return [true, false] true if a waiter was signalled, false otherwise
80
67
  #
81
68
  # @example
@@ -93,8 +80,6 @@ module AtomicRuby
93
80
 
94
81
  # Wakes every parked waiter.
95
82
  #
96
- # Each woken thread observes the wake the same way as with {#signal}.
97
- #
98
83
  # @return [Integer] The number of waiters signalled
99
84
  #
100
85
  # @example
@@ -111,12 +96,10 @@ module AtomicRuby
111
96
  # Blocks until the given block returns a truthy value, then returns that
112
97
  # value.
113
98
  #
114
- # The block is evaluated optimistically first. If it returns truthy on
115
- # that pass, no waiter is registered and the call returns immediately.
116
- # Otherwise the calling thread registers itself, re-evaluates the
117
- # block, and parks via `Thread.stop` until a {#signal} or {#broadcast}
118
- # wakes it. The block may run more than once and may run concurrently
119
- # with a signalling thread.
99
+ # The block may run more than once and may run concurrently with a
100
+ # signalling thread. Rechecking it after registration prevents lost
101
+ # wakeups. Interrupted waits are unregistered before the exception is
102
+ # propagated.
120
103
  #
121
104
  # @yieldreturn [untyped] Truthy to wake, falsy to keep waiting
122
105
  # @return [untyped] The first truthy value returned by the block
@@ -140,15 +123,19 @@ module AtomicRuby
140
123
 
141
124
  self_thread = Thread.current
142
125
  loop do
143
- waiter = _add_waiter(self_thread)
144
- result = yield
145
- if result
146
- _remove_waiter(waiter)
147
- return result
148
- end
126
+ Thread.handle_interrupt(Exception => :never) do
127
+ waiter = _add_waiter(self_thread)
128
+ begin
129
+ Thread.handle_interrupt(Exception => :immediate) do
130
+ result = yield
131
+ return result if result
149
132
 
150
- Thread.stop
151
- _remove_waiter(waiter)
133
+ Thread.stop
134
+ end
135
+ ensure
136
+ _remove_waiter(waiter)
137
+ end
138
+ end
152
139
  end
153
140
  end
154
141
  end
@@ -133,10 +133,6 @@ module AtomicRuby
133
133
 
134
134
  # Blocks the current thread until the count reaches zero.
135
135
  #
136
- # This method will block the calling thread until other threads have
137
- # called {#count_down} enough times to reduce the count to zero.
138
- # The method uses busy-waiting (Thread.pass) to check the count.
139
- #
140
136
  # @return [void]
141
137
  #
142
138
  # @example Simple wait
@@ -166,7 +162,7 @@ module AtomicRuby
166
162
  #
167
163
  # @rbs () -> void
168
164
  def wait
169
- Thread.pass while @count.value > 0
165
+ sleep 0.001 while @count.value > 0
170
166
  end
171
167
  end
172
168
  end
@@ -51,11 +51,10 @@ module AtomicRuby
51
51
  # @note This class is NOT Ractor-safe as it contains mutable thread state
52
52
  # that cannot be safely shared across ractors.
53
53
  class AtomicThreadPool
54
- AUTOSCALE_GROWTH_SAMPLES = 3
55
- AUTOSCALE_IDLE_TIME = 1
54
+ AUTOSCALE_IDLE_TIME = 5
56
55
  AUTOSCALE_INTERVAL = 0.01
57
56
  AUTOSCALE_WINDOW = 0.05
58
- private_constant :AUTOSCALE_GROWTH_SAMPLES, :AUTOSCALE_IDLE_TIME, :AUTOSCALE_INTERVAL, :AUTOSCALE_WINDOW
57
+ private_constant :AUTOSCALE_IDLE_TIME, :AUTOSCALE_INTERVAL, :AUTOSCALE_WINDOW
59
58
 
60
59
  class Error < StandardError; end
61
60
 
@@ -71,7 +70,7 @@ module AtomicRuby
71
70
  # if work remains queued while its active workers spend most of their time
72
71
  # blocked outside the GVL, but not when Ruby execution is using the
73
72
  # available CPU.
74
- # Temporary workers leave after the queue remains empty, returning the pool
73
+ # Temporary workers remain available between bursts before the pool returns
75
74
  # to its baseline size. Omitting `max_size` creates a fixed-size pool.
76
75
  #
77
76
  # @param size [Integer] The baseline number of worker threads (must be positive)
@@ -155,12 +154,13 @@ module AtomicRuby
155
154
  #
156
155
  # @rbs (Proc work) -> void
157
156
  def <<(work)
158
- raise EnqueuedWorkAfterShutdownError if @shutdown.true?
157
+ Thread.handle_interrupt(Exception => :never) do
158
+ @trim_requested&.make_false
159
+ raise EnqueuedWorkAfterShutdownError unless @queue.push(work)
159
160
 
160
- @trim_requested&.make_false
161
- @queue.push(work)
162
- @work_available.signal
163
- @autoscale_available&.signal
161
+ @work_available.signal
162
+ @autoscale_available&.signal
163
+ end
164
164
  end
165
165
 
166
166
  # Returns the number of currently alive worker threads.
@@ -242,6 +242,9 @@ module AtomicRuby
242
242
  # 2. Waits for all currently queued work to complete
243
243
  # 3. Waits for all worker threads to terminate
244
244
  #
245
+ # Enqueues accepted before shutdown are processed before the workers
246
+ # terminate.
247
+ #
245
248
  # After shutdown, all worker threads will be terminated and the pool
246
249
  # cannot be restarted. Attempting to enqueue work after shutdown
247
250
  # will raise an exception.
@@ -258,11 +261,12 @@ module AtomicRuby
258
261
  #
259
262
  # @rbs () -> void
260
263
  def shutdown
261
- return if @shutdown.true?
262
-
263
- @shutdown.make_true
264
- @autoscale_available&.broadcast
265
- @work_available.broadcast
264
+ Thread.handle_interrupt(Exception => :never) do
265
+ @queue.send(:_close)
266
+ @shutdown.make_true
267
+ @autoscale_available&.broadcast
268
+ @work_available.broadcast
269
+ end
266
270
  @autoscaler&.join
267
271
  @threads.each(&:join)
268
272
  @thread_pool_monitor&.stop
@@ -374,7 +378,6 @@ module AtomicRuby
374
378
  previous_snapshot = @thread_pool_monitor.snapshot
375
379
  pressure_started_at = nil
376
380
  idle_started_at = nil
377
- growth_samples = 0
378
381
  phase_samples = [0, 0, 0]
379
382
 
380
383
  loop do
@@ -390,7 +393,6 @@ module AtomicRuby
390
393
  pressure_started_at = nil
391
394
  previous_snapshot = snapshot
392
395
  idle_started_at ||= now
393
- growth_samples = 0
394
396
  phase_samples.fill(0)
395
397
 
396
398
  if now - idle_started_at >= AUTOSCALE_IDLE_TIME
@@ -407,13 +409,9 @@ module AtomicRuby
407
409
  phase_samples.fill(0)
408
410
  elsif now - pressure_started_at >= AUTOSCALE_WINDOW
409
411
  if should_grow?(previous_snapshot, snapshot, phase_samples, now - pressure_started_at)
410
- growth_samples += 1
411
- if growth_samples >= AUTOSCALE_GROWTH_SAMPLES
412
- spawn_worker(temporary: true)
413
- growth_samples = 0
414
- end
415
- else
416
- growth_samples = 0
412
+ worker_count = @threads.length
413
+ workers_to_add = [worker_count, @queue.size, @max_size - worker_count].min
414
+ workers_to_add.times { spawn_worker(temporary: true) }
417
415
  end
418
416
  previous_snapshot = snapshot
419
417
  pressure_started_at = now
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtomicRuby
4
- VERSION = "0.15.2"
4
+ VERSION = "0.15.4"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: atomic-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.2
4
+ version: 0.15.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Young