procrastinator 0.6.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7162a00d304b7e97d03893a92fbe45b11534e305
4
- data.tar.gz: e6e0fe48cf1cc1940aed6f85ccf889e36d40d0ff
3
+ metadata.gz: b3428d03ab5288918a2d1305ca9e8234303ba0fd
4
+ data.tar.gz: 75ab4bae088f10401a976a29ad84fbfb1b9a6bf7
5
5
  SHA512:
6
- metadata.gz: 3d23c3bba5d8df4963a7a33cbe70606dba64b86cba3f0c5861d37a9a878728e180bbea1afa196431fef7f2121e2a073cb2450989970a3b1c899378bd82f5a101
7
- data.tar.gz: b2c35bafb8c18af846ab8199a923ef9bc9d09d81c731c3d9c0b46a7ca7f1ab1c87adf8f9f0ff1768341c86d9a59044b6f94db8230bcba6da15c68f0e849831df
6
+ metadata.gz: e9e3ebd949697326212b199bbf17e9424a8dac47e4ac7ebbfb3154764330462fc46931ccc67aee4fc6c81a70d4e5513f7c45b66f9ac63d272987a1dc79f62519
7
+ data.tar.gz: 99df6bdc69e8af68572fa1619f5ecaaeb0083853e60c2ec2f9da7f5a644f5f9bfffc4e54ff64ac0eda3004c93ca7c4a4cb4c438816dc9b044025f8f2da53c311
@@ -0,0 +1,7 @@
1
+ inherit_from: ../.rubocop.yml
2
+
3
+ AllCops:
4
+ Exclude:
5
+ - 'bin/*'
6
+
7
+ TargetRubyVersion: 2.3
@@ -1 +1 @@
1
- ruby-2.1.2
1
+ ruby-2.4.2
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in procrastinator.gemspec
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
- Procrastinator is a framework-independent job scheduling gem to allow your app to put stuff of until later. It creates
4
- a subprocess for each queue to performs tasks at the designated times. Or maybe later, depending on how busy it is.
6
+ If the task fails to complete or takes too long, it delays it and tries again later.
5
7
 
6
- Don't worry, it'll get done eventually.
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
- ## Usage
21
- Setup a procrastination environment:
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
- procrastinator = Procrastinator.setup(TaskPersister.new) do |env|
25
- env.define_queue(:email)
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
- And then delay some tasks:
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
- procrastinator.delay(queue: :email, task: EmailGreeting.new('bob@example.com'))
34
- procrastinator.delay(queue: :cleanup, run_at: Time.now + 3600, task: ClearTempData.new)
97
+ Procrastinator.setup do |env|
98
+ env.define_queue :greeting, SendWelcomeEmail
99
+ end
35
100
  ```
36
101
 
37
- Read on for more details on each step.
102
+ The first two parameters are the queue name symbol and the task class to run on that queue.
38
103
 
39
- ### Setup Phase
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
- #### Declaring a Persistence Strategy
45
- The persister instance is the last step between Procrastinator and your data storage (eg. database). As core Procrastinator is framework-agnostic, it needs you to provide an object that knows how to read and write task data.
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
- Your [strategy](https://en.wikipedia.org/wiki/Strategy_pattern) class is required to provide the following methods:
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
- * `#read_tasks(queue_name)` - Returns a list of hashes from your data storage. Each hash must contains the properites of one task, as seen in the *Attributes Hash*
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
- If the strategy does not have all of these methods, Procrastinator will explode with a `MalformedPersisterError` and you will be sad.
138
+ ```ruby
139
+ loader = MyTaskLoader.new('tasks.csv')
55
140
 
56
- ***Attributes Hash***
141
+ scheduler = Procrastinator.setup do |env|
142
+ # ... other setup stuff ...
57
143
 
58
- | Hash Key | Type | Description |
59
- |-------------------|--------| --------------------------------------------------------------------------------------|
60
- | `:id` | int | Unique identifier for this exact task |
61
- | `:queue` | symbol | Name of the queue the task is inside |
62
- | `:run_at` | int | Unix timestamp of when to next attempt running the task |
63
- | `:initial_run_at` | int | Unix timestamp of the original run_at; before the first attempt, this is equal to run_at |
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
- | `:task` | string | YAML-dumped ruby object definition of the task. |
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
- Notice that the times are all given as unix epoch timestamps. This is to avoid any confusion with timezones, and it is recommended that you store times in this manner for the same reason.
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
- #### Defining Queues
73
- `Procrastinator.setup` requires a block be provided, and that in the block call `#define_queue` be called on the provided environment. Define queue takes a queue name symbol and these properies as a hash
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
- * :timeout - Time, in seconds, after which it should fail tasks in this queue for taking too long to execute.
76
- * :max_attempts - Maximum number of attempts for tasks in this queue. If attempts is >= max_attempts, the task will be final_failed and marked to never run again
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
- **Examples**
81
- ```ruby
82
- Procrastinator.setup(some_persister) do |env|
83
- env.define_queue(:email)
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
- # with all defaults set explicitly
86
- env.define_queue(:email, timeout: 3600, max_attempts: 20, update_period: 10, max_tasks: 10)
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
- <!-- , and each process multi-threaded to handle more than one task at a time. This should help prevent a single task from clogging up the whole queue, or a single queue clogging up the entire system. -->
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
- The sub-processes checks that the parent process is still alive every 5 seconds. If there is no process with the parent's PID, the sub-process will self-exit.
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
- Sub-processes can be given a name prefix with the process_prefix method:
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
- procrastinator = Procrastinator.setup(task_persister) do |env|
101
- env.process_prefix('myapp')
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
- ###Scheduling Tasks For Later
106
- Procrastinator will let you be lazy:
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
- procrastinator = Procrastinator.setup(task_persister) do |env|
110
- env.define_queue(:email)
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
- procrastinator.delay(task: EmailReminder.new('bob@example.com'))
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
- ... unless there are multiple queues defined. Thne you must provide a queue name with your task:
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
- procrastinator = Procrastinator.setup(task_persister) do |env|
120
- env.define_queue(:email)
121
- env.define_queue(:cleanup)
400
+ scheduler = Procrastinator.setup do |env|
401
+ env.define_queue :reminder, EmailReminder
402
+ env.define_queue :thumbnail, CreateThumbnail
122
403
  end
123
404
 
124
- procrastinator.delay(:email, task: EmailReminder.new('bob@example.com'))
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 get run some time after `run_at`, unless it's after `expire_at`.
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
- procrastinator = Procrastinator.setup(task_persister) do |env|
132
- env.define_queue(:email)
133
- end
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
- # run on or after 1 January 3000
136
- procrastinator.delay(run_at: Time.new(3000, 1, 1), task: EmailGreeting.new('philip_j_fry@example.com'))
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
- # explicitly setting default run_at
139
- procrastinator.delay(run_at: Time.now, task: EmailReminder.new('bob@example.com'))
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
- You can also set an `expire_at` deadline on when to run a task. If the task has not been run before `expire_at` is passed, then it will be final-failed the next time it is attempted. Setting `expire_at` to `nil` will mean it will never expire (but may still fail permanently if, say, `max_attempts` is reached).
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
- procrastinator = Procrastinator.setup(task_persister) do |env|
146
- env.define_queue(:email)
456
+ scheduler = Procrastinator.setup do |env|
457
+ env.define_queue :reminder, EmailReminder
147
458
  end
148
459
 
149
- procrastinator.delay(expire_at: , task: EmailGreeting.new('bob@example.com'))
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
- # explicitly setting default
152
- procrastinator.delay(expire_at: nil, task: EmailGreeting.new('bob@example.com'))
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
- #### Task Definition
156
- Like the persister provided to `.setup`, your task is a strategy object that fills in the details of what to do. For this,
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
- * `#run` - Performs the core work of the task.
476
+ It also resets `:attempts` to `0` and clears both `:last_error` and `:last_error_at` to `nil`.
160
477
 
