concurrent-ruby 1.2.2 → 1.3.1.pre

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: 1cad803df2c0fe187f3208dce2972feaa94daefcbd3749770abe3abfc0642fe9
4
- data.tar.gz: 216b0196496fde70f53cc82b83cd593ec5267a53dcac9a312f254ae627f7761e
3
+ metadata.gz: 5b3f7a68382f7fab40500191814c5f26ae1bbe1cbc05d462c813584c28bbb548
4
+ data.tar.gz: f3ff0e09369fe9871bfe6de7e378e583ae750253f691a467fe9bda159cceebb5
5
5
  SHA512:
6
- metadata.gz: 605762fd9c26a5a783dcb834f9db5d818ba2b52431b382a7abd33aaddfcc0df8c5005abf0e1a7ebacf3aced9ebb61ee7080fd59050805422f4fc5593925ae935
7
- data.tar.gz: ae916f03459f583497ab268aa82cc79d24dcddb2c51a86abf86313ad77faf975d10d22048ca03179c6583832fc0e25f3f3c1618de0668f9a2774486d7e15e41c
6
+ metadata.gz: 118e62d97725f1394dc99738fc63ddb8a2bec0fb054f38b2d89acc3bd667e22ca010c9057313b465edfa612718816da1140743f8015e08c0b0d16b001b272961
7
+ data.tar.gz: c460ea75b66f76b34e42dddce248bceeab9d7c0166b4a80f6ab79d7c6978b815170f06558ea10c35ee94361122bc9974176afaef5c0f0532c887ac379b3c2301
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## Current
2
2
 
3
+ ## Release v1.3.1 (28 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
+
3
16
  ## Release v1.2.2 (24 Feb 2023)
4
17
 
5
18
  * (#993) Fix arguments passed to `Concurrent::Map`'s `default_proc`.
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?
@@ -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