procrastinator 0.6.1 → 1.0.0.pre.rc2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,9 +1,80 @@
1
1
  # Procrastinator
2
2
 
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.
3
+ Procrastinator is a pure Ruby job scheduling gem. Put off tasks until later or for another process to handle.
5
4
 
6
- Don't worry, it'll get done eventually.
5
+ Tasks are can be rescheduled and retried after failures, and you can use whatever storage mechanism is needed.
6
+
7
+ ## Big Picture
8
+
9
+ If you have tasks like this:
10
+
11
+ ```ruby
12
+ # Sends a welcome email
13
+ class SendWelcomeEmail
14
+ def run
15
+ # ... etc
16
+ end
17
+ end
18
+ ```
19
+
20
+ Setup a procrastination environment like:
21
+
22
+ ```ruby
23
+ scheduler = Procrastinator.setup do |env|
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
30
+ end
31
+ ```
32
+
33
+ Put jobs off until later:
34
+
35
+ ```ruby
36
+ scheduler.delay(:greeting, data: 'bob@example.com')
37
+
38
+ scheduler.delay(:thumbnail, data: {file: 'full_image.png', width: 100, height: 100})
39
+
40
+ scheduler.delay(:send_birthday_email, run_at: Time.now + 3600, data: {user_id: 5})
41
+ ```
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
+
51
+ ## Contents
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)
76
+
77
+ <!-- ToC generated with http://ecotrust-canada.github.io/markdown-toc/ -->
7
78
 
8
79
  ## Installation
9
80
 
@@ -17,238 +88,515 @@ And then run:
17
88
 
18
89
  bundle install
19
90
 
20
- ## Usage
21
- Setup a procrastination environment:
91
+ ## Setup
92
+
93
+ `Procrastinator.setup` allows you to define which queues are available and other settings.
22
94
 
23
95
  ```ruby
24
- procrastinator = Procrastinator.setup(TaskPersister.new) do |env|
25
- env.define_queue(:email)
26
- env.define_queue(:cleanup, max_attempts: 3)
96
+ require 'procrastinator'
97
+
98
+ scheduler = Procrastinator.setup do |config|
99
+ # ...
27
100
  end
28
101
  ```
29
102
 
