procrastinator 1.0.1 → 1.2.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: 51731c709ddfb60d6a9bd082e2fec2d2c0d71da125311dd54e77778c206f662f
4
- data.tar.gz: 95153dd8707910af0a3ee9c54cc48e74ad25e3a2eca8f7f69e7b042a838f8ba2
3
+ metadata.gz: dc44a4968d52be8482c61e47a86cd1773cd160e827f4ac8a6e4ac5fd3bbe6b8f
4
+ data.tar.gz: 8e3494bfa1a6494f16736c2937a20385cd8231b7615194c8f3b52dbf1d16a4e5
5
5
  SHA512:
6
- metadata.gz: 076b5c80dcebb746405dc9a27992bc51665f90d5aeb40a67cd982e0b1eba66f8c4b85e65ba213d7e36ed27a7c736574e3cb396d5619d73a51e6877bc6df7dd99
7
- data.tar.gz: 02574c2182eb4f7f6c861a413b6c3764bdb772c3201740b8460381543e16d78779963b9dc64a9e11d8fc12f4bf100196de8e730cab697cb9b495fa3c9e0bd5ff
6
+ metadata.gz: c0644921316f9db7ef0d1d3ee24fab20a5cf458077b16636e42d793387c9f9198e122ca959f113e777b82eb7d61983229f6a3afb09aab0931dce0fa507c6a13b
7
+ data.tar.gz: 4a2a86a807672c8b9335f1c835f90a80440045b787b75ff51d2e791e2f7075abb50be8e42362419742573ef1b160f262dae6eebb1d73b6377a635f5f805d85eb
data/.rubocop.yml CHANGED
@@ -1,10 +1,10 @@
1
- inherit_from: ../.rubocop.yml
1
+ inherit_from: ~/.config/rubocop/config.yml
2
2
 
3
3
  AllCops:
4
4
  Exclude:
5
5
  - 'bin/*'
6
6
 
7
- TargetRubyVersion: 2.4
7
+ TargetRubyVersion: 2.7
8
8
 
9
9
  Layout/LineLength:
10
10
  Exclude:
@@ -23,4 +23,3 @@ Metrics/BlockLength:
23
23
  Metrics/ModuleLength:
24
24
  Exclude:
25
25
  - 'spec/**/*.rb'
26
-
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-2.4.2
1
+ ruby-2.7.8
data/Gemfile CHANGED
@@ -2,5 +2,17 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
4
 
5
+ group :development do
6
+ gem 'bundler', '~> 2.3'
7
+ gem 'fakefs', '~> 1.8'
8
+ gem 'rake', '~> 13.0'
9
+ gem 'rspec', '~> 3.9'
10
+ gem 'rubocop', '~> 1.12'
11
+ gem 'rubocop-performance', '~> 1.10'
12
+ gem 'simplecov', '~> 0.18.0'
13
+ gem 'timecop', '~> 0.9'
14
+ gem 'yard', '~> 0.9'
15
+ end
16
+
5
17
  # Specify your gem's dependencies in procrastinator.gemspec
6
18
  gemspec
data/README.md CHANGED
@@ -514,7 +514,7 @@ Procrastinator::Rake::DaemonTasks.define do
514
514
  end
515
515
  ```
516
516
 
517
- You can name the daemon process by specifying the pid_path with a specific .pid file. If does not end with '.pid' it is
517
+ You can title the daemon process by specifying the pid_path with a specific .pid file. If does not end with '.pid' it is
518
518
  assumed to be a directory name, and `procrastinator.pid` is appended.
519
519
 
520
520
  ```ruby
@@ -531,6 +531,11 @@ Procrastinator::Rake::DaemonTasks.define(pid_path: 'pids') do
531
531
  end
532
532
  ```
533
533
 
534
+ > **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.
536
+ >
537
+ > Procrastinator uses Ruby's `Process.setproctitle`, which only affects the title.
538
+
534
539
  Either run the generated Rake tasks in a terminal or with your daemon monitoring tool of choice (eg. Monit, systemd)
535
540
 
536
541
  ```bash
data/RELEASE_NOTES.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Release Notes
2
2
 
3
+ ## 1.2.0 ( )
4
+
5
+ ### Major Changes
6
+
7
+ * none
8
+
9
+ ### Minor Changes
10
+
11
+ * Updated minimum ruby to 2.7
12
+
13
+ ### Bugfixes
14
+
15
+ * When logging is disabled, it points to `File::NULL` instead of a dead-end StringIO
16
+
17
+ ## 1.1.0 (2022-10-16)
18
+
19
+ ### Major Changes
20
+
21
+ * none
22
+
23
+ ### Minor Changes
24
+
25
+ * Removed process name limit.
26
+
27
+ ### Bugfixes
28
+
29
+ * have_task matcher:
30
+ * Fixed have_task handling of nested matchers like be_within
31
+ * Improved have_task handling of string queue names vs symbols
32
+
3
33
  ## 1.0.1 (2022-09-20)
