rails-four-queueing 0.1.0
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/MIT-LICENSE +20 -0
- data/README.md +97 -0
- data/Rakefile +38 -0
- data/lib/rails-four-queueing.rb +8 -0
- data/lib/rails/four/application.rb +17 -0
- data/lib/rails/four/configuration.rb +12 -0
- data/lib/rails/four/queueing.rb +106 -0
- data/lib/rails/four/queueing/engine.rb +50 -0
- data/lib/rails/four/queueing/version.rb +7 -0
- data/lib/tasks/rails-four-queueing_tasks.rake +4 -0
- data/test/ci/before_script.sh +4 -0
- data/test/rails-four-queueing_test.rb +7 -0
- data/test/rails/four/queueing/synchronous_queue_test.rb +28 -0
- data/test/rails/four/queueing/test_queue_test.rb +147 -0
- data/test/rails/four/queueing/threaded_consumer_test.rb +111 -0
- data/test/rails/initializers/frameworks_test.rb +247 -0
- data/test/rails/queue_test.rb +169 -0
- data/test/support/active_support_abstract_unit.rb +26 -0
- data/test/support/empty_bool.rb +8 -0
- data/test/support/isolation_abstract_unit.rb +288 -0
- metadata +100 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2012 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
# Rails::Four::Queueing
|
2
|
+
|
3
|
+
[](https://next.travis-ci.org/phlipper/rails-four-queueing)
|
4
|
+
|
5
|
+
## Description
|
6
|
+
|
7
|
+
It's the Queue from Rails 4, backported to Rails 3.2+ for your Queueing pleasure.
|
8
|
+
|
9
|
+
|
10
|
+
## Requirements
|
11
|
+
|
12
|
+
This gem requires Rails 3.2+ and has been tested on the following versions:
|
13
|
+
|
14
|
+
* 3.2.8
|
15
|
+
|
16
|
+
This gem has been tested against the following Ruby versions:
|
17
|
+
|
18
|
+
* 1.9.2
|
19
|
+
* 1.9.3
|
20
|
+
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
Add this line to your application's Gemfile:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
gem "rails-four-queueing"
|
28
|
+
```
|
29
|
+
|
30
|
+
And then execute:
|
31
|
+
|
32
|
+
```
|
33
|
+
$ bundle
|
34
|
+
```
|
35
|
+
|
36
|
+
Or install it yourself as:
|
37
|
+
|
38
|
+
```
|
39
|
+
$ gem install rails-four-queueing
|
40
|
+
```
|
41
|
+
|
42
|
+
|
43
|
+
## Usage
|
44
|
+
|
45
|
+
It's the [Rails 4 Queue](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/queueing.rb):
|
46
|
+
|
47
|
+
```
|
48
|
+
Rails.queue is the application's queue. You can push a job onto
|
49
|
+
the queue by:
|
50
|
+
|
51
|
+
Rails.queue.push job
|
52
|
+
|
53
|
+
A job is an object that responds to +run+. Queue consumers will
|
54
|
+
pop jobs off of the queue and invoke the queue's +run+ method.
|
55
|
+
|
56
|
+
Note that depending on your queue implementation, jobs may not
|
57
|
+
be executed in the same process as they were created in, and
|
58
|
+
are never executed in the same thread as they were created in.
|
59
|
+
|
60
|
+
If necessary, a queue implementation may need to serialize your
|
61
|
+
job for distribution to another process. The documentation of
|
62
|
+
your queue will specify the requirements for that serialization
|
63
|
+
```
|
64
|
+
|
65
|
+
|
66
|
+
## Contributing
|
67
|
+
|
68
|
+
1. [Fork it](https://github.com/phlipper/rails-four-queueing/fork_select)
|
69
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
70
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
71
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
72
|
+
5. [Create a Pull Request](hhttps://github.com/phlipper/rails-four-queueing/pull/new)
|
73
|
+
|
74
|
+
|
75
|
+
## Contributors
|
76
|
+
|
77
|
+
Many thanks go to the following who have contributed to making this gem even better:
|
78
|
+
|
79
|
+
* **[@rails](https://github.com/rails)**
|
80
|
+
* for making this all possible in the first place
|
81
|
+
* **[@elskwid](https://github.com/elskwid)**
|
82
|
+
* for providing the inspiration, and the persperation, to see this through to the bitter end
|
83
|
+
* **[@phlipper](https://github.com/phlipper)**
|
84
|
+
* for providing the inspiration, and the persperation, to see this through to the bitter end
|
85
|
+
|
86
|
+
|
87
|
+
## License
|
88
|
+
|
89
|
+
**rails-four-queueing**
|
90
|
+
|
91
|
+
* Freely distributable and licensed under the [MIT license](http://phlipper.mit-license.org/2012/license.html).
|
92
|
+
* Copyright (c) 2012 Phil Cohen (github@phlippers.net) [](http://coderwall.com/phlipper)
|
93
|
+
* http://phlippers.net/
|
94
|
+
|
95
|
+
**rails**
|
96
|
+
|
97
|
+
* Ruby on Rails is released under the [MIT License](http://www.opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'Rails::Four::Queueing'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
|
26
|
+
Bundler::GemHelper.install_tasks
|
27
|
+
|
28
|
+
require 'rake/testtask'
|
29
|
+
|
30
|
+
Rake::TestTask.new(:test) do |t|
|
31
|
+
t.libs << 'lib'
|
32
|
+
t.libs << 'test'
|
33
|
+
t.pattern = 'test/**/*_test.rb'
|
34
|
+
t.verbose = false
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
task :default => :test
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Rails
|
2
|
+
module Four
|
3
|
+
module Application
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
attr_accessor :queue_consumer
|
8
|
+
attr_writer :queue
|
9
|
+
end
|
10
|
+
|
11
|
+
def queue #:nodoc:
|
12
|
+
@queue ||= config.queue || Rails::Four::Queueing::Queue.new
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# Original File: RAILS_ROOT/activesupport/lib/active_support/queueing.rb
|
2
|
+
require "delegate"
|
3
|
+
require "thread"
|
4
|
+
|
5
|
+
module Rails::Four::Queueing
|
6
|
+
# A Queue that simply inherits from STDLIB's Queue. When this
|
7
|
+
# queue is used, Rails automatically starts a job runner in a
|
8
|
+
# background thread.
|
9
|
+
class Queue < ::Queue
|
10
|
+
attr_writer :consumer
|
11
|
+
|
12
|
+
def initialize(consumer_options = {})
|
13
|
+
super()
|
14
|
+
@consumer_options = consumer_options
|
15
|
+
end
|
16
|
+
|
17
|
+
def consumer
|
18
|
+
@consumer ||= ThreadedQueueConsumer.new(self, @consumer_options)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Drain the queue, running all jobs in a different thread. This method
|
22
|
+
# may not be available on production queues.
|
23
|
+
def drain
|
24
|
+
# run the jobs in a separate thread so assumptions of synchronous
|
25
|
+
# jobs are caught in test mode.
|
26
|
+
consumer.drain
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class SynchronousQueue < Queue
|
31
|
+
def push(job)
|
32
|
+
super.tap { drain }
|
33
|
+
end
|
34
|
+
alias << push
|
35
|
+
alias enq push
|
36
|
+
end
|
37
|
+
|
38
|
+
# In test mode, the Rails queue is backed by an Array so that assertions
|
39
|
+
# can be made about its contents. The test queue provides a +jobs+
|
40
|
+
# method to make assertions about the queue's contents and a +drain+
|
41
|
+
# method to drain the queue and run the jobs.
|
42
|
+
#
|
43
|
+
# Jobs are run in a separate thread to catch mistakes where code
|
44
|
+
# assumes that the job is run in the same thread.
|
45
|
+
class TestQueue < Queue
|
46
|
+
# Get a list of the jobs off this queue. This method may not be
|
47
|
+
# available on production queues.
|
48
|
+
def jobs
|
49
|
+
@que.dup
|
50
|
+
end
|
51
|
+
|
52
|
+
# Marshal and unmarshal job before pushing it onto the queue. This will
|
53
|
+
# raise an exception on any attempts in tests to push jobs that can't (or
|
54
|
+
# shouldn't) be marshalled.
|
55
|
+
def push(job)
|
56
|
+
super Marshal.load(Marshal.dump(job))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# The threaded consumer will run jobs in a background thread in
|
61
|
+
# development mode or in a VM where running jobs on a thread in
|
62
|
+
# production mode makes sense.
|
63
|
+
#
|
64
|
+
# When the process exits, the consumer pushes a nil onto the
|
65
|
+
# queue and joins the thread, which will ensure that all jobs
|
66
|
+
# are executed before the process finally dies.
|
67
|
+
class ThreadedQueueConsumer
|
68
|
+
attr_accessor :logger
|
69
|
+
|
70
|
+
def initialize(queue, options = {})
|
71
|
+
@queue = queue
|
72
|
+
@logger = options[:logger]
|
73
|
+
@fallback_logger = Logger.new($stderr)
|
74
|
+
end
|
75
|
+
|
76
|
+
def start
|
77
|
+
@thread = Thread.new { consume }
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
def shutdown
|
82
|
+
@queue.push nil
|
83
|
+
@thread.join
|
84
|
+
end
|
85
|
+
|
86
|
+
def drain
|
87
|
+
@queue.pop.run until @queue.empty?
|
88
|
+
end
|
89
|
+
|
90
|
+
def consume
|
91
|
+
while job = @queue.pop
|
92
|
+
run job
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def run(job)
|
97
|
+
job.run
|
98
|
+
rescue Exception => exception
|
99
|
+
handle_exception job, exception
|
100
|
+
end
|
101
|
+
|
102
|
+
def handle_exception(job, exception)
|
103
|
+
(logger || @fallback_logger).error "Job Error: #{job.inspect}\n#{exception.message}\n#{exception.backtrace.join("\n")}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require "rails"
|
2
|
+
require "rails/four/configuration"
|
3
|
+
require "rails/four/application"
|
4
|
+
|
5
|
+
module Rails
|
6
|
+
module Four
|
7
|
+
module Queueing
|
8
|
+
|
9
|
+
# Public: Initialize Rails 4 Queue
|
10
|
+
class Engine < ::Rails::Engine
|
11
|
+
|
12
|
+
# mixin the queue and queue_consumer to Configuration
|
13
|
+
Rails::Application::Configuration.send :include,
|
14
|
+
Rails::Four::Configuration
|
15
|
+
|
16
|
+
# mixin the queue and queue_consumer to Application with default Queue
|
17
|
+
Rails::Application.send :include, Rails::Four::Application
|
18
|
+
|
19
|
+
# provide a `queue` accessor from the top-level module
|
20
|
+
def Rails.queue
|
21
|
+
application.queue
|
22
|
+
end
|
23
|
+
|
24
|
+
# initialize the configuration instance queue if needed
|
25
|
+
config.after_initialize do |app|
|
26
|
+
unless app.config.instance_variable_get(:"@queue")
|
27
|
+
app.config.instance_variable_set(
|
28
|
+
:"@queue", Rails::Four::Queueing::SynchronousQueue.new
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# initialize the queue_consumer
|
34
|
+
# copied from: railties/lib/rails/application/finisher.rb
|
35
|
+
initializer :activate_queue_consumer, after: "finisher_hook" do |app|
|
36
|
+
if app.config.queue.class == Rails::Four::Queueing::Queue
|
37
|
+
app.queue_consumer = app.config.queue_consumer ||
|
38
|
+
app.config.queue.consumer
|
39
|
+
if app.queue_consumer.respond_to?(:logger=)
|
40
|
+
app.queue_consumer.logger ||= Rails.logger
|
41
|
+
end
|
42
|
+
app.queue_consumer.start
|
43
|
+
at_exit { app.queue_consumer.shutdown }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# Original File: RAILS_ROOT/activesupport/test/queueing/synchronous_queue_test.rb
|
2
|
+
require "support/active_support_abstract_unit"
|
3
|
+
require "rails/four/queueing"
|
4
|
+
|
5
|
+
class SynchronousQueueTest < ActiveSupport::TestCase
|
6
|
+
class Job
|
7
|
+
attr_reader :ran
|
8
|
+
def run; @ran = true end
|
9
|
+
end
|
10
|
+
|
11
|
+
class ExceptionRaisingJob
|
12
|
+
def run; raise end
|
13
|
+
end
|
14
|
+
|
15
|
+
def setup
|
16
|
+
@queue = Rails::Four::Queueing::SynchronousQueue.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_runs_jobs_immediately
|
20
|
+
job = Job.new
|
21
|
+
@queue.push job
|
22
|
+
assert job.ran
|
23
|
+
|
24
|
+
assert_raises RuntimeError do
|
25
|
+
@queue.push ExceptionRaisingJob.new
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# Original File: RAILS_ROOT/activesupport/test/queueing/test_queue_test.rb
|
2
|
+
require "support/active_support_abstract_unit"
|
3
|
+
require "rails/four/queueing"
|
4
|
+
|
5
|
+
class TestQueueTest < ActiveSupport::TestCase
|
6
|
+
def setup
|
7
|
+
@queue = Rails::Four::Queueing::TestQueue.new
|
8
|
+
end
|
9
|
+
|
10
|
+
class ExceptionRaisingJob
|
11
|
+
def run
|
12
|
+
raise
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_drain_raises_exceptions_from_running_jobs
|
17
|
+
@queue.push ExceptionRaisingJob.new
|
18
|
+
assert_raises(RuntimeError) { @queue.drain }
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_jobs
|
22
|
+
@queue.push 1
|
23
|
+
@queue.push 2
|
24
|
+
assert_equal [1,2], @queue.jobs
|
25
|
+
end
|
26
|
+
|
27
|
+
class EquivalentJob
|
28
|
+
def initialize
|
29
|
+
@initial_id = self.object_id
|
30
|
+
end
|
31
|
+
|
32
|
+
def run
|
33
|
+
end
|
34
|
+
|
35
|
+
def ==(other)
|
36
|
+
other.same_initial_id?(@initial_id)
|
37
|
+
end
|
38
|
+
|
39
|
+
def same_initial_id?(other_id)
|
40
|
+
other_id == @initial_id
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_contents
|
45
|
+
job = EquivalentJob.new
|
46
|
+
assert @queue.empty?
|
47
|
+
@queue.push job
|
48
|
+
refute @queue.empty?
|
49
|
+
assert_equal job, @queue.pop
|
50
|
+
end
|
51
|
+
|
52
|
+
class ProcessingJob
|
53
|
+
def self.clear_processed
|
54
|
+
@processed = []
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.processed
|
58
|
+
@processed
|
59
|
+
end
|
60
|
+
|
61
|
+
def initialize(object)
|
62
|
+
@object = object
|
63
|
+
end
|
64
|
+
|
65
|
+
def run
|
66
|
+
self.class.processed << @object
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_order
|
71
|
+
ProcessingJob.clear_processed
|
72
|
+
job1 = ProcessingJob.new(1)
|
73
|
+
job2 = ProcessingJob.new(2)
|
74
|
+
|
75
|
+
@queue.push job1
|
76
|
+
@queue.push job2
|
77
|
+
@queue.drain
|
78
|
+
|
79
|
+
assert_equal [1,2], ProcessingJob.processed
|
80
|
+
end
|
81
|
+
|
82
|
+
class ThreadTrackingJob
|
83
|
+
attr_reader :thread_id
|
84
|
+
|
85
|
+
def run
|
86
|
+
@thread_id = Thread.current.object_id
|
87
|
+
end
|
88
|
+
|
89
|
+
def ran?
|
90
|
+
@thread_id
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_drain
|
95
|
+
@queue.push ThreadTrackingJob.new
|
96
|
+
job = @queue.jobs.last
|
97
|
+
@queue.drain
|
98
|
+
|
99
|
+
assert @queue.empty?
|
100
|
+
assert job.ran?, "The job runs synchronously when the queue is drained"
|
101
|
+
assert_equal job.thread_id, Thread.current.object_id
|
102
|
+
end
|
103
|
+
|
104
|
+
class IdentifiableJob
|
105
|
+
def initialize(id)
|
106
|
+
@id = id
|
107
|
+
end
|
108
|
+
|
109
|
+
def ==(other)
|
110
|
+
other.same_id?(@id)
|
111
|
+
end
|
112
|
+
|
113
|
+
def same_id?(other_id)
|
114
|
+
other_id == @id
|
115
|
+
end
|
116
|
+
|
117
|
+
def run
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def test_queue_can_be_observed
|
122
|
+
jobs = (1..10).map do |id|
|
123
|
+
IdentifiableJob.new(id)
|
124
|
+
end
|
125
|
+
|
126
|
+
jobs.each do |job|
|
127
|
+
@queue.push job
|
128
|
+
end
|
129
|
+
|
130
|
+
assert_equal jobs, @queue.jobs
|
131
|
+
end
|
132
|
+
|
133
|
+
def test_adding_an_unmarshallable_job
|
134
|
+
anonymous_class_instance = Struct.new(:run).new
|
135
|
+
|
136
|
+
assert_raises TypeError do
|
137
|
+
@queue.push anonymous_class_instance
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def test_attempting_to_add_a_reference_to_itself
|
142
|
+
job = {reference: @queue}
|
143
|
+
assert_raises TypeError do
|
144
|
+
@queue.push job
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|