q 0.0.0 → 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -4
- data/CHANGELOG.md +11 -0
- data/Gemfile +2 -3
- data/QUEUE_AUTHORS.md +444 -0
- data/README.md +179 -24
- data/Rakefile +13 -0
- data/lib/q.rb +79 -2
- data/lib/q/errors.rb +32 -0
- data/lib/q/helpers.rb +25 -0
- data/lib/q/methods.rb +30 -0
- data/lib/q/methods/base.rb +53 -0
- data/lib/q/methods/delayed_job.rb +76 -0
- data/lib/q/methods/resque.rb +73 -0
- data/lib/q/methods/sidekiq.rb +123 -0
- data/lib/q/methods/threaded_in_memory_queue.rb +66 -0
- data/lib/q/tasks.rb +14 -0
- data/lib/q/version.rb +1 -1
- data/q.gemspec +25 -19
- data/test/methods/base_test.rb +103 -0
- data/test/methods/delayed_job_test.rb +35 -0
- data/test/methods/resque_test.rb +37 -0
- data/test/methods/sidekiq_test.rb +37 -0
- data/test/methods/threaded_test.rb +89 -0
- data/test/methods_test.rb +63 -0
- data/test/q_test.rb +12 -0
- data/test/support/delayed_job.rb +66 -0
- data/test/support/resque.rb +25 -0
- data/test/support/sidekiq.rb +38 -0
- data/test/support/threaded_in_memory_queue.rb +5 -0
- data/test/test_helper.rb +22 -0
- metadata +174 -120
- data/.rvmrc +0 -2
- data/bin/q +0 -7
- data/spec/.gitignore +0 -0
data/README.md
CHANGED
@@ -1,24 +1,179 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
1
|
+
## Q
|
2
|
+
|
3
|
+
Forget queue boilerplate: focus on your code.
|
4
|
+
|
5
|
+
## Install
|
6
|
+
|
7
|
+
In your `Gemfile` add:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'q'
|
11
|
+
```
|
12
|
+
|
13
|
+
Then run `$ bundle install`
|
14
|
+
|
15
|
+
## What
|
16
|
+
|
17
|
+
Q is an interface for your background queues. Are you using Resque, Sidekiq, delayed_job, queue_classic, or some other queue? Awesome sauce, because with `Q` you can write your queuing code once and re-use against different backends.
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
Q.setup do |config|
|
21
|
+
config.queue = :resque
|
22
|
+
end
|
23
|
+
```
|
24
|
+
|
25
|
+
Now in your code when you need to enqueue something first you need to add the `Q::Methods` module:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
class Poro
|
29
|
+
include Q::Methods
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
Now you can define tasks using the `queue` class method like this:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
class Poro
|
37
|
+
include Q::Methods
|
38
|
+
|
39
|
+
queue(:send_issues) do |id, state|
|
40
|
+
user = User.find(id)
|
41
|
+
issues = user.issues.where(state: state).all
|
42
|
+
UserMailer.send_issues(user: user, issues: issues).deliver
|
43
|
+
end
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
Here we're building a background task called `send_issues` that will send out an email when executed.
|
48
|
+
|
49
|
+
Now that the task is defined, you can enqueue a `send_issues` job to be executed later by calling `queue` and then `send_issues` like this:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
user = User.last
|
53
|
+
state = 'open'
|
54
|
+
Poro.queue.send_issues(user.id, state)
|
55
|
+
```
|
56
|
+
|
57
|
+
The Q interface expects json-able objects, numbers, arrays, hashes, etc. This is important if you want your code to be re-usable across multiple queue backends.
|
58
|
+
|
59
|
+
## No Queue? No Problem
|
60
|
+
|
61
|
+
The `Q` library comes with a threaded queue that does not need a backend (such as Redis) by default, so you can write your code today and figure out what queue you want to use tomorrow.
|
62
|
+
|
63
|
+
Note: that this threaded queue is very basic and should not be used in production. If you stop your Ruby process while there are jobs in memory you will lose your jobs see [threaded_in_memory_queue](https://github.com/schneems/threaded_in_memory_queue) for more information.
|
64
|
+
|
65
|
+
## Starting your Queue
|
66
|
+
|
67
|
+
Most background queue libraries must be run in a seperate process. The `Q` library makes starting these background tasks easy.
|
68
|
+
|
69
|
+
Make sure there is a Rake task named `:environment` that loads your app (Rails provides one by default). Then add this line to your `Procfile`:
|
70
|
+
|
71
|
+
```
|
72
|
+
worker: bundle exec rake q:work
|
73
|
+
```
|
74
|
+
|
75
|
+
Now if you are running on Heroku the background task will automatically be run. Locally you can run the task manually by executing:
|
76
|
+
|
77
|
+
```sh
|
78
|
+
$ bundle exec rake q:work
|
79
|
+
```
|
80
|
+
|
81
|
+
Or through your `Procfile` with foreman:
|
82
|
+
|
83
|
+
```sh
|
84
|
+
$ foreman start
|
85
|
+
```
|
86
|
+
|
87
|
+
If your queueing library supports any custom environment variables or flags you can add them to your `rake q:work` command and they will be passed to the supporting background queue's task.
|
88
|
+
|
89
|
+
Note: the default threaded queue does not need to be started as it runs in your web process
|
90
|
+
|
91
|
+
## Config
|
92
|
+
|
93
|
+
You can configure the behavior of your background queue using `Q.queue_config`. For example if you are using Resque and want to run commands inline you could execute:
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
Q.queue_config.inline = true
|
97
|
+
```
|
98
|
+
|
99
|
+
Now any calls to `enqueue` will bypass resque and be run immediately. Different queues will have different configuration options so you will need to see their docs for configuration options.
|
100
|
+
|
101
|
+
You can access this config in the setup command:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
Q.setup do |config|
|
105
|
+
config.queue = :resque
|
106
|
+
config.queue_config.inline = true
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
It also accepts a block:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
Q.queue_config do |config|
|
114
|
+
config.inline = true
|
115
|
+
end
|
116
|
+
```
|
117
|
+
|
118
|
+
Don't confuse `queue_config` which will configure your background queue (such as Resque) with `setup` which configures the `Q` library itself.
|
119
|
+
|
120
|
+
## Diverging Backends
|
121
|
+
|
122
|
+
As much as we try to make all front end code similar, you'll still need to setup your queue. To make sqitching back and forth easier, we provide a `Q.env` object that responds to the backend you are using such as `Q.env.resque?`.
|
123
|
+
|
124
|
+
That way you could keep multiple queue configurations in your app and it won't raise any errors if you're running a different backend.
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
if Q.env.resque?
|
128
|
+
# config resque here
|
129
|
+
end
|
130
|
+
|
131
|
+
if Q.env.sidekiq?
|
132
|
+
# configure sidekiq here
|
133
|
+
else
|
134
|
+
```
|
135
|
+
|
136
|
+
## Supported Queue Backends
|
137
|
+
|
138
|
+
```
|
139
|
+
config.queue = :sidekiq
|
140
|
+
config.queue = :resque
|
141
|
+
config.queue = :threaded
|
142
|
+
```
|
143
|
+
|
144
|
+
Coming soon:
|
145
|
+
|
146
|
+
```
|
147
|
+
config.queue = :delayed_job
|
148
|
+
```
|
149
|
+
|
150
|
+
|
151
|
+
|
152
|
+
## Blocks
|
153
|
+
|
154
|
+
You can set default values in blocks like this:
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
queue(:foo) do |id, state = 'open', username = 'schneems'|
|
158
|
+
# ...
|
159
|
+
end
|
160
|
+
```
|
161
|
+
|
162
|
+
You can have an unlimited amount of args using a splat:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
queue(:foo) do |id, *args|
|
166
|
+
# ...
|
167
|
+
end
|
168
|
+
```
|
169
|
+
|
170
|
+
|
171
|
+
## Q Authors
|
172
|
+
|
173
|
+
Did you write a background queuing library? Want to add support for the `Q` interface? Check out the [QUEUE_AUTHORS.md](QUEUE_AUTHORS.md) file to get started.
|
174
|
+
|
175
|
+
## License
|
176
|
+
|
177
|
+
Brought to you by [@schneems](http://twitter.com/schneems)
|
178
|
+
|
179
|
+
MIT
|
data/Rakefile
CHANGED
@@ -1 +1,14 @@
|
|
1
|
+
# encoding: UTF-8
|
1
2
|
require 'bundler/gem_tasks'
|
3
|
+
|
4
|
+
require 'rake'
|
5
|
+
require 'rake/testtask'
|
6
|
+
|
7
|
+
task :default => [:test]
|
8
|
+
|
9
|
+
test_task = Rake::TestTask.new(:test) do |t|
|
10
|
+
t.libs << 'lib'
|
11
|
+
t.libs << 'test'
|
12
|
+
t.pattern = 'test/**/*_test.rb'
|
13
|
+
t.verbose = false
|
14
|
+
end
|
data/lib/q.rb
CHANGED
@@ -1,5 +1,82 @@
|
|
1
|
-
require
|
1
|
+
require 'proc_to_lambda'
|
2
2
|
|
3
3
|
module Q
|
4
|
-
|
4
|
+
class Queue
|
5
|
+
end
|
5
6
|
end
|
7
|
+
|
8
|
+
require 'q/version'
|
9
|
+
require 'q/helpers'
|
10
|
+
require 'q/errors'
|
11
|
+
require 'q/methods/base'
|
12
|
+
require 'q/methods'
|
13
|
+
require 'q/tasks'
|
14
|
+
|
15
|
+
module Q
|
16
|
+
extend Q::Helpers
|
17
|
+
DEFAULT_QUEUE = ->{ @env = :threaded; Q::Methods::Threaded }
|
18
|
+
FALSEY_HASH = Hash.new(false)
|
19
|
+
|
20
|
+
def self.queue
|
21
|
+
@queue_method || DEFAULT_QUEUE.call
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.setup(&block)
|
25
|
+
yield self
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.env
|
29
|
+
name = queue.to_s.split("::").last
|
30
|
+
@env ||= Q.underscore(name)
|
31
|
+
|
32
|
+
OpenStruct.new(FALSEY_HASH.merge("#{@env}?" => true))
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.reset_queue!
|
36
|
+
@queue_method = nil
|
37
|
+
@env = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.module_from_klass_name(name)
|
41
|
+
unless defined?(Q::Methods.const_get(name))
|
42
|
+
require "q/methods/#{name}"
|
43
|
+
end
|
44
|
+
return Q::Methods.const_get(name)
|
45
|
+
rescue LoadError => e
|
46
|
+
raise LoadError, "Could not find queue: #{name}, expected to be defined in q/methods/#{name}\n" + e.message
|
47
|
+
rescue NameError => e
|
48
|
+
raise NameError, "Could not load queue: #{name}, expected to be defined in q/methods/#{name}\n" + e.message
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.module_from_queue_name(queue_name)
|
52
|
+
module_from_klass_name(camelize(queue_name))
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.queue_lookup
|
56
|
+
@queue_lookup ||= Hash.new do |hash, key|
|
57
|
+
hash[key] = -> {
|
58
|
+
require "q/methods/#{key}"
|
59
|
+
const = Q.camelize(key)
|
60
|
+
::Q::Methods.const_get(const)
|
61
|
+
}
|
62
|
+
end
|
63
|
+
@queue_lookup
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.queue=(queue)
|
67
|
+
if queue.is_a?(Module)
|
68
|
+
@queue_method = queue
|
69
|
+
else
|
70
|
+
@env = queue
|
71
|
+
@queue_method = queue_lookup[queue].call
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.queue_config(&block)
|
76
|
+
@config_class ||= queue::QueueConfig.call
|
77
|
+
yield @config_class if block_given?
|
78
|
+
@config_class
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
require 'q/methods/threaded_in_memory_queue'
|
data/lib/q/errors.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module Q
|
2
|
+
class StandardError < ::StandardError; end
|
3
|
+
|
4
|
+
class MissingClassError < StandardError
|
5
|
+
def initialize(base, missing_klass)
|
6
|
+
msg = "#{base} must define '#{missing_klass}' class with a call method"
|
7
|
+
super(msg)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class InstanceQueueDefinitionError < StandardError
|
12
|
+
def initialize(obj)
|
13
|
+
msg = "Cannot define a queue on an instance: #{obj}. Try defining it directly on the class #{obj.class}"
|
14
|
+
super(msg)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class DuplicateQueueClassError < StandardError
|
19
|
+
def initialize(base, duplicate_klass)
|
20
|
+
msg = "Cannot create queue class: '#{duplicate_klass}' because #{duplicate_klass} is already defined on #{base}"
|
21
|
+
super(msg)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class DuplicateQueueMethodError < StandardError
|
26
|
+
def initialize(base, method)
|
27
|
+
msg = "Cannot create queue method: '#{method}'. Method already exists on #{base}.queue, cannot overwrite"
|
28
|
+
msg << "Originally defined at #{base.queue.method(method).source_location}"
|
29
|
+
super(msg)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/q/helpers.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module Q
|
2
|
+
module Helpers
|
3
|
+
def camelize(term)
|
4
|
+
string = term.to_s
|
5
|
+
string = string.sub(/^[a-z\d]*/) { $&.capitalize }
|
6
|
+
string = string.gsub(/(?:_|(\/))([a-z\d]*)/i) { "#{$2.capitalize}" }.gsub('/', '::')
|
7
|
+
string
|
8
|
+
end
|
9
|
+
|
10
|
+
def underscore(term)
|
11
|
+
string = term.to_s
|
12
|
+
string = string.sub(/^[a-z\d]*/) { "#{$&.downcase}_" }
|
13
|
+
string = string.gsub(/^_/, '')
|
14
|
+
string
|
15
|
+
end
|
16
|
+
|
17
|
+
def const_defined_on?(on, const)
|
18
|
+
on.constants.include?(const.to_sym)
|
19
|
+
end
|
20
|
+
|
21
|
+
def proc_to_lambda(block = nil, &proc)
|
22
|
+
::ProcToLambda.to_lambda(block || proc)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/q/methods.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
module Q
|
2
|
+
module Methods
|
3
|
+
include Q::Methods::Base
|
4
|
+
|
5
|
+
class QueueConfig
|
6
|
+
def self.call
|
7
|
+
Q.queue::QueueConfig
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class QueueTask
|
12
|
+
def self.call(*rake_args)
|
13
|
+
Q.queue::QueueTask.call(rake_args)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class QueueBuild
|
18
|
+
def self.call(options={}, &job)
|
19
|
+
Q.queue::QueueBuild.call(options, &job)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class QueueMethod
|
24
|
+
def self.call(options = {})
|
25
|
+
Q.queue::QueueMethod.call(options)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
Q::Method = Q::Methods
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Q
|
2
|
+
module Methods
|
3
|
+
module Base
|
4
|
+
def self.included(base)
|
5
|
+
base.const_set("Queue", Class.new(::Q::Queue)) unless base.const_get("Queue") != ::Queue
|
6
|
+
|
7
|
+
included = base.method(:included) if base.respond_to?(:included)
|
8
|
+
base.define_singleton_method(:included) do |target|
|
9
|
+
included.call(target) unless included.nil?
|
10
|
+
|
11
|
+
raise Q::MissingClassError.new(base, :QueueMethod) unless Q.const_defined_on?(base, :QueueMethod)
|
12
|
+
raise Q::MissingClassError.new(base, :QueueBuild) unless Q.const_defined_on?(base, :QueueBuild)
|
13
|
+
raise Q::MissingClassError.new(base, :QueueTask) unless Q.const_defined_on?(base, :QueueTask)
|
14
|
+
raise Q::MissingClassError.new(base, :QueueConfig) unless Q.const_defined_on?(base, :QueueConfig)
|
15
|
+
|
16
|
+
target.extend(ClassMethods)
|
17
|
+
target.send(:include, InstanceMethods)
|
18
|
+
|
19
|
+
target.class_variable_set(:@@_q_klass, base) unless target.class_variable_defined?(:@@_q_klass)
|
20
|
+
target.class_variable_set(:@@_q_queue, base::Queue.new) unless target.class_variable_defined?(:@@_q_queue)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module InstanceMethods
|
25
|
+
def queue
|
26
|
+
raise Q::InstanceQueueDefinitionError.new(self) if block_given?
|
27
|
+
self.class.queue
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module ClassMethods
|
32
|
+
def queue(*args, &block)
|
33
|
+
queue = self.class_variable_get(:@@_q_queue)
|
34
|
+
|
35
|
+
return queue unless block_given?
|
36
|
+
|
37
|
+
queue_name = args.shift
|
38
|
+
job = Q.proc_to_lambda(&block)
|
39
|
+
|
40
|
+
raise "first argument #{queue_name.inspect} must be a symbol to define a queue" unless queue_name.is_a?(Symbol)
|
41
|
+
|
42
|
+
options = { base: self,
|
43
|
+
queue_name: queue_name,
|
44
|
+
queue_klass_name: Q.camelize(queue_name) }
|
45
|
+
|
46
|
+
queue_klass = self.class_variable_get(:@@_q_klass)
|
47
|
+
queue_klass::QueueBuild.call(options, &job)
|
48
|
+
queue_klass::QueueMethod.call(options)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|