30
- And then delay some tasks:
103
+ It then returns a `Scheduler` that your code can use to schedule tasks or tell to start working.
104
+
105
+ * See [Scheduling Tasks](#scheduling-tasks)
106
+ * See [Start Working](#start-working)
107
+
108
+ ### Defining Queues
109
+
110
+ In setup, call `#define_queue` with a symbol name and the class that performs those jobs:
111
+
112
+ ```ruby
113
+ # You must provide a queue name and the class that handles those jobs
114
+ config.define_queue :greeting, SendWelcomeEmail
115
+
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
118
+
119
+ # all defaults set explicitly
120
+ config.define_queue :greeting, SendWelcomeEmail, store: 'procrastinator.csv', timeout: 3600, max_attempts: 20, update_period: 10
121
+ ```
122
+
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).
31
136
 
32
137
  ```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)
138
+ task_store = ReminderStore.new # eg. some SQL task storage class you wrote
139
+
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')
145
+ end
35
146
  ```
36
147
 
37
- Read on for more details on each step.
148
+ A task store is required to implement *all* of the following methods or else it will raise a
149
+ `MalformedPersisterError`:
150
+
151
+ 1. `#read(attributes)`
152
+
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
+
157
+ 2. `#create(queue:, run_at:, initial_run_at:, expire_at:, data:)`
158
+
159
+ Saves a task in your storage. Receives a hash with [Task Data](#task-data) keys:
160
+ `:queue`, `:run_at`, `:initial_run_at`, `:expire_at`, and `:data`.
161
+
162
+ 3. `#update(id, new_data)`
38
163
 
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.
164
+ Saves the provided full [Task Data](#task-data) hash to your datastore.
42
165
 
166
+ 4. `#delete(id)`
43
167
 
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.
168
+ Deletes the task with the given identifier from storage
46
169
 
47
- Your [strategy](https://en.wikipedia.org/wiki/Strategy_pattern) class is required to provide the following methods:
170
+ Procrastinator comes with a simple CSV file task store by default, but you are encouraged to build one that suits your
171
+ situation.
48
172
 
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.
173
+ _Warning_: Task stores shared between queues **must** be thread-safe if using threaded or daemonized work modes.
53
174
 
54
- If the strategy does not have all of these methods, Procrastinator will explode with a `MalformedPersisterError` and you will be sad.
175
+ #### Data Fields
55
176
 
56
- ***Attributes Hash***
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.
57
179
 
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 |
64
- | `: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. |
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. |
69
191
 
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.
192
+ > ¹ `nil` indicates that it is permanently failed and will never run, either due to expiry or too many attempts.
71
193
 
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
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.
74
197
 
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.
198
+ Times are all handled as [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) formatted strings.
79
199
 
80
- **Examples**
81
- ```ruby
82
- Procrastinator.setup(some_persister) do |env|
83
- env.define_queue(:email)
84
-
85
- # with all defaults set explicitly
86
- env.define_queue(:email, timeout: 3600, max_attempts: 20, update_period: 10, max_tasks: 10)
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)
87
211
  end
88
212
  ```
89
-
90
- #### Sub-Processes
91
- Each queue is worked in a separate process.
92
-
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. -->
94
213
 
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.
214
+ #### Shared Task Stores
96
215
 
97
- Sub-processes can be given a name prefix with the process_prefix method:
216
+ When there are tasks use the same storage, you can wrap them in a `with_store` block.
98
217
 
99
218
  ```ruby
100
- procrastinator = Procrastinator.setup(task_persister) do |env|
101
- env.process_prefix('myapp')
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)
226
+ end
227
+
228
+ # and this will not use it
229
+ config.define_queue(:thumbnails, ThumbnailTask)
102
230
  end
103
231
  ```
104
232
 
105
- ###Scheduling Tasks For Later
106
- Procrastinator will let you be lazy:
233
+ ### Task Container
234
+
235
+ Whatever is given to `#provide_container` will available to Tasks through the task attribute `:container`.
236
+
237
+ This can be useful for things like app containers, but you can use it for whatever you like.
107
238
 
108
239
  ```ruby
109
- procrastinator = Procrastinator.setup(task_persister) do |env|
110
- env.define_queue(:email)
240
+ Procrastinator.setup do |env|
241
+ env.provide_container lunch: 'Lasagna'
242
+
243
+ # .. other setup stuff ...
111
244
  end
112
245
 
113
- procrastinator.delay(task: EmailReminder.new('bob@example.com'))
246
+ # ... and in your task ...
247
+ class LunchTask
248
+ attr_accessor :logger, :scheduler, :container
249
+
250
+ def run
251
+ logger.info("Today's Lunch is: #{ container[:lunch] }")
252
+ end
253
+ end
114
254
  ```
115
255
 
116
- ... unless there are multiple queues defined. Thne you must provide a queue name with your task:
256
+ ## Tasks
257
+
258
+ Your task class is what actually gets run on the task queue. They'll look like this:
117
259
 
118
260
  ```ruby
119
- procrastinator = Procrastinator.setup(task_persister) do |env|
120
- env.define_queue(:email)
121
- env.define_queue(:cleanup)
122
- end
123
261
 
124
- procrastinator.delay(:email, task: EmailReminder.new('bob@example.com'))
262
+ class MyTask
263
+ # These attributes will be assigned by Procrastinator when the task is run.
264
+ attr_accessor :logger, :scheduler, :container, :data
265
+
266
+ # Performs the core work of the task.
267
+ def run
268
+ # ... perform your task ...
269
+ end
270
+
271
+ # ========================================
272
+ # OPTIONAL HOOKS
273
+ #
274
+ # You can always omit any of the methods
275
+ # below. Only #run is mandatory.
276
+ #
277
+ # ========================================
278
+
279
+ # Called after the task has completed successfully.
280
+ # Receives the result of #run.
281
+ def success(run_result)
282
+ # ...
283
+ end
284
+
285
+ # Called after #run raises any StandardError (or subclass).
286
+ # Receives the raised error.
287
+ def fail(error)
288
+ # ...
289
+ end
290
+
291
+ # Called after either is true:
292
+ # 1. the time reported by Time.now is past the task's expire_at time.
293
+ # 2. the task has failed and the number of attempts is equal to or greater than the queue's `max_attempts`.
294
+ # In this case, #fail will not be executed, only #final_fail.
295
+ #
296
+ # When called, the task will be marked to never be run again.
297
+ # Receives the raised error.
298
+ def final_fail(error)
299
+ # ...
300
+ end
301
+ end
125
302
  ```
126
303
 
127
- 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`.
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`).
129
326
 
130
327
  ```ruby
131
- procrastinator = Procrastinator.setup(task_persister) do |env|
132
- env.define_queue(:email)
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)
133
337
  end
338
+ ```
134
339
 
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'))
340
+ The logger can be accessed in your tasks by calling `logger` or `@logger`.
137
341
 
138
- # explicitly setting default run_at
139
- procrastinator.delay(run_at: Time.now, task: EmailReminder.new('bob@example.com'))
342
+ ```ruby
343
+
344
+ class MyTask
345
+ attr_accessor :logger, :scheduler, :container
346
+
347
+ def run
348
+ logger.info('This task got run. Hooray!')
349
+ end
350
+ end
140
351
  ```
141
352
 
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).
353
+ Some events are always logged by default:
354
+
355
+ |event |level |
356
+ |--------------------|-------|
357
+ |process started | INFO |
358
+ |#success called | DEBUG |
359
+ |#fail called | DEBUG |
360
+ |#final_fail called | DEBUG |
361
+
362
+ ## Scheduling Tasks
363
+
364
+ To schedule tasks, just call `#delay` on the environment returned from `Procrastinator.setup`:
143
365
 
