concurrent-ruby 1.2.0 → 1.3.1

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.
@@ -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