4
34
 
5
35
  ### Major Changes
@@ -81,13 +81,18 @@ module Procrastinator
81
81
  # Fetch a task matching the given identifier
82
82
  #
83
83
  # @param identifier [Hash] attributes to match
84
+ #
85
+ # @raise [NoSuchTaskError] when no task matches the identifier.
86
+ # @raise [AmbiguousTaskFilterError] when many tasks match the identifier, meaning you need to be more specific.
84
87
  def fetch_task(identifier)
85
88
  identifier[:data] = JSON.dump(identifier[:data]) if identifier[:data]
86
89
 
87
90
  tasks = read(**identifier)
88
91
 
89
- raise "no task found matching #{ identifier }" if tasks.nil? || tasks.empty?
90
- raise "too many (#{ tasks.size }) tasks match #{ identifier }. Found: #{ tasks }" if tasks.size > 1
92
+ raise NoSuchTaskError, "no task found matching #{ identifier }" if tasks.nil? || tasks.empty?
93
+ if tasks.size > 1
94
+ raise AmbiguousTaskFilterError, "too many (#{ tasks.size }) tasks match #{ identifier }. Found: #{ tasks }"
95
+ end
91
96
 
92
97
  TaskMetaData.new(tasks.first.merge(queue: self))
93
98
  end
@@ -97,6 +102,9 @@ module Procrastinator
97
102
  # @param run_at [Time] Earliest time to attempt running the task
98
103
  # @param expire_at [Time, nil] Time after which the task will not be attempted
99
104
  # @param data [Hash, String, Numeric, nil] The data to save
105
+ #
106
+ # @raise [ArgumentError] when the keyword `:data` is needed by the task handler, but is missing
107
+ # @raise [MalformedTaskError] when the keyword `:data` is provided but not expected by the task handler.
100
108
  def create(run_at:, expire_at:, data:)
101
109
  if data.nil? && expects_data?
102
110
  raise ArgumentError, "task #{ @task_class } expects to receive :data. Provide :data to #delay."
@@ -220,8 +228,16 @@ module Procrastinator
220
228
  include QueueValidation
221
229
  end
222
230
 
231
+ # Raised when a task matching certain criteria is requested but nothing matching is found
232
+ class NoSuchTaskError < RuntimeError
233
+ end
234
+
235
+ # Raised when a task matching certain criteria is requested but more than one option is found
236
+ class AmbiguousTaskFilterError < RuntimeError
237
+ end
238
+
223
239
  # Raised when a Task Handler does not conform to the expected API
224
- class MalformedTaskError < StandardError
240
+ class MalformedTaskError < RuntimeError
225
241
  end
226
242
 
227
243
  # Raised when a Task Store strategy does not conform to the expected API
@@ -13,6 +13,8 @@ module Procrastinator
13
13
  # expected methods for all persistence strategies
14
14
  PERSISTER_METHODS = [:read, :update, :delete].freeze
15
15
 
16
+ NULL_FILE = File.open(File::NULL, File::WRONLY)
17
+
16
18
  def initialize(queue:, config:)
17
19
  raise ArgumentError, ':queue cannot be nil' if queue.nil?
18
20
  raise ArgumentError, ':config cannot be nil' if config.nil?
@@ -26,13 +28,13 @@ module Procrastinator
26
28
  end
27
29
 
28
30
  @scheduler = Scheduler.new(config)
29
- @logger = Logger.new(StringIO.new)
31
+ @logger = Logger.new(File::NULL)
30
32
  end
31
33
 
32
34
  # Works on jobs forever
33
35
  def work!
34
36
  @logger = open_log!("#{ name }-queue-worker", @config)
35
- @logger.info("Started worker thread to consume queue: #{ name }")
37
+ @logger.info "Started worker thread to consume queue: #{ name }"
36
38
 
37
39
  loop do
38
40
  sleep(@queue.update_period)
@@ -72,12 +74,14 @@ module Procrastinator
72
74
 
73
75
  # Starts a log file and returns the created Logger
74
76
  def open_log!(name, config)
75
- return @logger unless config.log_level
76
-
77
- log_path = config.log_dir / "#{ name }.log"
77
+ if config.log_level
78
+ log_path = config.log_dir / "#{ name }.log"
78
79
 
