toro 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,2 +1,533 @@
1
1
  Toro
2
2
  ====
3
+ Full-featured background processing for Ruby & PostgreSQL
4
+
5
+ Overview
6
+ --------
7
+
8
+ Toro is a job queueing system (similar to Sidekiq or Resque) that runs on PostgreSQL and focuses on concurrency, visibility, extensibility, and durability:
9
+
10
+ #### Concurrency
11
+ * Toro can run many jobs simultaneously in a single process (a la Sidekiq; it uses [Celluloid](https://github.com/celluloid/celluloid))
12
+
13
+ #### Visibility
14
+
15
+ An extensive dashboard:
16
+
17
+ * Sort jobs by queue, worker, start time, queued time, duration, customizable name, status
18
+ * Filter jobs by queue, worker, customizable name, status
19
+ * Stacked histograms show the status distribution for each queue
20
+ * A process table showing which machines/processes are active and which jobs they're running
21
+ * Buttons for manually retrying failed jobs
22
+ * Job detail view with in-depth job information:
23
+ * Basics: worker class, arguments, start time, duration, process name
24
+ * Exception class, message, and backtrace of failed jobs
25
+ * A list of the exceptions and start times of retried jobs
26
+ * Customizable job properties
27
+
28
+ #### Extensibility
29
+ * Middleware support
30
+ * Customizable UI views
31
+ * Customizable job names
32
+ * Customizable job properties
33
+ * Store job-related metadata that's set during the job's execution
34
+ * Stored in an hstore
35
+ * Properties can be indexed and queried against
36
+ * Jobs can be associated with other ActiveRecord models using a property as the foreign key
37
+
38
+ #### Durability
39
+ * Toro runs on PostgreSQL
40
+
41
+ #### Other Features
42
+ * Scheduled jobs
43
+ * Configurable retry of failed jobs
44
+
45
+ #### UI
46
+
47
+ Toro has an extensive dashboard that provides in-depth information about jobs, queues, processes, and more:
48
+
49
+ [<img src="https://raw.github.com/tombenner/toro/master/examples/jobs.png" width="48%" />](https://raw.github.com/tombenner/toro/master/examples/jobs.png)
50
+ [<img src="https://raw.github.com/tombenner/toro/master/examples/job.png" width="48%" />](https://raw.github.com/tombenner/toro/master/examples/job.png)
51
+ [<img src="https://raw.github.com/tombenner/toro/master/examples/queues.png" width="48%" />](https://raw.github.com/tombenner/toro/master/examples/queues.png)
52
+ [<img src="https://raw.github.com/tombenner/toro/master/examples/chart.png" width="48%" />](https://raw.github.com/tombenner/toro/master/examples/chart.png)
53
+
54
+ Installation
55
+ ------------
56
+
57
+ Add Toro to your Gemfile:
58
+
59
+ ```ruby
60
+ gem 'toro'
61
+ ```
62
+
63
+ Mount the UI at a route in `routes.rb`:
64
+
65
+ ```ruby
66
+ mount Toro::Monitor::Engine => '/toro'
67
+ ```
68
+
69
+ And install and run the migration:
70
+
71
+ ```bash
72
+ rails g toro:install
73
+ rake db:migrate
74
+ ```
75
+
76
+ Quick Start
77
+ -----------
78
+
79
+ Create a worker:
80
+
81
+ ```ruby
82
+ # app/workers/my_worker.rb
83
+ class MyWorker
84
+ include Toro::Worker
85
+
86
+ def perform(user_id)
87
+ puts "Processing user #{user_id}..."
88
+ end
89
+ end
90
+ ```
91
+
92
+ In your controller action, model, or elsewhere, queue a job:
93
+ ```ruby
94
+ MyWorker.perform_async(15)
95
+ ```
96
+
97
+ Start Toro in the root directory of your Rails app:
98
+ ```bash
99
+ rake toro:start
100
+ ```
101
+
102
+ Basics
103
+ ------
104
+
105
+ ### Queues
106
+
107
+ By default, workers and processes use the `default` queue.
108
+
109
+ To set a worker's queue, use `toro_options`:
110
+
111
+ ```ruby
112
+ # app/workers/my_worker.rb
113
+ class MyWorker
114
+ include Toro::Worker
115
+ toro_options queue: 'users'
116
+
117
+ def perform(user_id)
118
+ puts "Processing user #{user_id}..."
119
+ end
120
+ end
121
+ ```
122
+
123
+ To set a process's queue, use `-q`:
124
+
125
+ ```bash
126
+ rake toro:start -- -q users
127
+ ```
128
+
129
+ Or specify multiple queues:
130
+
131
+ ```bash
132
+ rake toro:start -- -q users -q comments
133
+ ```
134
+
135
+ ### Concurrency
136
+
137
+ To specify a process's concurrency (how many jobs it can run simultaneously), use `-c`:
138
+
139
+ ```bash
140
+ rake toro:start -- -c 10
141
+ ```
142
+
143
+ ### Scheduled Jobs
144
+
145
+ To schedule a job for a specific time, use `perform_in(interval, *args)` or `perform_at(timestamp, *args)` instead of the standard `perform_async(*args)`:
146
+
147
+ ```ruby
148
+ MyWorker.perform_in(2.hours, 'First arg', 'Second arg')
149
+ MyWorker.perform_at(2.hours.from_now, 'First arg', 'Second arg')
150
+ ```
151
+
152
+ ### Retrying Jobs
153
+
154
+ Failing jobs aren't retried by default. If you'd like Toro to retry a worker's failed jobs, specify the retry interval in the worker:
155
+
156
+ ```ruby
157
+ # app/workers/my_worker.rb
158
+ class MyWorker
159
+ include Toro::Worker
160
+ toro_options retry_interval: 2.hours
161
+
162
+ def perform(user_id)
163
+ puts "Processing user #{user_id}..."
164
+ end
165
+ end
166
+ ```
167
+
168
+ The error classes and times of retried jobs are stored as job properties.
169
+
170
+ ### Querying Jobs
171
+
172
+ `Toro::Job` is an ActiveRecord model, which allows you to easily create complex queries against jobs that aren't easily performed in Redis-based job queueing systems. The model has the following columns:
173
+
174
+ * queue - Queue
175
+ * class_name - Worker class
176
+ * args - Arguments
177
+ * name - Name
178
+ * created_at - When the job was created
179
+ * scheduled_at - When the job was scheduled for (if it's a scheduled job)
180
+ * started_at - When the job was started
181
+ * finished_at - When the job finished (regardless of whether it succeeded or failed)
182
+ * status - `queued`, `running`, `complete`, `failed`, or `scheduled`
183
+ * started_by - Host and PID of the process running the job (e.g. `ip-10-55-10-151:1623`)
184
+ * properties - An hstore containing customizable job properties
185
+
186
+
187
+ Job Customization
188
+ -----------------
189
+
190
+ ### Job Name
191
+
192
+ To set a job's name, define a `self.job_name` method that takes the same arguments the `perform` method:
193
+
194
+ ```ruby
195
+ class MyWorker
196
+ include Toro::Worker
197
+
198
+ def perform(user_id)
199
+ end
200
+
201
+ def self.job_name(user_id)
202
+ User.find(user_id).username
203
+ end
204
+ end
205
+ ```
206
+
207
+ A job name makes the job more recognizable in the UI. The UI also lets you search by name.
208
+
209
+ ### Job Properties
210
+
211
+ Job properties let you store custom data about your jobs and their results.
212
+
213
+ To set job properties, make the `perform` method return a hash with a `:job_properties` key:
214
+
215
+ ```ruby
216
+ class MyWorker
217
+ include Toro::Worker
218
+
219
+ def perform(user_id)
220
+ comments = User.find(user_id).comments
221
+ # Do some processing...
222
+ {
223
+ job_properties: {
224
+ user_id: user_id,
225
+ comments_count: comments.length
226
+ }
227
+ }
228
+ end
229
+ end
230
+ ```
231
+
232
+ The job properties will be shown in the job detail view in the UI.
233
+
234
+ Properties are stored using [Nested Hstore](https://github.com/tombenner/nested-hstore), so you can store nested hashes, arrays, or any other types, allowing for NoSQL-like document storage:
235
+
236
+ ```ruby
237
+ class MyWorker
238
+ include Toro::Worker
239
+
240
+ def perform(user_id)
241
+ user = User.find(user_id)
242
+ comments = user.comments
243
+ # Do some processing...
244
+ {
245
+ job_properties: {
246
+ user: {
247
+ id: user.id,
248
+ is_blacklisted: user.is_blacklisted?,
249
+ timeline: {
250
+ is_private: user.timeline.is_private
251
+ }
252
+ },
253
+ comment_ids: comments.map(&:id)
254
+ }
255
+ }
256
+ end
257
+ end
258
+ ```
259
+
260
+ #### Querying Job Properties
261
+
262
+ Job properties are stored in an hstore, so you can query them (e.g. for reporting):
263
+
264
+ ```ruby
265
+ big_jobs = Toro::Job.where("(properties->'comments_count')::int > ?", 100)
266
+ ```
267
+
268
+ #### Associating Jobs with Other Models
269
+
270
+ You can create associations between jobs and other models using them:
271
+
272
+ ```ruby
273
+ class User < ActiveRecord::Base
274
+ has_many :jobs, foreign_key: "toro_jobs.properties->'user_id'", class_name: 'Toro::Job'
275
+ end
276
+ ```
277
+
278
+ You can then, for example, find the failed jobs for a user:
279
+
280
+ ```ruby
281
+ failed_jobs = User.find(1).jobs.where(status: 'failed')
282
+ ```
283
+
284
+ Middleware
285
+ ----------
286
+
287
+ Toro's middleware support lets you run code "around" the processing of a job. Writing middleware is easy:
288
+
289
+ ```ruby
290
+ # lib/my_middleware.rb
291
+ class MyMiddleware
292
+ def call(job, worker)
293
+ begin
294
+ puts "Starting to process Job ##{job.id}"
295
+ yield
296
+ puts "Finished running Job ##{job.id}"
297
+ rescue Exception => exception
298
+ puts "Exception raised for Job ##{job.id}: #{exception}"
299
+ job.update_attribute(status: 'failed')
300
+ raise exception
301
+ end
302
+ end
303
+ end
304
+ ```
305
+
306
+ Then register your middleware as part of the chain:
307
+
308
+ ```ruby
309
+ # config/initializers/toro.rb
310
+ Toro.configure_server do |config|
311
+ config.server_middleware do |chain|
312
+ chain.add MyMiddleware
313
+ end
314
+ end
315
+ ```
316
+
317
+ Toro supports the same server middleware inferface that Sidekiq does (including arguments, middleware removal, etc). Please see the [Sidekiq Middleware documentation](https://github.com/mperham/sidekiq/wiki/Middleware) for details.
318
+
319
+ Monitor Customization
320
+ ---------------------
321
+
322
+ ### Chart
323
+
324
+ A single histogram will be shown by default in the Chart view, but you can also split the queues into multiple histograms. (This is especially useful if you have a large number of queues and the single histogram has too many bars to be readable.) The keys of this hash are JS regex patterns for matching queues, and the values of the hash will be the titles of each histogram:
325
+
326
+ ```ruby
327
+ # config/initializers/toro.rb
328
+ Toro::Monitor.options[:charts] = {
329
+ 'ALL' => 'All',
330
+ 'OTHER' => 'Default Priority',
331
+ '_high$' => 'High Priority',
332
+ '_low$' => 'Low Priority'
333
+ }
334
+ ```
335
+
336
+ `ALL` and `OTHER` are special keys: `ALL` will show all queues and `OTHER` will show all queues that aren't matched by the regex keys.
337
+
338
+ ### Poll Interval
339
+
340
+ The UI uses polling to update its data. By default, the polling interval is 3000ms, but you can adjust this like so:
341
+
342
+ ```ruby
343
+ # config/initializers/toro.rb
344
+ Toro::Monitor.options[:poll_interval] = 5000
345
+ ```
346
+
347
+ ### Custom Statuses
348
+
349
+ The UI's status filter and histograms show the most common job statuses, but if you'd like to add additional statuses to them (for example, if you have added custom statuses through middleware), you can make the UI include them like so:
350
+
351
+ ```ruby
352
+ # config/initializers/toro.rb
353
+ Toro::Monitor::Job.add_status('my_custom_status')
354
+ ```
355
+
356
+ ### Custom Job Views
357
+
358
+ When you click on a job, a modal showing its properties is displayed. You can add subviews to this modal by creating a view in your app and calling `Toro::Monitor::CustomViews.add`, passing it the subview's title, the subview's filepath, and a block. The subview is only rendered if the block evaluates to true for the given job.
359
+
360
+ If you need to add JavaScript for the subview, you can do so by adding an asset path to `Toro::Monitor.options[:javascripts]`.
361
+
362
+ For example, the following code adds a subview that shows a "Retry" button for jobs with the specified statuses:
363
+
364
+ ```ruby
365
+ # config/initializers/toro.rb
366
+ view_path = Rails.root.join('app', 'views', 'toro', 'monitor', 'retry').to_s
367
+ Toro::Monitor::CustomViews.add('My View Title', view_path) do |job|
368
+ %w{complete failed}.include?(job.status)
369
+ end
370
+ Toro::Monitor.options[:javascripts] << 'toro/monitor/retry'
371
+ ```
372
+
373
+ ```
374
+ / app/views/toro/monitor/retry.slim
375
+ a class='btn btn-success' href='#' data-action='retry_job' data-job-id=job.id = 'Retry'
376
+ ```
377
+
378
+ ```coffee
379
+ # app/assets/javascripts/toro/monitor/retry.js.coffee
380
+ $ ->
381
+ $('body').on 'click', '.job-modal [data-action=retry_job]', (e) ->
382
+ id = $(e.target).attr('data-job-id')
383
+ $.get ToroMonitor.settings.api_url("jobs/retry/#{id}")
384
+ alert 'Job has been retried'
385
+ false
386
+ ```
387
+
388
+ ### Authentication
389
+
390
+ You'll likely want to restrict access to the UI in a production environment. To do this, you can use routing constraints:
391
+
392
+ #### Devise
393
+
394
+ Checks a `User` model instance that responds to `admin?`
395
+
396
+ ```ruby
397
+ constraint = lambda { |request| request.env["warden"].authenticate? and request.env['warden'].user.admin? }
398
+ constraints constraint do
399
+ mount Toro::Monitor::Engine => '/toro'
400
+ end
401
+ ```
402
+
403
+ Allow any authenticated `User`
404
+
405
+ ```ruby
406
+ constraint = lambda { |request| request.env['warden'].authenticate!({ scope: :user }) }
407
+ constraints constraint do
408
+ mount Toro::Monitor::Engine => '/toro'
409
+ end
410
+ ```
411
+
412
+ Short version
413
+
414
+ ```ruby
415
+ authenticate :user do
416
+ mount Toro::Monitor::Engine => '/toro'
417
+ end
418
+ ```
419
+
420
+ #### Authlogic
421
+
422
+ ```ruby
423
+ # lib/admin_constraint.rb
424
+ class AdminConstraint
425
+ def matches?(request)
426
+ return false unless request.cookies['user_credentials'].present?
427
+ user = User.find_by_persistence_token(request.cookies['user_credentials'].split(':')[0])
428
+ user && user.admin?
429
+ end
430
+ end
431
+
432
+ # config/routes.rb
433
+ require "admin_constraint"
434
+ mount Toro::Monitor::Engine => '/toro', :constraints => AdminConstraint.new
435
+ ```
436
+
437
+ #### Restful Authentication
438
+
439
+ Checks a `User` model instance that responds to `admin?`
440
+
441
+ ```
442
+ # lib/admin_constraint.rb
443
+ class AdminConstraint
444
+ def matches?(request)
445
+ return false unless request.session[:user_id]
446
+ user = User.find request.session[:user_id]
447
+ user && user.admin?
448
+ end
449
+ end
450
+
451
+ # config/routes.rb
452
+ require "admin_constraint"
453
+ mount Toro::Monitor::Engine => '/toro', :constraints => AdminConstraint.new
454
+ ```
455
+
456
+ #### Custom External Authentication
457
+
458
+ ```ruby
459
+ class AuthConstraint
460
+ def self.admin?(request)
461
+ return false unless (cookie = request.cookies['auth'])
462
+
463
+ Rails.cache.fetch(cookie['user'], :expires_in => 1.minute) do
464
+ auth_data = JSON.parse(Base64.decode64(cookie['data']))
465
+ response = HTTParty.post(Auth.validate_url, :query => auth_data)
466
+
467
+ response.code == 200 && JSON.parse(response.body)['roles'].to_a.include?('Admin')
468
+ end
469
+ end
470
+ end
471
+
472
+ # config/routes.rb
473
+ constraints lambda {|request| AuthConstraint.admin?(request) } do
474
+ mount Toro::Monitor::Engine => '/admin/toro'
475
+ end
476
+ ```
477
+
478
+ _(This authentication documentation was borrowed from the [Sidekiq wiki](https://github.com/mperham/sidekiq/wiki/Monitoring).)_
479
+
480
+
481
+ Logging
482
+ -------
483
+
484
+ Logging can be especially useful in debugging concurrent systems like Toro. You can modify Toro's logger:
485
+
486
+
487
+ ```ruby
488
+ # config/initializers/toro.rb
489
+
490
+ # Adjust attributes of Toro's logger
491
+ Toro.logger.level = Logger::DEBUG
492
+
493
+ # Or create a custom Logger
494
+ Toro.logger = Logger.new(Rails.root.join('log', 'toro.log'))
495
+ Toro.logger.level = Logger::DEBUG
496
+
497
+ ```
498
+
499
+ See the [Logger docs](http://www.ruby-doc.org/stdlib-2.0/libdoc/logger/rdoc/Logger.html) for more.
500
+
501
+ Testing
502
+ -------
503
+
504
+ Copy and set up the database config:
505
+
506
+ ```bash
507
+ cp spec/config/database.yml.example spec/config/database.yml
508
+ ```
509
+
510
+ Toro is tested against Rails 3 and 4, so please run the tests with [Appraisal](https://github.com/thoughtbot/appraisal) before submitting a PR. Thanks!
511
+
512
+ ```bash
513
+ appraisal rspec
514
+ ```
515
+
516
+ FAQ
517
+ ---
518
+
519
+ ### Toro?
520
+ * [Toro](http://en.wikipedia.org/wiki/Tuna) is robust, quick, and values strength in numbers.
521
+ * [Toro](http://en.wikipedia.org/wiki/Bull) is durable and runs a little large.
522
+ * [Toro](http://en.wikipedia.org/wiki/T%C5%8Dr%C5%8D) brings visibility.
523
+
524
+
525
+ Notes
526
+ -----
527
+
528
+ A good deal of architecture and code was borrowed from [@mperham](https://github.com/mperham)'s excellent [Sidekiq](https://github.com/mperham/sidekiq), so many thanks to him and all of Sidekiq's contributors!
529
+
530
+ License
531
+ -------
532
+
533
+ Toro is released under the MIT License. Please see the MIT-LICENSE file for details.