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 +4 -4
- data/.rubocop.yml +2 -3
- data/.ruby-version +1 -1
- data/Gemfile +12 -0
- data/README.md +6 -1
- data/RELEASE_NOTES.md +30 -0
- data/lib/procrastinator/queue.rb +19 -3
- data/lib/procrastinator/queue_worker.rb +12 -8
- data/lib/procrastinator/rspec/matchers.rb +22 -15
- data/lib/procrastinator/scheduler.rb +0 -8
- data/lib/procrastinator/task.rb +3 -0
- data/lib/procrastinator/task_store/simple_comma_store.rb +2 -5
- data/lib/procrastinator/version.rb +1 -1
- data/procrastinator.gemspec +1 -11
- metadata +5 -131
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dc44a4968d52be8482c61e47a86cd1773cd160e827f4ac8a6e4ac5fd3bbe6b8f
|
4
|
+
data.tar.gz: 8e3494bfa1a6494f16736c2937a20385cd8231b7615194c8f3b52dbf1d16a4e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c0644921316f9db7ef0d1d3ee24fab20a5cf458077b16636e42d793387c9f9198e122ca959f113e777b82eb7d61983229f6a3afb09aab0931dce0fa507c6a13b
|
7
|
+
data.tar.gz: 4a2a86a807672c8b9335f1c835f90a80440045b787b75ff51d2e791e2f7075abb50be8e42362419742573ef1b160f262dae6eebb1d73b6377a635f5f805d85eb
|
data/.rubocop.yml
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
inherit_from:
|
1
|
+
inherit_from: ~/.config/rubocop/config.yml
|
2
2
|
|
3
3
|
AllCops:
|
4
4
|
Exclude:
|
5
5
|
- 'bin/*'
|
6
6
|
|
7
|
-
TargetRubyVersion: 2.
|
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.
|
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
|
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
|
data/lib/procrastinator/queue.rb
CHANGED
@@ -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
|
-
|
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 <
|
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(
|
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
|
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
|
-
|
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
|
-
|
80
|
-
|
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 <
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
data/lib/procrastinator/task.rb
CHANGED
@@ -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
|
data/procrastinator.gemspec
CHANGED
@@ -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.
|
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
|
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:
|
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.
|
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.
|
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
|