79
- config.log_dir.mkpath
80
- FileUtils.touch(log_path)
80
+ config.log_dir.mkpath
81
+ FileUtils.touch(log_path)
82
+ else
83
+ log_path = NULL_FILE
84
+ end
81
85
 
82
86
  Logger.new(log_path.to_path,
83
87
  config.log_shift_age, config.log_shift_size,
@@ -90,6 +94,6 @@ module Procrastinator
90
94
  # Raised when a Task Storage strategy is missing a required part of the API.
91
95
  #
92
96
  # @see TaskStore
93
- class MalformedTaskPersisterError < StandardError
97
+ class MalformedTaskPersisterError < RuntimeError
94
98
  end
95
99
  end
@@ -5,23 +5,30 @@ require 'rspec/expectations'
5
5
  # Determines if the given task store has a task that matches the expectation hash
6
6
  RSpec::Matchers.define :have_task do |expected_task|
7
7
  match do |task_store|
8
- task_store.read.any? do |task|
9
- task_hash = task.to_h
10
- task_hash[:data] = JSON.parse(task_hash[:data], symbolize_names: true) unless task_hash[:data].empty?
11
-
12
- expected_task.all? do |field, expected_value|
13
- expected_value = case field
14
- when :queue
15
- expected_value.to_sym
16
- when :run_at, :initial_run_at, :expire_at, :last_fail_at
17
- Time.at(expected_value.to_i)
18
- else
19
- expected_value
20
- end
21
-
22
- values_match? expected_value, task_hash[field]
8
+ expected_task[:queue] = expected_task[:queue].to_sym if expected_task[:queue]
9
+
10
+ Procrastinator::Task::TIME_FIELDS.each do |time_field|
11
+ if expected_task[time_field]&.respond_to?(:to_i)
12
+ expected_task[time_field] = Time.at(expected_task[time_field].to_i)
13
+ end
14
+ end
15
+
16
+ expected = a_hash_including(expected_task)
17
+
18
+ actual_tasks = task_store.read.collect do |task|
19
+ task_hash = task.to_h
20
+ unless task_hash[:data].nil? || task_hash[:data].empty?
21
+ task_hash[:data] = JSON.parse(task_hash[:data], symbolize_names: true)
22
+ end
23
+ task_hash[:queue] = task_hash[:queue].to_sym if task_hash[:queue]
24
+ Procrastinator::Task::TIME_FIELDS.each do |time_field|
25
+ task_hash[time_field] = Time.at(task_hash[time_field].to_i) if task_hash[time_field]&.respond_to?(:to_i)
23
26
  end
27
+
28
+ task_hash
24
29
  end
30
+
31
+ values_match? a_collection_including(expected), actual_tasks
25
32
  end
26
33
 
27
34
  description do
@@ -253,9 +253,6 @@ module Procrastinator
253
253
  # Default directory to store PID files in.
254
254
  DEFAULT_PID_DIR = Pathname.new('/tmp').freeze
255
255
 
256
- # Maximum process name size. 15 chars is linux limit
257
- MAX_PROC_LEN = 15
258
-
259
256
  # Consumes the current process and turns it into a background daemon and proceed as #threaded.
260
257
  # Additional logging is recorded in the directory specified by the Procrastinator.setup configuration.
261
258
  #
@@ -363,11 +360,6 @@ module Procrastinator
363
360
  def rename_process(pid_path)
364
361
  name = pid_path.basename(PID_EXT).to_s
365
362
 
366
- if name.size > MAX_PROC_LEN
367
- @logger.warn "Process name is longer than max length (#{ MAX_PROC_LEN }). Trimming to fit."
368
- name = name[0, MAX_PROC_LEN]
369
- end
370
-
371
363
  if system('pidof', name, out: File::NULL)
372
364
  @logger.warn "Another process is already named '#{ name }'. Consider the 'name:' keyword to distinguish."
373
365
  end
@@ -10,6 +10,9 @@ module Procrastinator
10
10
  class Task
11
11
  extend Forwardable
12
12
 
13
+ # Fields that store time information
14
+ TIME_FIELDS = [:run_at, :initial_run_at, :expire_at, :last_fail_at].freeze
15
+
13
16
  def_delegators :@metadata,
14
17
  :id, :run_at, :initial_run_at, :expire_at,
15
18
  :attempts, :last_fail_at, :last_error,
@@ -19,9 +19,6 @@ module Procrastinator
19
19
  HEADERS = [:id, :queue, :run_at, :initial_run_at, :expire_at,
20
20
  :attempts, :last_fail_at, :last_error, :data].freeze
21
21
 
22
- # Columns that store time information
23
- TIME_FIELDS = [:run_at, :initial_run_at, :expire_at, :last_fail_at].freeze
24
-
25
22
  # CSV file extension
26
23
  EXT = 'csv'
27
24
 
@@ -34,7 +31,7 @@ module Procrastinator
34
31
  READ_CONVERTER = proc do |value, field_info|
35
32
  if field_info.header == :data
36
33
  value
37
- elsif TIME_FIELDS.include? field_info.header
34
+ elsif Task::TIME_FIELDS.include? field_info.header
38
35
  value.empty? ? nil : Time.parse(value)
39
36
  else
40
37
  begin
@@ -130,7 +127,7 @@ module Procrastinator
130
127
  # @return [String] Generated CSV string
131
128
  def generate(data)
132
129
  lines = data.collect do |d|
133
- TIME_FIELDS.each do |field|
130
+ Task::TIME_FIELDS.each do |field|
134
131
  d[field] = d[field]&.iso8601
135
132
  end
136
133
  CSV.generate_line(d, headers: HEADERS, force_quotes: true).strip
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Procrastinator
4
4
  # Version number of this release
5
- VERSION = '1.0.1'
5
+ VERSION = '1.2.0'
6
6
  end
@@ -23,15 +23,5 @@ 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 = '>= 2.4'
27
-
28
- spec.add_development_dependency 'bundler', '~> 2.3'
29
- spec.add_development_dependency 'fakefs', '~> 1.8'
30
- spec.add_development_dependency 'rake', '~> 13.0'
31
- spec.add_development_dependency 'rspec', '~> 3.9'
32
- spec.add_development_dependency 'rubocop', '~> 1.12'
33
- spec.add_development_dependency 'rubocop-performance', '~> 1.10'
34
- spec.add_development_dependency 'simplecov', '~> 0.18.0'
35
- spec.add_development_dependency 'timecop', '~> 0.9'
36
- spec.add_development_dependency 'yard', '~> 0.9'
26
+ spec.required_ruby_version = '>= 2.7'
37
27
  end
metadata CHANGED
@@ -1,141 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: procrastinator
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robin Miller
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-09-28 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: bundler
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '2.3'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '2.3'
27
- - !ruby/object:Gem::Dependency
28
- name: fakefs
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '1.8'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '1.8'
41
- - !ruby/object:Gem::Dependency
42
- name: rake
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '13.0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '13.0'
55
- - !ruby/object:Gem::Dependency
56
- name: rspec
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '3.9'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '3.9'
69
- - !ruby/object:Gem::Dependency
70
- name: rubocop
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '1.12'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '1.12'
83
- - !ruby/object:Gem::Dependency
84
- name: rubocop-performance
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: '1.10'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "~>"
95
- - !ruby/object:Gem::Version
96
- version: '1.10'
97
- - !ruby/object:Gem::Dependency
98
- name: simplecov
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - "~>"
102
- - !ruby/object:Gem::Version
103
- version: 0.18.0
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - "~>"
109
- - !ruby/object:Gem::Version
110
- version: 0.18.0
111
- - !ruby/object:Gem::Dependency
112
- name: timecop
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - "~>"
116
- - !ruby/object:Gem::Version
117
- version: '0.9'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - "~>"
123
- - !ruby/object:Gem::Version
124
- version: '0.9'
125
- - !ruby/object:Gem::Dependency
126
- name: yard
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - "~>"
130
- - !ruby/object:Gem::Version
131
- version: '0.9'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - "~>"
137
- - !ruby/object:Gem::Version
138
- version: '0.9'
11
+ date: 2023-06-17 00:00:00.000000000 Z
12
+ dependencies: []
139
13
  description: A flexible pure Ruby job queue. Tasks are reschedulable after failures.
140
14
  email:
141
15
  - robin@tenjin.ca
@@ -184,14 +58,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
184
58
  requirements:
185
59
  - - ">="
186
60
  - !ruby/object:Gem::Version
187
- version: '2.4'
61
+ version: '2.7'
188
62
  required_rubygems_version: !ruby/object:Gem::Requirement
189
63
  requirements:
190
64
  - - ">="
191
65
  - !ruby/object:Gem::Version
192
66
  version: '0'
193
67
  requirements: []
194
- rubygems_version: 3.1.2
68
+ rubygems_version: 3.4.10
195
69
  signing_key:
196
70
  specification_version: 4
197
71
  summary: For apps to put off work until later