procrastinator 0.9.0 → 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 +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).
|