procrastinator 0.6.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +7 -0
- data/.ruby-version +1 -1
- data/Gemfile +2 -0
- data/README.md +482 -128
- data/Rakefile +5 -3
- data/lib/procrastinator.rb +39 -18
- data/lib/procrastinator/config.rb +185 -0
- data/lib/procrastinator/loaders/csv_loader.rb +107 -0
- data/lib/procrastinator/queue.rb +50 -0
- data/lib/procrastinator/queue_manager.rb +201 -0
- data/lib/procrastinator/queue_worker.rb +91 -71
- data/lib/procrastinator/scheduler.rb +171 -0
- data/lib/procrastinator/task.rb +46 -0
- data/lib/procrastinator/task_meta_data.rb +128 -0
- data/lib/procrastinator/task_worker.rb +66 -86
- data/lib/procrastinator/version.rb +3 -1
- data/lib/rake/procrastinator_task.rb +34 -0
- data/procrastinator.gemspec +11 -9
- metadata +40 -19
- data/lib/procrastinator/environment.rb +0 -148
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b3428d03ab5288918a2d1305ca9e8234303ba0fd
|
4
|
+
data.tar.gz: 75ab4bae088f10401a976a29ad84fbfb1b9a6bf7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e9e3ebd949697326212b199bbf17e9424a8dac47e4ac7ebbfb3154764330462fc46931ccc67aee4fc6c81a70d4e5513f7c45b66f9ac63d272987a1dc79f62519
|
7
|
+
data.tar.gz: 99df6bdc69e8af68572fa1619f5ecaaeb0083853e60c2ec2f9da7f5a644f5f9bfffc4e54ff64ac0eda3004c93ca7c4a4cb4c438816dc9b044025f8f2da53c311
|
data/.rubocop.yml
ADDED
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby-2.
|
1
|
+
ruby-2.4.2
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,9 +1,67 @@
|
|
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.
|
2
5
|
|
3
|
-
|
4
|
-
a subprocess for each queue to performs tasks at the designated times. Or maybe later, depending on how busy it is.
|
6
|
+
If the task fails to complete or takes too long, it delays it and tries again later.
|
5
7
|
|
6
|
-
|
8
|
+
## Big Picture
|
9
|
+
If you have tasks like this:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
class SendWelcomeEmail
|
13
|
+
def run
|
14
|
+
# ... email stuff ...
|
15
|
+
end
|
16
|
+
end
|
17
|
+
```
|
18
|
+
|
19
|
+
Setup a procrastination environment:
|
20
|
+
```ruby
|
21
|
+
scheduler = Procrastinator.setup do |env|
|
22
|
+
env.define_queue :greeting, SendWelcomeEmail
|
23
|
+
env.define_queue :thumbnail, GenerateThumbnail, timeout: 60
|
24
|
+
env.define_queue :birthday, SendBirthdayEmail, max_attempts: 3
|
25
|
+
end
|
26
|
+
```
|
27
|
+
|
28
|
+
And then get your lazy on:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
scheduler.delay(:greeting, data: 'bob@example.com')
|
32
|
+
|
33
|
+
scheduler.delay(:thumbnail, data: {file: 'full_image.png', width: 100, height: 100})
|
34
|
+
|
35
|
+
scheduler.delay(:send_birthday_email, run_at: Time.now + 3600, data: {user_id: 5})
|
36
|
+
```
|
37
|
+
|
38
|
+
## Contents
|
39
|
+
* [Installation](#installation)
|
40
|
+
* [Setup](#setup)
|
41
|
+
+ [Defining Queues: `#define_queue`](#defining-queues----define-queue-)
|
42
|
+
+ [The Task Loader: `#load_with`](#the-task-loader----load-with-)
|
43
|
+
- [Task Data](#task-data)
|
44
|
+
+ [The Task Context: `#provide_context`](#the-task-context----provide-context-)
|
45
|
+
+ [Controlling Queue Processes: `#each_process`](#controlling-queue-processes----each-process-)
|
46
|
+
- [The Subprocess Hook](#the-subprocess-hook)
|
47
|
+
- [Naming Processes](#naming-processes)
|
48
|
+
- [Tracking Process IDs](#tracking-process-ids)
|
49
|
+
* [Tasks](#tasks)
|
50
|
+
+ [Accessing Task Attributes](#accessing-task-attributes)
|
51
|
+
+ [Retries](#retries)
|
52
|
+
* [Scheduling Tasks](#scheduling-tasks)
|
53
|
+
+ [Providing Data](#providing-data)
|
54
|
+
+ [Controlling Timing](#controlling-timing)
|
55
|
+
+ [Rescheduling](#rescheduling)
|
56
|
+
+ [Cancelling](#cancelling)
|
57
|
+
* [Preventing Queue Workers](#preventing-queue-workers)
|
58
|
+
* [Test Mode](#test-mode)
|
59
|
+
* [Errors & Logging](#errors---logging)
|
60
|
+
* [Contributing](#contributing)
|
61
|
+
+ [Developers](#developers)
|
62
|
+
* [License](#license)
|
63
|
+
|
64
|
+
<!-- ToC generated with http://ecotrust-canada.github.io/markdown-toc/ -->
|
7
65
|
|
8
66
|
## Installation
|
9
67
|
|
@@ -17,224 +75,518 @@ And then run:
|
|
17
75
|
|
18
76
|
bundle install
|
19
77
|
|
20
|
-
##
|
21
|
-
|
78
|
+
## 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.
|
22
81
|
|
23
82
|
```ruby
|
24
|
-
|
25
|
-
|
26
|
-
env.define_queue(:cleanup, max_attempts: 3)
|
83
|
+
Procrastinator.setup do |env|
|
84
|
+
# call methods on env to set configurations
|
27
85
|
end
|
28
86
|
```
|
29
87
|
|
30
|
-
|
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:
|
31
95
|
|
32
96
|
```ruby
|
33
|
-
|
34
|
-
|
97
|
+
Procrastinator.setup do |env|
|
98
|
+
env.define_queue :greeting, SendWelcomeEmail
|
99
|
+
end
|
35
100
|
```
|
36
101
|
|
37
|
-
|
102
|
+
The first two parameters are the queue name symbol and the task class to run on that queue.
|
38
103
|
|
39
|
-
|
40
|
-
The setup phase first defines which queues are available and the persistence strategy to use for reading
|
41
|
-
and writing tasks. It then spins off a sub process for working on each queue within that environment.
|
104
|
+
You can also provide these keyword arguments:
|
42
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
|
+
```
|
43
128
|
|
44
|
-
|
45
|
-
The
|
129
|
+
### The Task Loader: `#load_with`
|
130
|
+
The task loader is a [strategy](https://en.wikipedia.org/wiki/Strategy_pattern) pattern object
|
131
|
+
that knows how to read and write tasks in your data storage (eg. file, database, etc).
|
46
132
|
|
47
|
-
|
133
|
+
Procrastinator comes with a simple CSV file task loader by default, but you are encouraged to build one that suits
|
134
|
+
your situation.
|
48
135
|
|
49
|
-
|
50
|
-
* `#create_task(data)` - Creates a task in your datastore. Receives a hash with keys `:queue`, `:run_at`, `:initial_run_at`, `:expire_at`, and `:task` as described in *Attributes Hash*
|
51
|
-
* `#update_task(attributes)` - Receives the Attributes Hash as the data to be saved
|
52
|
-
* `#delete_task(id)` - Deletes the task with the given id.
|
136
|
+
In setup, the environment's `#load_with` method expects an task loader instance:
|
53
137
|
|
54
|
-
|
138
|
+
```ruby
|
139
|
+
loader = MyTaskLoader.new('tasks.csv')
|
55
140
|
|
56
|
-
|
141
|
+
scheduler = Procrastinator.setup do |env|
|
142
|
+
# ... other setup stuff ...
|
57
143
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
144
|
+
env.load_with loader
|
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
|
150
|
+
```
|
151
|
+
|
152
|
+
For per-process resource management (eg. independent database connections), you can build and assign a
|
153
|
+
task loader in the `#each_process` block.
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
connection = SomeDatabaseLibrary.connect('my_app_development')
|
157
|
+
|
158
|
+
scheduler = Procrastinator.setup do |env|
|
159
|
+
env.load_with MyTaskLoader.new(connection)
|
160
|
+
|
161
|
+
env.each_process do
|
162
|
+
# make a fresh connection
|
163
|
+
connection.reconnect
|
164
|
+
env.load_with MyTaskLoader.new(connection)
|
165
|
+
end
|
166
|
+
|
167
|
+
# .. other setup stuff ...
|
168
|
+
end
|
169
|
+
```
|
170
|
+
|
171
|
+
A task loader class is required to implement *all* of the following four methods:
|
172
|
+
|
173
|
+
1. `#read(attributes)`
|
174
|
+
|
175
|
+
Returns a list of hashes from your datastore that match the given attributes hash. The search
|
176
|
+
attributes will be in their final form (eg. `:data` will already be serialized).
|
177
|
+
Each hash must contain the properties listed in [Task Data](#task-data) below.
|
178
|
+
|
179
|
+
2. `#create(queue:, run_at:, initial_run_at:, expire_at:, data:)`
|
180
|
+
|
181
|
+
Creates a task in your datastore. Receives a hash with [Task Data](#task-data) keys:
|
182
|
+
`:queue`, `:run_at`, `:initial_run_at`, `:expire_at`, and `:data`.
|
183
|
+
|
184
|
+
3. `#update(id, new_data)`
|
185
|
+
|
186
|
+
Saves the provided full [Task Data](#task-data) hash to your datastore.
|
187
|
+
|
188
|
+
4. `#delete(id)`
|
189
|
+
|
190
|
+
Deletes the task with the given identifier in your datastore.
|
191
|
+
|
192
|
+
<!-- This paragraph is here to allow people to google for the error keyword -->
|
193
|
+
If your loader is missing any of the above methods, Procrastinator will explode
|
194
|
+
with a `MalformedPersisterError` and you will be sad.
|
195
|
+
|
196
|
+
#### Task Data
|
197
|
+
These are the data fields for each individual scheduled task. When using the built-in task loader,
|
198
|
+
these are the field names. If you have a database, use this to inform your table schema.
|
199
|
+
|
200
|
+
| Hash Key | Type | Description |
|
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 |
|
64
206
|
| `:expire_at` | int | Unix timestamp of when to permanently fail the task because it is too late to be useful |
|
65
|
-
| `:attempts` | int | Number of times the task has tried to run; this should only be > 0 if the task fails
|
66
|
-
| `:last_fail_at` | int | Unix timestamp of when the most recent failure happened
|
67
|
-
| `:last_error` | string | Error message + bracktrace of the most recent failure. May be very long.
|
68
|
-
| `:
|
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.² |
|
69
211
|
|
70
|
-
|
212
|
+
> ¹ If `nil`, that indicates that it is permanently failed and will never run, either due to expiry or too many attempts.
|
71
213
|
|
72
|
-
|
73
|
-
|
214
|
+
> ² Serialized using YAML.dump and unserialized with YAML.safe_load. Keep to simple data types (eg. id numbers) to
|
215
|
+
> reduce storage space, eliminate redundancy, and reduce the chance of a serialization error.
|
74
216
|
|
75
|
-
|
76
|
-
|
77
|
-
* :update_period - Delay, in seconds, between refreshing the task list from the persister
|
78
|
-
* :max_tasks - The maximum number of tasks to run concurrently with multi-threading.
|
217
|
+
Notice that the times are all stored as unix epoch integer timestamps. This is to avoid confusion or conversion
|
218
|
+
errors with timezones or daylight savings.
|
79
219
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
220
|
+
### The Task Context: `#provide_context`
|
221
|
+
Whatever you give to `#provide_context` will be made available to your Task through the task attribute `:context`.
|
222
|
+
|
223
|
+
This can be useful for things like app containers, but you can use it for whatever you like.
|
224
|
+
|
225
|
+
```ruby
|
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
|
231
|
+
|
232
|
+
# ... and in your task ...
|
233
|
+
class MyTask
|
234
|
+
include Procrastinator::Task
|
235
|
+
|
236
|
+
task_attr :context
|
84
237
|
|
85
|
-
|
86
|
-
|
238
|
+
def run
|
239
|
+
puts "My context is: #{context}"
|
240
|
+
end
|
87
241
|
end
|
88
242
|
```
|
89
|
-
|
90
|
-
#### Sub-Processes
|
91
|
-
Each queue is worked in a separate process.
|
92
243
|
|
93
|
-
|
244
|
+
### Controlling Queue Processes: `#each_process`
|
245
|
+
In the setup block, use `#each_process` to configure details about the queue subprocesses.
|
246
|
+
|
247
|
+
#### The Subprocess Hook
|
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.
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
Procrastinator.setup do |env|
|
253
|
+
# ... other setup stuff ...
|
254
|
+
|
255
|
+
env.each_process do
|
256
|
+
# create process-specific resources here, like database connections
|
257
|
+
# (the parent process's connection could disappear, because they're asychnronous)
|
258
|
+
connection = SomeDatabase.connect('bob@mainframe/my_database')
|
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)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
```
|
266
|
+
|
267
|
+
NB: The `each_process` block **does not run in Test Mode**.
|
268
|
+
|
269
|
+
#### Naming Processes
|
270
|
+
Each queue subprocess is named after the queue it's working on, eg. `greeting-queue-worker` or
|
271
|
+
`thumbnails-queue-worker`.
|
94
272
|
|
95
|
-
|
273
|
+
If you're running multiple apps on the same machine, then you may want to distinguish which app the queue worker
|
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.
|
96
276
|
|
97
|
-
|
277
|
+
```ruby
|
278
|
+
scheduler = Procrastinator.setup do |env|
|
279
|
+
# ... other setup stuff ...
|
280
|
+
|
281
|
+
env.each_process(prefix: 'myapp')
|
282
|
+
end
|
283
|
+
```
|
284
|
+
|
285
|
+
#### Tracking Process IDs
|
286
|
+
The sub processes are tracked by saving their PIDs in files. You can set the directory where those files are saved:
|
98
287
|
|
99
288
|
```ruby
|
100
|
-
|
101
|
-
|
289
|
+
scheduler = Procrastinator.setup do |env|
|
290
|
+
# ... other setup stuff ...
|
291
|
+
|
292
|
+
env.each_process(pid_dir: '/var/run/myapp/')
|
102
293
|
end
|
103
294
|
```
|
104
295
|
|
105
|
-
|
106
|
-
|
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
|
+
## Tasks
|
300
|
+
Your task class is what actually gets run on the task queue. They will look something like this:
|
301
|
+
|
107
302
|
|
108
303
|
```ruby
|
109
|
-
|
110
|
-
|
304
|
+
class MyTask
|
305
|
+
include Procrastinator::Task
|
306
|
+
|
307
|
+
# Give any of these symbols to task_attr and they will become available as methods
|
308
|
+
# task_attr :data, :logger, :context, :scheduler
|
309
|
+
|
310
|
+
# Performs the core work of the task.
|
311
|
+
def run
|
312
|
+
# ... perform your task ...
|
313
|
+
end
|
314
|
+
|
315
|
+
|
316
|
+
# ========================================
|
317
|
+
# OPTIONAL HOOKS
|
318
|
+
#
|
319
|
+
# You can always omit any of the methods
|
320
|
+
# below. Only #run is mandatory.
|
321
|
+
#
|
322
|
+
# ========================================
|
323
|
+
|
324
|
+
# Called after the task has completed successfully.
|
325
|
+
# Receives the result of #run.
|
326
|
+
def success(run_result)
|
327
|
+
# ...
|
328
|
+
end
|
329
|
+
|
330
|
+
# Called after #run raises any StandardError (or subclass).
|
331
|
+
# Receives the raised error.
|
332
|
+
def fail(error)
|
333
|
+
# ...
|
334
|
+
end
|
335
|
+
|
336
|
+
# Called after either is true:
|
337
|
+
# 1. the time reported by Time.now is past the task's expire_at time.
|
338
|
+
# 2. the task has failed and the number of attempts is equal to or greater than the queue's `max_attempts`.
|
339
|
+
# In this case, #fail will not be executed, only #final_fail.
|
340
|
+
#
|
341
|
+
# When called, the task will be marked to never be run again.
|
342
|
+
# Receives the raised error.
|
343
|
+
def final_fail(error)
|
344
|
+
# ...
|
345
|
+
end
|
111
346
|
end
|
347
|
+
```
|
348
|
+
|
349
|
+
### Accessing Task Attributes
|
350
|
+
Include `Procrastinator::Task` in your task class and then use `task_attr` to register which task attributes your
|
351
|
+
task wants access to.
|
112
352
|
|
113
|
-
|
353
|
+
```ruby
|
354
|
+
class MyTask
|
355
|
+
include Procrastinator::Task
|
356
|
+
|
357
|
+
# declare the task attributes you care about by calling task_attr.
|
358
|
+
# You can use any of these symbols:
|
359
|
+
# :data, :logger, :context, :scheduler
|
360
|
+
task_attr :data, :logger
|
361
|
+
|
362
|
+
def run
|
363
|
+
# the attributes listed in task_attr become methods like attr_accessor
|
364
|
+
logger.info("The data for this task is #{data}")
|
365
|
+
end
|
366
|
+
end
|
114
367
|
```
|
115
368
|
|
116
|
-
|
369
|
+
* `:data`
|
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`).
|
385
|
+
|
386
|
+
|
387
|
+
### Retries
|
388
|
+
Failed tasks have their `run_at` rescheduled on an increasing delay (in seconds) according to this formula:
|
389
|
+
|
390
|
+
> 30 + (number_of_attempts)<sup>4</sup>
|
391
|
+
|
392
|
+
Situations that call `#fail` or `#final_fail` will cause the error timestamp and reason to be stored in `:last_fail_at`
|
393
|
+
and `:last_error`.
|
394
|
+
|
395
|
+
|
396
|
+
## Scheduling Tasks
|
397
|
+
To schedule tasks, just call `#delay` on the environment returned from `Procrastinator.setup`:
|
117
398
|
|
118
399
|
```ruby
|
119
|
-
|
120
|
-
env.define_queue
|
121
|
-
env.define_queue
|
400
|
+
scheduler = Procrastinator.setup do |env|
|
401
|
+
env.define_queue :reminder, EmailReminder
|
402
|
+
env.define_queue :thumbnail, CreateThumbnail
|
122
403
|
end
|
123
404
|
|
124
|
-
|
405
|
+
# Provide the queue name and any data you want passed in
|
406
|
+
scheduler.delay(:reminder, data: 'bob@example.com')
|
407
|
+
```
|
408
|
+
|
409
|
+
If you have only one queue, you can omit the queue name:
|
410
|
+
|
411
|
+
```ruby
|
412
|
+
scheduler = Procrastinator.setup do |env|
|
413
|
+
env.define_queue :reminder, EmailReminder
|
414
|
+
end
|
415
|
+
|
416
|
+
scheduler.delay(data: 'bob@example.com')
|
125
417
|
```
|
126
418
|
|
419
|
+
### Providing Data
|
420
|
+
Most tasks need some additional information to complete their work, like id numbers,
|
421
|
+
|
422
|
+
The `:data` parameter is serialized to string as YAML, so it's better to keep it as simple as possible. For example, if
|
423
|
+
you have a database instead of passing in a complex Ruby object, pass in just the primary key and reload it in the
|
424
|
+
task's `#run`. This will require less space in your database and avoids obsolete or duplicated information.
|
425
|
+
|
426
|
+
### Controlling Timing
|
127
427
|
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
|
128
|
-
to run at a precise time; the only promise is that the task will
|
428
|
+
to run at a precise time; the only promise is that the task will be attempted *after* `run_at` and before `expire_at`.
|
129
429
|
|
130
430
|
```ruby
|
131
|
-
|
132
|
-
|
133
|
-
|
431
|
+
# runs on or after 1 January 3000
|
432
|
+
scheduler.delay(:greeting, run_at: Time.new(3000, 1, 1), data: 'philip_j_fry@example.com')
|
433
|
+
|
434
|
+
# run_at defaults to right now:
|
435
|
+
scheduler.delay(:thumbnail, run_at: Time.now, data: 'shut_up_and_take_my_money.gif')
|
436
|
+
```
|
134
437
|
|
135
|
-
|
136
|
-
|
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
|
+
Setting `expire_at` to `nil` means it will never expire (but may still fail permanently if,
|
441
|
+
say, `max_attempts` is reached).
|
137
442
|
|
138
|
-
|
139
|
-
|
443
|
+
```ruby
|
444
|
+
# will not run at or after
|
445
|
+
scheduler.delay(:happy_birthday, expire_at: Time.new(2018, 03, 17, 12, 00, '-06:00'), data: 'contact@tenjin.ca'))
|
446
|
+
|
447
|
+
# expire_at defaults to nil:
|
448
|
+
scheduler.delay(:greeting, expire_at: nil, data: 'bob@example.com')
|
140
449
|
```
|
141
450
|
|
142
|
-
|
451
|
+
### Rescheduling
|
452
|
+
Call `#reschedule` with the queue name and some identifying
|
453
|
+
information, and then calling #to on that to provide the new time.
|
143
454
|
|
144
455
|
```ruby
|
145
|
-
|
146
|
-
env.define_queue
|
456
|
+
scheduler = Procrastinator.setup do |env|
|
457
|
+
env.define_queue :reminder, EmailReminder
|
147
458
|
end
|
148
459
|
|
149
|
-
|
460
|
+
scheduler.delay(:reminder, run_at: Time.parse('June 1'), data: 'bob@example.com')
|
461
|
+
|
462
|
+
# we can reschedule the task made above
|
463
|
+
scheduler.reschedule(:reminder, data: 'bob@example.com').to(run_at: Time.parse('June 20 12:00'))
|
464
|
+
|
465
|
+
# we can also change the expiry time
|
466
|
+
scheduler.reschedule(:reminder, data: 'bob@example.com').to(expire_at: Time.parse('June 23 12:00'))
|
150
467
|
|
151
|
-
#
|
152
|
-
|
468
|
+
# or both
|
469
|
+
scheduler.reschedule(:reminder, data: 'bob@example.com').to(run_at: Time.parse('June 20 12:00'),
|
470
|
+
expire_at: Time.parse('June 23 12:00'))
|
153
471
|
```
|
154
472
|
|
155
|
-
|
156
|
-
|
157
|
-
your task **must provide** a `#run` method:
|
473
|
+
Rescheduling updates the task's `:run_at` and `:initial_run_at` to a new value, if provided and/or
|
474
|
+
`:expire_at` to a new value if provided. A `RuntimeError` is raised if the resulting runtime is after the expiry.
|
158
475
|
|
159
|
-
|
476
|
+
It also resets `:attempts` to `0` and clears both `:last_error` and `:last_error_at` to `nil`.
|
160
477
|
|
161
|
-
|
478
|
+
Rescheduling will not change `:id`, `:queue` or `:data`.
|
162
479
|
|
163
|
-
|
164
|
-
|
165
|
-
* `#final_fail(logger, error)` - run after the task has failed for the last time because either:
|
166
|
-
1. the number of attempts is >= the `max_attempts` defined for the queue; or
|
167
|
-
2. the time reported by `Time.now` is past the task's `expire_at` time.
|
480
|
+
### Cancelling
|
481
|
+
Call `#cancel` with the queue name and some identifying information to narrow the search to a single task.
|
168
482
|
|
169
|
-
|
483
|
+
```ruby
|
484
|
+
scheduler = Procrastinator.setup do |env|
|
485
|
+
env.define_queue :reminder, EmailReminder
|
486
|
+
end
|
170
487
|
|
171
|
-
|
488
|
+
scheduler.delay(:reminder, run_at: Time.parse('June 1'), data: 'bob@example.com')
|
172
489
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
n = the number of attempts
|
490
|
+
# we can cancel the task made above using whatever we know about it, like the saved :data
|
491
|
+
scheduler.reschedule(:reminder, data: 'bob@example.com')
|
177
492
|
|
178
|
-
|
493
|
+
# or multiple attributes
|
494
|
+
scheduler.reschedule(:reminder, run_at: Time.parse('June 1'), data: 'bob@example.com')
|
179
495
|
|
180
|
-
|
181
|
-
|
182
|
-
|
496
|
+
# you could also use the id number directly, if you have it
|
497
|
+
scheduler.reschedule(:reminder, id: 137)
|
498
|
+
```
|
183
499
|
|
184
|
-
|
185
|
-
|
500
|
+
## Preventing Queue Workers
|
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.
|
504
|
+
|
505
|
+
To kill workers, set `PROCRASTINATOR_STOP` to 1 and then restart your app. It will
|
506
|
+
find all the old subprocesses and halt them, but not create new ones.
|
507
|
+
|
508
|
+
This is distinct from Test Mode, because Test Mode will still create workers, but on the
|
509
|
+
same process as the original caller to setup.
|
510
|
+
|
511
|
+
## Test Mode
|
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.
|
514
|
+
|
515
|
+
Set `Procrastinator.test_mode = true` before setup, or call `#enable_test_mode` on
|
516
|
+
the procrastination environment:
|
186
517
|
|
187
518
|
```ruby
|
188
519
|
# all further calls to `Procrastinator.setup` will produce a procrastination environment where Test Mode is enabled
|
189
520
|
Procrastinator.test_mode = true
|
190
521
|
|
191
|
-
# or you can also enable it
|
192
|
-
|
522
|
+
# or you can also enable it in the setup
|
523
|
+
scheduler = Procrastinator.setup do |env|
|
193
524
|
env.enable_test_mode
|
194
525
|
|
195
|
-
# other settings...
|
526
|
+
# ... other settings...
|
196
527
|
end
|
197
528
|
```
|
198
529
|
|
199
|
-
|
530
|
+
Then in your tests, tell the procrastinator environment to work off one item:
|
200
531
|
|
201
532
|
```
|
202
|
-
#
|
533
|
+
# execute one task on all queues
|
203
534
|
env.act
|
204
535
|
|
205
|
-
# provide queue names to
|
206
|
-
|
536
|
+
# or provide queue names to execute one task on those specific queues
|
537
|
+
scheduler.act(:cleanup, :email)
|
207
538
|
```
|
208
539
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
540
|
+
## Errors & Logging
|
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`.
|
543
|
+
|
544
|
+
Each queue worker also writes its own log using the Ruby
|
545
|
+
[Logger class](https://ruby-doc.org/stdlib-2.5.1/libdoc/logger/rdoc/Logger.html).
|
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.
|
214
548
|
|
215
549
|
```ruby
|
216
|
-
|
217
|
-
|
550
|
+
scheduler = Procrastinator.setup do |env|
|
551
|
+
# ... other setup stuff ...
|
552
|
+
|
553
|
+
# you can set custom log directory and level:
|
554
|
+
env.log_inside '/var/log/myapp/'
|
555
|
+
env.log_at_level Logger::DEBUG
|
556
|
+
|
557
|
+
# these are the defaults:
|
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
|
218
563
|
end
|
219
564
|
```
|
220
|
-
|
221
|
-
Each worker creates its own log named after the queue it is working on (eg. `log/email-queue-worker.log`). The default
|
222
|
-
directory is `./log/`, relative to wherever the application is running. Logging will not occur at all if `log_dir` is
|
223
|
-
assigned a falsey value.
|
224
565
|
|
225
|
-
The
|
226
|
-
|
227
|
-
|
228
|
-
It logs process start at level `INFO`, process termination due to parent disppearance at level `ERROR` and task hooks
|
229
|
-
`#success`, `#fail`, and `#final_fail` are at a level `DEBUG`.
|
566
|
+
The logger can be accessed in your tasks by including Procrastinator::Task in your task class and then calling
|
567
|
+
`task_attr :logger`.
|
230
568
|
|
231
569
|
```ruby
|
232
|
-
|
233
|
-
|
234
|
-
|
570
|
+
class MyTask
|
571
|
+
include Procrastinator::Task
|
572
|
+
|
573
|
+
task_attr :logger
|
574
|
+
|
575
|
+
def run
|
576
|
+
logger.info('This task got run. Hooray!')
|
577
|
+
end
|
235
578
|
end
|
236
579
|
```
|
237
580
|
|
581
|
+
**Default Log Messages**
|
582
|
+
|
583
|
+
|event |level |
|
584
|
+
|--------------------|-------|
|
585
|
+
|process started | INFO |
|
586
|
+
|#success called | DEBUG |
|
587
|
+
|#fail called | DEBUG |
|
588
|
+
|#final_fail called | DEBUG |
|
589
|
+
|
238
590
|
## Contributing
|
239
591
|
Bug reports and pull requests are welcome on GitHub at
|
240
592
|
[https://github.com/TenjinInc/procrastinator](https://github.com/TenjinInc/procrastinator).
|
@@ -242,7 +594,9 @@ Bug reports and pull requests are welcome on GitHub at
|
|
242
594
|
This project is intended to be a friendly space for collaboration, and contributors are expected to adhere to the
|
243
595
|
[Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
244
596
|
|
245
|
-
|
597
|
+
Play nice.
|
598
|
+
|
599
|
+
### Developers
|
246
600
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can
|
247
601
|
also run `bin/console` for an interactive prompt that will allow you to experiment.
|
248
602
|
|