que 1.0.0.beta → 1.0.0.beta5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/tests.yml +43 -0
- data/CHANGELOG.1.0.beta.md +137 -0
- data/CHANGELOG.md +34 -12
- data/README.md +67 -7
- data/bin/command_line_interface.rb +61 -49
- data/docs/README.md +785 -32
- data/lib/que/active_record/connection.rb +4 -4
- data/lib/que/active_record/model.rb +4 -4
- data/lib/que/connection.rb +35 -16
- data/lib/que/connection_pool.rb +2 -2
- data/lib/que/job.rb +1 -1
- data/lib/que/{job_cache.rb → job_buffer.rb} +96 -72
- data/lib/que/job_methods.rb +4 -0
- data/lib/que/locker.rb +170 -139
- data/lib/que/poller.rb +1 -1
- data/lib/que/rails/railtie.rb +2 -4
- data/lib/que/result_queue.rb +2 -2
- data/lib/que/sequel/model.rb +14 -16
- data/lib/que/utils/constantization.rb +1 -1
- data/lib/que/utils/logging.rb +2 -1
- data/lib/que/utils/middleware.rb +26 -13
- data/lib/que/version.rb +1 -1
- data/lib/que/worker.rb +43 -21
- data/lib/que.rb +9 -4
- data/que.gemspec +2 -2
- metadata +11 -25
- data/docs/active_job.md +0 -6
- data/docs/advanced_setup.md +0 -49
- data/docs/command_line_interface.md +0 -45
- data/docs/error_handling.md +0 -94
- data/docs/inspecting_the_queue.md +0 -64
- data/docs/job_helper_methods.md +0 -27
- data/docs/logging.md +0 -31
- data/docs/managing_workers.md +0 -25
- data/docs/middleware.md +0 -15
- data/docs/migrating.md +0 -27
- data/docs/multiple_queues.md +0 -31
- data/docs/shutting_down_safely.md +0 -7
- data/docs/using_plain_connections.md +0 -65
- data/docs/using_sequel.md +0 -33
- data/docs/writing_reliable_jobs.md +0 -108
data/docs/multiple_queues.md
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
## Multiple Queues
|
2
|
-
|
3
|
-
Que supports the use of multiple queues in a single job table. Please note that this feature is intended to support the case where multiple codebases are sharing the same job queue - if you want to support jobs of differing priorities, the numeric priority system offers better flexibility and performance.
|
4
|
-
|
5
|
-
For instance, you might have a separate Ruby application that handles only processing credit cards. In that case, you can run that application's workers against a specific queue:
|
6
|
-
|
7
|
-
```shell
|
8
|
-
que --queue-name credit_cards
|
9
|
-
# The -q flag is equivalent, and either can be passed multiple times.
|
10
|
-
que -q default -q credit_cards
|
11
|
-
```
|
12
|
-
|
13
|
-
Then you can set jobs to be enqueued in that queue specifically:
|
14
|
-
|
15
|
-
```ruby
|
16
|
-
ProcessCreditCard.enqueue current_user.id, queue: 'credit_cards'
|
17
|
-
|
18
|
-
# Or:
|
19
|
-
|
20
|
-
class ProcessCreditCard < Que::Job
|
21
|
-
# Set a default queue for this job class; this can be overridden by
|
22
|
-
# passing the :queue parameter to enqueue like above.
|
23
|
-
self.queue = 'credit_cards'
|
24
|
-
end
|
25
|
-
```
|
26
|
-
|
27
|
-
In some cases, the ProcessCreditCard class may not be defined in the application that is enqueueing the job. In that case, you can specify the job class as a string:
|
28
|
-
|
29
|
-
```ruby
|
30
|
-
Que.enqueue current_user.id, job_class: 'ProcessCreditCard', queue: 'credit_cards'
|
31
|
-
```
|
@@ -1,7 +0,0 @@
|
|
1
|
-
## Shutting Down Safely
|
2
|
-
|
3
|
-
To ensure safe operation, Que needs to be very careful in how it shuts down. When a Ruby process ends normally, it calls Thread#kill on any threads that are still running - unfortunately, if a thread is in the middle of a transaction when this happens, there is a risk that it will be prematurely commited, resulting in data corruption. See [here](http://blog.headius.com/2008/02/ruby-threadraise-threadkill-timeoutrb.html) and [here](http://coderrr.wordpress.com/2011/05/03/beware-of-threadkill-or-your-activerecord-transactions-are-in-danger-of-being-partially-committed/) for more detail on this.
|
4
|
-
|
5
|
-
To prevent this, Que will block the worker process from exiting until all jobs it is working have completed normally. Unfortunately, if you have long-running jobs, this may take a very long time (and if something goes wrong with a job's logic, it may never happen). The solution in this case is SIGKILL - luckily, Ruby processes that are killed via SIGKILL will end without using Thread#kill on its running threads. This is safer than exiting normally - when PostgreSQL loses the connection it will simply roll back the open transaction, if any, and unlock the job so it can be retried later by another worker. Be sure to read [Writing Reliable Jobs](https://github.com/chanks/que/blob/master/docs/writing_reliable_jobs.md) for information on how to design your jobs to fail safely.
|
6
|
-
|
7
|
-
So, be prepared to use SIGKILL on your Ruby processes if they run for too long. For example, Heroku takes a good approach to this - when Heroku's platform is shutting down a process, it sends SIGTERM, waits ten seconds, then sends SIGKILL if the process still hasn't exited. This is a nice compromise - it will give each of your currently running jobs ten seconds to complete, and any jobs that haven't finished by then will be interrupted and retried later.
|
@@ -1,65 +0,0 @@
|
|
1
|
-
## Using Plain Postgres Connections
|
2
|
-
|
3
|
-
If you're not using an ORM like ActiveRecord or Sequel, you can use a distinct connection pool to manage your Postgres connections. Please be aware that if you **are** using ActiveRecord or Sequel, there's no reason for you to be using any 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.
|
4
|
-
|
5
|
-
## Using ConnectionPool or Pond
|
6
|
-
|
7
|
-
Support for two connection pool gems is included in Que. The first is the ConnectionPool gem (be sure to add `gem 'connection_pool'` to your Gemfile):
|
8
|
-
|
9
|
-
```ruby
|
10
|
-
require 'uri'
|
11
|
-
require 'pg'
|
12
|
-
require 'connection_pool'
|
13
|
-
|
14
|
-
uri = URI.parse(ENV['DATABASE_URL'])
|
15
|
-
|
16
|
-
Que.connection = ConnectionPool.new(size: 10) do
|
17
|
-
PG::Connection.open(
|
18
|
-
host: uri.host,
|
19
|
-
user: uri.user,
|
20
|
-
password: uri.password,
|
21
|
-
port: uri.port || 5432,
|
22
|
-
dbname: uri.path[1..-1]
|
23
|
-
)end
|
24
|
-
```
|
25
|
-
|
26
|
-
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.
|
27
|
-
|
28
|
-
The Pond gem doesn't have this drawback - it is very similar to ConnectionPool, but establishes connections lazily (add `gem 'pond'` to your Gemfile):
|
29
|
-
|
30
|
-
```ruby
|
31
|
-
require 'uri'
|
32
|
-
require 'pg'
|
33
|
-
require 'pond'
|
34
|
-
|
35
|
-
uri = URI.parse(ENV['DATABASE_URL'])
|
36
|
-
|
37
|
-
Que.connection = Pond.new(maximum_size: 10) do
|
38
|
-
PG::Connection.open(
|
39
|
-
host: uri.host,
|
40
|
-
user: uri.user,
|
41
|
-
password: uri.password,
|
42
|
-
port: uri.port || 5432,
|
43
|
-
dbname: uri.path[1..-1]
|
44
|
-
)
|
45
|
-
end
|
46
|
-
```
|
47
|
-
|
48
|
-
## Using Any Other Connection Pool
|
49
|
-
|
50
|
-
You can use any other in-process connection pool by defining access to it in a proc that's passed to `Que.connection_proc = proc`. The proc you pass should accept a block and call it with a connection object. For instance, Que's built-in interface to Sequel's connection pool is basically implemented like:
|
51
|
-
|
52
|
-
```ruby
|
53
|
-
Que.connection_proc = proc do |&block|
|
54
|
-
DB.synchronize do |connection|
|
55
|
-
block.call(connection)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
```
|
59
|
-
|
60
|
-
This proc must meet a few requirements:
|
61
|
-
- The yielded object must be an instance of `PG::Connection`.
|
62
|
-
- It must be reentrant - if it is called with a block, and then called again inside that block, it must return the same object. For example, in `proc.call{|conn1| proc.call{|conn2| conn1.object_id == conn2.object_id}}` the innermost condition must be true.
|
63
|
-
- It must lock the connection object and prevent any other thread from accessing it for the duration of the block.
|
64
|
-
|
65
|
-
If any of these conditions aren't met, Que will raise an error.
|
data/docs/using_sequel.md
DELETED
@@ -1,33 +0,0 @@
|
|
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
|
-
```ruby
|
6
|
-
DB = Sequel.connect(ENV['DATABASE_URL'])
|
7
|
-
Que.connection = DB
|
8
|
-
```
|
9
|
-
|
10
|
-
Then you can safely use the same database object to transactionally protect your jobs:
|
11
|
-
|
12
|
-
```ruby
|
13
|
-
class MyJob < Que::Job
|
14
|
-
def run(user_id:)
|
15
|
-
# Do stuff.
|
16
|
-
|
17
|
-
DB.transaction do
|
18
|
-
# Make changes to the database.
|
19
|
-
|
20
|
-
# Destroying this job will be protected by the same transaction.
|
21
|
-
destroy
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
# Or, in your controller action:
|
27
|
-
DB.transaction do
|
28
|
-
@user = User.create(params[:user])
|
29
|
-
MyJob.enqueue user_id: @user.id
|
30
|
-
end
|
31
|
-
```
|
32
|
-
|
33
|
-
Sequel automatically wraps model persistance actions (create, update, destroy) in transactions, so you can simply call #enqueue methods from your models' callbacks, if you wish.
|
@@ -1,108 +0,0 @@
|
|
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 destroy the job inside that transaction, like so:
|
6
|
-
|
7
|
-
```ruby
|
8
|
-
class UpdateWidgetPrice < Que::Job
|
9
|
-
def run(widget_id)
|
10
|
-
widget = Widget[widget_id]
|
11
|
-
price = ExternalService.get_widget_price(widget_id)
|
12
|
-
|
13
|
-
ActiveRecord::Base.transaction do
|
14
|
-
# Make changes to the database.
|
15
|
-
widget.update price: price
|
16
|
-
|
17
|
-
# Mark the job as destroyed, so it doesn't run again.
|
18
|
-
destroy
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
```
|
23
|
-
|
24
|
-
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.
|
25
|
-
|
26
|
-
The more difficult type of job is one that makes changes that can't be controlled transactionally. For example, writing to an external service:
|
27
|
-
|
28
|
-
```ruby
|
29
|
-
class ChargeCreditCard < Que::Job
|
30
|
-
def run(user_id, credit_card_id)
|
31
|
-
CreditCardService.charge(credit_card_id, amount: "$10.00")
|
32
|
-
|
33
|
-
ActiveRecord::Base.transaction do
|
34
|
-
User.where(id: user_id).update_all charged_at: Time.now
|
35
|
-
destroy
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
```
|
40
|
-
|
41
|
-
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:
|
42
|
-
|
43
|
-
```ruby
|
44
|
-
class ChargeCreditCard < Que::Job
|
45
|
-
def run(user_id, credit_card_id)
|
46
|
-
unless CreditCardService.check_for_previous_charge(credit_card_id)
|
47
|
-
CreditCardService.charge(credit_card_id, amount: "$10.00")
|
48
|
-
end
|
49
|
-
|
50
|
-
ActiveRecord::Base.transaction do
|
51
|
-
User.where(id: user_id).update_all charged_at: Time.now
|
52
|
-
destroy
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
```
|
57
|
-
|
58
|
-
This makes the job slightly more complex, but reliable (or, at least, as reliable as your credit card service).
|
59
|
-
|
60
|
-
Finally, there are some jobs where you won't want to write to the database at all:
|
61
|
-
|
62
|
-
```ruby
|
63
|
-
class SendVerificationEmail < Que::Job
|
64
|
-
def run(email_address)
|
65
|
-
Mailer.verification_email(email_address).deliver
|
66
|
-
end
|
67
|
-
end
|
68
|
-
```
|
69
|
-
|
70
|
-
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.
|
71
|
-
|
72
|
-
### Timeouts
|
73
|
-
|
74
|
-
Long-running jobs aren't necessarily a problem for the database, since the overhead of an individual job is very small (just an advisory lock held in memory). But jobs that hang indefinitely can tie up a worker and [block the Ruby process from exiting gracefully](https://github.com/chanks/que/blob/master/docs/shutting_down_safely.md), which is a pain.
|
75
|
-
|
76
|
-
If there's part of your job that is prone to hang (due to an API call or other HTTP request that never returns, for example), you can (and should) timeout those parts of your job. For example, consider a job that needs to make an HTTP request and then write to the database:
|
77
|
-
|
78
|
-
```ruby
|
79
|
-
class ScrapeStuff < Que::Job
|
80
|
-
def run(url_to_scrape)
|
81
|
-
result = YourHTTPLibrary.get(url_to_scrape)
|
82
|
-
|
83
|
-
ActiveRecord::Base.transaction do
|
84
|
-
# Insert result...
|
85
|
-
|
86
|
-
destroy
|
87
|
-
end
|
88
|
-
end
|
89
|
-
end
|
90
|
-
```
|
91
|
-
|
92
|
-
That request could take a very long time, or never return at all. Let's use the timeout feature that almost all HTTP libraries offer some version of:
|
93
|
-
|
94
|
-
```ruby
|
95
|
-
class ScrapeStuff < Que::Job
|
96
|
-
def run(url_to_scrape)
|
97
|
-
result = YourHTTPLibrary.get(url_to_scrape, timeout: 5)
|
98
|
-
|
99
|
-
ActiveRecord::Base.transaction do
|
100
|
-
# Insert result...
|
101
|
-
|
102
|
-
destroy
|
103
|
-
end
|
104
|
-
end
|
105
|
-
end
|
106
|
-
```
|
107
|
-
|
108
|
-
Now, if the request takes more than five seconds, an error will be raised (probably - check your library's documentation) and Que will just retry the job later.
|