procrastinator 2.0.0 → 2.1.0

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: 86ce767f3807bc5184e0d658392cbf45b03ab68465a5161c8ffd9434157bd007
4
- data.tar.gz: 65b3b517a6bdd6150b34ee0e8419834f581abbe1ced3d1ae5aef50613a452c94
3
+ metadata.gz: 49bfb07d03e59388e8deefc6fa6eaea6342cf973943f3ce7ccbaa3835095e768
4
+ data.tar.gz: 498838544ce3371270b8a71f076820811b9a8c51b6b8227725be5ab4c59f8dac
5
5
  SHA512:
6
- metadata.gz: e00357dccd1ac7bdbc2ce72fdaada932f3155b155c88c4af8ccd7674f1db196cd186b0f96a3c1b99f2751cf30652f77fabcfab733e091539e3748b90fe04c0ac
7
- data.tar.gz: ee3425d98d5eca4d321e36b37428e50300cad78db202b34ef4150058c3a2a42fd0452f76015e51f3d5830c8e289f823e3d1e94aa086edcd694fb5d412558bdee
6
+ metadata.gz: e779c44fbd3edab3d32f387d20563a7c2a7d00acb270e567370692a78b0520bf11aa9fd9ce65cb205894ffd7a7f18e09839a4ca72571e9b0e5b2cf814d3a0470
7
+ data.tar.gz: c7235fec5d0db4dbf2d201e55e36d719750ea2a3875eca9ede878d054c55e1abd627319a686b082a402d7c62745831f2bee180f87a85b78c0c0c74ab1285bf57
data/.rubocop.yml CHANGED
@@ -4,7 +4,7 @@ AllCops:
4
4
  Exclude:
5
5
  - 'bin/*'
6
6
 
7
- TargetRubyVersion: 3.0
7
+ TargetRubyVersion: 3.3
8
8
 
9
9
  Layout/LineLength:
10
10
  Exclude:
@@ -14,6 +14,10 @@ Layout/LineLength:
14
14
  Layout/FirstArrayElementIndentation:
15
15
  IndentationWidth: 6
16
16
 
17
+ # setting to 6 to match RubyMine autoformat
18
+ Layout/MultilineMethodCallIndentation:
19
+ EnforcedStyle: indented_relative_to_receiver
20
+ IndentationWidth: 6
17
21
 
18
22
  # rspec blocks are huge by design
19
23
  Metrics/BlockLength:
@@ -23,3 +27,30 @@ Metrics/BlockLength:
23
27
  Metrics/ModuleLength:
24
28
  Exclude:
25
29
  - 'spec/**/*.rb'
30
+
31
+ # Disabling because it's marginally better, if at all, and not worth rewriting all step descripts right now
32
+ RSpec/ExampleWording:
33
+ Enabled: false
34
+
35
+ # My style is often to use multiple smaller expectations in a row instead of one giant one
36
+ RSpec/MultipleExpectations:
37
+ Enabled: false
38
+
39
+ # My style is often to abuse contexts for logical grouping
40
+ RSpec/ContextWording:
41
+ Enabled: false
42
+
43
+ # TODO: re-enable when FakeFS is removed
44
+ RSpec/AnyInstance:
45
+ Enabled: false
46
+
47
+ # Not convinced described_class is better. Makes everything harder to track mentally.
48
+ RSpec/DescribedClass:
49
+ Enabled: false
50
+
51
+ # TODO: re-enable and replace generic doubles with verifying ones
52
+ RSpec/VerifiedDoubles:
53
+ Enabled: false
54
+
55
+ RSpec/MultipleMemoizedHelpers:
56
+ Max: 7
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-3.1.4
1
+ ruby-3.4.5
data/Gemfile CHANGED
@@ -5,10 +5,13 @@ source 'https://rubygems.org'
5
5
  group :development do
6
6
  gem 'bundler', '~> 2.3'
7
7
  gem 'fakefs', '~> 1.8'
