procrastinator 0.9.0 → 1.0.0.pre.rc2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +6 -1
- data/.rubocop.yml +20 -1
- data/README.md +327 -333
- data/RELEASE_NOTES.md +44 -0
- data/lib/procrastinator/config.rb +93 -129
- data/lib/procrastinator/logged_task.rb +50 -0
- data/lib/procrastinator/queue.rb +168 -12
- data/lib/procrastinator/queue_worker.rb +52 -97
- data/lib/procrastinator/rake/daemon_tasks.rb +54 -0
- data/lib/procrastinator/rake/tasks.rb +3 -0
- data/lib/procrastinator/scheduler.rb +299 -77
- data/lib/procrastinator/task.rb +46 -28
- data/lib/procrastinator/task_meta_data.rb +96 -52
- 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 +1 -1
- data/lib/procrastinator.rb +9 -24
- data/procrastinator.gemspec +13 -9
- metadata +43 -26
- data/lib/procrastinator/loaders/csv_loader.rb +0 -107
- data/lib/procrastinator/queue_manager.rb +0 -201
- data/lib/procrastinator/task_worker.rb +0 -100
- data/lib/rake/procrastinator_task.rb +0 -34
data/README.md
CHANGED
@@ -1,31 +1,36 @@
|
|
1
1
|
# Procrastinator
|
2
|
-
Procrastinator is a pure ruby job scheduling gem to allow your app to put off work for later.
|
3
|
-
Tasks are scheduled in queues and those queues are monitored by separate worker subprocesses.
|
4
|
-
Once the scheduled time arrives, the queue worker performs that task.
|
5
2
|
|
6
|
-
|
3
|
+
Procrastinator is a pure Ruby job scheduling gem. Put off tasks until later or for another process to handle.
|
4
|
+
|
5
|
+
Tasks are can be rescheduled and retried after failures, and you can use whatever storage mechanism is needed.
|
7
6
|
|
8
7
|
## Big Picture
|
8
|
+
|
9
9
|
If you have tasks like this:
|
10
10
|
|
11
11
|
```ruby
|
12
|
+
# Sends a welcome email
|
12
13
|
class SendWelcomeEmail
|
13
14
|
def run
|
14
|
-
# ...
|
15
|
+
# ... etc
|
15
16
|
end
|
16
17
|
end
|
17
18
|
```
|
18
19
|
|
19
|
-
Setup a procrastination environment:
|
20
|
+
Setup a procrastination environment like:
|
21
|
+
|
20
22
|
```ruby
|
21
23
|
scheduler = Procrastinator.setup do |env|
|
22
|
-
env.
|
23
|
-
|
24
|
-
|
24
|
+
env.with_store some_email_task_database do
|
25
|
+
env.define_queue :greeting, SendWelcomeEmail
|
26
|
+
env.define_queue :birthday, SendBirthdayEmail, max_attempts: 3
|
27
|
+
end
|
28
|
+
|
29
|
+
env.define_queue :thumbnail, GenerateThumbnail, store: 'imgtasks.csv', timeout: 60
|
25
30
|
end
|
26
31
|
```
|
27
32
|
|
28
|
-
|
33
|
+
Put jobs off until later:
|
29
34
|
|
30
35
|
```ruby
|
31
36
|
scheduler.delay(:greeting, data: 'bob@example.com')
|
@@ -35,31 +40,39 @@ scheduler.delay(:thumbnail, data: {file: 'full_image.png', width: 100, height: 1
|
|
35
40
|
scheduler.delay(:send_birthday_email, run_at: Time.now + 3600, data: {user_id: 5})
|
36
41
|
```
|
37
42
|
|
43
|
+
And tell a process to actually do them:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
# Starts a daemon with a thread for each queue.
|
47
|
+
# Other options are direct control (for testing) or threaded (for terminals)
|
48
|
+
scheduler.work.daemonized!
|
49
|
+
```
|
50
|
+
|
38
51
|
## Contents
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
52
|
+
|
53
|
+
- [Installation](#installation)
|
54
|
+
- [Setup](#setup)
|
55
|
+
* [Defining Queues](#defining-queues)
|
56
|
+
* [Task Store](#task-store)
|
57
|
+
+ [Data Fields](#data-fields)
|
58
|
+
* [Task Container](#task-container)
|
59
|
+
- [Tasks](#tasks)
|
60
|
+
* [Accessing Task Attributes](#accessing-task-attributes)
|
61
|
+
* [Errors & Logging](#errors-logging)
|
62
|
+
- [Scheduling Tasks](#scheduling-tasks)
|
63
|
+
* [Providing Data](#providing-data)
|
64
|
+
* [Scheduling](#scheduling)
|
65
|
+
* [Rescheduling](#rescheduling)
|
66
|
+
* [Retries](#retries)
|
67
|
+
* [Cancelling](#cancelling)
|
68
|
+
- [Working on Tasks](#working-on-tasks)
|
69
|
+
* [Stepwise Working](#stepwise-working)
|
70
|
+
* [Threaded Working](#threaded-working)
|
71
|
+
* [Daemonized Working](#daemonized-working)
|
72
|
+
+ [PID Files](#pid-files)
|
73
|
+
- [Contributing](#contributing)
|
74
|
+
* [Developers](#developers)
|
75
|
+
- [License](#license)
|
63
76
|
|
64
77
|
<!-- ToC generated with http://ecotrust-canada.github.io/markdown-toc/ -->
|
65
78
|
|
@@ -76,243 +89,185 @@ And then run:
|
|
76
89
|
bundle install
|
77
90
|
|
78
91
|
## Setup
|
79
|
-
`Procrastinator.setup` allows you to define which queues are available. You can also optionally
|
80
|
-
specify a task loader IO object, task context, and other settings.
|
81
92
|
|
82
|
-
|
83
|
-
Procrastinator.setup do |env|
|
84
|
-
# call methods on env to set configurations
|
85
|
-
end
|
86
|
-
```
|
87
|
-
|
88
|
-
When setup is complete, Procrastinator spins off a sub process to work on each queue and
|
89
|
-
returns the configured scheduler, which is used to `#delay` tasks.
|
90
|
-
|
91
|
-
If there are old queue processes, Procrastinator will kill them before spawning a replacement.
|
92
|
-
|
93
|
-
### Defining Queues: `#define_queue`
|
94
|
-
In the setup block, you can call `#define_queue` on the environment:
|
93
|
+
`Procrastinator.setup` allows you to define which queues are available and other settings.
|
95
94
|
|
96
95
|
```ruby
|
97
|
-
|
98
|
-
|
96
|
+
require 'procrastinator'
|
97
|
+
|
98
|
+
scheduler = Procrastinator.setup do |config|
|
99
|
+
# ...
|
99
100
|
end
|
100
101
|
```
|
101
102
|
|
102
|
-
|
103
|
-
|
104
|
-
You can also provide these keyword arguments:
|
105
|
-
|
106
|
-
* `:timeout`
|
107
|
-
|
108
|
-
Duration (seconds) after which tasks in this queue should fail for taking too long.
|
109
|
-
|
110
|
-
* `:max_attempts`
|
111
|
-
|
112
|
-
Maximum number of attempts for tasks in this queue. Once `attempts` meets or exceeds `max_attempts`, the task will
|
113
|
-
be permanently failed.
|
114
|
-
|
115
|
-
* `:update_period`
|
116
|
-
|
117
|
-
Delay (seconds) between reloads of all tasks from the task loader.
|
118
|
-
|
119
|
-
* `:max_tasks`
|
120
|
-
|
121
|
-
The maximum number of tasks to run concurrently within a queue worker process.
|
122
|
-
|
123
|
-
|
124
|
-
```ruby
|
125
|
-
# all defaults set explicitly:
|
126
|
-
env.define_queue :queue_name, YourTaskClass, timeout: 3600, max_attempts: 20, update_period: 10, max_tasks: 10
|
127
|
-
```
|
103
|
+
It then returns a `Scheduler` that your code can use to schedule tasks or tell to start working.
|
128
104
|
|
129
|
-
|
130
|
-
|
131
|
-
that knows how to read and write tasks in your data storage (eg. file, database, etc).
|
105
|
+
* See [Scheduling Tasks](#scheduling-tasks)
|
106
|
+
* See [Start Working](#start-working)
|
132
107
|
|
133
|
-
|
134
|
-
your situation.
|
108
|
+
### Defining Queues
|
135
109
|
|
136
|
-
In setup,
|
110
|
+
In setup, call `#define_queue` with a symbol name and the class that performs those jobs:
|
137
111
|
|
138
112
|
```ruby
|
139
|
-
|
113
|
+
# You must provide a queue name and the class that handles those jobs
|
114
|
+
config.define_queue :greeting, SendWelcomeEmail
|
140
115
|
|
141
|
-
|
142
|
-
|
116
|
+
# but queues have some optional settings, too
|
117
|
+
config.define_queue :greeting, SendWelcomeEmail, store: 'tasks.csv', timeout: 60, max_attempts: 2, update_period: 1
|
143
118
|
|
144
|
-
|
145
|
-
|
146
|
-
# if you're using the default CSV loader and want to set where it saves the CSV,
|
147
|
-
# provide the keyword argument :location with your path or filename
|
148
|
-
env.load_with location: '/var/myapp/'
|
149
|
-
end
|
119
|
+
# all defaults set explicitly
|
120
|
+
config.define_queue :greeting, SendWelcomeEmail, store: 'procrastinator.csv', timeout: 3600, max_attempts: 20, update_period: 10
|
150
121
|
```
|
151
122
|
|
152
|
-
|
153
|
-
|
123
|
+
Description of keyword options:
|
124
|
+
|
125
|
+
| Option | Description |
|
126
|
+
|------------------| ------------- |
|
127
|
+
| `:store` | Storage IO object for tasks. See [Task Store](#task-store) |
|
128
|
+
| `:timeout` | Max duration (seconds) before tasks are failed for taking too long |
|
129
|
+
| `:max_attempts` | Once a task has been attempted `max_attempts` times, it will be permanently failed. |
|
130
|
+
| `:update_period` | Delay (seconds) between reloads of all tasks from the task store |
|
131
|
+
|
132
|
+
### Task Store
|
133
|
+
|
134
|
+
A task store is a [strategy](https://en.wikipedia.org/wiki/Strategy_pattern) pattern object that knows how to read and
|
135
|
+
write tasks in your data storage (eg. database, CSV file, etc).
|
154
136
|
|
155
137
|
```ruby
|
156
|
-
|
138
|
+
task_store = ReminderStore.new # eg. some SQL task storage class you wrote
|
157
139
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
connection.reconnect
|
164
|
-
env.load_with MyTaskLoader.new(connection)
|
165
|
-
end
|
166
|
-
|
167
|
-
# .. other setup stuff ...
|
140
|
+
Procrastinator.setup do |config|
|
141
|
+
config.define_queue(:reminder, ReminderTask, store: task_store)
|
142
|
+
|
143
|
+
# to use the default CSV storage, provide :store with a string or Pathname
|
144
|
+
config.define_queue(:reminder, ReminderTask, store: '/var/myapp/tasks.csv')
|
168
145
|
end
|
169
146
|
```
|
170
147
|
|
171
|
-
A task
|
148
|
+
A task store is required to implement *all* of the following methods or else it will raise a
|
149
|
+
`MalformedPersisterError`:
|
172
150
|
|
173
151
|
1. `#read(attributes)`
|
174
152
|
|
175
|
-
Returns a list of hashes from your datastore that match the given attributes hash. The search
|
176
|
-
|
177
|
-
|
178
|
-
|
153
|
+
Returns a list of hashes from your datastore that match the given attributes hash. The search attributes will be in
|
154
|
+
their final form (eg. `:data` will already be serialized). Each hash must contain the properties listed
|
155
|
+
in [Task Data](#task-data) below.
|
156
|
+
|
179
157
|
2. `#create(queue:, run_at:, initial_run_at:, expire_at:, data:)`
|
180
158
|
|
181
|
-
|
159
|
+
Saves a task in your storage. Receives a hash with [Task Data](#task-data) keys:
|
182
160
|
`:queue`, `:run_at`, `:initial_run_at`, `:expire_at`, and `:data`.
|
183
|
-
|
161
|
+
|
184
162
|
3. `#update(id, new_data)`
|
185
|
-
|
163
|
+
|
186
164
|
Saves the provided full [Task Data](#task-data) hash to your datastore.
|
187
|
-
|
165
|
+
|
188
166
|
4. `#delete(id)`
|
189
|
-
|
190
|
-
Deletes the task with the given identifier in your datastore.
|
191
167
|
|
192
|
-
|
193
|
-
If your loader is missing any of the above methods, Procrastinator will explode
|
194
|
-
with a `MalformedPersisterError` and you will be sad.
|
168
|
+
Deletes the task with the given identifier from storage
|
195
169
|
|
196
|
-
|
197
|
-
|
198
|
-
these are the field names. If you have a database, use this to inform your table schema.
|
170
|
+
Procrastinator comes with a simple CSV file task store by default, but you are encouraged to build one that suits your
|
171
|
+
situation.
|
199
172
|
|
200
|
-
|
201
|
-
|-------------------|--------| ----------------------------------------------------------------------------------------|
|
202
|
-
| `:id` | int | Unique identifier for this exact task |
|
203
|
-
| `:queue` | symbol | Name of the queue the task is inside |
|
204
|
-
| `:run_at` | int | Unix timestamp of when to next attempt running the task. ¹ |
|
205
|
-
| `:initial_run_at` | int | Unix timestamp of the originally requested run |
|
206
|
-
| `:expire_at` | int | Unix timestamp of when to permanently fail the task because it is too late to be useful |
|
207
|
-
| `:attempts` | int | Number of times the task has tried to run; this should only be > 0 if the task fails |
|
208
|
-
| `:last_fail_at` | int | Unix timestamp of when the most recent failure happened |
|
209
|
-
| `:last_error` | string | Error message + bracktrace of the most recent failure. May be very long. |
|
210
|
-
| `:data` | string | Serialized data accessible in the task instance.² |
|
173
|
+
_Warning_: Task stores shared between queues **must** be thread-safe if using threaded or daemonized work modes.
|
211
174
|
|
212
|
-
|
175
|
+
#### Data Fields
|
213
176
|
|
214
|
-
|
215
|
-
|
177
|
+
These are the data fields for each individual scheduled task. When using the built-in task store, these are the field
|
178
|
+
names. If you have a database, use this to inform your table schema.
|
216
179
|
|
217
|
-
|
218
|
-
|
180
|
+
| Hash Key | Type | Description |
|
181
|
+
|-------------------|---------| ----------------------------------------------------------------------------------------|
|
182
|
+
| `:id` | int | Unique identifier for this exact task |
|
183
|
+
| `:queue` | symbol | Name of the queue the task is inside |
|
184
|
+
| `:run_at` | iso8601 | Time to attempt running the task next. ¹ |
|
185
|
+
| `:initial_run_at` | iso8601 | Originally requested run_at. Reset when rescheduled. |
|
186
|
+
| `:expire_at` | iso8601 | Time to permanently fail the task because it is too late to be useful |
|
187
|
+
| `:attempts` | int | Number of times the task has tried to run; this should only be > 0 if the task fails |
|
188
|
+
| `:last_fail_at` | iso8601 | Unix timestamp of when the most recent failure happened |
|
189
|
+
| `:last_error` | string | Error message + bracktrace of the most recent failure. May be very long. |
|
190
|
+
| `:data` | string | Serialized data accessible in the task instance. |
|
219
191
|
|
220
|
-
|
221
|
-
Whatever you give to `#provide_context` will be made available to your Task through the task attribute `:context`.
|
192
|
+
> ¹ `nil` indicates that it is permanently failed and will never run, either due to expiry or too many attempts.
|
222
193
|
|
223
|
-
|
194
|
+
Data is serialized using `JSON.dump` and `JSON.parse` with **symbolized keys**. It is strongly recommended to only
|
195
|
+
supply simple data types (eg. id numbers) to reduce storage space, eliminate redundancy, and reduce the chance of a
|
196
|
+
serialization error.
|
224
197
|
|
225
|
-
|
226
|
-
Procrastinator.setup do |env|
|
227
|
-
# .. other setup stuff ...
|
228
|
-
|
229
|
-
env.provide_context some_key: "This hash will be passed into your task's methods"
|
230
|
-
end
|
198
|
+
Times are all handled as [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) formatted strings.
|
231
199
|
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
200
|
+
#### Default Task Store
|
201
|
+
|
202
|
+
Specifying no storage will cause Procrastinator to save tasks using the very basic built-in CSV storage. It is not
|
203
|
+
designed for heavy loads, so you should replace it in a production environment.
|
204
|
+
|
205
|
+
The file path is defined in `Procrastinator::Store::SimpleCommaStore::DEFAULT_FILE`.
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
Procrastinator.setup do |config|
|
209
|
+
# this will use the default CSV task store.
|
210
|
+
config.define_queue(:reminder, ReminderTask)
|
241
211
|
end
|
242
212
|
```
|
243
213
|
|
244
|
-
|
245
|
-
In the setup block, use `#each_process` to configure details about the queue subprocesses.
|
214
|
+
#### Shared Task Stores
|
246
215
|
|
247
|
-
|
248
|
-
If you pass a block to `#each_process`, it will be run after the process is forked,
|
249
|
-
but before the queue worker starts working on tasks.
|
216
|
+
When there are tasks use the same storage, you can wrap them in a `with_store` block.
|
250
217
|
|
251
218
|
```ruby
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
#
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
# these two are the configuration methods you're most likely to use in #each_process
|
261
|
-
env.provide_context MyApp.build_task_package
|
262
|
-
env.load_with MyDatabase.new(connection)
|
219
|
+
email_task_store = EmailTaskStore.new # eg. some SQL task storage class you wrote
|
220
|
+
|
221
|
+
Procrastinator.setup do |config|
|
222
|
+
with_store(email_task_store) do
|
223
|
+
# queues defined inside this block will use the email task store
|
224
|
+
config.define_queue(:welcome, WelcomeTask)
|
225
|
+
config.define_queue(:reminder, ReminderTask)
|
263
226
|
end
|
227
|
+
|
228
|
+
# and this will not use it
|
229
|
+
config.define_queue(:thumbnails, ThumbnailTask)
|
264
230
|
end
|
265
231
|
```
|
266
232
|
|
267
|
-
|
233
|
+
### Task Container
|
268
234
|
|
269
|
-
|
270
|
-
Each queue subprocess is named after the queue it's working on, eg. `greeting-queue-worker` or
|
271
|
-
`thumbnails-queue-worker`.
|
235
|
+
Whatever is given to `#provide_container` will available to Tasks through the task attribute `:container`.
|
272
236
|
|
273
|
-
|
274
|
-
was spawned for. You can provide a `prefix:` argument to `#each_process` with a string that will be prepended to the
|
275
|
-
process command name.
|
237
|
+
This can be useful for things like app containers, but you can use it for whatever you like.
|
276
238
|
|
277
239
|
```ruby
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
240
|
+
Procrastinator.setup do |env|
|
241
|
+
env.provide_container lunch: 'Lasagna'
|
242
|
+
|
243
|
+
# .. other setup stuff ...
|
282
244
|
end
|
283
|
-
```
|
284
245
|
|
285
|
-
|
286
|
-
|
246
|
+
# ... and in your task ...
|
247
|
+
class LunchTask
|
248
|
+
attr_accessor :logger, :scheduler, :container
|
287
249
|
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
env.each_process(pid_dir: '/var/run/myapp/')
|
250
|
+
def run
|
251
|
+
logger.info("Today's Lunch is: #{ container[:lunch] }")
|
252
|
+
end
|
293
253
|
end
|
294
254
|
```
|
295
255
|
|
296
|
-
Any PIDs found in those files are killed right before Procrastinator spawns queues,
|
297
|
-
so that it can replace them with new code.
|
298
|
-
|
299
256
|
## Tasks
|
300
|
-
Your task class is what actually gets run on the task queue. They will look something like this:
|
301
257
|
|
258
|
+
Your task class is what actually gets run on the task queue. They'll look like this:
|
302
259
|
|
303
260
|
```ruby
|
261
|
+
|
304
262
|
class MyTask
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
# task_attr :data, :logger, :context, :scheduler
|
309
|
-
|
263
|
+
# These attributes will be assigned by Procrastinator when the task is run.
|
264
|
+
attr_accessor :logger, :scheduler, :container, :data
|
265
|
+
|
310
266
|
# Performs the core work of the task.
|
311
267
|
def run
|
312
268
|
# ... perform your task ...
|
313
269
|
end
|
314
|
-
|
315
|
-
|
270
|
+
|
316
271
|
# ========================================
|
317
272
|
# OPTIONAL HOOKS
|
318
273
|
#
|
@@ -320,19 +275,19 @@ class MyTask
|
|
320
275
|
# below. Only #run is mandatory.
|
321
276
|
#
|
322
277
|
# ========================================
|
323
|
-
|
278
|
+
|
324
279
|
# Called after the task has completed successfully.
|
325
280
|
# Receives the result of #run.
|
326
281
|
def success(run_result)
|
327
282
|
# ...
|
328
283
|
end
|
329
|
-
|
284
|
+
|
330
285
|
# Called after #run raises any StandardError (or subclass).
|
331
286
|
# Receives the raised error.
|
332
287
|
def fail(error)
|
333
288
|
# ...
|
334
289
|
end
|
335
|
-
|
290
|
+
|
336
291
|
# Called after either is true:
|
337
292
|
# 1. the time reported by Time.now is past the task's expire_at time.
|
338
293
|
# 2. the task has failed and the number of attempts is equal to or greater than the queue's `max_attempts`.
|
@@ -346,55 +301,67 @@ class MyTask
|
|
346
301
|
end
|
347
302
|
```
|
348
303
|
|
349
|
-
###
|
350
|
-
|
351
|
-
|
304
|
+
### Attribute Accessors
|
305
|
+
|
306
|
+
Tasks must provide accessors for `:logger`, `:container`, and `:scheduler`, while the `:data` accessor is semi-optional
|
307
|
+
(see below). This is to prevent the tasks from referencing unknown variables once they actually get run.
|
308
|
+
|
309
|
+
* `:data` The data you provided in the call to `#delay`. Any task with a `:data` accessor will require data be passed
|
310
|
+
to `#delay`, and vice versa. See [Task Data](#task-data) for more.
|
311
|
+
|
312
|
+
* `:container` The container you've provided in your setup. See [Task Container](#task-container) for more.
|
313
|
+
|
314
|
+
* `:logger` The queue's Logger object. See [Logging](#logging) for more.
|
315
|
+
|
316
|
+
* `:scheduler` A scheduler object that you can use to schedule new tasks (eg. with `#delay`).
|
317
|
+
|
318
|
+
### Errors & Logging
|
319
|
+
|
320
|
+
Errors that trigger `#fail` or `#final_fail` are saved to the task storage under columns `last_error` and
|
321
|
+
`last_error_at`.
|
322
|
+
|
323
|
+
Each queue worker also keeps a logfile log using the Ruby
|
324
|
+
[Logger class](https://ruby-doc.org/stdlib-2.7.1/libdoc/logger/rdoc/Logger.html). Log files are named after the queue (
|
325
|
+
eg. `log/welcome-queue-worker.log`).
|
352
326
|
|
353
327
|
```ruby
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
#
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
# the attributes listed in task_attr become methods like attr_accessor
|
364
|
-
logger.info("The data for this task is #{data}")
|
365
|
-
end
|
328
|
+
scheduler = Procrastinator.setup do |env|
|
329
|
+
# you can set custom log location and level:
|
330
|
+
env.log_with(directory: '/var/log/myapp/', level: Logger::DEBUG)
|
331
|
+
|
332
|
+
# you can also set the log rotation age or size (see Logger docs for details)
|
333
|
+
env.log_with(shift: 1024, age: 5)
|
334
|
+
|
335
|
+
# use a falsey log level to disable logging entirely:
|
336
|
+
env.log_with(level: false)
|
366
337
|
end
|
367
338
|
```
|
368
339
|
|
369
|
-
|
370
|
-
This is the data that you provided in the call to `#delay`. Any task that registers `:data` as a task
|
371
|
-
attribute will require data be passed to `#delay`.
|
372
|
-
See [Task Data](#task-data) for more.
|
373
|
-
|
374
|
-
* `:context`
|
375
|
-
|
376
|
-
The context you've provided in your setup. See [Task Context](#task-context-provide_context) for more.
|
377
|
-
|
378
|
-
* `:logger`
|
379
|
-
|
380
|
-
The queue's Logger object. See [Logging](#logging) for more.
|
381
|
-
|
382
|
-
* `:scheduler`
|
383
|
-
|
384
|
-
A scheduler object that you can use to schedule new tasks (eg. with `#delay`).
|
340
|
+
The logger can be accessed in your tasks by calling `logger` or `@logger`.
|
385
341
|
|
342
|
+
```ruby
|
386
343
|
|
387
|
-
|
388
|
-
|
344
|
+
class MyTask
|
345
|
+
attr_accessor :logger, :scheduler, :container
|
389
346
|
|
390
|
-
|
347
|
+
def run
|
348
|
+
logger.info('This task got run. Hooray!')
|
349
|
+
end
|
350
|
+
end
|
351
|
+
```
|
391
352
|
|
392
|
-
|
393
|
-
and `:last_error`.
|
353
|
+
Some events are always logged by default:
|
394
354
|
|
355
|
+
|event |level |
|
356
|
+
|--------------------|-------|
|
357
|
+
|process started | INFO |
|
358
|
+
|#success called | DEBUG |
|
359
|
+
|#fail called | DEBUG |
|
360
|
+
|#final_fail called | DEBUG |
|
395
361
|
|
396
362
|
## Scheduling Tasks
|
397
|
-
|
363
|
+
|
364
|
+
To schedule tasks, just call `#delay` on the environment returned from `Procrastinator.setup`:
|
398
365
|
|
399
366
|
```ruby
|
400
367
|
scheduler = Procrastinator.setup do |env|
|
@@ -406,7 +373,7 @@ end
|
|
406
373
|
scheduler.delay(:reminder, data: 'bob@example.com')
|
407
374
|
```
|
408
375
|
|
409
|
-
If
|
376
|
+
If there is only one queue, you may omit the queue name:
|
410
377
|
|
411
378
|
```ruby
|
412
379
|
scheduler = Procrastinator.setup do |env|
|
@@ -417,14 +384,16 @@ scheduler.delay(data: 'bob@example.com')
|
|
417
384
|
```
|
418
385
|
|
419
386
|
### Providing Data
|
420
|
-
Most tasks need some additional information to complete their work, like id numbers,
|
421
387
|
|
422
|
-
|
423
|
-
|
424
|
-
|
388
|
+
Most tasks need some additional information to complete their work, like id numbers,
|
389
|
+
|
390
|
+
The `:data` parameter is serialized to string as YAML, so it's better to keep it as simple as possible. For example, if
|
391
|
+
you have a database instead of passing in a complex Ruby object, pass in just the primary key and reload it in the
|
392
|
+
task's `#run`. This will require less space in your database and avoids obsolete or duplicated information.
|
425
393
|
|
426
|
-
###
|
427
|
-
|
394
|
+
### Scheduling
|
395
|
+
|
396
|
+
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
|
428
397
|
to run at a precise time; the only promise is that the task will be attempted *after* `run_at` and before `expire_at`.
|
429
398
|
|
430
399
|
```ruby
|
@@ -435,22 +404,22 @@ scheduler.delay(:greeting, run_at: Time.new(3000, 1, 1), data: 'philip_j_fry@exa
|
|
435
404
|
scheduler.delay(:thumbnail, run_at: Time.now, data: 'shut_up_and_take_my_money.gif')
|
436
405
|
```
|
437
406
|
|
438
|
-
You can also set an `expire_at` deadline. If the task has not been run before `expire_at` is passed, then it will be
|
439
|
-
final-failed the next time it would be attempted.
|
440
|
-
|
441
|
-
say, `max_attempts` is reached).
|
407
|
+
You can also set an `expire_at` deadline. If the task has not been run before `expire_at` is passed, then it will be
|
408
|
+
final-failed the next time it would be attempted. Setting `expire_at` to `nil` means it will never expire (but may still
|
409
|
+
fail permanently if, say, `max_attempts` is reached).
|
442
410
|
|
443
411
|
```ruby
|
444
412
|
# will not run at or after
|
445
|
-
scheduler.delay(:happy_birthday, expire_at: Time.new(2018, 03, 17, 12, 00, '-06:00'),
|
413
|
+
scheduler.delay(:happy_birthday, expire_at: Time.new(2018, 03, 17, 12, 00, '-06:00'), data: 'contact@tenjin.ca')
|
446
414
|
|
447
415
|
# expire_at defaults to nil:
|
448
416
|
scheduler.delay(:greeting, expire_at: nil, data: 'bob@example.com')
|
449
417
|
```
|
450
418
|
|
451
419
|
### Rescheduling
|
452
|
-
|
453
|
-
information, and then calling #to on that to provide the new
|
420
|
+
|
421
|
+
Call `#reschedule` with the queue name and some identifying information, and then calling #to on that to provide the new
|
422
|
+
time.
|
454
423
|
|
455
424
|
```ruby
|
456
425
|
scheduler = Procrastinator.setup do |env|
|
@@ -466,18 +435,30 @@ scheduler.reschedule(:reminder, data: 'bob@example.com').to(run_at: Time.parse('
|
|
466
435
|
scheduler.reschedule(:reminder, data: 'bob@example.com').to(expire_at: Time.parse('June 23 12:00'))
|
467
436
|
|
468
437
|
# or both
|
469
|
-
scheduler.reschedule(:reminder, data: 'bob@example.com').to(run_at: Time.parse('June 20 12:00'),
|
438
|
+
scheduler.reschedule(:reminder, data: 'bob@example.com').to(run_at: Time.parse('June 20 12:00'),
|
470
439
|
expire_at: Time.parse('June 23 12:00'))
|
471
440
|
```
|
472
441
|
|
473
|
-
Rescheduling
|
474
|
-
|
442
|
+
Rescheduling sets the task's:
|
443
|
+
|
444
|
+
* `:run_at` and `:initial_run_at` to a new value, if provided
|
445
|
+
* `:expire_at` to a new value if provided.
|
446
|
+
* `:attempts` to `0`
|
447
|
+
* `:last_error` and `:last_error_at` to `nil`.
|
448
|
+
|
449
|
+
Rescheduling will not change `:id`, `:queue` or `:data`. A `RuntimeError` is raised if the runtime is after the expiry.
|
475
450
|
|
476
|
-
|
451
|
+
### Retries
|
452
|
+
|
453
|
+
Failed tasks have their `run_at` rescheduled on an increasing delay (in seconds) according to this formula:
|
477
454
|
|
478
|
-
|
455
|
+
> 30 + (number_of_attempts)<sup>4</sup>
|
456
|
+
|
457
|
+
Situations that call `#fail` or `#final_fail` will cause the error timestamp and reason to be stored in `:last_fail_at`
|
458
|
+
and `:last_error`.
|
479
459
|
|
480
460
|
### Cancelling
|
461
|
+
|
481
462
|
Call `#cancel` with the queue name and some identifying information to narrow the search to a single task.
|
482
463
|
|
483
464
|
```ruby
|
@@ -497,112 +478,125 @@ scheduler.reschedule(:reminder, run_at: Time.parse('June 1'), data: 'bob@example
|
|
497
478
|
scheduler.reschedule(:reminder, id: 137)
|
498
479
|
```
|
499
480
|
|
500
|
-
##
|
501
|
-
Sometimes you want to be able to put your app into a maintenance mode temporarily.
|
502
|
-
If Procrastinator sees the environment variable `PROCRASTINATOR_STOP` is set, it
|
503
|
-
will not spawn child process queue workers at all.
|
481
|
+
## Working on Tasks
|
504
482
|
|
505
|
-
|
506
|
-
find all the old subprocesses and halt them, but not create new ones.
|
483
|
+
Use the scheduler object returned by setup to `#work` queues **serially**, **threaded**, or **daemonized**.
|
507
484
|
|
508
|
-
|
509
|
-
same process as the original caller to setup.
|
485
|
+
### Serial Working
|
510
486
|
|
511
|
-
|
512
|
-
Procrastinator uses multi-threading and multi-processing internally, which is a nightmare for automated testing.
|
513
|
-
Test Mode will disable all of that and rely on your tests to tell it when to act.
|
487
|
+
Working serially performs a task from each queue directly. There is no multithreading or daemonizing.
|
514
488
|
|
515
|
-
|
516
|
-
the procrastination environment:
|
489
|
+
Work serially for TDD tests or other situations you need close direct control.
|
517
490
|
|
518
491
|
```ruby
|
519
|
-
#
|
520
|
-
|
521
|
-
|
522
|
-
# or you can also enable it in the setup
|
523
|
-
scheduler = Procrastinator.setup do |env|
|
524
|
-
env.enable_test_mode
|
525
|
-
|
526
|
-
# ... other settings...
|
527
|
-
end
|
528
|
-
```
|
492
|
+
# work just one task, no threading
|
493
|
+
scheduler.work.serially
|
529
494
|
|
530
|
-
|
495
|
+
# work the first five tasks
|
496
|
+
scheduler.work.serially(steps: 5)
|
531
497
|
|
498
|
+
# only work tasks on greeting and reminder queues
|
499
|
+
scheduler.work(:greeting, :reminders).serially(steps: 2)
|
532
500
|
```
|
533
|
-
# execute one task on all queues
|
534
|
-
env.act
|
535
501
|
|
536
|
-
|
537
|
-
scheduler.act(:cleanup, :email)
|
538
|
-
```
|
502
|
+
### Threaded Working
|
539
503
|
|
540
|
-
|
541
|
-
Errors that trigger #fail or #final_fail are saved in the task persistence (database, file, etc) under `last_error` and
|
542
|
-
`last_error_at`.
|
504
|
+
Threaded working will spawn a worker thread per queue.
|
543
505
|
|
544
|
-
|
545
|
-
|
546
|
-
The log files are named after its queue process name (eg. `log/welcome-queue-worker.log`) and
|
547
|
-
they are saved in the log directory defined in setup.
|
506
|
+
Use threaded working for task queues that should only run while the main application is running. This includes the usual
|
507
|
+
caveats around multithreading, so proceed with caution.
|
548
508
|
|
549
509
|
```ruby
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
env.log_inside 'log/' # relative to the running directory
|
559
|
-
env.log_at_level Logger::INFO
|
560
|
-
|
561
|
-
# use nil to disable logging entirely:
|
562
|
-
env.log_inside nil
|
563
|
-
end
|
510
|
+
# work tasks until the application exits
|
511
|
+
scheduler.work.threaded
|
512
|
+
|
513
|
+
# work tasks for 5 seconds
|
514
|
+
scheduler.work.threaded(timeout: 5)
|
515
|
+
|
516
|
+
# only work tasks on greeting and reminder queues
|
517
|
+
scheduler.work(:greeting, :reminders).threaded
|
564
518
|
```
|
565
519
|
|
566
|
-
|
567
|
-
|
520
|
+
### Daemonized Working
|
521
|
+
|
522
|
+
Daemonized working **consumes the current process** and then proceeds with threaded working in the new daemon.
|
523
|
+
|
524
|
+
Use daemonized working for production environments, especially in conjunction with daemon monitors
|
525
|
+
like [Monit](https://mmonit.com/monit/). Provide a block to daemonized! to get
|
568
526
|
|
569
527
|
```ruby
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
task_attr :logger
|
528
|
+
# work tasks forever as a headless daemon process.
|
529
|
+
scheduler.work.daemonized!
|
574
530
|
|
575
|
-
|
576
|
-
|
577
|
-
|
531
|
+
# you can specify the new process name and the directory to save the procrastinator.pid file
|
532
|
+
scheduler.work.daemonized!(name: 'myapp-queue', pid_path: '/var/run')
|
533
|
+
|
534
|
+
# ... or set the pid file name precisely by giving a .pid path
|
535
|
+
scheduler.work.daemonized!(pid_path: '/var/run/myapp.pid')
|
536
|
+
|
537
|
+
# only work tasks in the 'greeting' and 'reminder' queues
|
538
|
+
scheduler.work(:greeting, :reminders).daemonized!
|
539
|
+
|
540
|
+
# supply a block to run code after the daemon subprocess has forked off
|
541
|
+
scheduler.work.daemonized! do
|
542
|
+
# this gets run after the daemon is spawned
|
543
|
+
task_store.reconnect_mysql
|
578
544
|
end
|
579
545
|
```
|
580
546
|
|
581
|
-
|
547
|
+
Procrastinator endeavours to be thread-safe and support concurrency, but this flexibility allows for many possible
|
548
|
+
combinations.
|
582
549
|
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
550
|
+
Expected use is a single process with one thread per queue. More complex use is possible but Procrastinator can't
|
551
|
+
guarantee concurrency in your Task Store.
|
552
|
+
|
553
|
+
#### PID Files
|
554
|
+
|
555
|
+
Process ID files are a single-line file that saves the daemon's process ID number. It's saved to the directory given
|
556
|
+
by `:pid_dir`. The default location is `pids/` relative to the file that called `#daemonized!`.
|
557
|
+
|
558
|
+
## Similar Tools
|
559
|
+
|
560
|
+
Procrastinator is a library that exists to enable job queues with flexibility in storage mechanism and minimal
|
561
|
+
dependencies. It's neat but it is specifically intended for smaller datasets. Some other approaches include:
|
562
|
+
|
563
|
+
### Linux etc: Cron and At
|
564
|
+
|
565
|
+
Consider [Cron](https://en.wikipedia.org/wiki/Cron) for tasks that run on a regular schedule.
|
566
|
+
|
567
|
+
Consider [At](https://en.wikipedia.org/wiki/At_(command)) for tasks that run once at a particular time.
|
568
|
+
|
569
|
+
While neither tool natively supports retry, they can be great solutions for simple situations.
|
570
|
+
|
571
|
+
### Gem: Resque
|
572
|
+
|
573
|
+
Consider [Resque](https://rubygems.org/gems/resque) for larger datasets (eg. 10,000+ jobs) where performance
|
574
|
+
optimization becomes relevant.
|
575
|
+
|
576
|
+
### Gem: Rails ActiveJob / DelayedJob
|
577
|
+
|
578
|
+
Consider [DelayedJob](https://rubygems.org/gems/delayed_job) for projects that are tightly integrated with Rails and
|
579
|
+
fully commit to live in that ecosystem.
|
589
580
|
|
590
581
|
## Contributing
|
591
|
-
|
582
|
+
|
583
|
+
Bug reports and pull requests are welcome on GitHub at
|
592
584
|
[https://github.com/TenjinInc/procrastinator](https://github.com/TenjinInc/procrastinator).
|
593
|
-
|
594
|
-
This project is intended to be a friendly space for collaboration, and contributors are expected to adhere to the
|
585
|
+
|
586
|
+
This project is intended to be a friendly space for collaboration, and contributors are expected to adhere to the
|
595
587
|
[Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
596
588
|
|
597
589
|
Play nice.
|
598
590
|
|
599
591
|
### Developers
|
600
|
-
|
592
|
+
|
593
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can
|
601
594
|
also run `bin/console` for an interactive prompt that will allow you to experiment.
|
602
595
|
|
603
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
|
604
|
-
version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version,
|
596
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
|
597
|
+
version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version,
|
605
598
|
push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
606
599
|
|
607
600
|
## License
|
601
|
+
|
608
602
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|