concurrent-ruby 1.2.1 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 366b0c4f0d3184143a58a6b30538fe9997e7dfad19c8ea38a468168d680f3bdf
4
- data.tar.gz: 010f0ee92f5132a2ed41da7d789a72955691dbcf880c579fd121cb366ba000bc
3
+ metadata.gz: 7841a791b1b148d8e8efea668fc70f3acc98c247f8d49fea58a4c672b45a62e6
4
+ data.tar.gz: 380ca8252b5b9251f30cd925d8b08f2afae56db19a210a8726b44c7304e5cd84
5
5
  SHA512:
6
- metadata.gz: 876b3302b218236cb89bd838e3030e2c5937d9682edd8e88a47d538ec5f0da21a7d49b55452e278e6c01b7210cb51c4bad3afc6b5767294f16890f5bd630e69b
7
- data.tar.gz: 938906cad82d61617a0267dc28dc99e18420c7872593344c1c6adb4fa54c751839226703e1ce6ea81e79dcd35b2650e5868c6c11bcaf83c02d2fb105aa8b35a2
6
+ metadata.gz: e479a5475e8d6dd7b3589a76300ccc73162cbceb8a15b721462394ff92f02054ba06e8ec5ee3ab08aa391a846d00492c708c893416b4c0c159c53d9b065f755f
7
+ data.tar.gz: 52daa298a6820a10756bfbf472aff566f5b5508f4e014352aa7a8d2516873b1e69b6392cde1379c0feaeba7c054c6c6c919613c3e09ac4d2328939cde3f01444
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  ## Current
2
2
 