144
366
  ```ruby
145
- procrastinator = Procrastinator.setup(task_persister) do |env|
146
- env.define_queue(:email)
367
+ scheduler = Procrastinator.setup do |env|
368
+ env.define_queue :reminder, EmailReminder
369
+ env.define_queue :thumbnail, CreateThumbnail
147
370
  end
148
371
 
149
- procrastinator.delay(expire_at: , task: EmailGreeting.new('bob@example.com'))
372
+ # Provide the queue name and any data you want passed in
373
+ scheduler.delay(:reminder, data: 'bob@example.com')
374
+ ```
375
+
376
+ If there is only one queue, you may omit the queue name:
377
+
378
+ ```ruby
379
+ scheduler = Procrastinator.setup do |env|
380
+ env.define_queue :reminder, EmailReminder
381
+ end
150
382
 
151
- # explicitly setting default
152
- procrastinator.delay(expire_at: nil, task: EmailGreeting.new('bob@example.com'))
383
+ scheduler.delay(data: 'bob@example.com')
153
384
  ```
154
385
 
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:
386
+ ### Providing Data
158
387
 
159
- * `#run` - Performs the core work of the task.
388
+ Most tasks need some additional information to complete their work, like id numbers,
160
389
 
161
- You may also optionally provide these hook methods, which are run during different points in the process:
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.
162
393
 
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.
394
+ ### Scheduling
168
395
 
169
- If a task reaches `#final_fail` it will be marked to never be run again.
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
397
+ to run at a precise time; the only promise is that the task will be attempted *after* `run_at` and before `expire_at`.
170
398
 
171
- ***Task Failure & Rescheduling***
399
+ ```ruby
400
+ # runs on or after 1 January 3000
401
+ scheduler.delay(:greeting, run_at: Time.new(3000, 1, 1), data: 'philip_j_fry@example.com')
172
402
 
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
403
+ # run_at defaults to right now:
404
+ scheduler.delay(:thumbnail, run_at: Time.now, data: 'shut_up_and_take_my_money.gif')
405
+ ```
177
406
 
178
- Both failing and final_failing will cause the error timestamp and reason to be stored in `:last_fail_at` and `:last_error`.
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).
179
410
 
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.
411
+ ```ruby
412
+ # will not run at or after
413
+ scheduler.delay(:happy_birthday, expire_at: Time.new(2018, 03, 17, 12, 00, '-06:00'), data: 'contact@tenjin.ca')
183
414
 
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.
415
+ # expire_at defaults to nil:
416
+ scheduler.delay(:greeting, expire_at: nil, data: 'bob@example.com')
417
+ ```
418
+
419
+ ### Rescheduling
420
+
421
+ Call `#reschedule` with the queue name and some identifying information, and then calling #to on that to provide the new
422
+ time.
186
423
 
