procrastinator 0.9.0 → 1.0.0.pre.rc2

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