161
- You may also optionally provide these hook methods, which are run during different points in the process:
478
+ Rescheduling will not change `:id`, `:queue` or `:data`.
162
479
 
163
- * `#success(logger)` - run after the task has completed successfully
164
- * `#fail(logger, error)` - run after the task has failed due to `#run` producing a `StandardError` or subclass.
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
- If a task reaches `#final_fail` it will be marked to never be run again.
483
+ ```ruby
484
+ scheduler = Procrastinator.setup do |env|
485
+ env.define_queue :reminder, EmailReminder
486
+ end
170
487
 
171
- ***Task Failure & Rescheduling***
488
+ scheduler.delay(:reminder, run_at: Time.parse('June 1'), data: 'bob@example.com')
172
489
 
173
- Tasks that fail have their `run_at` rescheduled on an increasing delay **(in seconds)** according to this formula:
174
- * 30 + n<sup>4</sup>
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
- Both failing and final_failing will cause the error timestamp and reason to be stored in `:last_fail_at` and `:last_error`.
493
+ # or multiple attributes
494
+ scheduler.reschedule(:reminder, run_at: Time.parse('June 1'), data: 'bob@example.com')
179
495
 
180
- ### Testing With Procrastinator
181
- Procrastinator uses multi-threading and multi-processing internally, which is a nightmare for testing. Fortunately for you,
182
- Test Mode will disable all of that, and rely on your tests to tell it when to tick.
496
+ # you could also use the id number directly, if you have it
497
+ scheduler.reschedule(:reminder, id: 137)
498
+ ```
183
499
 
184
- Enable Test Mode by setting `Procrastinator.test_mode` to `true` before setting up, or by calling enable_test_mode on
185
- the procrastination environment.
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 directly in the setup
192
- env = Procrastinator.setup do |env|
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
- In your tests, tell the procrastinator environment to work off one item from its queues:
530
+ Then in your tests, tell the procrastinator environment to work off one item:
200
531
 
201
532
  ```
202
- # works one task on all queues
533
+ # execute one task on all queues
203
534
  env.act
204
535
 
205
- # provide queue names to works one task on just those queues
206
- env.act(:cleanup, :email)
536
+ # or provide queue names to execute one task on those specific queues
537
+ scheduler.act(:cleanup, :email)
207
538
  ```
208
539
 
209
- ### Logging
210
- Logging is crucial to knowing what went wrong in an application after the fact, and because Procrastinator runs workers
211
- in separate processes, providing a logger instance isn't really an option.
212
-
213
- Instead, provide a directory that your Procrastinator instance should write log entries into:
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
- procrastinator = Procrastinator.setup do |env|
217
- env.log_dir('log/')
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 logging level can be set using `log_level` and a value from the Ruby standard library
226
- [Logger class](https://ruby-doc.org/stdlib-2.2.3/libdoc/logger/rdoc/Logger.html) (eg. `Logger::WARN`, `Logger::DEBUG`, etc.).
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
- procrastinator = Procrastinator.setup do |env|
233
- env.log_dir('log/')
234
- env.log_level(Logger::INFO) # setting the default explicity
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
- ### Core Developers
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