procrastinator 0.2.3 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +47 -15
- data/lib/procrastinator.rb +12 -2
- data/lib/procrastinator/environment.rb +46 -18
- data/lib/procrastinator/queue_worker.rb +17 -13
- data/lib/procrastinator/task_worker.rb +1 -1
- data/lib/procrastinator/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 46f0ec23b44a55033b241f8f1eea274a99dcc481
|
4
|
+
data.tar.gz: 84476d93786e0e63f53e11cbf913c8df9f641134
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0f54aba2184d65a6dba03a756688565bfc6d45520ea521a0ce488353ea81c02ff1946d072145b176bc5ddc643f01670256bfb9b73819f70a7961d10f63ce1712
|
7
|
+
data.tar.gz: 8181179a9437191dc43b34e61e67c7f846a83633962d7f3f41b1a69696b22769e85b5a15a1f8672c03af7c1f3fb8d0f3290fea6cef908e3bcf8b7da2128589b8
|
data/README.md
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
# Procrastinator
|
2
2
|
|
3
|
-
Procrastinator is a framework-independent job scheduling gem
|
4
|
-
tasks at the
|
3
|
+
Procrastinator is a framework-independent job scheduling gem to allow your app to put stuff of until later. It creates
|
4
|
+
a subprocess for each queue to performs tasks at the designated times. Or maybe later, depending on how busy it is.
|
5
|
+
|
6
|
+
Don't worry, it'll get done eventually.
|
5
7
|
|
6
8
|
## Installation
|
7
9
|
|
@@ -38,12 +40,12 @@ procrastinator.delay(queue: :cleanup, run_at: Time.now + 3600, task: ClearTempDa
|
|
38
40
|
|
39
41
|
Read on for more details on each step.
|
40
42
|
|
41
|
-
|
43
|
+
### Setup Phase
|
42
44
|
The setup phase first defines which queues are available and the persistence strategy to use for reading
|
43
45
|
and writing tasks. It then starts up a sub process for working on each queue within that environment.
|
44
46
|
|
45
47
|
|
46
|
-
####
|
48
|
+
#### Declaring a Persistence Strategy
|
47
49
|
The persister instance is the last step between Procrastinator and your data storage (eg. database). As core Procrastinator is framework-agnostic, it needs you to provide an object that knows how to read and write task data.
|
48
50
|
|
49
51
|
Your [strategy](https://en.wikipedia.org/wiki/Strategy_pattern) class is required to provide the following methods:
|
@@ -71,7 +73,7 @@ If the strategy does not have all of these methods, Procrastinator will explode
|
|
71
73
|
|
72
74
|
Notice that the times are all given as unix epoch timestamps. This is to avoid any confusion with timezones, and it is recommended that you store times in this manner for the same reason.
|
73
75
|
|
74
|
-
####Defining Queues
|
76
|
+
#### Defining Queues
|
75
77
|
`Procrastinator.setup` requires a block be provided, and that in the block call `#define_queue` be called on the provided environment. Define queue takes a queue name symbol and these properies as a hash
|
76
78
|
|
77
79
|
* :timeout - Time, in seconds, after which it should fail tasks in this queue for taking too long to execute.
|
@@ -89,36 +91,37 @@ Procrastinator.setup(some_persister) do |env|
|
|
89
91
|
end
|
90
92
|
```
|
91
93
|
|
92
|
-
####
|
94
|
+
#### Sub-Processes
|
93
95
|
Each queue is worked in a separate process.
|
94
96
|
|
95
97
|
<!-- , and each process multi-threaded to handle more than one task at a time. This should help prevent a single task from clogging up the whole queue, or a single queue clogging up the entire system. -->
|
96
98
|
|
97
|
-
The sub-processes
|
99
|
+
The sub-processes checks that the parent process is still alive every 5 seconds. If there is no process with the parent's PID, the sub-process will self-exit.
|
98
100
|
|
99
|
-
|
100
|
-
|
101
|
+
###Scheduling Tasks For Later
|
102
|
+
Procrastinator will let you be lazy:
|
101
103
|
|
102
104
|
```ruby
|
103
105
|
procrastinator = Procrastinator.setup(task_persister) do |env|
|
104
106
|
env.define_queue(:email)
|
105
|
-
env.define_queue(:cleanup)
|
106
107
|
end
|
107
108
|
|
108
|
-
procrastinator.delay(
|
109
|
+
procrastinator.delay(task: EmailReminder.new('bob@example.com'))
|
109
110
|
```
|
110
111
|
|
111
|
-
|
112
|
+
... unless there are multiple queues defined. Thne you must provide a queue name with your task:
|
112
113
|
|
113
114
|
```ruby
|
114
115
|
procrastinator = Procrastinator.setup(task_persister) do |env|
|
115
116
|
env.define_queue(:email)
|
117
|
+
env.define_queue(:cleanup)
|
116
118
|
end
|
117
119
|
|
118
|
-
procrastinator.delay(task: EmailReminder.new('bob@example.com'))
|
120
|
+
procrastinator.delay(:email, task: EmailReminder.new('bob@example.com'))
|
119
121
|
```
|
120
122
|
|
121
|
-
You can set when the particular task is to be run and/or when it should expire. Be aware that
|
123
|
+
You can set when the particular task is to be run and/or when it should expire. Be aware that the task is not guaranteed
|
124
|
+
to run at a precise time; the only promise is that the task will get run some time after `run_at`, unless it's after `expire_at`.
|
122
125
|
|
123
126
|
```ruby
|
124
127
|
procrastinator = Procrastinator.setup(task_persister) do |env|
|
@@ -145,7 +148,7 @@ procrastinator.delay(expire_at: , task: EmailGreeting.new('bob@example.com'))
|
|
145
148
|
procrastinator.delay(expire_at: nil, task: EmailGreeting.new('bob@example.com'))
|
146
149
|
```
|
147
150
|
|
148
|
-
#### Task
|
151
|
+
#### Task Definition
|
149
152
|
Like the persister provided to `.setup`, your task is a strategy object that fills in the details of what to do. For this,
|
150
153
|
your task **must provide** a `#run` method:
|
151
154
|
|
@@ -168,6 +171,35 @@ Tasks that fail have their `run_at` rescheduled on an increasing delay according
|
|
168
171
|
|
169
172
|
Both failing and final_failing will cause the error timestamp and reason to be stored in `:last_fail_at` and `:last_error`.
|
170
173
|
|
174
|
+
### Testing With Procrastinator
|
175
|
+
Procrastinator uses multi-threading and multi-processing internally, which is a nightmare for testing. Fortunately for you,
|
176
|
+
Test Mode will disable all of that, and rely on your tests to tell it when to tick.
|
177
|
+
|
178
|
+
Enable Test Mode by setting `Procrastinator.test_mode` to `true` before setting up, or by calling enable_test_mode on
|
179
|
+
the procrastination environment.
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
# all further calls to `Procrastinator.setup` will produce a procrastination environment where Test Mode is enabled
|
183
|
+
Procrastinator.test_mode = true
|
184
|
+
|
185
|
+
# or you can also enable it directly in the setup
|
186
|
+
env = Procrastinator.setup do |env|
|
187
|
+
env.enable_test_mode
|
188
|
+
|
189
|
+
# other settings...
|
190
|
+
end
|
191
|
+
```
|
192
|
+
|
193
|
+
In your tests, tell the procrastinator environment to work off one item from its queues:
|
194
|
+
|
195
|
+
```
|
196
|
+
# works one task on all queues
|
197
|
+
env.act
|
198
|
+
|
199
|
+
# provide queue names to works one task on just those queues
|
200
|
+
env.act(:cleanup, :email)
|
201
|
+
```
|
202
|
+
|
171
203
|
## Contributing
|
172
204
|
Bug reports and pull requests are welcome on GitHub at
|
173
205
|
[https://github.com/TenjinInc/procrastinator](https://github.com/TenjinInc/procrastinator).
|
data/lib/procrastinator.rb
CHANGED
@@ -5,17 +5,27 @@ require 'procrastinator/environment'
|
|
5
5
|
|
6
6
|
|
7
7
|
module Procrastinator
|
8
|
+
@@test_mode = false
|
9
|
+
|
8
10
|
def self.setup(persister, &block)
|
9
11
|
raise ArgumentError.new('Procrastinator.setup must be given a block') if block.nil?
|
10
12
|
|
11
|
-
env = Environment.new(persister)
|
13
|
+
env = Environment.new(persister: persister, test_mode: @@test_mode)
|
12
14
|
|
13
15
|
yield(env)
|
14
16
|
|
15
|
-
raise RuntimeError.new('setup block did not define any queues') if env.
|
17
|
+
raise RuntimeError.new('setup block did not define any queues') if env.queue_definitions.empty?
|
16
18
|
|
17
19
|
env.spawn_workers
|
18
20
|
|
19
21
|
env
|
20
22
|
end
|
23
|
+
|
24
|
+
def self.test_mode=(value)
|
25
|
+
@@test_mode = value
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.test_mode
|
29
|
+
@@test_mode
|
30
|
+
end
|
21
31
|
end
|
@@ -1,50 +1,74 @@
|
|
1
1
|
module Procrastinator
|
2
2
|
class Environment
|
3
|
-
attr_reader :persister, :
|
3
|
+
attr_reader :persister, :queue_definitions, :queue_workers, :processes, :test_mode
|
4
4
|
|
5
|
-
def initialize(persister)
|
5
|
+
def initialize(persister:, test_mode: false)
|
6
6
|
raise ArgumentError.new('persister cannot be nil') if persister.nil?
|
7
7
|
|
8
8
|
[:read_tasks, :create_task, :update_task, :delete_task].each do |method|
|
9
9
|
raise MalformedPersisterError.new("persister must repond to ##{method}") unless persister.respond_to? method
|
10
10
|
end
|
11
11
|
|
12
|
-
@persister
|
13
|
-
@
|
14
|
-
@
|
12
|
+
@persister = persister
|
13
|
+
@test_mode = test_mode
|
14
|
+
@queue_definitions = {}
|
15
|
+
@queue_workers = []
|
16
|
+
@processes = []
|
15
17
|
end
|
16
18
|
|
17
19
|
def define_queue(name, properties={})
|
18
20
|
raise ArgumentError.new('queue name cannot be nil') if name.nil?
|
19
21
|
|
20
|
-
@
|
22
|
+
@queue_definitions[name] = properties
|
21
23
|
end
|
22
24
|
|
23
25
|
def spawn_workers
|
24
|
-
@
|
25
|
-
|
26
|
-
|
26
|
+
if @test_mode
|
27
|
+
@queue_definitions.each do |name, props|
|
28
|
+
@queue_workers << QueueWorker.new(props.merge(name: name, persister: @persister))
|
29
|
+
end
|
30
|
+
else
|
31
|
+
@queue_definitions.each do |name, props|
|
32
|
+
pid = fork do
|
33
|
+
Process.setproctitle("#{name}-queue-worker")
|
34
|
+
|
35
|
+
worker = QueueWorker.new(props.merge(name: name, persister: @persister))
|
27
36
|
|
28
|
-
|
37
|
+
monitor_parent
|
29
38
|
|
30
|
-
|
39
|
+
worker.work
|
40
|
+
end
|
31
41
|
|
32
|
-
|
42
|
+
Process.detach(pid) unless pid.nil?
|
43
|
+
@processes << pid
|
33
44
|
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def act(*queue_names)
|
49
|
+
unless @test_mode
|
50
|
+
raise RuntimeError.new('Procrastinator.act called outside Test Mode. Enable test mode by setting Procrastinator.test_mode = true before running setup')
|
51
|
+
end
|
34
52
|
|
35
|
-
|
36
|
-
@
|
53
|
+
if queue_names.empty?
|
54
|
+
@queue_workers.each do |worker|
|
55
|
+
worker.act
|
56
|
+
end
|
57
|
+
else
|
58
|
+
queue_names.each do |name|
|
59
|
+
@queue_workers.find { |worker| worker.name == name }.act
|
60
|
+
end
|
37
61
|
end
|
38
62
|
end
|
39
63
|
|
40
64
|
def delay(queue: nil, run_at: Time.now.to_i, expire_at: nil, task:)
|
41
65
|
raise ArgumentError.new('task may not be nil') if task.nil?
|
42
66
|
raise MalformedTaskError.new('given task does not support #run method') unless task.respond_to? :run
|
43
|
-
if queue.nil? && @
|
44
|
-
raise ArgumentError.new(
|
67
|
+
if queue.nil? && @queue_definitions.size > 1
|
68
|
+
raise ArgumentError.new("queue must be specified when more than one is registered. Defined queues are: #{queue_definitions.keys.map { |k| ':' + k.to_s }.join(', ')}")
|
45
69
|
else
|
46
|
-
queue ||= @
|
47
|
-
raise ArgumentError.new(%Q{there is no "#{queue}" queue registered in this environment}) if @
|
70
|
+
queue ||= @queue_definitions.keys.first
|
71
|
+
raise ArgumentError.new(%Q{there is no "#{queue}" queue registered in this environment}) if @queue_definitions[queue].nil?
|
48
72
|
end
|
49
73
|
|
50
74
|
@persister.create_task(queue: queue,
|
@@ -54,6 +78,10 @@ module Procrastinator
|
|
54
78
|
task: YAML.dump(task))
|
55
79
|
end
|
56
80
|
|
81
|
+
def enable_test_mode
|
82
|
+
@test_mode = true
|
83
|
+
end
|
84
|
+
|
57
85
|
private
|
58
86
|
def monitor_parent
|
59
87
|
heartbeat_thread = Thread.new(Process.ppid) do |ppid|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Procrastinator
|
2
2
|
class QueueWorker
|
3
|
-
DEFAULT_TIMEOUT = 3600 # seconds
|
3
|
+
DEFAULT_TIMEOUT = 3600 # in seconds; one hour total
|
4
4
|
DEFAULT_MAX_ATTEMPTS = 20
|
5
5
|
DEFAULT_UPDATE_PERIOD = 10 # seconds
|
6
6
|
DEFAULT_MAX_TASKS = 10
|
@@ -34,21 +34,25 @@ module Procrastinator
|
|
34
34
|
loop do
|
35
35
|
sleep(@update_period)
|
36
36
|
|
37
|
-
|
38
|
-
|
39
|
-
|
37
|
+
act
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def act
|
42
|
+
# shuffling and re-sorting to avoid worst case O(n^2) on quicksort
|
43
|
+
# when receiving already sorted data. Ideally, we'd use a better algo, but this will do for now
|
44
|
+
tasks = @persister.read_tasks(@name).shuffle.sort_by { |t| t[:run_at] }
|
40
45
|
|
41
|
-
|
42
|
-
|
43
|
-
|
46
|
+
tasks.first(@max_tasks).each do |task_data|
|
47
|
+
if Time.now.to_i >= task_data[:run_at].to_i
|
48
|
+
tw = TaskWorker.new(task_data)
|
44
49
|
|
45
|
-
|
50
|
+
tw.work
|
46
51
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
end
|
52
|
+
if tw.successful?
|
53
|
+
@persister.delete_task(task_data[:id])
|
54
|
+
else
|
55
|
+
@persister.update_task(tw.to_hash.merge(queue: @name))
|
52
56
|
end
|
53
57
|
end
|
54
58
|
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: 0.3.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: 2016-
|
11
|
+
date: 2016-06-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|