3
+ ## Release v1.3.1 (29 May 2024)
4
+
5
+ * Release 1.3.0 was broken when pushed to RubyGems. 1.3.1 is a packaging fix.
6
+
7
+ ## Release v1.3.0 (28 May 2024)
8
+
9
+ * (#1042) Align Java Executor Service behavior for `shuttingdown?`, `shutdown?`
10
+ * (#1038) Add `Concurrent.usable_processor_count` that is cgroups aware.
11
+
12
+ ## Release v1.2.3 (16 Jan 2024)
13
+
14
+ * See [the GitHub release](https://github.com/ruby-concurrency/concurrent-ruby/releases/tag/v1.2.3) for details.
15
+
16
+ ## Release v1.2.2 (24 Feb 2023)
17
+
18
+ * (#993) Fix arguments passed to `Concurrent::Map`'s `default_proc`.
19
+
20
+ ## Release v1.2.1 (24 Feb 2023)
21
+
3
22
  * (#990) Add missing `require 'fiber'` for `FiberLocalVar`.
4
23
  * (#989) Optimize `Concurrent::Map#[]` on CRuby by letting the backing Hash handle the `default_proc`.
5
24
 
data/Gemfile CHANGED
@@ -12,7 +12,7 @@ gem 'concurrent-ruby-ext', Concurrent::VERSION, options.merge(platform: :mri)
12
12
 
13
13
  group :development do
14
14
  gem 'rake', '~> 13.0'
15
- gem 'rake-compiler', '~> 1.0', '>= 1.0.7'
15
+ gem 'rake-compiler', '~> 1.0', '>= 1.0.7', '!= 1.2.4'
16
16
  gem 'rake-compiler-dock', '~> 1.0'
17
17
  gem 'pry', '~> 0.11', platforms: :mri
18
18
  end
data/README.md CHANGED
@@ -375,6 +375,8 @@ best practice is to depend on `concurrent-ruby` and let users to decide if they
375
375
  * [Benoit Daloze](https://github.com/eregon)
376
376
  * [Matthew Draper](https://github.com/matthewd)
377
377
  * [Rafael França](https://github.com/rafaelfranca)
378
+ * [Charles Oliver Nutter](https://github.com/headius)
379
+ * [Ben Sheldon](https://github.com/bensheldon)
378
380
  * [Samuel Williams](https://github.com/ioquatix)
379
381
 
380
382
  ### Special Thanks to
@@ -21,9 +21,9 @@ module Concurrent
21
21
  # @!macro internal_implementation_note
22
22
  ArrayImplementation = case
23
23
  when Concurrent.on_cruby?
24
- # Array is thread-safe in practice because CRuby runs
25
- # threads one at a time and does not do context
26
- # switching during the execution of C functions.
24
+ # Array is not fully thread-safe on CRuby, see
25
+ # https://github.com/ruby-concurrency/concurrent-ruby/issues/929
26
+ # So we will need to add synchronization here
27
27
  ::Array
28
28
 
29
29
  when Concurrent.on_jruby?
@@ -14,7 +14,7 @@ module Concurrent
14
14
  # reasons.
15
15
  def initialize(options = nil, &default_proc)
16
16
  validate_options_hash!(options) if options.kind_of?(::Hash)
17
- @backend = Hash.new(&default_proc)
17
+ set_backend(default_proc)
18
18
  @default_proc = default_proc
19
19
  end
20
20
 
@@ -113,9 +113,17 @@ module Concurrent
113
113
 
114
114
  private
115
115
 
116
+ def set_backend(default_proc)
117
+ if default_proc
118
+ @backend = ::Hash.new { |_h, key| default_proc.call(self, key) }
119
+ else
120
+ @backend = {}
121
+ end
122
+ end
123
+
116
124
  def initialize_copy(other)
117
125
  super
118
- @backend = Hash.new(&@default_proc)
126
+ set_backend(@default_proc)
119
127
  self
120
128
  end
121
129
 
@@ -8,74 +8,77 @@ module Concurrent
8
8
  # @!visibility private
9
9
  class SynchronizedMapBackend < NonConcurrentMapBackend
10
10
 
11
- require 'mutex_m'
12
- include Mutex_m
13
- # WARNING: Mutex_m is a non-reentrant lock, so the synchronized methods are
14
- # not allowed to call each other.
11
+ def initialize(*args, &block)
12
+ super
13
+
14
+ # WARNING: Mutex is a non-reentrant lock, so the synchronized methods are
15
+ # not allowed to call each other.
16
+ @mutex = Mutex.new
17
+ end
15
18
 
16
19
  def [](key)
17
- synchronize { super }
20
+ @mutex.synchronize { super }
18
21
  end
19
22
 
20
23
  def []=(key, value)
21
- synchronize { super }
24
+ @mutex.synchronize { super }
22
25
  end
23
26
 
24
27
  def compute_if_absent(key)
25
- synchronize { super }
28
+ @mutex.synchronize { super }
26
29
  end
27
30
 
28
31
  def compute_if_present(key)
29
- synchronize { super }
32
+ @mutex.synchronize { super }
30
33
  end
31
34
 
32
35
  def compute(key)
33
- synchronize { super }
36
+ @mutex.synchronize { super }
34
37
  end
35
38
 
36
39
  def merge_pair(key, value)
37
- synchronize { super }
40
+ @mutex.synchronize { super }
38
41
  end
39
42
 
40
43
  def replace_pair(key, old_value, new_value)
41
- synchronize { super }
44
+ @mutex.synchronize { super }
42
45
  end
43
46
 
44
47
  def replace_if_exists(key, new_value)
45
- synchronize { super }
48
+ @mutex.synchronize { super }
46
49
  end
47
50
 
48
51
  def get_and_set(key, value)
49
- synchronize { super }
52
+ @mutex.synchronize { super }
50
53
  end
51
54
 
52
55
  def key?(key)
53
- synchronize { super }
56
+ @mutex.synchronize { super }
54
57
  end
55
58
 
56
59
  def delete(key)
57
- synchronize { super }
60
+ @mutex.synchronize { super }
58
61
  end
59
62
 
60
63
  def delete_pair(key, value)
61
- synchronize { super }
64
+ @mutex.synchronize { super }
62
65
  end
63
66
 
64
67
  def clear
65
- synchronize { super }
68
+ @mutex.synchronize { super }
66
69
  end
67
70
 
68
71
  def size
69
- synchronize { super }
72
+ @mutex.synchronize { super }
70
73
  end
71
74
 
72
75
  def get_or_default(key, default_value)
73
- synchronize { super }
76
+ @mutex.synchronize { super }
74
77
  end
75
78
 
76
79
  private
77
80
  def dupped_backend
78
- synchronize { super }
81
+ @mutex.synchronize { super }
79
82
  end
80
83
  end
81
84
  end
@@ -39,6 +39,10 @@ module Concurrent
39
39
  # The number of tasks that have been completed by the pool since construction.
40
40
  # @return [Integer] The number of tasks that have been completed by the pool since construction.
41
41
 
42
+ # @!macro thread_pool_executor_method_active_count
43
+ # The number of threads that are actively executing tasks.
44
+ # @return [Integer] The number of threads that are actively executing tasks.
45
+
42
46
  # @!macro thread_pool_executor_attr_reader_idletime
43
47
  # The number of seconds that a thread may be idle before being reclaimed.
44
48
  # @return [Integer] The number of seconds that a thread may be idle before being reclaimed.
@@ -57,15 +57,11 @@ if Concurrent.on_jruby?
57
57
  end
58
58
 
59
59
  def ns_shuttingdown?
60
- if @executor.respond_to? :isTerminating
61
- @executor.isTerminating
62
- else
63
- false
64
- end
60
+ @executor.isShutdown && !@executor.isTerminated
65
61
  end
66
62
 
67
63
  def ns_shutdown?
68
- @executor.isShutdown || @executor.isTerminated
64
+ @executor.isTerminated
69
65
  end
70
66
 
71
67
  class Job
@@ -88,10 +84,11 @@ if Concurrent.on_jruby?
88
84
 
89
85
  def initialize(daemonize = true)
90
86
  @daemonize = daemonize
87
+ @java_thread_factory = java.util.concurrent.Executors.defaultThreadFactory
91
88
  end
92
89
 
93
90
  def newThread(runnable)
94
- thread = java.util.concurrent.Executors.defaultThreadFactory().newThread(runnable)
91
+ thread = @java_thread_factory.newThread(runnable)
95
92
  thread.setDaemon(@daemonize)
96
93
  return thread
97
94
  end
@@ -73,6 +73,11 @@ if Concurrent.on_jruby?
73
73
  @executor.getCompletedTaskCount
74
74
  end
75
75
 
76
+ # @!macro thread_pool_executor_method_active_count
77
+ def active_count
78
+ @executor.getActiveCount
79
+ end
80
+
76
81
  # @!macro thread_pool_executor_attr_reader_idletime
77
82
  def idletime
78
83
  @executor.getKeepAliveTime(java.util.concurrent.TimeUnit::SECONDS)
@@ -61,6 +61,13 @@ module Concurrent
61
61
  synchronize { @completed_task_count }
62
62
  end
63
63
 
64
+ # @!macro thread_pool_executor_method_active_count
65
+ def active_count
66
+ synchronize do
67
+ @pool.length - @ready.length
68
+ end
69
+ end
70
+
64
71
  # @!macro executor_service_method_can_overflow_question
65
72
  def can_overflow?
66
73
  synchronize { ns_limited_queue? }
@@ -3,7 +3,7 @@ require 'concurrent/atomic/event'
3
3
  require 'concurrent/collection/non_concurrent_priority_queue'
4
4
  require 'concurrent/executor/executor_service'
5
5
  require 'concurrent/executor/single_thread_executor'
6
-
6
+ require 'concurrent/errors'
7
7
  require 'concurrent/options'
8
8
 
9
9
  module Concurrent
@@ -162,7 +162,11 @@ module Concurrent
162
162
  # queue now must have the same pop time, or a closer one, as
163
163
  # when we peeked).
164
164
  task = synchronize { @queue.pop }
165
- task.executor.post { task.process_task }
165
+ begin
166
+ task.executor.post { task.process_task }
167
+ rescue RejectedExecutionError
168
+ # ignore and continue
169
+ end
166
170
  else
167
171
  @condition.wait([diff, 60].min)
168
172
  end
@@ -15,9 +15,11 @@ module Concurrent
15
15
  # @!macro internal_implementation_note
16
16
  HashImplementation = case
17
17
  when Concurrent.on_cruby?
18
- # Hash is thread-safe in practice because CRuby runs
19
- # threads one at a time and does not do context
20
- # switching during the execution of C functions.
18
+ # Hash is not fully thread-safe on CRuby, see
19
+ # https://bugs.ruby-lang.org/issues/19237
20
+ # https://github.com/ruby/ruby/commit/ffd52412ab
21
+ # https://github.com/ruby-concurrency/concurrent-ruby/issues/929
22
+ # So we will need to add synchronization here (similar to Concurrent::Map).
21
23
  ::Hash
22
24
 
23
25
  when Concurrent.on_jruby?
@@ -20,8 +20,8 @@ module Concurrent
20
20
  require 'concurrent/collection/map/truffleruby_map_backend'
21
21
  TruffleRubyMapBackend
22
22
  else
23
- require 'concurrent/collection/map/atomic_reference_map_backend'
24
- AtomicReferenceMapBackend
23
+ require 'concurrent/collection/map/synchronized_map_backend'
24
+ SynchronizedMapBackend
25
25
  end
26
26
  else
27
27
  warn 'Concurrent::Map: unsupported Ruby engine, using a fully synchronized Concurrent::Map implementation'
@@ -5,6 +5,7 @@ require 'concurrent/collection/lock_free_stack'
5
5
  require 'concurrent/configuration'
6
6
  require 'concurrent/errors'
7
7
  require 'concurrent/re_include'
8
+ require 'concurrent/utility/monotonic_time'
8
9
 
9
10
  module Concurrent
10
11
 
@@ -22,7 +23,7 @@ module Concurrent
22
23
  #
23
24
  # @!macro promises.param.args
24
25
  # @param [Object] args arguments which are passed to the task when it's executed.
25
- # (It might be prepended with other arguments, see the @yeild section).
26
+ # (It might be prepended with other arguments, see the @yield section).
26
27
  #
27
28
  # @!macro promises.shortcut.on
28
29
  # Shortcut of {#$0_on} with default `:io` executor supplied.
@@ -63,8 +64,8 @@ module Concurrent
63
64
  resolvable_event_on default_executor
64
65
  end
65
66
 
66
- # Created resolvable event, user is responsible for resolving the event once by
67
- # {Promises::ResolvableEvent#resolve}.
67
+ # Creates a resolvable event, user is responsible for resolving the event once
68
+ # by calling {Promises::ResolvableEvent#resolve}.
68
69
  #
69
70
  # @!macro promises.param.default_executor
70
71
  # @return [ResolvableEvent]
@@ -94,7 +95,7 @@ module Concurrent
94
95
  future_on(default_executor, *args, &task)
95
96
  end
96
97
 
97
- # Constructs new Future which will be resolved after block is evaluated on default executor.
98
+ # Constructs a new Future which will be resolved after block is evaluated on default executor.
98
99
  # Evaluation begins immediately.
99
100
  #
100
101
  # @!macro promises.param.default_executor
@@ -106,7 +107,7 @@ module Concurrent
106
107
  ImmediateEventPromise.new(default_executor).future.then(*args, &task)
107
108
  end
108
109
 
109
- # Creates resolved future with will be either fulfilled with the given value or rejection with
110
+ # Creates a resolved future with will be either fulfilled with the given value or rejected with
110
111
  # the given reason.
111
112
  #
112
113
  # @param [true, false] fulfilled
@@ -118,7 +119,7 @@ module Concurrent
118
119
  ImmediateFuturePromise.new(default_executor, fulfilled, value, reason).future
119
120
  end
120
121
 
121
- # Creates resolved future with will be fulfilled with the given value.
122
+ # Creates a resolved future which will be fulfilled with the given value.
122
123
  #
123
124
  # @!macro promises.param.default_executor
124
125
  # @param [Object] value
@@ -127,7 +128,7 @@ module Concurrent
127
128
  resolved_future true, value, nil, default_executor
128
129
  end
129
130
 
130
- # Creates resolved future with will be rejected with the given reason.
131
+ # Creates a resolved future which will be rejected with the given reason.
131
132
  #
132
133
  # @!macro promises.param.default_executor
133
134
  # @param [Object] reason
@@ -190,7 +191,7 @@ module Concurrent
190
191
  delay_on default_executor, *args, &task
191
192
  end
192
193
 
193
- # Creates new event or future which is resolved only after it is touched,
194
+ # Creates a new event or future which is resolved only after it is touched,
194
195
  # see {Concurrent::AbstractEventFuture#touch}.
195
196
  #
196
197
  # @!macro promises.param.default_executor
@@ -214,7 +215,7 @@ module Concurrent
214
215
  schedule_on default_executor, intended_time, *args, &task
215
216
  end
216
217
 
217
- # Creates new event or future which is resolved in intended_time.
218
+ # Creates a new event or future which is resolved in intended_time.
218
219
  #
219
220
  # @!macro promises.param.default_executor
220
221
  # @!macro promises.param.intended_time
@@ -240,8 +241,8 @@ module Concurrent
240
241
  zip_futures_on default_executor, *futures_and_or_events
241
242
  end
242
243
 
243
- # Creates new future which is resolved after all futures_and_or_events are resolved.
244
- # Its value is array of zipped future values. Its reason is array of reasons for rejection.
244
+ # Creates a new future which is resolved after all futures_and_or_events are resolved.
245
+ # Its value is an array of zipped future values. Its reason is an array of reasons for rejection.
245
246
  # If there is an error it rejects.
246
247
  # @!macro promises.event-conversion
247
248
  # If event is supplied, which does not have value and can be only resolved, it's
@@ -262,7 +263,7 @@ module Concurrent
262
263
  zip_events_on default_executor, *futures_and_or_events
263
264
  end
264
265
 
265
- # Creates new event which is resolved after all futures_and_or_events are resolved.
266
+ # Creates a new event which is resolved after all futures_and_or_events are resolved.
266
267
  # (Future is resolved when fulfilled or rejected.)
267
268
  #
268
269
  # @!macro promises.param.default_executor
@@ -280,8 +281,8 @@ module Concurrent
280
281
 
281
282
  alias_method :any, :any_resolved_future
282
283
 
283
- # Creates new future which is resolved after first futures_and_or_events is resolved.
284
- # Its result equals result of the first resolved future.
284
+ # Creates a new future which is resolved after the first futures_and_or_events is resolved.
285
+ # Its result equals the result of the first resolved future.
285
286
  # @!macro promises.any-touch
286
287
  # If resolved it does not propagate {Concurrent::AbstractEventFuture#touch}, leaving delayed
287
288
  # futures un-executed if they are not required any more.
@@ -300,9 +301,9 @@ module Concurrent
300
301
  any_fulfilled_future_on default_executor, *futures_and_or_events
301
302
  end
302
303
 
303
- # Creates new future which is resolved after first of futures_and_or_events is fulfilled.
304
- # Its result equals result of the first resolved future or if all futures_and_or_events reject,
305
- # it has reason of the last resolved future.
304
+ # Creates a new future which is resolved after the first futures_and_or_events is fulfilled.
305
+ # Its result equals the result of the first resolved future or if all futures_and_or_events reject,
306
+ # it has reason of the last rejected future.
306
307
  # @!macro promises.any-touch
307
308
  # @!macro promises.event-conversion
308
309
  #
@@ -319,7 +320,7 @@ module Concurrent
319
320
  any_event_on default_executor, *futures_and_or_events
320
321
  end
321
322
 
322
- # Creates new event which becomes resolved after first of the futures_and_or_events resolves.
323
+ # Creates a new event which becomes resolved after the first futures_and_or_events resolves.
323
324
  # @!macro promises.any-touch
324
325
  #
325
326
  # @!macro promises.param.default_executor
@@ -611,7 +612,7 @@ module Concurrent
611
612
  # @yieldparam [Object] value
612
613
  # @yieldparam [Object] reason
613
614
  def chain_on(executor, *args, &task)
614
- ChainPromise.new_blocked_by1(self, @DefaultExecutor, executor, args, &task).future
615
+ ChainPromise.new_blocked_by1(self, executor, executor, args, &task).future
615
616
  end
616
617
 
617
618
  # @return [String] Short string representation.
@@ -772,8 +773,17 @@ module Concurrent
772
773
  @Lock.synchronize do
773
774
  @Waiters.increment
774
775
  begin
775
- unless resolved?
776
- @Condition.wait @Lock, timeout
776
+ if timeout
777
+ start = Concurrent.monotonic_time
778
+ until resolved?
779
+ break if @Condition.wait(@Lock, timeout) == nil # nil means timeout
780
+ timeout -= (Concurrent.monotonic_time - start)
781
+ break if timeout <= 0
782
+ end
783
+ else
784
+ until resolved?
785
+ @Condition.wait(@Lock, timeout)
786
+ end
777
787
  end
778
788
  ensure
779
789
  # JRuby may raise ConcurrencyError
@@ -1034,7 +1044,7 @@ module Concurrent
1034
1044
  # @return [Future]
1035
1045
  # @yield [value, *args] to the task.
1036
1046
  def then_on(executor, *args, &task)
1037
- ThenPromise.new_blocked_by1(self, @DefaultExecutor, executor, args, &task).future
1047
+ ThenPromise.new_blocked_by1(self, executor, executor, args, &task).future
1038
1048
  end
1039
1049
 
1040
1050
  # @!macro promises.shortcut.on
@@ -1052,7 +1062,7 @@ module Concurrent
1052
1062
  # @return [Future]
1053
1063
  # @yield [reason, *args] to the task.
1054
1064
  def rescue_on(executor, *args, &task)
1055
- RescuePromise.new_blocked_by1(self, @DefaultExecutor, executor, args, &task).future
1065
+ RescuePromise.new_blocked_by1(self, executor, executor, args, &task).future
1056
1066
  end
1057
1067
 
1058
1068
  # @!macro promises.method.zip
@@ -32,6 +32,17 @@ module Concurrent
32
32
  # be tested separately then passed to the `TimerTask` for scheduling and
33
33
  # running.
34
34
  #
35
+ # A `TimerTask` supports two different types of interval calculations.
36
+ # A fixed delay will always wait the same amount of time between the
37
+ # completion of one task and the start of the next. A fixed rate will
38
+ # attempt to maintain a constant rate of execution regardless of the
39
+ # duration of the task. For example, if a fixed rate task is scheduled
40
+ # to run every 60 seconds but the task itself takes 10 seconds to
41
+ # complete, the next task will be scheduled to run 50 seconds after
42
+ # the start of the previous task. If the task takes 70 seconds to
43
+ # complete, the next task will be start immediately after the previous
44
+ # task completes. Tasks will not be executed concurrently.
45
+ #
35
46
  # In some cases it may be necessary for a `TimerTask` to affect its own
36
47
  # execution cycle. To facilitate this, a reference to the TimerTask instance
37
48
  # is passed as an argument to the provided block every time the task is
@@ -74,6 +85,12 @@ module Concurrent
74
85
  #
75
86
  # #=> 'Boom!'
76
87
  #
88
+ # @example Configuring `:interval_type` with either :fixed_delay or :fixed_rate, default is :fixed_delay
89
+ # task = Concurrent::TimerTask.new(execution_interval: 5, interval_type: :fixed_rate) do
90
+ # puts 'Boom!'
91
+ # end
92
+ # task.interval_type #=> :fixed_rate
93
+ #
77
94
  # @example Last `#value` and `Dereferenceable` mixin
78
95
  # task = Concurrent::TimerTask.new(
79
96
  # dup_on_deref: true,
@@ -87,7 +104,7 @@ module Concurrent
87
104
  #
88
105
  # @example Controlling execution from within the block
89
106
  # timer_task = Concurrent::TimerTask.new(execution_interval: 1) do |task|
90
- # task.execution_interval.times{ print 'Boom! ' }
107
+ # task.execution_interval.to_i.times{ print 'Boom! ' }
91
108
  # print "\n"
92
109
  # task.execution_interval += 1
93
110
  # if task.execution_interval > 5
@@ -96,7 +113,7 @@ module Concurrent
96
113
  # end
97
114
  # end
98
115
  #
99
- # timer_task.execute # blocking call - this task will stop itself
116
+ # timer_task.execute
100
117
  # #=> Boom!
101
118
  # #=> Boom! Boom!
102
119
  # #=> Boom! Boom! Boom!
@@ -152,18 +169,30 @@ module Concurrent
152
169
  # Default `:execution_interval` in seconds.
153
170
  EXECUTION_INTERVAL = 60
154
171
 
155
- # Default `:timeout_interval` in seconds.
156
- TIMEOUT_INTERVAL = 30
172
+ # Maintain the interval between the end of one execution and the start of the next execution.
173
+ FIXED_DELAY = :fixed_delay
174
+
175
+ # Maintain the interval between the start of one execution and the start of the next.
176
+ # If execution time exceeds the interval, the next execution will start immediately
177
+ # after the previous execution finishes. Executions will not run concurrently.
178
+ FIXED_RATE = :fixed_rate
179
+
180
+ # Default `:interval_type`
181
+ DEFAULT_INTERVAL_TYPE = FIXED_DELAY
157
182
 
158
183
  # Create a new TimerTask with the given task and configuration.
159
184
  #
160
185
  # @!macro timer_task_initialize
161
186
  # @param [Hash] opts the options defining task execution.
162
- # @option opts [Integer] :execution_interval number of seconds between
187
+ # @option opts [Float] :execution_interval number of seconds between
163
188
  # task executions (default: EXECUTION_INTERVAL)
164
189
  # @option opts [Boolean] :run_now Whether to run the task immediately
165
190
  # upon instantiation or to wait until the first # execution_interval
166
191
  # has passed (default: false)
192
+ # @options opts [Symbol] :interval_type method to calculate the interval
193
+ # between executions, can be either :fixed_rate or :fixed_delay.
194
+ # (default: :fixed_delay)
195
+ # @option opts [Executor] executor, default is `global_io_executor`
167
196
  #
168
197
  # @!macro deref_options
169
198
  #
@@ -242,6 +271,10 @@ module Concurrent
242
271
  end
243
272
  end
244
273
 
274
+ # @!attribute [r] interval_type
275
+ # @return [Symbol] method to calculate the interval between executions
276
+ attr_reader :interval_type
277
+
245
278
  # @!attribute [rw] timeout_interval
246
279
  # @return [Fixnum] Number of seconds the task can run before it is
247
280
  # considered to have failed.
@@ -264,11 +297,17 @@ module Concurrent
264
297
  set_deref_options(opts)
265
298
 
266
299
  self.execution_interval = opts[:execution] || opts[:execution_interval] || EXECUTION_INTERVAL
300
+ if opts[:interval_type] && ![FIXED_DELAY, FIXED_RATE].include?(opts[:interval_type])
301
+ raise ArgumentError.new('interval_type must be either :fixed_delay or :fixed_rate')
302
+ end
267
303
  if opts[:timeout] || opts[:timeout_interval]
268
304
  warn 'TimeTask timeouts are now ignored as these were not able to be implemented correctly'
269
305
  end
306
+
270
307
  @run_now = opts[:now] || opts[:run_now]
271
- @executor = Concurrent::SafeTaskExecutor.new(task)
308
+ @interval_type = opts[:interval_type] || DEFAULT_INTERVAL_TYPE
309
+ @task = Concurrent::SafeTaskExecutor.new(task)
310
+ @executor = opts[:executor] || Concurrent.global_io_executor
272
311
  @running = Concurrent::AtomicBoolean.new(false)
273
312
  @value = nil
274
313
 
@@ -289,17 +328,18 @@ module Concurrent
289
328
 
290
329
  # @!visibility private
291
330
  def schedule_next_task(interval = execution_interval)
292
- ScheduledTask.execute(interval, args: [Concurrent::Event.new], &method(:execute_task))
331
+ ScheduledTask.execute(interval, executor: @executor, args: [Concurrent::Event.new], &method(:execute_task))
293
332
  nil
294
333
  end
295
334
 
296
335
  # @!visibility private
297
336
  def execute_task(completion)
298
337
  return nil unless @running.true?
299
- _success, value, reason = @executor.execute(self)
338
+ start_time = Concurrent.monotonic_time
339
+ _success, value, reason = @task.execute(self)
300
340
  if completion.try?
301
341
  self.value = value
302
- schedule_next_task
342
+ schedule_next_task(calculate_next_interval(start_time))
303
343
  time = Time.now
304
344
  observers.notify_observers do
305
345
  [time, self.value, reason]
@@ -307,5 +347,15 @@ module Concurrent
307
347
  end
308
348
  nil
309
349
  end
350
+
351
+ # @!visibility private
352
+ def calculate_next_interval(start_time)
353
+ if @interval_type == FIXED_RATE
354
+ run_time = Concurrent.monotonic_time - start_time
355
+ [execution_interval - run_time, 0].max
356
+ else # FIXED_DELAY
357
+ execution_interval
358
+ end
359
+ end
310
360
  end
311
361
  end