procrastinator 0.6.1 → 1.0.0.pre.rc2
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 +5 -5
- data/.gitignore +6 -1
- data/.rubocop.yml +26 -0
- data/.ruby-version +1 -1
- data/Gemfile +2 -0
- data/README.md +492 -144
- data/RELEASE_NOTES.md +44 -0
- data/Rakefile +5 -3
- data/lib/procrastinator/config.rb +149 -0
- data/lib/procrastinator/logged_task.rb +50 -0
- data/lib/procrastinator/queue.rb +206 -0
- data/lib/procrastinator/queue_worker.rb +66 -91
- data/lib/procrastinator/rake/daemon_tasks.rb +54 -0
- data/lib/procrastinator/rake/tasks.rb +3 -0
- data/lib/procrastinator/scheduler.rb +393 -0
- data/lib/procrastinator/task.rb +64 -0
- data/lib/procrastinator/task_meta_data.rb +172 -0
- data/lib/procrastinator/task_store/file_transaction.rb +76 -0
- data/lib/procrastinator/task_store/simple_comma_store.rb +161 -0
- data/lib/procrastinator/test/mocks.rb +35 -0
- data/lib/procrastinator/version.rb +3 -1
- data/lib/procrastinator.rb +29 -23
- data/procrastinator.gemspec +17 -11
- metadata +66 -28
- data/lib/procrastinator/environment.rb +0 -148
- data/lib/procrastinator/task_worker.rb +0 -120
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
module Procrastinator
|
6
|
+
module TaskStore
|
7
|
+
# The general idea is that there may be two threads that need to do these actions on the same file:
|
8
|
+
# thread A: read
|
9
|
+
# thread B: read
|
10
|
+
# thread A/B: write
|
11
|
+
# thread A/B: write
|
12
|
+
#
|
13
|
+
# When this sequence happens, the second file write is based on old information and loses the info from
|
14
|
+
# the prior write. Using a global mutex per file path prevents this case.
|
15
|
+
#
|
16
|
+
# This situation can also occur with multi processing, so file locking is also used for solitary access.
|
17
|
+
# File locking is only advisory in some systems, though, so it may only work against other applications
|
18
|
+
# that request a lock.
|
19
|
+
#
|
20
|
+
# @author Robin Miller
|
21
|
+
class FileTransaction
|
22
|
+
# Holds the mutual exclusion locks for file paths by name
|
23
|
+
@file_mutex = {}
|
24
|
+
|
25
|
+
class << self
|
26
|
+
attr_reader :file_mutex
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(path)
|
30
|
+
@path = ensure_path(path)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Alias for transact(writable: false)
|
34
|
+
def read(&block)
|
35
|
+
transact(writable: false, &block)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Alias for transact(writable: true)
|
39
|
+
def write(&block)
|
40
|
+
transact(writable: true, &block)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Completes the given block as an atomic transaction locked using a global mutex table.
|
44
|
+
# The block is provided the current file contents.
|
45
|
+
# The block's result is written to the file.
|
46
|
+
def transact(writable: false)
|
47
|
+
semaphore = FileTransaction.file_mutex[@path.to_s] ||= Mutex.new
|
48
|
+
|
49
|
+
semaphore.synchronize do
|
50
|
+
@path.open(writable ? 'r+' : 'r') do |file|
|
51
|
+
file.flock(File::LOCK_EX)
|
52
|
+
|
53
|
+
yield_result = yield(file.read)
|
54
|
+
if writable
|
55
|
+
file.rewind
|
56
|
+
file.write yield_result
|
57
|
+
file.truncate(file.pos)
|
58
|
+
end
|
59
|
+
yield_result
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def ensure_path(path)
|
67
|
+
path = Pathname.new path
|
68
|
+
unless path.exist?
|
69
|
+
path.dirname.mkpath
|
70
|
+
FileUtils.touch path
|
71
|
+
end
|
72
|
+
path
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'csv'
|
4
|
+
require 'pathname'
|
5
|
+
|
6
|
+
module Procrastinator
|
7
|
+
module TaskStore
|
8
|
+
# Simple Task I/O adapter that writes task information (ie. TaskMetaData attributes) to a CSV file.
|
9
|
+
#
|
10
|
+
# SimpleCommaStore is not designed for efficiency or large loads (10,000+ tasks).
|
11
|
+
#
|
12
|
+
# For critical production environments, it is strongly recommended to use a more robust storage mechanism like a
|
13
|
+
# proper database.
|
14
|
+
#
|
15
|
+
# @author Robin Miller
|
16
|
+
class SimpleCommaStore
|
17
|
+
# ordered
|
18
|
+
HEADERS = [:id, :queue, :run_at, :initial_run_at, :expire_at,
|
19
|
+
:attempts, :last_fail_at, :last_error, :data].freeze
|
20
|
+
|
21
|
+
EXT = 'csv'
|
22
|
+
DEFAULT_FILE = Pathname.new("procrastinator-tasks.#{ EXT }").freeze
|
23
|
+
|
24
|
+
TIME_FIELDS = [:run_at, :initial_run_at, :expire_at, :last_fail_at].freeze
|
25
|
+
|
26
|
+
READ_CONVERTER = proc do |value, field_info|
|
27
|
+
if field_info.header == :data
|
28
|
+
value
|
29
|
+
elsif TIME_FIELDS.include? field_info.header
|
30
|
+
value.empty? ? nil : Time.parse(value)
|
31
|
+
else
|
32
|
+
begin
|
33
|
+
Integer(value)
|
34
|
+
rescue ArgumentError
|
35
|
+
value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
attr_reader :path
|
41
|
+
|
42
|
+
def initialize(file_path = DEFAULT_FILE)
|
43
|
+
@path = Pathname.new(file_path)
|
44
|
+
|
45
|
+
if @path.directory? || @path.to_s.end_with?('/')
|
46
|
+
@path /= DEFAULT_FILE
|
47
|
+
elsif @path.extname.empty?
|
48
|
+
@path = @path.dirname / "#{ @path.basename }.csv"
|
49
|
+
end
|
50
|
+
|
51
|
+
@path = @path.expand_path
|
52
|
+
|
53
|
+
freeze
|
54
|
+
end
|
55
|
+
|
56
|
+
def read(filter = {})
|
57
|
+
FileTransaction.new(@path).read do |existing_data|
|
58
|
+
parse(existing_data).select do |row|
|
59
|
+
filter.keys.all? do |key|
|
60
|
+
row[key] == filter[key]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Saves a task to the CSV file.
|
67
|
+
#
|
68
|
+
# @param queue [String] queue name
|
69
|
+
# @param run_at [Time, nil] time to run the task at
|
70
|
+
# @param initial_run_at [Time, nil] first time to run the task at. Defaults to run_at.
|
71
|
+
# @param expire_at [Time, nil] time to expire the task
|
72
|
+
def create(queue:, run_at:, expire_at:, data: '', initial_run_at: nil)
|
73
|
+
FileTransaction.new(@path).write do |existing_data|
|
74
|
+
tasks = parse(existing_data)
|
75
|
+
max_id = tasks.collect { |task| task[:id] }.max || 0
|
76
|
+
|
77
|
+
new_data = {
|
78
|
+
id: max_id + 1,
|
79
|
+
queue: queue,
|
80
|
+
run_at: run_at,
|
81
|
+
initial_run_at: initial_run_at || run_at,
|
82
|
+
expire_at: expire_at,
|
83
|
+
attempts: 0,
|
84
|
+
data: data
|
85
|
+
}
|
86
|
+
|
87
|
+
generate(tasks + [new_data])
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def update(id, data)
|
92
|
+
FileTransaction.new(@path).write do |existing_data|
|
93
|
+
tasks = parse(existing_data)
|
94
|
+
task_data = tasks.find do |task|
|
95
|
+
task[:id] == id
|
96
|
+
end
|
97
|
+
|
98
|
+
task_data&.merge!(data)
|
99
|
+
|
100
|
+
generate(tasks)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def delete(id)
|
105
|
+
FileTransaction.new(@path).write do |file_content|
|
106
|
+
existing_data = parse(file_content)
|
107
|
+
generate(existing_data.reject { |task| task[:id] == id })
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def generate(data)
|
112
|
+
lines = data.collect do |d|
|
113
|
+
TIME_FIELDS.each do |field|
|
114
|
+
d[field] = d[field]&.iso8601
|
115
|
+
end
|
116
|
+
CSV.generate_line(d, headers: HEADERS, force_quotes: true).strip
|
117
|
+
end
|
118
|
+
|
119
|
+
lines.unshift(HEADERS.join(','))
|
120
|
+
|
121
|
+
lines.join("\n") << "\n"
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def parse(csv_string)
|
127
|
+
data = CSV.parse(csv_string,
|
128
|
+
headers: true,
|
129
|
+
header_converters: :symbol,
|
130
|
+
skip_blanks: true,
|
131
|
+
converters: READ_CONVERTER,
|
132
|
+
force_quotes: true).to_a
|
133
|
+
|
134
|
+
headers = data.shift || HEADERS
|
135
|
+
|
136
|
+
data = data.collect do |d|
|
137
|
+
headers.zip(d).to_h
|
138
|
+
end
|
139
|
+
|
140
|
+
correct_types(data)
|
141
|
+
end
|
142
|
+
|
143
|
+
def correct_types(data)
|
144
|
+
non_empty_keys = [:run_at, :expire_at, :attempts, :last_fail_at]
|
145
|
+
|
146
|
+
data.collect do |hash|
|
147
|
+
non_empty_keys.each do |key|
|
148
|
+
hash.delete(key) if hash[key].is_a?(String) && hash[key].empty?
|
149
|
+
end
|
150
|
+
|
151
|
+
hash[:attempts] ||= 0
|
152
|
+
|
153
|
+
# hash[:data] = (hash[:data] || '').gsub('""', '"')
|
154
|
+
hash[:queue] = hash[:queue].to_sym
|
155
|
+
|
156
|
+
hash
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Procrastinator
|
4
|
+
module Test
|
5
|
+
# Testing mock Task class
|
6
|
+
#
|
7
|
+
# You can use this like:
|
8
|
+
#
|
9
|
+
# require 'procrastinator/rspec/mocks'
|
10
|
+
# # ...
|
11
|
+
# Procrastinator.config do |c|
|
12
|
+
# c.define_queue :test_queue, Procrastinator::RSpec::MockTask
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# @see MockDataTask for data-accepting tasks
|
16
|
+
class MockTask
|
17
|
+
attr_accessor :container, :logger, :scheduler
|
18
|
+
|
19
|
+
def run
|
20
|
+
@run = true
|
21
|
+
end
|
22
|
+
|
23
|
+
def run?
|
24
|
+
@run
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Data-accepting MockTask
|
29
|
+
#
|
30
|
+
# @see MockTask
|
31
|
+
class MockDataTask < MockTask
|
32
|
+
attr_accessor :data
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/procrastinator.rb
CHANGED
@@ -1,32 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'procrastinator/version'
|
4
|
+
require 'procrastinator/task_meta_data'
|
5
|
+
require 'procrastinator/logged_task'
|
6
|
+
require 'procrastinator/queue'
|
2
7
|
require 'procrastinator/queue_worker'
|
3
|
-
require 'procrastinator/
|
4
|
-
require 'procrastinator/
|
5
|
-
require '
|
6
|
-
|
8
|
+
require 'procrastinator/config'
|
9
|
+
require 'procrastinator/task'
|
10
|
+
require 'procrastinator/scheduler'
|
11
|
+
require 'procrastinator/task_store/file_transaction'
|
12
|
+
require 'procrastinator/task_store/simple_comma_store'
|
7
13
|
|
14
|
+
require 'logger'
|
15
|
+
require 'pathname'
|
16
|
+
|
17
|
+
# Top-level module for the Procrastinator Gem.
|
18
|
+
#
|
19
|
+
# Call Procrastinator.setup with a block to configure task queues.
|
20
|
+
#
|
21
|
+
# See README for details.
|
22
|
+
#
|
23
|
+
# @author Robin Miller
|
24
|
+
#
|
25
|
+
# @see https://github.com/TenjinInc/procrastinator
|
8
26
|
module Procrastinator
|
9
|
-
|
10
|
-
|
27
|
+
# Creates a configuration object and passes it into the given block.
|
28
|
+
#
|
29
|
+
# @yield the created configuration object
|
30
|
+
# @return [Scheduler] a scheduler object that can be used to interact with the queues
|
11
31
|
def self.setup(&block)
|
12
|
-
raise ArgumentError
|
13
|
-
|
14
|
-
env = Environment.new(test_mode: @@test_mode)
|
32
|
+
raise ArgumentError, 'Procrastinator.setup must be given a block' unless block
|
15
33
|
|
16
|
-
|
17
|
-
|
18
|
-
raise RuntimeError.new('setup block must call #persister_factory on the environment') if env.persister.nil?
|
19
|
-
raise RuntimeError.new('setup block must call #define_queue on the environment') if env.queue_definitions.empty?
|
20
|
-
env.spawn_workers
|
21
|
-
|
22
|
-
env
|
23
|
-
end
|
24
|
-
|
25
|
-
def self.test_mode=(value)
|
26
|
-
@@test_mode = value
|
27
|
-
end
|
34
|
+
config = Config.new(&block)
|
28
35
|
|
29
|
-
|
30
|
-
@@test_mode
|
36
|
+
Scheduler.new(config)
|
31
37
|
end
|
32
38
|
end
|
data/procrastinator.gemspec
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
5
|
require 'procrastinator/version'
|
5
6
|
|
@@ -9,22 +10,27 @@ Gem::Specification.new do |spec|
|
|
9
10
|
spec.authors = ['Robin Miller']
|
10
11
|
spec.email = ['robin@tenjin.ca']
|
11
12
|
|
12
|
-
spec.summary =
|
13
|
-
spec.description =
|
13
|
+
spec.summary = 'For apps to put off work until later'
|
14
|
+
spec.description = 'A flexible pure Ruby job queue. Tasks are reschedulable after failures.'
|
14
15
|
spec.homepage = 'https://github.com/TenjinInc/procrastinator'
|
15
16
|
spec.license = 'MIT'
|
17
|
+
spec.metadata = {
|
18
|
+
'rubygems_mfa_required' => 'true'
|
19
|
+
}
|
16
20
|
|
17
21
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
22
|
spec.bindir = 'exe'
|
19
23
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
24
|
spec.require_paths = ['lib']
|
21
25
|
|
22
|
-
spec.required_ruby_version = '
|
26
|
+
spec.required_ruby_version = '>= 2.4'
|
23
27
|
|
24
|
-
spec.add_development_dependency 'bundler', '~>
|
25
|
-
spec.add_development_dependency '
|
26
|
-
spec.add_development_dependency '
|
27
|
-
spec.add_development_dependency '
|
28
|
-
spec.add_development_dependency '
|
29
|
-
spec.add_development_dependency '
|
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'
|
30
36
|
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: 0.
|
4
|
+
version: 1.0.0.pre.rc2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Robin Miller
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-09-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -16,86 +16,113 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '2.3'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
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'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: rake
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
30
44
|
requirements:
|
31
45
|
- - "~>"
|
32
46
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
47
|
+
version: '13.0'
|
34
48
|
type: :development
|
35
49
|
prerelease: false
|
36
50
|
version_requirements: !ruby/object:Gem::Requirement
|
37
51
|
requirements:
|
38
52
|
- - "~>"
|
39
53
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
54
|
+
version: '13.0'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: rspec
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
44
58
|
requirements:
|
45
59
|
- - "~>"
|
46
60
|
- !ruby/object:Gem::Version
|
47
|
-
version: '3.
|
61
|
+
version: '3.9'
|
48
62
|
type: :development
|
49
63
|
prerelease: false
|
50
64
|
version_requirements: !ruby/object:Gem::Requirement
|
51
65
|
requirements:
|
52
66
|
- - "~>"
|
53
67
|
- !ruby/object:Gem::Version
|
54
|
-
version: '3.
|
68
|
+
version: '3.9'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
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
|
57
85
|
requirement: !ruby/object:Gem::Requirement
|
58
86
|
requirements:
|
59
87
|
- - "~>"
|
60
88
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
89
|
+
version: '1.10'
|
62
90
|
type: :development
|
63
91
|
prerelease: false
|
64
92
|
version_requirements: !ruby/object:Gem::Requirement
|
65
93
|
requirements:
|
66
94
|
- - "~>"
|
67
95
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
96
|
+
version: '1.10'
|
69
97
|
- !ruby/object:Gem::Dependency
|
70
98
|
name: simplecov
|
71
99
|
requirement: !ruby/object:Gem::Requirement
|
72
100
|
requirements:
|
73
101
|
- - "~>"
|
74
102
|
- !ruby/object:Gem::Version
|
75
|
-
version:
|
103
|
+
version: 0.18.0
|
76
104
|
type: :development
|
77
105
|
prerelease: false
|
78
106
|
version_requirements: !ruby/object:Gem::Requirement
|
79
107
|
requirements:
|
80
108
|
- - "~>"
|
81
109
|
- !ruby/object:Gem::Version
|
82
|
-
version:
|
110
|
+
version: 0.18.0
|
83
111
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
112
|
+
name: timecop
|
85
113
|
requirement: !ruby/object:Gem::Requirement
|
86
114
|
requirements:
|
87
115
|
- - "~>"
|
88
116
|
- !ruby/object:Gem::Version
|
89
|
-
version: '0.
|
117
|
+
version: '0.9'
|
90
118
|
type: :development
|
91
119
|
prerelease: false
|
92
120
|
version_requirements: !ruby/object:Gem::Requirement
|
93
121
|
requirements:
|
94
122
|
- - "~>"
|
95
123
|
- !ruby/object:Gem::Version
|
96
|
-
version: '0.
|
97
|
-
description: A
|
98
|
-
database or persistence mechanism.
|
124
|
+
version: '0.9'
|
125
|
+
description: A flexible pure Ruby job queue. Tasks are reschedulable after failures.
|
99
126
|
email:
|
100
127
|
- robin@tenjin.ca
|
101
128
|
executables: []
|
@@ -104,43 +131,54 @@ extra_rdoc_files: []
|
|
104
131
|
files:
|
105
132
|
- ".gitignore"
|
106
133
|
- ".rspec"
|
134
|
+
- ".rubocop.yml"
|
107
135
|
- ".ruby-version"
|
108
136
|
- ".travis.yml"
|
109
137
|
- CODE_OF_CONDUCT.md
|
110
138
|
- Gemfile
|
111
139
|
- LICENSE.txt
|
112
140
|
- README.md
|
141
|
+
- RELEASE_NOTES.md
|
113
142
|
- Rakefile
|
114
143
|
- bin/console
|
115
144
|
- bin/setup
|
116
145
|
- lib/procrastinator.rb
|
117
|
-
- lib/procrastinator/
|
146
|
+
- lib/procrastinator/config.rb
|
147
|
+
- lib/procrastinator/logged_task.rb
|
148
|
+
- lib/procrastinator/queue.rb
|
118
149
|
- lib/procrastinator/queue_worker.rb
|
119
|
-
- lib/procrastinator/
|
150
|
+
- lib/procrastinator/rake/daemon_tasks.rb
|
151
|
+
- lib/procrastinator/rake/tasks.rb
|
152
|
+
- lib/procrastinator/scheduler.rb
|
153
|
+
- lib/procrastinator/task.rb
|
154
|
+
- lib/procrastinator/task_meta_data.rb
|
155
|
+
- lib/procrastinator/task_store/file_transaction.rb
|
156
|
+
- lib/procrastinator/task_store/simple_comma_store.rb
|
157
|
+
- lib/procrastinator/test/mocks.rb
|
120
158
|
- lib/procrastinator/version.rb
|
121
159
|
- procrastinator.gemspec
|
122
160
|
homepage: https://github.com/TenjinInc/procrastinator
|
123
161
|
licenses:
|
124
162
|
- MIT
|
125
|
-
metadata:
|
163
|
+
metadata:
|
164
|
+
rubygems_mfa_required: 'true'
|
126
165
|
post_install_message:
|
127
166
|
rdoc_options: []
|
128
167
|
require_paths:
|
129
168
|
- lib
|
130
169
|
required_ruby_version: !ruby/object:Gem::Requirement
|
131
170
|
requirements:
|
132
|
-
- - "
|
171
|
+
- - ">="
|
133
172
|
- !ruby/object:Gem::Version
|
134
|
-
version: '2.
|
173
|
+
version: '2.4'
|
135
174
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
136
175
|
requirements:
|
137
|
-
- - "
|
176
|
+
- - ">"
|
138
177
|
- !ruby/object:Gem::Version
|
139
|
-
version:
|
178
|
+
version: 1.3.1
|
140
179
|
requirements: []
|
141
|
-
|
142
|
-
rubygems_version: 2.2.2
|
180
|
+
rubygems_version: 3.1.2
|
143
181
|
signing_key:
|
144
182
|
specification_version: 4
|
145
|
-
summary:
|
183
|
+
summary: For apps to put off work until later
|
146
184
|
test_files: []
|