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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +4 -2
- data/Rakefile +2 -3
- data/ext/concurrent-ruby/com/concurrent_ruby/ext/AtomicReferenceLibrary.java +3 -38
- data/lib/concurrent-ruby/concurrent/atomic/atomic_reference.rb +9 -2
- data/lib/concurrent-ruby/concurrent/atomic/lock_local_var.rb +1 -0
- data/lib/concurrent-ruby/concurrent/atomic/read_write_lock.rb +15 -2
- data/lib/concurrent-ruby/concurrent/atomic/reentrant_read_write_lock.rb +8 -1
- data/lib/concurrent-ruby/concurrent/atomic_reference/mutex_atomic.rb +1 -2
- data/lib/concurrent-ruby/concurrent/atomic_reference/numeric_cas_wrapper.rb +9 -1
- data/lib/concurrent-ruby/concurrent/collection/ruby_timeout_queue.rb +55 -0
- data/lib/concurrent-ruby/concurrent/collection/timeout_queue.rb +18 -0
- data/lib/concurrent-ruby/concurrent/concurrent_ruby.jar +0 -0
- data/lib/concurrent-ruby/concurrent/executor/fixed_thread_pool.rb +2 -4
- data/lib/concurrent-ruby/concurrent/executor/java_executor_service.rb +1 -0
- data/lib/concurrent-ruby/concurrent/executor/java_thread_pool_executor.rb +2 -0
- data/lib/concurrent-ruby/concurrent/executor/ruby_single_thread_executor.rb +2 -0
- data/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb +54 -31
- data/lib/concurrent-ruby/concurrent/executor/timer_set.rb +4 -1
- data/lib/concurrent-ruby/concurrent/executors.rb +0 -1
- data/lib/concurrent-ruby/concurrent/mvar.rb +4 -4
- data/lib/concurrent-ruby/concurrent/promise.rb +1 -1
- data/lib/concurrent-ruby/concurrent/timer_task.rb +7 -2
- data/lib/concurrent-ruby/concurrent/version.rb +1 -1
- metadata +7 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0d094624e5f9f2dffc45b80892987f339d8b8c09ff2f01cf923e3825dd6409f7
|
|
4
|
+
data.tar.gz: 02adb496e81a3adb7e3c6042f72ec421677851033d57eb77b0e1b3641ef70684
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 (
|
|
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
|
-
|
|
339
|
-
puts 'Manually: create a release on GitHub
|
|
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 =
|
|
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
|
|
@@ -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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
Binary file
|
|
@@ -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
|
|
85
|
-
#
|
|
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
|
#
|
|
@@ -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
|
|
99
|
-
synchronize
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
359
|
+
prunable = true
|
|
342
360
|
|
|
343
|
-
|
|
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.
|
|
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
|
-
@
|
|
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
|
-
# `#
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
|
|
@@ -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?
|
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.
|
|
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:
|
|
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:
|
|
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.
|