concurrent-ruby 1.2.0 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -11,6 +11,7 @@ module Concurrent
11
11
  def initialize
12
12
  @processor_count = Delay.new { compute_processor_count }
13
13
  @physical_processor_count = Delay.new { compute_physical_processor_count }
14
+ @cpu_quota = Delay.new { compute_cpu_quota }
14
15
  end
15
16
 
16
17
  def processor_count
@@ -21,6 +22,25 @@ module Concurrent
21
22
  @physical_processor_count.value
22
23
  end
23
24
 
25
+ def available_processor_count
26
+ cpu_count = processor_count.to_f
27
+ quota = cpu_quota
28
+
29
+ return cpu_count if quota.nil?
30
+
31
+ # cgroup cpus quotas have no limits, so they can be set to higher than the
32
+ # real count of cores.
33
+ if quota > cpu_count
34
+ cpu_count
35
+ else
36
+ quota
37
+ end
38
+ end
39
+
40
+ def cpu_quota
41
+ @cpu_quota.value
42
+ end
43
+
24
44
  private
25
45
 
26
46
  def compute_processor_count
@@ -60,6 +80,24 @@ module Concurrent
60
80
  rescue
61
81
  return 1
62
82
  end
83
+
84
+ def compute_cpu_quota
85
+ if RbConfig::CONFIG["target_os"].include?("linux")
86
+ if File.exist?("/sys/fs/cgroup/cpu.max")
87
+ # cgroups v2: https://docs.kernel.org/admin-guide/cgroup-v2.html#cpu-interface-files
88
+ cpu_max = File.read("/sys/fs/cgroup/cpu.max")
89
+ return nil if cpu_max.start_with?("max ") # no limit
90
+ max, period = cpu_max.split.map(&:to_f)
91
+ max / period
92
+ elsif File.exist?("/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us")
93
+ # cgroups v1: https://kernel.googlesource.com/pub/scm/linux/kernel/git/glommer/memcg/+/cpu_stat/Documentation/cgroups/cpu.txt
94
+ max = File.read("/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us").to_i
95
+ return nil if max == 0
96
+ period = File.read("/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us").to_f
97
+ max / period
98
+ end
99
+ end
100
+ end
63
101
  end
64
102
  end
65
103
 
@@ -107,4 +145,31 @@ module Concurrent
107
145
  def self.physical_processor_count
108
146
  processor_counter.physical_processor_count
109
147
  end
148
+
149
+ # Number of processors cores available for process scheduling.
150
+ # Returns `nil` if there is no #cpu_quota, or a `Float` if the
151
+ # process is inside a cgroup with a dedicated CPU quota (typically Docker).
152
+ #
153
+ # For performance reasons the calculated value will be memoized on the first
154
+ # call.
155
+ #
156
+ # @return [nil, Float] number of available processors
157
+ def self.available_processor_count
158
+ processor_counter.available_processor_count
159
+ end
160
+
161
+ # The maximum number of processors cores available for process scheduling.
162
+ # Returns `nil` if there is no enforced limit, or a `Float` if the
163
+ # process is inside a cgroup with a dedicated CPU quota (typically Docker).
164
+ #
165
+ # Note that nothing prevents setting a CPU quota higher than the actual number of
166
+ # cores on the system.
167
+ #
168
+ # For performance reasons the calculated value will be memoized on the first
169
+ # call.
170
+ #
171
+ # @return [nil, Float] Maximum number of available processors as set by a cgroup CPU quota, or nil if none set
172
+ def self.cpu_quota
173
+ processor_counter.cpu_quota
174
+ end
110
175
  end
@@ -1,3 +1,3 @@
1
1
  module Concurrent
2
- VERSION = '1.2.0'
2
+ VERSION = '1.3.1'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: concurrent-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jerry D'Antonio
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2023-01-23 00:00:00.000000000 Z
13
+ date: 2024-05-29 00:00:00.000000000 Z
14
14
  dependencies: []
15
15
  description: |
16
16
  Modern concurrency tools including agents, futures, promises, thread pools, actors, supervisors, and more.
@@ -76,7 +76,6 @@ files:
76
76
  - lib/concurrent-ruby/concurrent/collection/copy_on_write_observer_set.rb
77
77
  - lib/concurrent-ruby/concurrent/collection/java_non_concurrent_priority_queue.rb
78
78
  - lib/concurrent-ruby/concurrent/collection/lock_free_stack.rb
79
- - lib/concurrent-ruby/concurrent/collection/map/atomic_reference_map_backend.rb
80
79
  - lib/concurrent-ruby/concurrent/collection/map/mri_map_backend.rb
81
80
  - lib/concurrent-ruby/concurrent/collection/map/non_concurrent_map_backend.rb
82
81
  - lib/concurrent-ruby/concurrent/collection/map/synchronized_map_backend.rb
@@ -147,7 +146,6 @@ files:
147
146
  - lib/concurrent-ruby/concurrent/thread_safe/synchronized_delegator.rb
148
147
  - lib/concurrent-ruby/concurrent/thread_safe/util.rb
149
148
  - lib/concurrent-ruby/concurrent/thread_safe/util/adder.rb
150
- - lib/concurrent-ruby/concurrent/thread_safe/util/cheap_lockable.rb
151
149
  - lib/concurrent-ruby/concurrent/thread_safe/util/data_structures.rb
152
150
  - lib/concurrent-ruby/concurrent/thread_safe/util/power_of_two_tuple.rb
153
151
  - lib/concurrent-ruby/concurrent/thread_safe/util/striped64.rb