187
424
  ```ruby
188
- # all further calls to `Procrastinator.setup` will produce a procrastination environment where Test Mode is enabled
189
- Procrastinator.test_mode = true
190
-
191
- # or you can also enable it directly in the setup
192
- env = Procrastinator.setup do |env|
193
- env.enable_test_mode
194
-
195
- # other settings...
425
+ scheduler = Procrastinator.setup do |env|
426
+ env.define_queue :reminder, EmailReminder
196
427
  end
428
+
429
+ scheduler.delay(:reminder, run_at: Time.parse('June 1'), data: 'bob@example.com')
430
+
431
+ # we can reschedule the task made above
432
+ scheduler.reschedule(:reminder, data: 'bob@example.com').to(run_at: Time.parse('June 20 12:00'))
433
+
434
+ # we can also change the expiry time
435
+ scheduler.reschedule(:reminder, data: 'bob@example.com').to(expire_at: Time.parse('June 23 12:00'))
436
+
437
+ # or both
438
+ scheduler.reschedule(:reminder, data: 'bob@example.com').to(run_at: Time.parse('June 20 12:00'),
439
+ expire_at: Time.parse('June 23 12:00'))
197
440
  ```
198
441
 
199
- In your tests, tell the procrastinator environment to work off one item from its queues:
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.
450
+
451
+ ### Retries
452
+
453
+ Failed tasks have their `run_at` rescheduled on an increasing delay (in seconds) according to this formula:
454
+
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`.
459
+
460
+ ### Cancelling
200
461
 
462
+ Call `#cancel` with the queue name and some identifying information to narrow the search to a single task.
463
+
464
+ ```ruby
465
+ scheduler = Procrastinator.setup do |env|
466
+ env.define_queue :reminder, EmailReminder
467
+ end
468
+
469
+ scheduler.delay(:reminder, run_at: Time.parse('June 1'), data: 'bob@example.com')
470
+
471
+ # we can cancel the task made above using whatever we know about it, like the saved :data
472
+ scheduler.reschedule(:reminder, data: 'bob@example.com')
473
+
474
+ # or multiple attributes
475
+ scheduler.reschedule(:reminder, run_at: Time.parse('June 1'), data: 'bob@example.com')
476
+
477
+ # you could also use the id number directly, if you have it
478
+ scheduler.reschedule(:reminder, id: 137)
201
479
  ```
202
- # works one task on all queues
203
- env.act
204
480
 
205
- # provide queue names to works one task on just those queues
206
- env.act(:cleanup, :email)
481
+ ## Working on Tasks
482
+
483
+ Use the scheduler object returned by setup to `#work` queues **serially**, **threaded**, or **daemonized**.
484
+
485
+ ### Serial Working
486
+
487
+ Working serially performs a task from each queue directly. There is no multithreading or daemonizing.
488
+
489
+ Work serially for TDD tests or other situations you need close direct control.
490
+
491
+ ```ruby
492
+ # work just one task, no threading
493
+ scheduler.work.serially
494
+
495
+ # work the first five tasks
496
+ scheduler.work.serially(steps: 5)
497
+
498
+ # only work tasks on greeting and reminder queues
499
+ scheduler.work(:greeting, :reminders).serially(steps: 2)
207
500
  ```
208
501
 
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:
502
+ ### Threaded Working
503
+
504
+ Threaded working will spawn a worker thread per queue.
505
+
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.
214
508
 
215
509
  ```ruby
216
- procrastinator = Procrastinator.setup do |env|
217
- env.log_dir('log/')
218
- 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
219
518
  ```
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
519
 
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.).
520
+ ### Daemonized Working
521
+
522
+ Daemonized working **consumes the current process** and then proceeds with threaded working in the new daemon.
227
523
 
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`.
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
230
526
 
231
527
  ```ruby
232
- procrastinator = Procrastinator.setup do |env|
233
- env.log_dir('log/')
234
- env.log_level(Logger::INFO) # setting the default explicity
528
+ # work tasks forever as a headless daemon process.
529
+ scheduler.work.daemonized!
530
+
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
235
544
  end
236
545
  ```
237
546
 
547
+ Procrastinator endeavours to be thread-safe and support concurrency, but this flexibility allows for many possible
548
+ combinations.
549
+
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.
580
+
238
581
  ## Contributing
239
- Bug reports and pull requests are welcome on GitHub at
582
+
583
+ Bug reports and pull requests are welcome on GitHub at
240
584
  [https://github.com/TenjinInc/procrastinator](https://github.com/TenjinInc/procrastinator).
241
-
242
- 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
243
587
  [Contributor Covenant](http://contributor-covenant.org) code of conduct.
244
588
 
245
- ### Core Developers
246
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can
589
+ Play nice.
590
+
591
+ ### Developers
592
+
593
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can
247
594
  also run `bin/console` for an interactive prompt that will allow you to experiment.
248
595
 
249
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
250
- 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,
251
598
  push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
252
599
 
253
600
  ## License
601
+
254
602
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).