procrastinator 1.0.0 → 1.1.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: e3d2c026b206d52c22ad9cb182fa7ea48ff2569eb99647a5cf75aa20e56bde5e
4
- data.tar.gz: 1a82220d0b7485bb3de7263fa6bc2265f1612601eab1769eb306c9a5930073ca
3
+ metadata.gz: 355845de0042676644aa4fd14ba5c37bc173b1f0cac9c73889dbb36aa8e5cd9c
4
+ data.tar.gz: f4faa2e3644557027c8fc08f88c973deb19cf16e43fb3312106d8d25a3c61054
5
5
  SHA512:
6
- metadata.gz: bc20963d1541d72024af63effe933823162012deea562f04bbe4303c6c380b26476d8305b6a94551ac50424101e0d7a072d705d01dcbc1ba1f231cd5c51f3184
7
- data.tar.gz: 670bc2fe498c40a7c32d2557956321237d9307941397a5bd1c1c7c1718b24e3ceaaa24ee2b8e5960b9967120467b46ad3c3df313ed00ab3a6e77830e626937fb
6
+ metadata.gz: 3f59116b21f6218d3ef3059e5f5532b1ef23a95e7a5f0b82a1d21a041ff84e64795e2fd7181bd277cba19e6c8fd01fa04146103ec2de9703169e9db7072a817f
7
+ data.tar.gz: 235b273d6f07e21f492e82083ea89b0f67eb4275233dddbb04d52e25a5a2f3b6a0674e5d0b8dda9b5c43359ea257b081c4d7bf1b7561b4c1a7242f24660b932c
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.1.0 (2022-10-16)
4
+
5
+ ### Major Changes
6
+
7
+ * none
8
+
9
+ ### Minor Changes
10
+
11
+ * Removed process name limit.
12
+
13
+ ### Bugfixes
14
+
15
+ * have_task matcher:
16
+ * Fixed have_task handling of nested matchers like be_within
17
+ * Improved have_task handling of string queue names vs symbols
18
+
19
+ ## 1.0.1 (2022-09-20)
20
+
21
+ ### Major Changes
22
+
23
+ * none
24
+
25
+ ### Minor Changes
26
+
27
+ * none
28
+
29
+ ### Bugfixes
30
+
31
+ * Fixed integration error in rescheduling tasks
32
+
3
33
  ## 1.0.0 (2022-09-18)
4
34
 
5
35
  ### Major Changes
@@ -32,7 +62,7 @@
32
62
  * `load_with` has been removed
33
63
  * Removed `task_attr` and `Procrastinator::Task` module. Tasks is now duck-type checked for accessors instead.
34
64
  * Added Rake tasks to manage process daemon
35
- * Times are passed to Task Store as a Ruby Time object instead of an epoch time integer
65
+ * Times are passed to Task Store as a Ruby Time object instead of an epoch time integer
36
66
  * `#delay` is now `#defer`
37
67
 
38
68
  ### Minor Changes
@@ -10,11 +10,11 @@ module Procrastinator
10
10
  # @!attribute [r] :task_class
11
11
  # @return [Class] Class that defines the work to be done for jobs in this queue.
12
12
  # @!attribute [r] :timeout
13
- # @return [Object] Duration (seconds) after which tasks in this queue should fail for taking too long.
13
+ # @return [Numeric] Duration (seconds) after which tasks in this queue should fail for taking too long.
14
14
  # @!attribute [r] :max_attempts
15
- # @return [Object] Maximum number of attempts for tasks in this queue.
15
+ # @return [Integer] Maximum number of attempts for tasks in this queue.
16
16
  # @!attribute [r] :update_period
17
- # @return [Pathname] Delay (seconds) between reloads of tasks from the task store.
17
+ # @return [Numeric] Delay (seconds) between reloads of tasks from the task store.
18
18
  class Queue
19
19
  extend Forwardable
20
20
 
@@ -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
@@ -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
@@ -3,9 +3,10 @@
3
3
  require 'stringio'
4
4
 
5
5
  module Procrastinator
6
- # A Scheduler object provides the API for client applications to manage delayed tasks.
6
+ # A Scheduler object provides the API for client applications to manage scheduled tasks.
7
7
  #
8
- # Use #delay to schedule new tasks, #reschedule to alter existing tasks, and #cancel to remove unwanted tasks.
8
+ # Use Scheduler#defer to schedule new tasks, Scheduler#reschedule to alter existing tasks, and Scheduler#cancel to
9
+ # remove unwanted tasks.
9
10
  #
10
11
  # @author Robin Miller
11
12
  class Scheduler
@@ -46,7 +47,7 @@ module Procrastinator
46
47
  #
47
48
  # @see TaskMetaData
48
49
  def reschedule(queue, identifier)
49
- UpdateProxy.new(@config, identifier: identifier.merge(queue: queue.to_s))
50
+ UpdateProxy.new(@config.queue(name: queue), identifier: identifier)
50
51
  end
51
52
 
52
53
  # Removes an existing task, as located by the given identifying information.
@@ -88,7 +89,7 @@ module Procrastinator
88
89
  class UpdateProxy
89
90
  def initialize(queue, identifier:)
90
91
  @queue = queue
91
- @identifier = identifier
92
+ @identifier = identifier.merge(queue: queue.name.to_sym)
92
93
  end
93
94
 
94
95
  def to(run_at: nil, expire_at: nil)
@@ -252,9 +253,6 @@ module Procrastinator
252
253
  # Default directory to store PID files in.
253
254
  DEFAULT_PID_DIR = Pathname.new('/tmp').freeze
254
255
 
255
- # Maximum process name size. 15 chars is linux limit
256
- MAX_PROC_LEN = 15
257
-
258
256
  # Consumes the current process and turns it into a background daemon and proceed as #threaded.
259
257
  # Additional logging is recorded in the directory specified by the Procrastinator.setup configuration.
260
258
  #
@@ -362,11 +360,6 @@ module Procrastinator
362
360
  def rename_process(pid_path)
363
361
  name = pid_path.basename(PID_EXT).to_s
364
362
 
365
- if name.size > MAX_PROC_LEN
366
- @logger.warn "Process name is longer than max length (#{ MAX_PROC_LEN }). Trimming to fit."
367
- name = name[0, MAX_PROC_LEN]
368
- end
369
-
370
363
  if system('pidof', name, out: File::NULL)
371
364
  @logger.warn "Another process is already named '#{ name }'. Consider the 'name:' keyword to distinguish."
372
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.0'
5
+ VERSION = '1.1.0'
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: procrastinator
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.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-19 00:00:00.000000000 Z
11
+ date: 2022-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -147,7 +147,6 @@ files:
147
147
  - ".rspec"
148
148
  - ".rubocop.yml"
149
149
  - ".ruby-version"
150
- - ".travis.yml"
151
150
  - CODE_OF_CONDUCT.md
152
151
  - Gemfile
153
152
  - LICENSE.txt
data/.travis.yml DELETED
@@ -1,4 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.2.1
4
- before_install: gem install bundler -v 1.11.2