concurrent-ruby 1.3.5 → 1.3.7

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.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +4 -2
  4. data/Rakefile +2 -3
  5. data/ext/concurrent-ruby/com/concurrent_ruby/ext/AtomicReferenceLibrary.java +3 -38
  6. data/lib/concurrent-ruby/concurrent/atomic/atomic_reference.rb +9 -2
  7. data/lib/concurrent-ruby/concurrent/atomic/lock_local_var.rb +1 -0
  8. data/lib/concurrent-ruby/concurrent/atomic/read_write_lock.rb +15 -2
  9. data/lib/concurrent-ruby/concurrent/atomic/reentrant_read_write_lock.rb +8 -1
  10. data/lib/concurrent-ruby/concurrent/atomic_reference/mutex_atomic.rb +1 -2
  11. data/lib/concurrent-ruby/concurrent/atomic_reference/numeric_cas_wrapper.rb +9 -1
  12. data/lib/concurrent-ruby/concurrent/collection/ruby_timeout_queue.rb +55 -0
  13. data/lib/concurrent-ruby/concurrent/collection/timeout_queue.rb +18 -0
  14. data/lib/concurrent-ruby/concurrent/concurrent_ruby.jar +0 -0
  15. data/lib/concurrent-ruby/concurrent/executor/fixed_thread_pool.rb +2 -4
  16. data/lib/concurrent-ruby/concurrent/executor/java_executor_service.rb +1 -0
  17. data/lib/concurrent-ruby/concurrent/executor/java_thread_pool_executor.rb +2 -0
  18. data/lib/concurrent-ruby/concurrent/executor/ruby_single_thread_executor.rb +2 -0
  19. data/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb +54 -31
  20. data/lib/concurrent-ruby/concurrent/executor/timer_set.rb +4 -1
  21. data/lib/concurrent-ruby/concurrent/executors.rb +0 -1
  22. data/lib/concurrent-ruby/concurrent/mvar.rb +4 -4
  23. data/lib/concurrent-ruby/concurrent/promise.rb +1 -1
  24. data/lib/concurrent-ruby/concurrent/timer_task.rb +7 -2
  25. data/lib/concurrent-ruby/concurrent/version.rb +1 -1
  26. metadata +7 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 14d5a264c6fdc7b6087131a0f0a2fc1ce661d18029440e426e094443eb359ca6
4
- data.tar.gz: 42e7b1b8fb885aec9b7b08488c6682af2743e65ad6dcd731f8c86d34f7768981
3
+ metadata.gz: 0d094624e5f9f2dffc45b80892987f339d8b8c09ff2f01cf923e3825dd6409f7
4
+ data.tar.gz: 02adb496e81a3adb7e3c6042f72ec421677851033d57eb77b0e1b3641ef70684
5
5
  SHA512:
6
- metadata.gz: 31b6cf4fcc533349681610e74f6261c95e5c664583014d86101578f0380a6889288b29a89314a5da59e92cd422bcd97783c34d99c3e5c21a90db0f0d3fcc57e1
7
- data.tar.gz: 4438de87d8ec3ff1cc6d7d246821002bbb76d5a9ddd8b77ebf4a15c0449f1509d694c4f57936b1e1a69e19f32736f21518fd8e82b8cdfbb99d5e2948976c230e
6
+ metadata.gz: 43a4af037c64d4ee8e128963db7a3f40213a45898289bb9143aa222e5664982b8c0fa08d2dae8e629fa7766be6a528b74b5ba17fbbb5cb741a9b5c08020eea15
7
+ data.tar.gz: 0203e085d78340af99c8d8cbf2f0aa8793db6bfdbe4c25cd2ff622816581004c2871851c621d23bcfd653be30fd3aca682e98a3117b64e5735d23bb2a3bfb4c0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## Current
2
2
 