8
+ gem 'mutex_m', '~> 0.3' # for 'debase' debugger
8
9
  gem 'rake', '~> 13.0'
9
10
  gem 'rspec', '~> 3.12'
10
- gem 'rubocop', '~> 1.52'
11
- gem 'rubocop-performance', '~> 1.18'
11
+ gem 'rubocop', '~> 1.79'
12
+ gem 'rubocop-performance', '~> 1.25'
13
+ gem 'rubocop-rake', '~> 0.7'
14
+ gem 'rubocop-rspec', '~> 3.6'
12
15
  gem 'simplecov', '~> 0.22.0'
13
16
  gem 'timecop', '~> 0.9'
14
17
  gem 'yard', '~> 0.9'
data/README.md CHANGED
@@ -53,15 +53,15 @@ scheduler.defer(:birthday, run_at: Time.now + 3600, data: {user_id: 5})
53
53
  - [CSV Task Store](#csv-task-store)
54
54
  - [Shared Task Stores](#shared-task-stores)
55
55
  + [Task Container](#task-container)
56
+ + [Process Priority](#process-priority)
56
57
  * [Deferring Tasks](#deferring-tasks)
57
58
  + [Timing](#timing)
58
59
  + [Rescheduling Existing Tasks](#rescheduling-existing-tasks)
59
60
  + [Retries](#retries)
60
61
  + [Cancelling](#cancelling)
62
+ * [Testing with Procrastinator](#testing-with-procrastinator)
63
+ + [RSpec Matchers](#rspec-matchers)
61
64
  * [Running Tasks](#running-tasks)
62
- + [In Testing](#in-testing)
63
- - [RSpec Matchers](#rspec-matchers)
64
- + [In Production](#in-production)
65
65
  * [Similar Tools](#similar-tools)
66
66
  + [Linux etc: Cron and At](#linux-etc--cron-and-at)
67
67
  + [Gem: Resque](#gem--resque)
@@ -140,12 +140,12 @@ Task Handlers have attributes that are set after the Handler is created. The att
140
140
  the tasks from referencing unknown variables at whatever time they are run - if they're missing, you'll get
141
141
  a `MalformedTaskError`.
142
142
 
143
- | Attribute | Required | Description |
144
- |------------|----------|-------------|
145
- |`:container`| Yes | Container declared in `#setup` from the currently running instance |
146
- |`:logger` | Yes | Logger object for the Queue |
147
- |`:scheduler`| Yes | A scheduler object that you can use to schedule new tasks (eg. with `#defer`)|
148
- |`:data` | No | Data provided to `#defer`. Calls to `#defer` will error if they do not provide data when expected and vice-versa. |
143
+ | Attribute | Required | Description |
144
+ |--------------|----------|-------------------------------------------------------------------------------------------------------------------|
145
+ | `:container` | Yes | Container declared in `#setup` from the currently running instance |
146
+ | `:logger` | Yes | Logger object for the Queue |
147
+ | `:scheduler` | Yes | A scheduler object that you can use to schedule new tasks (eg. with `#defer`) |
148
+ | `:data` | No | Data provided to `#defer`. Calls to `#defer` will error if they do not provide data when expected and vice-versa. |
149
149
 
150
150
  ### Errors & Logging
151
151
 
@@ -184,10 +184,10 @@ end
184
184
 
185
185
  Some events are always logged by default:
186
186
 
187
- |event |level |
188
- |--------------------|-------|
189
- |Task completed | INFO |
190
- |Task cailure | ERROR |
187
+ | event | level |
188
+ |----------------|-------|
189
+ | Task completed | INFO |
190
+ | Task cailure | ERROR |
191
191
 
192
192
  ## Configuration
193
193
 
@@ -220,12 +220,12 @@ config.define_queue :greeting, SendWelcomeEmail, store: 'procrastinator.csv', ti
220
220
 
221
221
  Description of keyword options:
222
222
 
223
- | Option | Description |
224
- |------------------| ------------- |
225
- | `:store` | Storage IO object for tasks. See [Task Store](#task-store) |
226
- | `:timeout` | Max duration (seconds) before tasks are failed for taking too long |
223
+ | Option | Description |
224
+ |------------------|-------------------------------------------------------------------------------------|
225
+ | `:store` | Storage IO object for tasks. See [Task Store](#task-store) |
226
+ | `:timeout` | Max duration (seconds) before tasks are failed for taking too long |
227
227
  | `:max_attempts` | Once a task has been attempted `max_attempts` times, it will be permanently failed. |
228
- | `:update_period` | Delay (seconds) between reloads of all tasks from the task store |
228
+ | `:update_period` | Delay (seconds) between reloads of all tasks from the task store |
229
229
 
230
230
  ### Task Store
231
231
 
@@ -275,8 +275,8 @@ _Warning_: Task stores shared between queues **must** be thread-safe if using th
275
275
  These are the data fields for each individual scheduled task. When using the built-in task store, these are the field
276
276
  names. If you have a database, use this to inform your table schema.
277
277
 
278
- | Hash Key | Type | Description |
279
- |-------------------|----------| ------------------------------------------------------------------------|
278
+ | Hash Key | Type | Description |
279
+ |-------------------|----------|-------------------------------------------------------------------------|
280
280
  | `:id` | integer | Unique identifier for this exact task |
281
281
  | `:queue` | symbol | Name of the queue the task is inside |
282
282
  | `:run_at` | datetime | Time to attempt running the task next. Updated for retries¹ |
@@ -285,7 +285,7 @@ names. If you have a database, use this to inform your table schema.
285
285
  | `:attempts` | integer | Number of times the task has tried to run |
286
286
  | `:last_fail_at` | datetime | Time of the most recent failure |
287
287
  | `:last_error` | string | Error message + backtrace of the most recent failure. May be very long. |
288
- | `:data` | JSON | Data to be provided to the task handler, serialized² to JSON. |
288
+ | `:data` | JSON | Data to be provided to the task handler, serialized² to JSON. |
289
289
 
290
290
  ¹ `nil` indicates that it is permanently failed and will never run, either due to expiry or too many attempts.
291
291
 
@@ -350,6 +350,23 @@ class LunchTask
350
350
  end
351
351
  ```
352
352
 
353
+ ### Process Priority
354
+
355
+ The whole Procrastinator process may be made less urgent by providing an `adjust_priority` value (aka nice level
356
+ adjustment).
357
+
358
+ If not provided, Procrastinator will assume a priority adjustment of 1.
359
+
360
+ ```ruby
361
+ Procrastinator.setup do |config|
362
+ config.adjust_priority 10 # higher numbers are less urgent. This increases the default priority by 10
363
+
364
+ # .. other setup stuff ...
365
+ end
366
+ ```
367
+
368
+ If you want a negative priority nice value, then use system tools like `renice` at your own discretion.
369
+
353
370
  ## Deferring Tasks
354
371
 
355
372
  To add tasks to a queue, call `#defer` on the scheduler returned by `Procrastinator.setup`:
@@ -532,7 +549,8 @@ end
532
549
  ```
533
550
 
534
551
  > **Note:** There can be a distinction between process full title (`/proc/*/cmdline`) vs the shorter name
535
- > (`/proc/*/comm`). Some tools like `ps` and `top` display the process title, while others like `pstree` show the process name.
552
+ > (`/proc/*/comm`). Some tools like `ps` and `top` display the process title, while others like `pstree` show the
553
+ > process name.
536
554
  >
537
555
  > Procrastinator uses Ruby's `Process.setproctitle`, which only affects the title.
538
556
 
data/RELEASE_NOTES.md CHANGED
@@ -1,6 +1,36 @@
1
1
  # Release Notes
2
2
 
3
- ## 2.0.0 (2023-06-17)
3
+ ## [Unreleased]
4
+
5
+ ### Major Changes
6
+
7
+ * none
8
+
9
+ ### Minor Changes
10
+
11
+ * none
12
+
13
+ ### Bugfixes
14
+
15
+ * none
16
+
17
+ ## [2.1.0] (2025-08-16)
18
+
19
+ ### Major Changes
20
+
21
+ * none
22
+
23
+ ### Minor Changes
24
+
25
+ * Added process priority nice value handling
26
+ * Improved MultiIO implementation to avoid clobbering class methods
27
+ * Updated minimum ruby to 3.3
28
+
29
+ ### Bugfixes
30
+
31
+ * Compatible with Ruby 3.4.5
32
+
33
+ ## [2.0.0] (2023-06-17)
4
34
 
5
35
  ### Major Changes
6
36
 
@@ -14,7 +44,7 @@
14
44
 
15
45
  * none
16
46
 
17
- ## 1.2.0 (2023-06-17)
47
+ ## [1.2.0] (2023-06-17)
18
48
 
19
49
  ### Major Changes
20
50
 
@@ -28,7 +58,7 @@
28
58
 
29
59
  * When logging is disabled, it points to `File::NULL` instead of a dead-end StringIO
30
60
 
31
- ## 1.1.0 (2022-10-16)
61
+ ## [1.1.0] (2022-10-16)
32
62
 
33
63
  ### Major Changes
34
64
 
@@ -44,7 +74,7 @@
44
74
  * Fixed have_task handling of nested matchers like be_within
45
75
  * Improved have_task handling of string queue names vs symbols
46
76
 
47
- ## 1.0.1 (2022-09-20)
77
+ ## [1.0.1] (2022-09-20)
48
78
 
49
79
  ### Major Changes
50
80
 
@@ -58,7 +88,7 @@
58
88
 
59
89
  * Fixed integration error in rescheduling tasks
60
90
 
61
- ## 1.0.0 (2022-09-18)
91
+ ## [1.0.0] (2022-09-18)
62
92
 
63
93
  ### Major Changes
64
94
 
@@ -23,7 +23,9 @@ module Procrastinator
23
23
  # @!attribute [r] :log_shift_size
24
24
  # @return [Integer] Filesize before rotating to a new logfile (see Ruby Logger for details)
25
25
  class Config
26
- attr_reader :queues, :log_dir, :log_level, :log_shift_age, :log_shift_size, :container
26
+ attr_reader :queues, :log_dir, :log_level, :log_shift_age, :log_shift_size, :container, :priority
27
+
28
+ alias nice priority
27
29
 
28
30
  # Default directory to keep logs in.
29
31
  DEFAULT_LOG_DIRECTORY = Pathname.new('log').freeze
@@ -45,6 +47,9 @@ module Procrastinator
45
47
  msg].join("\t") << "\n"
46
48
  end
47
49
 
50
+ # Default priority 'nice' level.
51
+ DEFAULT_PRIORITY = 1
52
+
48
53
  def initialize
49
54
  @queues = []
50
55
  @container = nil
@@ -52,6 +57,7 @@ module Procrastinator
52
57
  @log_level = Logger::INFO
53
58
  @log_shift_age = DEFAULT_LOG_SHIFT_AGE
54
59
  @log_shift_size = DEFAULT_LOG_SHIFT_SIZE
60
+ @priority = DEFAULT_PRIORITY
55
61
 
56
62
  with_store(csv: TaskStore::SimpleCommaStore::DEFAULT_FILE) do
57
63
  if block_given?
@@ -109,7 +115,9 @@ module Procrastinator
109
115
 
110
116
  properties[:store] = interpret_store(properties[:store]) if properties.key? :store
111
117
 
112
- @queues << Queue.new(**{name: name, task_class: task_class, store: @default_store}.merge(properties))
118
+ args = {name: name, task_class: task_class, store: @default_store}.merge(properties)
119
+
120
+ @queues << Queue.new(**args)
113
121
  end
114
122
 
115
123
  # Sets details of logging behaviour
@@ -124,6 +132,19 @@ module Procrastinator
124
132
  @log_shift_age = shift_age
125
133
  @log_shift_size = shift_size
126
134
  end
135
+
136
+ # Sets the desired process 'nice' priority value adjustment. This value is added to whatever runtime priority
137
+ # Procrastinator starts with.
138
+ #
139
+ # Higher numbers are more nice to other processes (lower priority); think of it as the position in line.
140
+ # You can be more nice, but never less nice.
141
+ #
142
+ # @param new_priority [Number] the new priority.
143
+ def adjust_priority(new_priority)
144
+ @priority = new_priority
145
+ end
146
+
147
+ alias adjust_nice adjust_priority
127
148
  end
128
149
 
129
150
  include DSL
@@ -19,7 +19,7 @@ module Procrastinator
19
19
  alias task __getobj__
20
20
 
21
21
  def initialize(task, logger: Logger.new(StringIO.new))
22
- super task
22
+ super(task)
23
23
  @logger = logger || raise(ArgumentError, 'Logger cannot be nil')
24
24
  end
25
25
 
@@ -94,7 +94,9 @@ module Procrastinator
94
94
  raise AmbiguousTaskFilterError, "too many (#{ tasks.size }) tasks match #{ identifier }. Found: #{ tasks }"
95
95
  end
96
96
 
97
- TaskMetaData.new(**tasks.first.merge(queue: self))
97
+ args = tasks.first.merge(queue: self)
98
+
99
+ TaskMetaData.new(**args)
98
100
  end
99
101
 
100
102
  # Creates a task on the queue, saved using the Task Store strategy.
@@ -34,7 +34,7 @@ module Procrastinator
34
34
 
35
35
  # Alters an existing task to run at a new time, expire at a new time, or both.
36
36
  #
37
- # Call #to on the result and pass in the new :run_at and/or :expire_at.
37
+ # Call UpdateProxy#to on the result and pass in the new :run_at and/or :expire_at.
38
38
  #
39
39
  # Example:
40
40
  #
@@ -92,6 +92,11 @@ module Procrastinator
92
92
  @identifier = identifier.merge(queue: queue.name.to_sym)
93
93
  end
94
94
 
95
+ # Updates the task found by the identifier to run at and/or expire at the new provided time.
96
+ #
97
+ # @raise [NoSuchTaskError] when no task matches the identifier.
98
+ # @raise [AmbiguousTaskFilterError] when many tasks match the identifier, meaning you need to be more specific.
99
+ # @see Scheduler#reschedule
95
100
  def to(run_at: nil, expire_at: nil)
96
101
  task = @queue.fetch_task(@identifier)
97
102
 
@@ -227,12 +232,14 @@ module Procrastinator
227
232
  @streams = stream
228
233
  end
229
234
 
230
- (IO.methods << :path << :sync=).uniq.each do |method_name|
231
- define_method(method_name) do |*args|
232
- able_streams(method_name).collect do |stream|
233
- stream.send(method_name, *args)
234
- end.last # forces consistent return result type for callers (but may lose some info)
235
- end
235
+ def method_missing(method_name, *args)
236
+ able_streams(method_name).collect do |stream|
237
+ stream.send(method_name, *args)
238
+ end.last # forces consistent return result type for callers (but may lose some info)
239
+ end
240
+
241
+ def respond_to_missing?(method_name, include_private)
242
+ @streams.any? { |stream| stream.respond_to?(method_name, include_private) }
236
243
  end
237
244
 
238
245
  private
@@ -314,6 +321,7 @@ module Procrastinator
314
321
 
315
322
  manage_pid pid_path
316
323
  rename_process pid_path
324
+ update_priority
317
325
  rescue StandardError => e
318
326
  @logger&.fatal ([e.message] + e.backtrace).join("\n")
319
327
  raise e
@@ -368,6 +376,16 @@ module Procrastinator
368
376
  Process.setproctitle name
369
377
  end
370
378
 
379
+ def update_priority
380
+ new_priority = @config.priority
381
+ current_priority = Process.getpriority(Process::PRIO_PROCESS, 0)
382
+
383
+ raise ArgumentError, 'Process priority cannot be negative. Use system tools if you need a reduced nice value.' if new_priority.negative?
384
+
385
+ # second arg "integer" arg of zero means self
386
+ Process.setpriority(Process::PRIO_PROCESS, 0, current_priority + new_priority)
387
+ end
388
+
371
389
  include ThreadedWorking
372
390
  end
373
391
 
@@ -142,7 +142,7 @@ module Procrastinator
142
142
  class CSVFileTransaction < FileTransaction
143
143
  # (see FileTransaction#transact)
144
144
  def transact(writable: nil)
145
- super(writable: writable) do |file_str|
145
+ super do |file_str|
146
146
  yield(parse(file_str))
147
147
  end
148
148
  end
@@ -34,6 +34,50 @@ module Procrastinator
34
34
  end
35
35
  end
36
36
 
37
+ # Testing mock Task class that requires a data packet
38
+ class MockTaskWithData
39
+ attr_accessor :container, :logger, :scheduler, :data
40
+
41
+ # Records that the mock task was run.
42
+ def run
43
+ @run = true
44
+ end
45
+
46
+ # @return [Boolean] Whether the task was run
47
+ def run?
48
+ @run
49
+ end
50
+ end
51
+
52
+ # Testing mock Thread class
53
+ class MockThread
54
+ attr_reader :status
55
+
56
+ def initialize(name: nil, status: nil)
57
+ @name = name
58
+ @status = status
59
+ end
60
+
61
+ def join(_timeout)
62
+ end
63
+
64
+ def kill
65
+ end
66
+
67
+ def alive?
68
+ true
69
+ end
70
+
71
+ def thread_variable_get(var_name)
72
+ case var_name
73
+ when :name
74
+ @name
75
+ else
76
+ raise 'test error: undefined thread var'
77
+ end
78
+ end
79
+ end
80
+
37
81
  # Data-accepting MockTask
38
82
  #
39
83
  # @see MockTask
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Procrastinator
4
4
  # Version number of this release
5
- VERSION = '2.0.0'
5
+ VERSION = '2.1.0'
6
6
  end
@@ -23,5 +23,8 @@ Gem::Specification.new do |spec|
23
23
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
24
  spec.require_paths = ['lib']
25
25
 
26
- spec.required_ruby_version = '>= 3.0'
26
+ spec.required_ruby_version = '>= 3.3'
27
+
28
+ # TODO: remove when CSV persister is split to separate gem; used to be part of STDLIB but now is separate
29
+ spec.add_dependency 'csv', '>= 3.3'
27
30
  end
metadata CHANGED
@@ -1,15 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: procrastinator
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robin Miller
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2023-06-17 00:00:00.000000000 Z
12
- dependencies: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: csv
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '3.3'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '3.3'
13
26
  description: A flexible pure Ruby job queue. Tasks are reschedulable after failures.
14
27
  email:
15
28
  - robin@tenjin.ca
@@ -50,7 +63,6 @@ licenses:
50
63
  - MIT
51
64
  metadata:
52
65
  rubygems_mfa_required: 'true'
53
- post_install_message:
54
66
  rdoc_options: []
55
67
  require_paths:
56
68
  - lib
@@ -58,15 +70,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
58
70
  requirements:
59
71
  - - ">="
60
72
  - !ruby/object:Gem::Version
61
- version: '3.0'
73
+ version: '3.3'
62
74
  required_rubygems_version: !ruby/object:Gem::Requirement
63
75
  requirements:
64
76
  - - ">="
65
77
  - !ruby/object:Gem::Version
66
78
  version: '0'
67
79
  requirements: []
68
- rubygems_version: 3.3.26
69
- signing_key:
80
+ rubygems_version: 3.6.9
70
81
  specification_version: 4
71
82
  summary: For apps to put off work until later
72
83
  test_files: []