procrastinator 0.9.0 → 1.0.0.pre.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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).