procrastinator 1.0.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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