3
+ ## Release v1.3.7 (16 June 2026)
4
+
5
+ concurrent-ruby:
6
+
7
+ * See the [release notes on GitHub](https://github.com/ruby-concurrency/concurrent-ruby/releases/tag/v1.3.7).
8
+
9
+ ## Release v1.3.6 (13 December 2025)
10
+
11
+ concurrent-ruby:
12
+
13
+ * See the [release notes on GitHub](https://github.com/ruby-concurrency/concurrent-ruby/releases/tag/v1.3.6).
14
+
3
15
  ## Release v1.3.5, edge v0.7.2 (15 January 2025)
4
16
 
5
17
  concurrent-ruby:
data/README.md CHANGED
@@ -207,7 +207,7 @@ Deprecated features are still available and bugs are being fixed, but new featur
207
207
  These are available in the `concurrent-ruby-edge` companion gem.
208
208
 
209
209
  These features are under active development and may change frequently. They are expected not to
210
- keep backward compatibility (there may also lack tests and documentation). Semantic versions will
210
+ keep backward compatibility (they may also lack tests and documentation). Semantic versions will
211
211
  be obeyed though. Features developed in `concurrent-ruby-edge` are expected to move to
212
212
  `concurrent-ruby` when final.
213
213
 
@@ -358,7 +358,8 @@ best practice is to depend on `concurrent-ruby` and let users to decide if they
358
358
  * Recent CRuby
359
359
  * JRuby, `rbenv install jruby-9.2.17.0`
360
360
  * Set env variable `CONCURRENT_JRUBY_HOME` to point to it, e.g. `/usr/local/opt/rbenv/versions/jruby-9.2.17.0`
361
- * Install Docker, required for Windows builds
361
+ * Install Docker or Podman, required for Windows builds
362
+ * If `bundle config get path` is set, use `bundle config set --local path.system true` otherwise the `gem name, path: '.'` gems won't be found (Bundler limitation).
362
363
 
363
364
  ### Publishing the Gem
364
365
 
@@ -378,6 +379,7 @@ best practice is to depend on `concurrent-ruby` and let users to decide if they
378
379
  * [Charles Oliver Nutter](https://github.com/headius)
379
380
  * [Ben Sheldon](https://github.com/bensheldon)
380
381
  * [Samuel Williams](https://github.com/ioquatix)
382
+ * [Joshua Young](https://github.com/joshuay03)
381
383
 
382
384
  ### Special Thanks to
383
385
 
data/Rakefile CHANGED
@@ -335,9 +335,8 @@ namespace :release do
335
335
 
336
336
  desc '** print post release steps'
337
337
  task :post_steps do
338
- # TODO: (petr 05-Jun-2021) automate and renew the process
339
- puts 'Manually: create a release on GitHub with relevant changelog part'
340
- puts 'Manually: send email same as release with relevant changelog part'
338
+ puts
339
+ puts 'Manually: create a release on GitHub, use the "Generate release notes" button'
341
340
  puts 'Manually: tweet'
342
341
  end
343
342
  end
@@ -1,12 +1,12 @@
1
1
  package com.concurrent_ruby.ext;
2
2
 
3
+ import static org.jruby.runtime.Visibility.PRIVATE;
4
+
3
5
  import java.lang.reflect.Field;
4
6
  import java.io.IOException;
5
- import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
6
7
  import org.jruby.Ruby;
7
8
  import org.jruby.RubyClass;
8
9
  import org.jruby.RubyModule;
9
- import org.jruby.RubyNumeric;
10
10
  import org.jruby.RubyObject;
11
11
  import org.jruby.anno.JRubyClass;
12
12
  import org.jruby.anno.JRubyMethod;
@@ -91,15 +91,9 @@ public class AtomicReferenceLibrary implements Library {
91
91
  return newValue;
92
92
  }
93
93
 
94
- @JRubyMethod(name = {"compare_and_set", "compare_and_swap"})
94
+ @JRubyMethod(name = "_compare_and_set", visibility = PRIVATE)
95
95
  public IRubyObject compare_and_set(ThreadContext context, IRubyObject expectedValue, IRubyObject newValue) {
96
96
  Ruby runtime = context.runtime;
97
-
98
- if (expectedValue instanceof RubyNumeric) {
99
- // numerics are not always idempotent in Ruby, so we need to do slower logic
100
- return compareAndSetNumeric(context, expectedValue, newValue);
101
- }
102
-
103
97
  return runtime.newBoolean(UNSAFE.compareAndSwapObject(this, referenceOffset, expectedValue, newValue));
104
98
  }
105
99
 
@@ -113,35 +107,6 @@ public class AtomicReferenceLibrary implements Library {
113
107
  }
114
108
  }
115
109
  }
116
-
117
- private IRubyObject compareAndSetNumeric(ThreadContext context, IRubyObject expectedValue, IRubyObject newValue) {
118
- Ruby runtime = context.runtime;
119
-
120
- // loop until:
121
- // * reference CAS would succeed for same-valued objects
122
- // * current and expected have different values as determined by #equals
123
- while (true) {
124
- IRubyObject current = reference;
125
-
126
- if (!(current instanceof RubyNumeric)) {
127
- // old value is not numeric, CAS fails
128
- return runtime.getFalse();
129
- }
130
-
131
- RubyNumeric currentNumber = (RubyNumeric)current;
132
- if (!currentNumber.equals(expectedValue)) {
133
- // current number does not equal expected, fail CAS
134
- return runtime.getFalse();
135
- }
136
-
137
- // check that current has not changed, or else allow loop to repeat
138
- boolean success = UNSAFE.compareAndSwapObject(this, referenceOffset, current, newValue);
139
- if (success) {
140
- // value is same and did not change in interim...success
141
- return runtime.getTrue();
142
- }
143
- }
144
- }
145
110
  }
146
111
 
147
112
  private static final class UnsafeHolder {
@@ -22,7 +22,6 @@ module Concurrent
22
22
  class CAtomicReference
23
23
  include AtomicDirectUpdate
24
24
  include AtomicNumericCompareAndSetWrapper
25
- alias_method :compare_and_swap, :compare_and_set
26
25
  end
27
26
  CAtomicReference
28
27
  when Concurrent.on_jruby?
@@ -30,14 +29,22 @@ module Concurrent
30
29
  # @!macro internal_implementation_note
31
30
  class JavaAtomicReference
32
31
  include AtomicDirectUpdate
32
+ include AtomicNumericCompareAndSetWrapper
33
33
  end
34
34
  JavaAtomicReference
35
35
  when Concurrent.on_truffleruby?
36
36
  class TruffleRubyAtomicReference < TruffleRuby::AtomicReference
37
37
  include AtomicDirectUpdate
38
+ if private_method_defined?(:compare_and_set_reference)
39
+ alias_method :_compare_and_set, :compare_and_set_reference
40
+ private :_compare_and_set
41
+ include AtomicNumericCompareAndSetWrapper
42
+ else
43
+ # AtomicNumericCompareAndSetWrapper behavior already in TruffleRuby::AtomicReference
44
+ alias_method :compare_and_swap, :compare_and_set
45
+ end
38
46
  alias_method :value, :get
39
47
  alias_method :value=, :set
40
- alias_method :compare_and_swap, :compare_and_set
41
48
  alias_method :swap, :get_and_set
42
49
  end
43
50
  TruffleRubyAtomicReference
@@ -6,6 +6,7 @@ module Concurrent
6
6
  # @!visibility private
7
7
  def self.mutex_owned_per_thread?
8
8
  return false if Concurrent.on_jruby? || Concurrent.on_truffleruby?
9
+ return RUBY_VERSION < "3.0" if Concurrent.on_cruby?
9
10
 
10
11
  mutex = Mutex.new
11
12
  # Lock the mutex:
@@ -1,5 +1,6 @@
1
1
  require 'thread'
2
2
  require 'concurrent/atomic/atomic_fixnum'
3
+ require 'concurrent/atomic/atomic_reference'
3
4
  require 'concurrent/errors'
4
5
  require 'concurrent/synchronization/object'
5
6
  require 'concurrent/synchronization/lock'
@@ -58,7 +59,8 @@ module Concurrent
58
59
  # Create a new `ReadWriteLock` in the unlocked state.
59
60
  def initialize
60
61
  super()
61
- @Counter = AtomicFixnum.new(0) # single integer which represents lock state
62
+ @Counter = AtomicFixnum.new(0) # single integer which represents lock state
63
+ @Writer = AtomicReference.new(nil) # the thread currently holding the write lock
62
64
  @ReadLock = Synchronization::Lock.new
63
65
  @WriteLock = Synchronization::Lock.new
64
66
  end
@@ -137,9 +139,13 @@ module Concurrent
137
139
  # Release a previously acquired read lock.
138
140
  #
139
141
  # @return [Boolean] true if the lock is successfully released
142
+ #
143
+ # @raise [Concurrent::IllegalOperationError] if no read lock is currently held.
140
144
  def release_read_lock
141
145
  while true
142
146
  c = @Counter.value
147
+ raise IllegalOperationError, 'Cannot release a read lock which is not held' if running_readers(c) == 0
148
+
143
149
  if @Counter.compare_and_set(c, c-1)
144
150
  # If one or more writers were waiting, and we were the last reader, wake a writer up
145
151
  if waiting_writer?(c) && running_readers(c) == 1
@@ -187,14 +193,21 @@ module Concurrent
187
193
  break
188
194
  end
189
195
  end
196
+ @Writer.set(Thread.current)
190
197
  true
191
198
  end
192
199
 
193
200
  # Release a previously acquired write lock.
194
201
  #
195
202
  # @return [Boolean] true if the lock is successfully released
203
+ #
204
+ # @raise [Concurrent::IllegalOperationError] if the write lock is not held
205
+ # by the current thread.
196
206
  def release_write_lock
197
- return true unless running_writer?
207
+ unless @Writer.compare_and_set(Thread.current, nil)
208
+ raise IllegalOperationError, 'Cannot release a write lock which is not held by the current thread'
209
+ end
210
+
198
211
  c = @Counter.update { |counter| counter - RUNNING_WRITER }
199
212
  @ReadLock.broadcast
200
213
  @WriteLock.signal if waiting_writers(c) > 0
@@ -158,9 +158,11 @@ module Concurrent
158
158
  # @return [Boolean] true if the lock is successfully acquired
159
159
  #
160
160
  # @raise [Concurrent::ResourceLimitError] if the maximum number of readers
161
- # is exceeded.
161
+ # or per-thread reentrant acquires is exceeded.
162
162
  def acquire_read_lock
163
163
  if (held = @HeldCount.value) > 0
164
+ raise ResourceLimitError.new('Too many reader holds on this thread') if (held & READ_LOCK_MASK) == READ_LOCK_MASK
165
+
164
166
  # If we already have a lock, there's no need to wait
165
167
  if held & READ_LOCK_MASK == 0
166
168
  # But we do need to update the counter, if we were holding a write
@@ -212,8 +214,13 @@ module Concurrent
212
214
  # acquired immediately, return false.
213
215
  #
214
216
  # @return [Boolean] true if the lock is successfully acquired
217
+ #
218
+ # @raise [Concurrent::ResourceLimitError] if the maximum number of per-thread
219
+ # reentrant acquires is exceeded.
215
220
  def try_read_lock
216
221
  if (held = @HeldCount.value) > 0
222
+ raise ResourceLimitError.new('Too many reader holds on this thread') if (held & READ_LOCK_MASK) == READ_LOCK_MASK
223
+
217
224
  if held & READ_LOCK_MASK == 0
218
225
  # If we hold a write lock, but not a read lock...
219
226
  @Counter.update { |c| c + 1 }
@@ -10,7 +10,6 @@ module Concurrent
10
10
  extend Concurrent::Synchronization::SafeInitialization
11
11
  include AtomicDirectUpdate
12
12
  include AtomicNumericCompareAndSetWrapper
13
- alias_method :compare_and_swap, :compare_and_set
14
13
 
15
14
  # @!macro atomic_reference_method_initialize
16
15
  def initialize(value = nil)
@@ -42,7 +41,7 @@ module Concurrent
42
41
  alias_method :swap, :get_and_set
43
42
 
44
43
  # @!macro atomic_reference_method_compare_and_set
45
- def _compare_and_set(old_value, new_value)
44
+ private def _compare_and_set(old_value, new_value)
46
45
  synchronize do
47
46
  if @value.equal? old_value
48
47
  @value = new_value
@@ -9,12 +9,18 @@ module Concurrent
9
9
  # @!macro atomic_reference_method_compare_and_set
10
10
  def compare_and_set(old_value, new_value)
11
11
  if old_value.kind_of? Numeric
12
+ # NaN is never == to itself; match it explicitly so #update can terminate.
13
+ expected_nan = old_value.respond_to?(:nan?) && old_value.nan?
12
14
  while true
13
15
  old = get
14
16
 
15
17
  return false unless old.kind_of? Numeric
16
18
 
17
- return false unless old == old_value
19
+ if expected_nan
20
+ return false unless old.respond_to?(:nan?) && old.nan?
21
+ else
22
+ return false unless old == old_value
23
+ end
18
24
 
19
25
  result = _compare_and_set(old, new_value)
20
26
  return result if result
@@ -24,5 +30,7 @@ module Concurrent
24
30
  end
25
31
  end
26
32
 
33
+ alias_method :compare_and_swap, :compare_and_set
34
+
27
35
  end
28
36
  end
@@ -0,0 +1,55 @@
1
+ module Concurrent
2
+ module Collection
3
+ # @!visibility private
4
+ # @!macro ruby_timeout_queue
5
+ class RubyTimeoutQueue < ::Queue
6
+ def initialize(*args)
7
+ if RUBY_VERSION >= '3.2'
8
+ raise "#{self.class.name} is not needed on Ruby 3.2 or later, use ::Queue instead"
9
+ end
10
+
11
+ super(*args)
12
+
13
+ @mutex = Mutex.new
14
+ @cond_var = ConditionVariable.new
15
+ end
16
+
17
+ def push(obj)
18
+ @mutex.synchronize do
19
+ super(obj)
20
+ @cond_var.signal
21
+ end
22
+ end
23
+ alias_method :enq, :push
24
+ alias_method :<<, :push
25
+
26
+ def pop(non_block = false, timeout: nil)
27
+ if non_block && timeout
28
+ raise ArgumentError, "can't set a timeout if non_block is enabled"
29
+ end
30
+
31
+ if non_block
32
+ super(true)
33
+ elsif timeout
34
+ @mutex.synchronize do
35
+ deadline = Concurrent.monotonic_time + timeout
36
+ while (now = Concurrent.monotonic_time) < deadline && empty?
37
+ @cond_var.wait(@mutex, deadline - now)
38
+ end
39
+ begin
40
+ return super(true)
41
+ rescue ThreadError
42
+ # still empty
43
+ nil
44
+ end
45
+ end
46
+ else
47
+ super(false)
48
+ end
49
+ end
50
+ alias_method :deq, :pop
51
+ alias_method :shift, :pop
52
+ end
53
+ private_constant :RubyTimeoutQueue
54
+ end
55
+ end
@@ -0,0 +1,18 @@
1
+ module Concurrent
2
+ module Collection
3
+ # @!visibility private
4
+ # @!macro internal_implementation_note
5
+ TimeoutQueueImplementation = if RUBY_VERSION >= '3.2'
6
+ ::Queue
7
+ else
8
+ require 'concurrent/collection/ruby_timeout_queue'
9
+ RubyTimeoutQueue
10
+ end
11
+ private_constant :TimeoutQueueImplementation
12
+
13
+ # @!visibility private
14
+ # @!macro timeout_queue
15
+ class TimeoutQueue < TimeoutQueueImplementation
16
+ end
17
+ end
18
+ end
@@ -81,10 +81,8 @@ module Concurrent
81
81
  # What is being pruned is controlled by the min_threads and idletime
82
82
  # parameters passed at pool creation time
83
83
  #
84
- # This is a no-op on some pool implementation (e.g. the Java one). The Ruby
85
- # pool will auto-prune each time a new job is posted. You will need to call
86
- # this method explicitly in case your application post jobs in bursts (a
87
- # lot of jobs and then nothing for long periods)
84
+ # This is a no-op on all pool implementations as they prune themselves
85
+ # automatically, and has been deprecated.
88
86
 
89
87
  # @!macro thread_pool_executor_public_api
90
88
  #
@@ -46,6 +46,7 @@ if Concurrent.on_jruby?
46
46
  def kill
47
47
  synchronize do
48
48
  @executor.shutdownNow
49
+ wait_for_termination
49
50
  nil
50
51
  end
51
52
  end
@@ -8,6 +8,7 @@ if Concurrent.on_jruby?
8
8
  # @!macro thread_pool_options
9
9
  # @!visibility private
10
10
  class JavaThreadPoolExecutor < JavaExecutorService
11
+ include Concern::Deprecation
11
12
 
12
13
  # @!macro thread_pool_executor_constant_default_max_pool_size
13
14
  DEFAULT_MAX_POOL_SIZE = java.lang.Integer::MAX_VALUE # 2147483647
@@ -100,6 +101,7 @@ if Concurrent.on_jruby?
100
101
 
101
102
  # @!macro thread_pool_executor_method_prune_pool
102
103
  def prune_pool
104
+ deprecated "#prune_pool has no effect and will be removed in the next release."
103
105
  end
104
106
 
105
107
  private
@@ -1,4 +1,5 @@
1
1
  require 'concurrent/executor/ruby_thread_pool_executor'
2
+ require 'concurrent/executor/serial_executor_service'
2
3
 
3
4
  module Concurrent
4
5
 
@@ -6,6 +7,7 @@ module Concurrent
6
7
  # @!macro abstract_executor_service_public_api
7
8
  # @!visibility private
8
9
  class RubySingleThreadExecutor < RubyThreadPoolExecutor
10
+ include SerialExecutorService
9
11
 
10
12
  # @!macro single_thread_executor_method_initialize
11
13
  def initialize(opts = {})
@@ -3,6 +3,7 @@ require 'concurrent/atomic/event'
3
3
  require 'concurrent/concern/logging'
4
4
  require 'concurrent/executor/ruby_executor_service'
5
5
  require 'concurrent/utility/monotonic_time'
6
+ require 'concurrent/collection/timeout_queue'
6
7
 
7
8
  module Concurrent
8
9
 
@@ -10,6 +11,7 @@ module Concurrent
10
11
  # @!macro thread_pool_options
11
12
  # @!visibility private
12
13
  class RubyThreadPoolExecutor < RubyExecutorService
14
+ include Concern::Deprecation
13
15
 
14
16
  # @!macro thread_pool_executor_constant_default_max_pool_size
15
17
  DEFAULT_MAX_POOL_SIZE = 2_147_483_647 # java.lang.Integer::MAX_VALUE
@@ -94,9 +96,28 @@ module Concurrent
94
96
  end
95
97
  end
96
98
 
99
+ # removes the worker if it can be pruned
100
+ #
101
+ # @return [true, false] if the worker was pruned
102
+ #
97
103
  # @!visibility private
98
- def remove_busy_worker(worker)
99
- synchronize { ns_remove_busy_worker worker }
104
+ def prune_worker(worker)
105
+ synchronize do
106
+ if ns_prunable_capacity > 0
107
+ remove_worker worker
108
+ true
109
+ else
110
+ false
111
+ end
112
+ end
113
+ end
114
+
115
+ # @!visibility private
116
+ def remove_worker(worker)
117
+ synchronize do
118
+ ns_remove_ready_worker worker
119
+ ns_remove_busy_worker worker
120
+ end
100
121
  end
101
122
 
102
123
  # @!visibility private
@@ -116,7 +137,7 @@ module Concurrent
116
137
 
117
138
  # @!macro thread_pool_executor_method_prune_pool
118
139
  def prune_pool
119
- synchronize { ns_prune_pool }
140
+ deprecated "#prune_pool has no effect and will be removed in next the release, see https://github.com/ruby-concurrency/concurrent-ruby/pull/1082."
120
141
  end
121
142
 
122
143
  private
@@ -146,9 +167,6 @@ module Concurrent
146
167
  @largest_length = 0
147
168
  @workers_counter = 0
148
169
  @ruby_pid = $$ # detects if Ruby has forked
149
-
150
- @gc_interval = opts.fetch(:gc_interval, @idletime / 2.0).to_i # undocumented
151
- @next_gc_time = Concurrent.monotonic_time + @gc_interval
152
170
  end
153
171
 
154
172
  # @!visibility private
@@ -162,12 +180,10 @@ module Concurrent
162
180
 
163
181
  if ns_assign_worker(*args, &task) || ns_enqueue(*args, &task)
164
182
  @scheduled_task_count += 1
183
+ nil
165
184
  else
166
- return fallback_action(*args, &task)
185
+ fallback_action(*args, &task)
167
186
  end
168
-
169
- ns_prune_pool if @next_gc_time < Concurrent.monotonic_time
170
- nil
171
187
  end
172
188
 
173
189
  # @!visibility private
@@ -218,7 +234,7 @@ module Concurrent
218
234
  # @!visibility private
219
235
  def ns_enqueue(*args, &task)
220
236
  return false if @synchronous
221
-
237
+
222
238
  if !ns_limited_queue? || @queue.size < @max_queue
223
239
  @queue << [task, args]
224
240
  true
@@ -265,7 +281,7 @@ module Concurrent
265
281
  end
266
282
  end
267
283
 
268
- # removes a worker which is not in not tracked in @ready
284
+ # removes a worker which is not tracked in @ready
269
285
  #
270
286
  # @!visibility private
271
287
  def ns_remove_busy_worker(worker)
@@ -274,25 +290,27 @@ module Concurrent
274
290
  true
275
291
  end
276
292
 
277
- # try oldest worker if it is idle for enough time, it's returned back at the start
278
- #
279
293
  # @!visibility private
280
- def ns_prune_pool
281
- now = Concurrent.monotonic_time
282
- stopped_workers = 0
283
- while !@ready.empty? && (@pool.size - stopped_workers > @min_length)
284
- worker, last_message = @ready.first
285
- if now - last_message > self.idletime
286
- stopped_workers += 1
287
- @ready.shift
288
- worker << :stop
289
- else break
290
- end
294
+ def ns_remove_ready_worker(worker)
295
+ if index = @ready.index { |rw, _| rw == worker }
296
+ @ready.delete_at(index)
291
297
  end
298
+ true
299
+ end
292
300
 
293
- @next_gc_time = Concurrent.monotonic_time + @gc_interval
301
+ # @return [Integer] number of excess idle workers which can be removed without
302
+ # going below min_length, or all workers if not running
303
+ #
304
+ # @!visibility private
305
+ def ns_prunable_capacity
306
+ if running?
307
+ [@pool.size - @min_length, @ready.size].min
308
+ else
309
+ @pool.size
310
+ end
294
311
  end
295
312
 
313
+ # @!visibility private
296
314
  def ns_reset_if_forked
297
315
  if $$ != @ruby_pid
298
316
  @queue.clear
@@ -312,7 +330,7 @@ module Concurrent
312
330
 
313
331
  def initialize(pool, id)
314
332
  # instance variables accessed only under pool's lock so no need to sync here again
315
- @queue = Queue.new
333
+ @queue = Collection::TimeoutQueue.new
316
334
  @pool = pool
317
335
  @thread = create_worker @queue, pool, pool.idletime
318
336
 
@@ -338,17 +356,22 @@ module Concurrent
338
356
  def create_worker(queue, pool, idletime)
339
357
  Thread.new(queue, pool, idletime) do |my_queue, my_pool, my_idletime|
340
358
  catch(:stop) do
341
- loop do
359
+ prunable = true
342
360
 
343
- case message = my_queue.pop
361
+ loop do
362
+ timeout = prunable && my_pool.running? ? my_idletime : nil
363
+ case message = my_queue.pop(timeout: timeout)
364
+ when nil
365
+ throw :stop if my_pool.prune_worker(self)
366
+ prunable = false
344
367
  when :stop
345
- my_pool.remove_busy_worker(self)
368
+ my_pool.remove_worker(self)
346
369
  throw :stop
347
-
348
370
  else
349
371
  task, args = message
350
372
  run_task my_pool, task, args
351
373
  my_pool.ready_worker(self, Concurrent.monotonic_time)
374
+ prunable = true
352
375
  end
353
376
  end
354
377
  end
@@ -61,6 +61,7 @@ module Concurrent
61
61
  # not running.
62
62
  def kill
63
63
  shutdown
64
+ @timer_executor.kill
64
65
  end
65
66
 
66
67
  private :<<
@@ -122,7 +123,9 @@ module Concurrent
122
123
  def ns_shutdown_execution
123
124
  ns_reset_if_forked
124
125
  @queue.clear
125
- @timer_executor.kill
126
+ @condition.set
127
+ @condition.reset
128
+ @timer_executor.shutdown
126
129
  stopped_event.set
127
130
  end
128
131
 
@@ -10,7 +10,6 @@ require 'concurrent/executor/java_thread_pool_executor'
10
10
  require 'concurrent/executor/ruby_executor_service'
11
11
  require 'concurrent/executor/ruby_single_thread_executor'
12
12
  require 'concurrent/executor/ruby_thread_pool_executor'
13
- require 'concurrent/executor/cached_thread_pool'
14
13
  require 'concurrent/executor/safe_task_executor'
15
14
  require 'concurrent/executor/serial_executor_service'
16
15
  require 'concurrent/executor/serialized_execution'
@@ -9,7 +9,7 @@ module Concurrent
9
9
  # queue of length one, or a special kind of mutable variable.
10
10
  #
11
11
  # On top of the fundamental `#put` and `#take` operations, we also provide a
12
- # `#mutate` that is atomic with respect to operations on the same instance.
12
+ # `#modify` that is atomic with respect to operations on the same instance.
13
13
  # These operations all support timeouts.
14
14
  #
15
15
  # We also support non-blocking operations `#try_put!` and `#try_take!`, a
@@ -87,7 +87,7 @@ module Concurrent
87
87
  @mutex.synchronize do
88
88
  wait_for_full(timeout)
89
89
 
90
- # if we timeoud out we'll still be empty
90
+ # If we timed out we'll still be empty
91
91
  if unlocked_full?
92
92
  yield @value
93
93
  else
@@ -116,10 +116,10 @@ module Concurrent
116
116
  end
117
117
 
118
118
  # Atomically `take`, yield the value to a block for transformation, and then
119
- # `put` the transformed value. Returns the transformed value. A timeout can
119
+ # `put` the transformed value. Returns the pre-transform value. A timeout can
120
120
  # be set to limit the time spent blocked, in which case it returns `TIMEOUT`
121
121
  # if the time is exceeded.
122
- # @return [Object] the transformed value, or `TIMEOUT`
122
+ # @return [Object] the pre-transform value, or `TIMEOUT`
123
123
  def modify(timeout = nil)
124
124
  raise ArgumentError.new('no block given') unless block_given?
125
125
 
@@ -167,7 +167,7 @@ module Concurrent
167
167
  # c2 = p.then(-> reason { raise 'Boom!' })
168
168
  #
169
169
  # c1.wait.state #=> :fulfilled
170
- # c1.value #=> 45
170
+ # c1.value #=> 42
171
171
  # c2.wait.state #=> :rejected
172
172
  # c2.reason #=> #<RuntimeError: Boom!>
173
173
  # ```
@@ -2,6 +2,7 @@ require 'concurrent/collection/copy_on_notify_observer_set'
2
2
  require 'concurrent/concern/dereferenceable'
3
3
  require 'concurrent/concern/observable'
4
4
  require 'concurrent/atomic/atomic_boolean'
5
+ require 'concurrent/atomic/atomic_fixnum'
5
6
  require 'concurrent/executor/executor_service'
6
7
  require 'concurrent/executor/ruby_executor_service'
7
8
  require 'concurrent/executor/safe_task_executor'
@@ -236,6 +237,7 @@ module Concurrent
236
237
  synchronize do
237
238
  if @running.false?
238
239
  @running.make_true
240
+ @age.increment
239
241
  schedule_next_task(@run_now ? 0 : @execution_interval)
240
242
  end
241
243
  end
@@ -309,6 +311,7 @@ module Concurrent
309
311
  @task = Concurrent::SafeTaskExecutor.new(task)
310
312
  @executor = opts[:executor] || Concurrent.global_io_executor
311
313
  @running = Concurrent::AtomicBoolean.new(false)
314
+ @age = Concurrent::AtomicFixnum.new(0)
312
315
  @value = nil
313
316
 
314
317
  self.observers = Collection::CopyOnNotifyObserverSet.new
@@ -328,13 +331,15 @@ module Concurrent
328
331
 
329
332
  # @!visibility private
330
333
  def schedule_next_task(interval = execution_interval)
331
- ScheduledTask.execute(interval, executor: @executor, args: [Concurrent::Event.new], &method(:execute_task))
334
+ ScheduledTask.execute(interval, executor: @executor, args: [Concurrent::Event.new, @age.value], &method(:execute_task))
332
335
  nil
333
336
  end
334
337
 
335
338
  # @!visibility private
336
- def execute_task(completion)
339
+ def execute_task(completion, age_when_scheduled)
337
340
  return nil unless @running.true?
341
+ return nil unless @age.value == age_when_scheduled
342
+
338
343
  start_time = Concurrent.monotonic_time
339
344
  _success, value, reason = @task.execute(self)
340
345
  if completion.try?
@@ -1,3 +1,3 @@
1
1
  module Concurrent
2
- VERSION = '1.3.5'
2
+ VERSION = '1.3.7'
3
3
  end
metadata CHANGED
@@ -1,16 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: concurrent-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.5
4
+ version: 1.3.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jerry D'Antonio
8
8
  - Petr Chalupa
9
9
  - The Ruby Concurrency Team
10
- autorequire:
11
10
  bindir: bin
12
11
  cert_chain: []
13
- date: 2025-01-15 00:00:00.000000000 Z
12
+ date: 2026-06-16 00:00:00.000000000 Z
14
13
  dependencies: []
15
14
  description: |
16
15
  Modern concurrency tools including agents, futures, promises, thread pools, actors, supervisors, and more.
@@ -19,9 +18,9 @@ email: concurrent-ruby@googlegroups.com
19
18
  executables: []
20
19
  extensions: []
21
20
  extra_rdoc_files:
22
- - README.md
23
- - LICENSE.txt
24
21
  - CHANGELOG.md
22
+ - LICENSE.txt
23
+ - README.md
25
24
  files:
26
25
  - CHANGELOG.md
27
26
  - Gemfile
@@ -82,6 +81,8 @@ files:
82
81
  - lib/concurrent-ruby/concurrent/collection/map/truffleruby_map_backend.rb
83
82
  - lib/concurrent-ruby/concurrent/collection/non_concurrent_priority_queue.rb
84
83
  - lib/concurrent-ruby/concurrent/collection/ruby_non_concurrent_priority_queue.rb
84
+ - lib/concurrent-ruby/concurrent/collection/ruby_timeout_queue.rb
85
+ - lib/concurrent-ruby/concurrent/collection/timeout_queue.rb
85
86
  - lib/concurrent-ruby/concurrent/concern/deprecation.rb
86
87
  - lib/concurrent-ruby/concurrent/concern/dereferenceable.rb
87
88
  - lib/concurrent-ruby/concurrent/concern/logging.rb
@@ -166,7 +167,6 @@ licenses:
166
167
  metadata:
167
168
  source_code_uri: https://github.com/ruby-concurrency/concurrent-ruby
168
169
  changelog_uri: https://github.com/ruby-concurrency/concurrent-ruby/blob/master/CHANGELOG.md
169
- post_install_message:
170
170
  rdoc_options: []
171
171
  require_paths:
172
172
  - lib/concurrent-ruby
@@ -181,8 +181,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
181
181
  - !ruby/object:Gem::Version
182
182
  version: '0'
183
183
  requirements: []
184
- rubygems_version: 3.3.26
185
- signing_key:
184
+ rubygems_version: 4.0.6
186
185
  specification_version: 4
187
186
  summary: Modern concurrency tools for Ruby. Inspired by Erlang, Clojure, Scala, Haskell,
188
187
  F#, C#, Java, and classic concurrency patterns.