que 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +12 -31
- data/docs/advanced_setup.md +50 -0
- data/docs/error_handling.md +17 -0
- data/docs/inspecting_the_queue.md +100 -0
- data/docs/managing_workers.md +67 -0
- data/docs/using_plain_connections.md +34 -0
- data/docs/using_sequel.md +27 -0
- data/docs/writing_reliable_jobs.md +62 -0
- data/lib/que.rb +33 -5
- data/lib/que/adapters/base.rb +9 -1
- data/lib/que/adapters/pg.rb +2 -0
- data/lib/que/adapters/sequel.rb +4 -0
- data/lib/que/job.rb +30 -64
- data/lib/que/rake_tasks.rb +0 -1
- data/lib/que/sql.rb +119 -74
- data/lib/que/version.rb +1 -1
- data/lib/que/worker.rb +39 -26
- data/spec/adapters/active_record_spec.rb +7 -26
- data/spec/adapters/connection_pool_spec.rb +1 -2
- data/spec/adapters/pg_spec.rb +34 -0
- data/spec/adapters/sequel_spec.rb +16 -28
- data/spec/spec_helper.rb +46 -34
- data/spec/support/shared_examples/adapter.rb +16 -3
- data/spec/support/shared_examples/{multithreaded_adapter.rb → multi_threaded_adapter.rb} +3 -1
- data/spec/unit/helper_spec.rb +0 -5
- data/spec/unit/pool_spec.rb +75 -23
- data/spec/unit/queue_spec.rb +5 -1
- data/spec/unit/states_spec.rb +52 -0
- data/spec/unit/stats_spec.rb +42 -0
- data/spec/unit/work_spec.rb +15 -20
- data/spec/unit/worker_spec.rb +18 -3
- data/tasks/safe_shutdown.rb +5 -3
- metadata +16 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 221284cc1195f925ca4ad320ddc6ea16e2441a5f
|
4
|
+
data.tar.gz: 6233e8871fb93b5e2f738653e9070c019ecf1baa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9f85215c53bf87781cb503747738446878414dae0ac1afdb44536870f0df81a0d0761441b6a3863865fcc8d30a35ba52951399e0a656adfdfb54cdd1d0355ffb
|
7
|
+
data.tar.gz: 6d51c478c98f5fca91afd67c2c07779543fbae45cc7162cf041c94aeb8921e18b4df7a07238d42a3f20dd4d473c22c01727e92182d3b0bd5c332d08048920231
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,23 @@
|
|
1
|
+
### Unreleased
|
2
|
+
|
3
|
+
* Que.wake_all! was added, as a simple way to wake up all workers in the pool.
|
4
|
+
|
5
|
+
* Que.sleep_period was renamed to the more descriptive Que.wake_interval.
|
6
|
+
|
7
|
+
* When queueing a job, Que will wait until the current transaction commits and then wake a background worker, if possible. This allows newly queued jobs to be started immediately instead of waiting for a worker to wake up and poll, which may be up to `Que.wake_interval` seconds.
|
8
|
+
|
9
|
+
This feature currently only works with Sequel, since there doesn't seem to be a clean way to do it on ActiveRecord (if anyone can figure one out, please let me know). Note that if you're using ActiveRecord, you can always manually trigger a single worker to wake up and check for work by manually calling Que.wake! after your transaction completes.
|
10
|
+
|
11
|
+
* Add Que.job_stats, which queries the database and returns statistics on the different job classes - for each class, how many are queued, how many are currently being worked, what is the highest error_count, and so on.
|
12
|
+
|
13
|
+
* Add Que.worker_states, which queries the database and returns all currently-locked jobs and info on their workers' connections - what and when was the last query they ran, are they waiting on locks, and so on.
|
14
|
+
|
15
|
+
* Have Que only clear advisory locks that it has taken when locking jobs, and not touch any that may have been taken by other code using the same connection.
|
16
|
+
|
17
|
+
* Add Que.worker_count, to retrieve the current number of workers in the pool of the current process.
|
18
|
+
|
19
|
+
* Much more internal cleanup.
|
20
|
+
|
1
21
|
### 0.3.0 (2013-12-21)
|
2
22
|
|
3
23
|
* Add Que.stop!, which immediately kills all jobs being worked in the process.
|
data/README.md
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# Que
|
2
2
|
|
3
|
-
**TL;DR: Que is a high-performance alternative to DelayedJob or QueueClassic that improves the reliability of your application by
|
3
|
+
**TL;DR: Que is a high-performance alternative to DelayedJob or QueueClassic that improves the reliability of your application by protecting your jobs with the same [ACID guarantees](https://en.wikipedia.org/wiki/ACID) as the rest of your data.**
|
4
4
|
|
5
5
|
Que is a queue for Ruby and PostgreSQL that manages jobs using [advisory locks](http://www.postgresql.org/docs/current/static/explicit-locking.html#ADVISORY-LOCKS), which gives it several advantages over other RDBMS-backed queues:
|
6
6
|
|
7
7
|
* **Concurrency** - Workers don't block each other when trying to lock jobs, as often occurs with "SELECT FOR UPDATE"-style locking. This allows for very high throughput with a large number of workers.
|
8
|
-
* **Efficiency** - Locks are held in memory, so locking a job doesn't incur a disk write. These first two points are what limit performance with other queues - all workers trying to lock jobs have to wait behind one that's persisting its UPDATE on a locked_at column to disk (and the disks of however many other servers your database is replicating to). Under heavy load, Que's bottleneck is CPU, not I/O.
|
8
|
+
* **Efficiency** - Locks are held in memory, so locking a job doesn't incur a disk write. These first two points are what limit performance with other queues - all workers trying to lock jobs have to wait behind one that's persisting its UPDATE on a locked_at column to disk (and the disks of however many other servers your database is synchronously replicating to). Under heavy load, Que's bottleneck is CPU, not I/O.
|
9
9
|
* **Safety** - If a Ruby process dies, the jobs it is working won't be lost, or left in a locked or ambiguous state - they immediately become available for any other worker to pick up.
|
10
10
|
|
11
11
|
Additionally, there are the general benefits of storing jobs in Postgres, alongside the rest of your data, rather than in Redis or a dedicated queue:
|
@@ -14,11 +14,11 @@ Additionally, there are the general benefits of storing jobs in Postgres, alongs
|
|
14
14
|
* **Atomic Backups** - Your jobs and data can be backed up together and restored as a snapshot. If your jobs relate to your data (and they usually do), there's no risk of jobs falling through the cracks during a recovery.
|
15
15
|
* **Fewer Dependencies** - If you're already using Postgres (and you probably should be), a separate queue is another moving part that can break.
|
16
16
|
|
17
|
-
Que's primary goal is reliability. When it's stable, you should be able to leave your application running indefinitely without worrying about jobs being lost due to a lack of transactional support, or left in limbo due to a crashing process. Que does everything it can to ensure that jobs you queue are performed exactly once (though the occasional repetition of a job can be impossible to avoid - see the
|
17
|
+
Que's primary goal is reliability. When it's stable, you should be able to leave your application running indefinitely without worrying about jobs being lost due to a lack of transactional support, or left in limbo due to a crashing process. Que does everything it can to ensure that jobs you queue are performed exactly once (though the occasional repetition of a job can be impossible to avoid - see the docs on [how to write a reliable job](https://github.com/chanks/que/blob/master/docs/writing_reliable_jobs.md)).
|
18
18
|
|
19
|
-
Que's secondary goal is performance. It won't be able to match the speed or throughput of a dedicated queue, or maybe even a Redis-backed queue, but it should be fast enough for most use cases. In [benchmarks](https://github.com/chanks/queue-shootout) on
|
19
|
+
Que's secondary goal is performance. It won't be able to match the speed or throughput of a dedicated queue, or maybe even a Redis-backed queue, but it should be fast enough for most use cases. In [benchmarks of RDBMS queues](https://github.com/chanks/queue-shootout) using PostgreSQL 9.3 on a AWS c3.8xlarge instance, Que approaches 10,000 jobs per second, or about twenty times the throughput of DelayedJob or QueueClassic. You are encouraged to try things out on your own production hardware, though.
|
20
20
|
|
21
|
-
Que also includes a worker pool, so that multiple threads can process jobs in the same process. It can even do this in the background of your web process - if you're running on Heroku, for example, you
|
21
|
+
Que also includes a worker pool, so that multiple threads can process jobs in the same process. It can even do this in the background of your web process - if you're running on Heroku, for example, you don't need to run a separate worker dyno.
|
22
22
|
|
23
23
|
*Please be careful when running Que in production. It's still very new compared to other RDBMS-backed queues, and there may be issues that haven't been ironed out yet. Bug reports are welcome.*
|
24
24
|
|
@@ -40,9 +40,9 @@ Or install it yourself as:
|
|
40
40
|
|
41
41
|
## Usage
|
42
42
|
|
43
|
-
The following
|
43
|
+
The following assumes you're using Rails 4.0 and ActiveRecord. *Que hasn't been tested with versions of Rails before 4.0, and may or may not work with them.* For more information, or instructions on using Que outside of Rails or with Sequel or no ORM, see the [documentation](https://github.com/chanks/que/blob/master/docs).
|
44
44
|
|
45
|
-
First, generate a migration for the job table.
|
45
|
+
First, generate and run a migration for the job table.
|
46
46
|
|
47
47
|
rails generate que:install
|
48
48
|
rake db:migrate
|
@@ -51,7 +51,7 @@ Create a class for each type of job you want to run:
|
|
51
51
|
|
52
52
|
# app/jobs/charge_credit_card.rb
|
53
53
|
class ChargeCreditCard < Que::Job
|
54
|
-
#
|
54
|
+
# Default options for this job. These may be omitted.
|
55
55
|
@default_priority = 3
|
56
56
|
@default_run_at = proc { 1.minute.from_now }
|
57
57
|
|
@@ -84,30 +84,11 @@ You can also schedule it to run at a specific time, or with a specific priority:
|
|
84
84
|
# 1 is high priority, 5 is low priority.
|
85
85
|
ChargeCreditCard.queue current_user.id, card.id, :your_custom_option => 'whatever', :run_at => 1.day.from_now, :priority => 5
|
86
86
|
|
87
|
-
To determine what happens when a job is queued, you can set Que's mode
|
87
|
+
To determine what happens when a job is queued, you can set Que's mode in your application configuration. There are a few options for the mode:
|
88
88
|
|
89
|
-
*
|
90
|
-
*
|
91
|
-
*
|
92
|
-
|
93
|
-
If you don't want to run workers in your web process, you can also work jobs in a rake task, similar to how other queueing systems work:
|
94
|
-
|
95
|
-
# Run a pool of 4 workers.
|
96
|
-
rake que:work
|
97
|
-
|
98
|
-
# Or configure the number of workers.
|
99
|
-
WORKER_COUNT=8 rake que:work
|
100
|
-
|
101
|
-
# If your app code isn't thread-safe, be sure to stick to one worker.
|
102
|
-
WORKER_COUNT=1 rake que:work
|
103
|
-
|
104
|
-
If an error causes a job to fail, Que will repeat that job at exponentially-increasing intervals, similar to DelayedJob (the job will be retried at 4 seconds, 19 seconds, 84 seconds, 259 seconds...). You can also hook Que into whatever error notification system you're using:
|
105
|
-
|
106
|
-
config.que.error_handler = proc do |error|
|
107
|
-
# Do whatever you want with the error object.
|
108
|
-
end
|
109
|
-
|
110
|
-
You can find more documentation on the [Github wiki](https://github.com/chanks/que/wiki).
|
89
|
+
* `config.que.mode = :off` - In this mode, queueing a job will simply insert it into the database - the current process will make no effort to run it. You should use this if you want to use a dedicated process to work tasks (there's a rake task to do this, see below). This is the default when running `rails console` in the development or production environments.
|
90
|
+
* `config.que.mode = :async` - In this mode, a pool of background workers is spun up, each running in their own thread. This is the default when running `rails server` in the development or production environments. See the docs for [more information on managing workers](https://github.com/chanks/que/blob/master/docs/managing_workers.md).
|
91
|
+
* `config.que.mode = :sync` - In this mode, any jobs you queue will be run in the same thread, synchronously (that is, `MyJob.queue` runs the job and won't return until it's completed). This makes your application's behavior easier to test, so it's the default in the test environment.
|
111
92
|
|
112
93
|
## Contributing
|
113
94
|
|
@@ -0,0 +1,50 @@
|
|
1
|
+
## Advanced Setup
|
2
|
+
|
3
|
+
If you're using both Rails and ActiveRecord, the README describes how to get started with Que (which is pretty straightforward, since Que includes a Railtie that handles a lot of setup for you). Otherwise, you'll need to do some manual setup.
|
4
|
+
|
5
|
+
If you're using ActiveRecord outside of Rails, you'll need to tell Que to piggyback on its connection pool after you've connected to the database:
|
6
|
+
|
7
|
+
ActiveRecord::Base.establish_connection(ENV['DATABASE_URL'])
|
8
|
+
|
9
|
+
require 'que'
|
10
|
+
Que.connection = ActiveRecord
|
11
|
+
|
12
|
+
Then you can queue jobs just as you would in Rails:
|
13
|
+
|
14
|
+
ActiveRecord::Base.transaction do
|
15
|
+
@user = User.create(params[:user])
|
16
|
+
SendRegistrationEmail.queue :user_id => @user.id
|
17
|
+
end
|
18
|
+
|
19
|
+
There are other docs to read if you're using [Sequel](https://github.com/chanks/que/blob/master/docs/using_sequel.md) or [plain Postgres connections](https://github.com/chanks/que/blob/master/docs/using_plain_connections.md) (with no ORM at all) instead of ActiveRecord.
|
20
|
+
|
21
|
+
### Managing the Jobs Table
|
22
|
+
|
23
|
+
After you've connected Que to the database, you can manage the jobs table:
|
24
|
+
|
25
|
+
# Create the jobs table:
|
26
|
+
Que.create!
|
27
|
+
|
28
|
+
# Clear the jobs table of all jobs:
|
29
|
+
Que.clear!
|
30
|
+
|
31
|
+
# Drop the jobs table:
|
32
|
+
Que.drop!
|
33
|
+
|
34
|
+
### Other Setup
|
35
|
+
|
36
|
+
You can give Que a logger to use if you like:
|
37
|
+
|
38
|
+
Que.logger = Logger.new(STDOUT)
|
39
|
+
|
40
|
+
You'll also need to set Que's mode manually:
|
41
|
+
|
42
|
+
# Start the worker pool:
|
43
|
+
Que.mode = :async
|
44
|
+
|
45
|
+
# Or, when testing:
|
46
|
+
Que.mode = :sync
|
47
|
+
|
48
|
+
Be sure to read the docs on [managing workers](https://github.com/chanks/que/blob/master/docs/managing_workers.md) for more information on using the worker pool.
|
49
|
+
|
50
|
+
You may also want to set up an [error handler](https://github.com/chanks/que/blob/master/docs/error_handling.md) to track errors raised by jobs.
|
@@ -0,0 +1,17 @@
|
|
1
|
+
## Error Handling
|
2
|
+
|
3
|
+
If an error is raised and left uncaught by your job, Que will save the error message and backtrace to the database and schedule the job to be retried later.
|
4
|
+
|
5
|
+
If a given job fails repeatedly, Que will retry it at exponentially-increasing intervals equal to (failure_count[^4^] + 3) seconds. This means that a job will be retried 4 seconds after its first failure, 19 seconds after its second, 84 seconds after its third, 259 seconds after its fourth, and so on until it succeeds. This pattern is very similar to DelayedJob's.
|
6
|
+
|
7
|
+
Unlike DelayedJob, however, there is currently no maximum number of failures after which jobs will be deleted. Que's assumption is that if a job is erroring perpetually (and not just transiently), you will want to take action to get the job working properly rather than simply losing it silently.
|
8
|
+
|
9
|
+
If you're using an error notification system (highly recommended, of course), you can hook Que into it by setting a callable as the error handler:
|
10
|
+
|
11
|
+
Que.error_handler = proc do |error|
|
12
|
+
# Do whatever you want with the error object.
|
13
|
+
end
|
14
|
+
|
15
|
+
# Or, in your Rails configuration:
|
16
|
+
|
17
|
+
config.que.error_handler = proc { |error| ... }
|
@@ -0,0 +1,100 @@
|
|
1
|
+
## Inspecting the Queue
|
2
|
+
|
3
|
+
In order to remain simple and compatible with any ORM (or no ORM at all), Que is really just a very thin wrapper around some raw SQL. There are two methods available that query the jobs table and Postgres' system catalogs to retrieve information on the current state of the queue:
|
4
|
+
|
5
|
+
### Job Stats
|
6
|
+
|
7
|
+
You can call `Que.job_stats` to return some aggregate data on the types of jobs currently in the queue. Example output:
|
8
|
+
|
9
|
+
[
|
10
|
+
{
|
11
|
+
"job_class"=>"ChargeCreditCard",
|
12
|
+
"count"=>"10",
|
13
|
+
"count_working"=>"4",
|
14
|
+
"count_errored"=>"2",
|
15
|
+
"highest_error_count"=>"5",
|
16
|
+
"oldest_run_at"=>"2014-01-04 21:24:55.817129+00"
|
17
|
+
},
|
18
|
+
{
|
19
|
+
"job_class"=>"SendRegistrationEmail",
|
20
|
+
"count"=>"8",
|
21
|
+
"count_working"=>"0",
|
22
|
+
"count_errored"=>"0",
|
23
|
+
"highest_error_count"=>"0",
|
24
|
+
"oldest_run_at"=>"2014-01-04 22:24:55.81532+00"
|
25
|
+
}
|
26
|
+
]
|
27
|
+
|
28
|
+
This tells you that, for instance, there are ten ChargeCreditCard jobs in the queue, four of which are currently being worked, and two of which have experienced errors. One of them has started to process but experienced an error five times. The oldest_run_at is helpful for determining how long jobs have been sitting around, if you have backlog.
|
29
|
+
|
30
|
+
### Worker States
|
31
|
+
|
32
|
+
You can call `Que.worker_states` to return some information on every worker touching the queue (not just those in the current process). Example output:
|
33
|
+
|
34
|
+
[
|
35
|
+
{
|
36
|
+
"priority"=>"2",
|
37
|
+
"run_at"=>"2014-01-04 22:35:55.772324+00",
|
38
|
+
"job_id"=>"4592",
|
39
|
+
"job_class"=>"ChargeCreditCard",
|
40
|
+
"args"=>"[345,56]",
|
41
|
+
"error_count"=>"0",
|
42
|
+
"last_error"=>nil,
|
43
|
+
"pg_backend_pid"=>"1175",
|
44
|
+
"pg_state"=>"idle",
|
45
|
+
"pg_state_changed_at"=>"2014-01-04 22:35:55.777785+00",
|
46
|
+
"pg_last_query"=>"SELECT * FROM users",
|
47
|
+
"pg_last_query_started_at"=>"2014-01-04 22:35:55.777519+00",
|
48
|
+
"pg_transaction_started_at"=>nil,
|
49
|
+
"pg_waiting_on_lock"=>"f"
|
50
|
+
}
|
51
|
+
]
|
52
|
+
|
53
|
+
In this case, there is only one worker currently working the queue. The first seven fields are the attributes of the job it is currently running. The next seven fields are information about that worker's Postgres connection, and are taken from `pg_stat_activity` - see [Postgres' documentation](http://www.postgresql.org/docs/current/static/monitoring-stats.html#PG-STAT-ACTIVITY-VIEW) for more information on interpreting these fields.
|
54
|
+
|
55
|
+
* `pg_backend_pid` - The pid of the Postgres process serving this worker. This is useful if you wanted to kill that worker's connection, for example, by running "SELECT pg_terminate_backend(1175)". This would free up the job to be attempted by another worker.
|
56
|
+
* `pg_state` - The state of the Postgres backend. It may be "active" if the worker is currently running a query or "idle"/"idle in transaction" if it is not. It may also be in one of a few other less common states.
|
57
|
+
* `pg_state_changed_at` - The timestamp for when the backend's state was last changed. If the backend is idle, this would reflect the time that the last query finished.
|
58
|
+
* `pg_last_query` - The text of the current or most recent query that the worker sent to the database.
|
59
|
+
* `pg_last_query_started_at` - The timestamp for when the last query began to run.
|
60
|
+
* `pg_transaction_started_at` - The timestamp for when the worker's current transaction (if any) began.
|
61
|
+
* `pg_waiting_on_lock` - Whether or not the worker is waiting for a lock in Postgres to be released.
|
62
|
+
|
63
|
+
### Custom Queries
|
64
|
+
|
65
|
+
If you want to query the jobs table yourself to see what's been queued or to check the state of various jobs, you can always use Que to execute whatever SQL you want:
|
66
|
+
|
67
|
+
Que.execute("select count(*) from que_jobs") #=> [{"count"=>"492"}]
|
68
|
+
|
69
|
+
If you want to use ActiveRecord's features when querying, you can define your own model around Que's job table:
|
70
|
+
|
71
|
+
class QueJob < ActiveRecord::Base
|
72
|
+
end
|
73
|
+
|
74
|
+
# Or:
|
75
|
+
|
76
|
+
class MyJob < ActiveRecord::Base
|
77
|
+
self.table_name = :que_jobs
|
78
|
+
end
|
79
|
+
|
80
|
+
Then you can query just as you would with any other model. Since the jobs table has a composite primary key, however, you probably won't be able to update or destroy jobs this way, though.
|
81
|
+
|
82
|
+
If you're using Sequel, you can use the same technique:
|
83
|
+
|
84
|
+
class QueJob < Sequel::Model
|
85
|
+
end
|
86
|
+
|
87
|
+
# Or:
|
88
|
+
|
89
|
+
class MyJob < Sequel::Model(:que_jobs)
|
90
|
+
end
|
91
|
+
|
92
|
+
And note that Sequel *does* support composite primary keys:
|
93
|
+
|
94
|
+
job = QueJob.where(:job_class => "ChargeCreditCard").first
|
95
|
+
job.priority = 1
|
96
|
+
job.save
|
97
|
+
|
98
|
+
Or, you can just use Sequel's dataset methods:
|
99
|
+
|
100
|
+
DB[:que_jobs].where{priority > 3}.all
|
@@ -0,0 +1,67 @@
|
|
1
|
+
## Managing Workers
|
2
|
+
|
3
|
+
Que provides a pool of workers to process jobs in a multithreaded fashion - this allows you to save memory by working many jobs simultaneously in the same process.
|
4
|
+
|
5
|
+
When the worker pool is active (as it is by default when running `rails server`, or when you set Que.mode = :async), the default number of workers is 4. This is fine for most use cases, but the ideal number for your app will depend on your interpreter and what types of jobs you're running.
|
6
|
+
|
7
|
+
Ruby MRI has a global interpreter lock (GIL), which prevents it from using more than one CPU core at a time. Having multiple workers running makes sense if your jobs tend to spend a lot of time in I/O (waiting on complex database queries, sending emails, making HTTP requests, etc.), as most jobs do. However, if your jobs are doing a lot of work in Ruby, they'll be spending a lot of time blocking each other, and having too many workers running will just slow everything down.
|
8
|
+
|
9
|
+
JRuby and Rubinius, on the other hand, have no global interpreter lock, and so can make use of multiple CPU cores - you could potentially set the number of workers very high for them. You should experiment to find the best setting for your use case.
|
10
|
+
|
11
|
+
You can change the number of workers in the pool whenever you like by setting the `worker_count` option:
|
12
|
+
|
13
|
+
Que.worker_count = 8
|
14
|
+
|
15
|
+
# Or, in your Rails configuration:
|
16
|
+
config.que.worker_count = 8
|
17
|
+
|
18
|
+
### Working Jobs Via Rake Task
|
19
|
+
|
20
|
+
If you don't want to burden your web processes with too much work and want to run workers in a background process instead, similar to how most other queues work, you can:
|
21
|
+
|
22
|
+
# Run a pool of 4 workers:
|
23
|
+
rake que:work
|
24
|
+
|
25
|
+
# Or configure the number of workers:
|
26
|
+
WORKER_COUNT=8 rake que:work
|
27
|
+
|
28
|
+
### Thread-Unsafe Application Code
|
29
|
+
|
30
|
+
If your application code is not thread-safe, you won't want any workers to be processing jobs while anything else is happening in the Ruby process. So, you'll want to turn the worker pool off by default:
|
31
|
+
|
32
|
+
Que.mode = :off
|
33
|
+
|
34
|
+
# Or, in your Rails configuration:
|
35
|
+
config.que.mode = :off
|
36
|
+
|
37
|
+
This will prevent Que from trying to process jobs in the background of your web processes. In order to actually work jobs, you'll want to run a single worker at a time, and to do so via a separate rake task, like so:
|
38
|
+
|
39
|
+
WORKER_COUNT=1 rake que:work
|
40
|
+
|
41
|
+
### The Wake Interval
|
42
|
+
|
43
|
+
If a worker checks the job queue and finds no jobs ready for it to work, it will fall asleep. In order to make sure that newly-available jobs don't go unworked, a worker is awoken every so often to check for available work. By default, this happens every five seconds, but you can make it happen more or less often by setting a custom wake_interval:
|
44
|
+
|
45
|
+
Que.wake_interval = 2
|
46
|
+
|
47
|
+
# Or, in your Rails configuration:
|
48
|
+
config.que.wake_interval = 2 # 2.seconds also works fine.
|
49
|
+
|
50
|
+
You can also choose to never let workers wake up on their own:
|
51
|
+
|
52
|
+
# Never wake up any workers:
|
53
|
+
Que.wake_interval = nil
|
54
|
+
|
55
|
+
If you do this, though, you'll need to wake workers manually.
|
56
|
+
|
57
|
+
### Manually Waking Workers
|
58
|
+
|
59
|
+
Regardless of the `wake_interval` setting, you can always wake workers manually:
|
60
|
+
|
61
|
+
# Wake up a single worker to check the queue for work:
|
62
|
+
Que.wake!
|
63
|
+
|
64
|
+
# Wake up all workers in this process to check for work:
|
65
|
+
Que.wake_all!
|
66
|
+
|
67
|
+
`Que.wake_all!` is helpful if there are no jobs available and all your workers go to sleep, and then you queue a large number of jobs. Typically, it will take a little while for the entire pool of workers get going again - a new one will wake up every `wake_interval` seconds, but it will take up to `wake_interval * worker_count` seconds for all of them to get going. `Que.wake_all!` can get them all moving immediately.
|
@@ -0,0 +1,34 @@
|
|
1
|
+
## Using Plain Postgres Connections
|
2
|
+
|
3
|
+
If you're not using an ORM like ActiveRecord or Sequel, you can have Que access jobs using a plain Postgres connection:
|
4
|
+
|
5
|
+
require 'uri'
|
6
|
+
require 'pg'
|
7
|
+
|
8
|
+
uri = URI.parse(ENV['DATABASE_URL'])
|
9
|
+
|
10
|
+
Que.connection = PG::Connection.open :host => uri.host,
|
11
|
+
:user => uri.user,
|
12
|
+
:password => uri.password,
|
13
|
+
:port => uri.port || 5432,
|
14
|
+
:dbname => uri.path[1..-1]
|
15
|
+
|
16
|
+
If you want to be able to use multithreading to run multiple jobs simultaneously in the same process, though, you'll need the ConnectionPool gem (be sure to add `gem 'connection_pool'` to your Gemfile):
|
17
|
+
|
18
|
+
require 'uri'
|
19
|
+
require 'pg'
|
20
|
+
require 'connection_pool'
|
21
|
+
|
22
|
+
uri = URI.parse(ENV['DATABASE_URL'])
|
23
|
+
|
24
|
+
Que.connection = ConnectionPool.new :size => 10 do
|
25
|
+
PG::Connection.open :host => uri.host,
|
26
|
+
:user => uri.user,
|
27
|
+
:password => uri.password,
|
28
|
+
:port => uri.port || 5432,
|
29
|
+
:dbname => uri.path[1..-1]
|
30
|
+
end
|
31
|
+
|
32
|
+
Be sure to pick your pool size carefully - if you use 10 for the size, you'll incur the overhead of having 10 connections open to Postgres even if you never use more than a couple of them.
|
33
|
+
|
34
|
+
Please be aware that if you're using ActiveRecord or Sequel to manage your data, there's no reason for you to be using either of these methods - it's less efficient (unnecessary connections will waste memory on your database server) and you lose the reliability benefits of wrapping jobs in the same transactions as the rest of your data.
|
@@ -0,0 +1,27 @@
|
|
1
|
+
## Using Sequel
|
2
|
+
|
3
|
+
If you're using Sequel, with or without Rails, you'll need to give Que a specific database instance to use:
|
4
|
+
|
5
|
+
DB = Sequel.connect(ENV['DATABASE_URL'])
|
6
|
+
Que.connection = DB
|
7
|
+
|
8
|
+
Then you can safely use the same database object to transactionally protect your jobs:
|
9
|
+
|
10
|
+
class MyJob < Que::Job
|
11
|
+
def run
|
12
|
+
# Do stuff.
|
13
|
+
|
14
|
+
DB.transaction do
|
15
|
+
# Make changes to the database.
|
16
|
+
|
17
|
+
# Destroying this job will be protected by the same transaction.
|
18
|
+
destroy
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# In your controller action:
|
24
|
+
DB.transaction do
|
25
|
+
@user = User.create(params[:user])
|
26
|
+
SendRegistrationEmail.queue :user_id => @user.id
|
27
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
## Writing Reliable Jobs
|
2
|
+
|
3
|
+
Que does everything it can to ensure that jobs are worked exactly once, but if something bad happens when a job is halfway completed, there's no way around it - the job will need be repeated over again from the beginning, probably by a different worker. When you're writing jobs, you need to be prepared for this to happen.
|
4
|
+
|
5
|
+
The safest type of job is one that reads in data, either from the database or from external APIs, then does some number crunching and writes the results to the database. These jobs are easy to make safe - simply write the results to the database inside a transaction, and also have the job destroy itself inside that transaction, like so:
|
6
|
+
|
7
|
+
class UpdateWidgetPrice < Que::Job
|
8
|
+
def run(widget_id)
|
9
|
+
widget = Widget[widget_id]
|
10
|
+
price = ExternalService.get_widget_price(widget_id)
|
11
|
+
|
12
|
+
ActiveRecord::Base.transaction do
|
13
|
+
# Make changes to the database.
|
14
|
+
widget.update :price => price
|
15
|
+
|
16
|
+
# Destroy the job.
|
17
|
+
destroy
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
Here, you're taking advantage of the guarantees of an [ACID](https://en.wikipedia.org/wiki/ACID) database. The job is destroyed along with the other changes, so either the write will succeed and the job will be run only once, or it will fail and the database will be left untouched. But even if it fails, the job can simply be retried, and there are no lingering effects from the first attempt, so no big deal.
|
23
|
+
|
24
|
+
The more difficult type of job is one that makes changes that can't be controlled transactionally. For example, writing to an external service:
|
25
|
+
|
26
|
+
class ChargeCreditCard < Que::Job
|
27
|
+
def run(user_id, credit_card_id)
|
28
|
+
CreditCardService.charge(credit_card_id, :amount => "$10.00")
|
29
|
+
|
30
|
+
ActiveRecord::Base.transaction do
|
31
|
+
User.where(:id => user_id).update_all :charged_at => Time.now
|
32
|
+
destroy
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
What if the process abruptly dies after we tell the provider to charge the credit card, but before we finish the transaction? Que will retry the job, but there's no way to tell where (or even if) it failed the first time. The credit card will be charged a second time, and then you've got an angry customer. The ideal solution in this case is to make the job [idempotent](https://en.wikipedia.org/wiki/Idempotence), meaning that it will have the same effect no matter how many times it is run:
|
38
|
+
|
39
|
+
class ChargeCreditCard < Que::Job
|
40
|
+
def run(user_id, credit_card_id)
|
41
|
+
unless CreditCardService.check_for_previous_charge(credit_card_id)
|
42
|
+
CreditCardService.charge(credit_card_id, :amount => "$10.00")
|
43
|
+
end
|
44
|
+
|
45
|
+
ActiveRecord::Base.transaction do
|
46
|
+
User.where(:id => user_id).update_all :charged_at => Time.now
|
47
|
+
destroy
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
This makes the job slightly more complex, but reliable (or, at least, as reliable as your credit card service).
|
53
|
+
|
54
|
+
Finally, there are some jobs where you won't want to write to the database at all:
|
55
|
+
|
56
|
+
class SendVerificationEmail < Que::Job
|
57
|
+
def run(email_address)
|
58
|
+
Mailer.verification_email(email_address).deliver
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
In this case, we don't have any no way to prevent the occasional double-sending of an email. But, for ease of use, you can leave out the transaction and the `destroy` call entirely - Que will recognize that the job wasn't destroyed and will clean it up for you.
|