queueing_rabbit 0.1.0.rc1

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.
Files changed (38) hide show
  1. data/.gitignore +17 -0
  2. data/.rvmrc +48 -0
  3. data/Gemfile +9 -0
  4. data/LICENSE +22 -0
  5. data/README.md +38 -0
  6. data/Rakefile +5 -0
  7. data/lib/queueing_rabbit/callbacks.rb +31 -0
  8. data/lib/queueing_rabbit/client/amqp.rb +148 -0
  9. data/lib/queueing_rabbit/client/bunny.rb +62 -0
  10. data/lib/queueing_rabbit/client/callbacks.rb +14 -0
  11. data/lib/queueing_rabbit/configuration.rb +24 -0
  12. data/lib/queueing_rabbit/job.rb +32 -0
  13. data/lib/queueing_rabbit/logging.rb +17 -0
  14. data/lib/queueing_rabbit/serializer.rb +19 -0
  15. data/lib/queueing_rabbit/tasks.rb +37 -0
  16. data/lib/queueing_rabbit/version.rb +3 -0
  17. data/lib/queueing_rabbit/worker.rb +96 -0
  18. data/lib/queueing_rabbit.rb +67 -0
  19. data/lib/tasks/queueing_rabbit.rake +2 -0
  20. data/queueing_rabbit.gemspec +49 -0
  21. data/spec/integration/asynchronous_publishing_and_consuming_spec.rb +62 -0
  22. data/spec/integration/jobs/print_line_job.rb +17 -0
  23. data/spec/integration/synchronous_publishing_and_asynchronous_consuming_spec.rb +39 -0
  24. data/spec/integration/synchronous_publishing_spec.rb +24 -0
  25. data/spec/spec_helper.rb +26 -0
  26. data/spec/support/shared_contexts.rb +17 -0
  27. data/spec/support/shared_examples.rb +60 -0
  28. data/spec/unit/queueing_rabbit/callbacks_spec.rb +53 -0
  29. data/spec/unit/queueing_rabbit/client/amqp_spec.rb +193 -0
  30. data/spec/unit/queueing_rabbit/client/bunny_spec.rb +68 -0
  31. data/spec/unit/queueing_rabbit/client/callbacks_spec.rb +22 -0
  32. data/spec/unit/queueing_rabbit/configuration_spec.rb +19 -0
  33. data/spec/unit/queueing_rabbit/job_spec.rb +23 -0
  34. data/spec/unit/queueing_rabbit/logging_spec.rb +9 -0
  35. data/spec/unit/queueing_rabbit/serializer_spec.rb +26 -0
  36. data/spec/unit/queueing_rabbit/worker_spec.rb +133 -0
  37. data/spec/unit/queueing_rabbit_spec.rb +105 -0
  38. metadata +168 -0
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rvmrc ADDED
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # This is an RVM Project .rvmrc file, used to automatically load the ruby
4
+ # development environment upon cd'ing into the directory
5
+
6
+ # First we specify our desired <ruby>[@<gemset>], the @gemset name is optional,
7
+ # Only full ruby name is supported here, for short names use:
8
+ # echo "rvm use 1.9.3" > .rvmrc
9
+ environment_id="ruby-1.9.3-p392@queueing_rabbit"
10
+
11
+ # Uncomment the following lines if you want to verify rvm version per project
12
+ # rvmrc_rvm_version="1.18.18 (latest)" # 1.10.1 seams as a safe start
13
+ # eval "$(echo ${rvm_version}.${rvmrc_rvm_version} | awk -F. '{print "[[ "$1*65536+$2*256+$3" -ge "$4*65536+$5*256+$6" ]]"}' )" || {
14
+ # echo "This .rvmrc file requires at least RVM ${rvmrc_rvm_version}, aborting loading."
15
+ # return 1
16
+ # }
17
+
18
+ # First we attempt to load the desired environment directly from the environment
19
+ # file. This is very fast and efficient compared to running through the entire
20
+ # CLI and selector. If you want feedback on which environment was used then
21
+ # insert the word 'use' after --create as this triggers verbose mode.
22
+ if [[ -d "${rvm_path:-$HOME/.rvm}/environments"
23
+ && -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]]
24
+ then
25
+ \. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
26
+ [[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]] &&
27
+ \. "${rvm_path:-$HOME/.rvm}/hooks/after_use" || true
28
+ else
29
+ # If the environment file has not yet been created, use the RVM CLI to select.
30
+ rvm --create "$environment_id" || {
31
+ echo "Failed to create RVM environment '${environment_id}'."
32
+ return 1
33
+ }
34
+ fi
35
+
36
+ # If you use bundler, this might be useful to you:
37
+ # if [[ -s Gemfile ]] && {
38
+ # ! builtin command -v bundle >/dev/null ||
39
+ # builtin command -v bundle | GREP_OPTIONS= \grep $rvm_path/bin/bundle >/dev/null
40
+ # }
41
+ # then
42
+ # printf "%b" "The rubygem 'bundler' is not installed. Installing it now.\n"
43
+ # gem install bundler
44
+ # fi
45
+ # if [[ -s Gemfile ]] && builtin command -v bundle >/dev/null
46
+ # then
47
+ # bundle install | GREP_OPTIONS= \grep -vE '^Using|Your bundle is complete'
48
+ # fi
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in queueing_rabbit.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'rspec', '~> 2.13.0'
8
+ gem 'evented-spec', '~> 0.9.0'
9
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Artem Chistyakov
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # QueueingRabbit
2
+
3
+ QueueingRabbit is a Ruby library that provides a convenient object-oriented
4
+ syntax for managing background jobs with AMQP. All jobs' argumets are
5
+ serialized to JSON and transfered as AMQP message payloads. The library
6
+ implements amqp and bunny gems as adapters, making it possible to use
7
+ synchronous publishing and asynchronous consuming, which might be useful for
8
+ Rails app running on non-EventMachine based application servers (i. e.
9
+ Passenger).
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ gem 'queueing_rabbit'
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install queueing_rabbit
24
+
25
+ ## Usage
26
+
27
+ QueueingRabbit is currently in RC1 and is not recommended for production use.
28
+
29
+ The docs are coming soon, currently you can check out the examples in
30
+ `spec/integration` dir.
31
+
32
+ ## Contributing
33
+
34
+ 1. Fork it
35
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
36
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
37
+ 4. Push to the branch (`git push origin my-new-feature`)
38
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new
@@ -0,0 +1,31 @@
1
+ module QueueingRabbit
2
+
3
+ module Callbacks
4
+
5
+ def before_consuming(&block)
6
+ setup_callback(:consuming_started, &block)
7
+ end
8
+
9
+ def after_consuming(&block)
10
+ setup_callback(:consuming_done, &block)
11
+ end
12
+
13
+ def on_event_machine_start(&block)
14
+ setup_callback(:event_machine_started, &block)
15
+ end
16
+
17
+ def setup_callback(event, &block)
18
+ @callbacks ||= {}
19
+ @callbacks[event] ||= []
20
+ @callbacks[event] << block
21
+ end
22
+
23
+ def trigger_event(event)
24
+ if @callbacks && @callbacks[event]
25
+ @callbacks[event].each { |c| c.call }
26
+ end
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,148 @@
1
+ require 'amqp'
2
+
3
+ module QueueingRabbit
4
+
5
+ module Client
6
+
7
+ class AMQP
8
+
9
+ include QueueingRabbit::Serializer
10
+ include QueueingRabbit::Logging
11
+ extend QueueingRabbit::Logging
12
+ extend QueueingRabbit::Client::Callbacks
13
+
14
+ attr_reader :connection, :exchange_name, :exchange_options
15
+
16
+ define_callback :on_tcp_failure do |_|
17
+ fatal "unable to establish TCP connection to broker"
18
+ EM.stop
19
+ end
20
+
21
+ define_callback :on_tcp_loss do |c, _|
22
+ info "re-establishing TCP connection to broker"
23
+ c.reconnect(false, 1)
24
+ end
25
+
26
+ define_callback :on_tcp_recovery do
27
+ info "TCP connection to broker is back and running"
28
+ end
29
+
30
+ define_callback :on_channel_error do |ch, channel_close|
31
+ EM.stop
32
+ fatal "channel error occured: #{channel_close.reply_text}"
33
+ end
34
+
35
+ def self.connection_options
36
+ {:timeout => QueueingRabbit.tcp_timeout,
37
+ :heartbeat => QueueingRabbit.heartbeat,
38
+ :on_tcp_connection_failure => self.callback(:on_tcp_failure)}
39
+ end
40
+
41
+ def self.connect
42
+ self.run_event_machine
43
+
44
+ self.new(::AMQP.connect(QueueingRabbit.amqp_uri),
45
+ QueueingRabbit.amqp_exchange_name,
46
+ QueueingRabbit.amqp_exchange_options)
47
+ end
48
+
49
+ def self.run_event_machine
50
+ return if EM.reactor_running?
51
+
52
+ @event_machine_thread = Thread.new do
53
+ EM.run do
54
+ QueueingRabbit.trigger_event(:event_machine_started)
55
+ end
56
+ end
57
+ end
58
+
59
+ def self.join_event_machine_thread
60
+ @event_machine_thread.join if @event_machine_thread
61
+ end
62
+
63
+ def disconnect
64
+ info "closing AMQP broker connection..."
65
+
66
+ connection.close do
67
+ yield if block_given?
68
+
69
+ EM.stop if EM.reactor_running?
70
+ end
71
+ end
72
+
73
+ def define_queue(channel, queue_name, options={})
74
+ routing_keys = [*options.delete(:routing_keys)] + [queue_name]
75
+
76
+ channel.queue(queue_name.to_s, options) do |queue|
77
+ routing_keys.each do |key|
78
+ queue.bind(exchange(channel), :routing_key => key.to_s)
79
+ end
80
+ end
81
+ end
82
+
83
+ def listen_queue(channel, queue_name, options={}, &block)
84
+ define_queue(channel, queue_name, options)
85
+ .subscribe(:ack => true) do |metadata, payload|
86
+ begin
87
+ process_message(deserialize(payload), &block)
88
+ metadata.ack
89
+ rescue JSON::JSONError => e
90
+ error "JSON parser error occured: #{e.message}"
91
+ debug e
92
+ end
93
+ end
94
+ end
95
+
96
+ def process_message(arguments)
97
+ begin
98
+ yield arguments
99
+ rescue => e
100
+ error "unexpected error #{e.class} occured: #{e.message}"
101
+ debug e
102
+ end
103
+ end
104
+
105
+ def open_channel(options={})
106
+ ::AMQP::Channel.new(connection,
107
+ ::AMQP::Channel.next_channel_id,
108
+ options) do |c, open_ok|
109
+ c.on_error(&self.class.callback(:on_channel_error))
110
+ yield c, open_ok
111
+ end
112
+ end
113
+
114
+ def define_exchange(channel, options={})
115
+ channel.direct(exchange_name, exchange_options.merge(options))
116
+ end
117
+ alias_method :exchange, :define_exchange
118
+
119
+ def enqueue(channel, routing_key, payload)
120
+ exchange(channel).publish(serialize(payload), :key => routing_key.to_s,
121
+ :persistent => true)
122
+ end
123
+ alias_method :publish, :enqueue
124
+
125
+ def queue_size(queue)
126
+ raise NotImplementedError
127
+ end
128
+
129
+ private
130
+
131
+ def setup_callbacks
132
+ connection.on_tcp_connection_loss(&self.class.callback(:on_tcp_loss))
133
+ connection.on_recovery(&self.class.callback(:on_tcp_recovery))
134
+ end
135
+
136
+ def initialize(connection, exchange_name, exchange_options = {})
137
+ @connection = connection
138
+ @exchange_name = exchange_name
139
+ @exchange_options = exchange_options
140
+
141
+ setup_callbacks
142
+ end
143
+
144
+ end
145
+
146
+ end
147
+
148
+ end
@@ -0,0 +1,62 @@
1
+ require 'bunny'
2
+
3
+ module QueueingRabbit
4
+
5
+ module Client
6
+
7
+ class Bunny
8
+
9
+ include QueueingRabbit::Serializer
10
+
11
+ attr_reader :connection, :exchange_name, :exchange_options
12
+
13
+ def self.connect
14
+ self.new(::Bunny.new(QueueingRabbit.amqp_uri),
15
+ QueueingRabbit.amqp_exchange_name,
16
+ QueueingRabbit.amqp_exchange_options)
17
+ end
18
+
19
+ def open_channel(options = {})
20
+ ch = connection.create_channel
21
+ yield ch, nil
22
+ # ch.close
23
+ end
24
+
25
+ def define_queue(channel, name, options = {})
26
+ routing_keys = [*options.delete(:routing_keys)] + [name]
27
+
28
+ channel.queue(name.to_s, options) do |q|
29
+ routing_keys.each { |key| q.bind(exchange, :routing_key => key.to_s) }
30
+ end
31
+ end
32
+
33
+ def enqueue(channel, routing_key, payload)
34
+ exchange(channel).publish(serialize(payload), :key => routing_key.to_s,
35
+ :persistent => true)
36
+ end
37
+ alias_method :publish, :enqueue
38
+
39
+ def define_exchange(channel, options={})
40
+ channel.direct(exchange_name, exchange_options.merge(options))
41
+ end
42
+ alias_method :exchange, :define_exchange
43
+
44
+ def queue_size(queue)
45
+ queue.status[:message_count]
46
+ end
47
+
48
+ private
49
+
50
+ def initialize(connection, exchange_name, exchange_options)
51
+ @connection = connection
52
+ @exchange_name = exchange_name
53
+ @exchange_options = exchange_options
54
+
55
+ @connection.start
56
+ end
57
+
58
+ end
59
+
60
+ end
61
+
62
+ end
@@ -0,0 +1,14 @@
1
+ module QueueingRabbit
2
+ module Client
3
+ module Callbacks
4
+ def define_callback(name, &block)
5
+ @callbacks ||= {}
6
+ @callbacks[name] = block
7
+ end
8
+
9
+ def callback(name)
10
+ @callbacks[name] if @callbacks
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,24 @@
1
+ module QueueingRabbit
2
+
3
+ module Configuration
4
+ attr_accessor :amqp_uri, :amqp_exchange_name, :amqp_exchange_options
5
+ attr_writer :tcp_timeout, :heartbeat
6
+
7
+ def configure
8
+ yield self
9
+ end
10
+
11
+ def tcp_timeout
12
+ @tcp_timeout ||= 1
13
+ end
14
+
15
+ def heartbeat
16
+ @heartbeat ||= 10
17
+ end
18
+
19
+ def default_client
20
+ QueueingRabbit::Client::Bunny
21
+ end
22
+ end
23
+
24
+ end
@@ -0,0 +1,32 @@
1
+ module QueueingRabbit
2
+ module Job
3
+ def queue_name
4
+ @queue_name ||= self.name.split('::')[-1]
5
+ end
6
+
7
+ def queue_options
8
+ @queue_options ||= {}
9
+ end
10
+
11
+ def queue(name, options = {})
12
+ @queue_name = name
13
+ @queue_options = options
14
+ end
15
+
16
+ def queue_size
17
+ QueueingRabbit.queue_size(self)
18
+ end
19
+
20
+ def channel_options
21
+ @channel_options ||= {}
22
+ end
23
+
24
+ def channel(options={})
25
+ @channel_options = options
26
+ end
27
+ end
28
+
29
+ class AbstractJob
30
+ extend Job
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ require "logger"
2
+
3
+ module QueueingRabbit
4
+
5
+ module Logging
6
+
7
+ # Logging levels are defined at:
8
+ # http://www.ruby-doc.org/stdlib-1.9.3/libdoc/logger/rdoc/Logger.html
9
+ %w[fatal error warn info debug].each do |level|
10
+ define_method(level) do |message|
11
+ QueueingRabbit.logger.__send__(level, message) if QueueingRabbit.logger
12
+ end
13
+ end
14
+
15
+ end
16
+
17
+ end
@@ -0,0 +1,19 @@
1
+ require 'json'
2
+
3
+ module QueueingRabbit
4
+ module Serializer
5
+ def serialize(args)
6
+ JSON.dump(args)
7
+ end
8
+
9
+ def deserialize(msg)
10
+ symbolize_keys(JSON.parse(msg))
11
+ end
12
+
13
+ private
14
+
15
+ def symbolize_keys(hash)
16
+ hash.inject({}) { |memo, (k,v)| memo[k.to_sym] = v; memo }
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,37 @@
1
+ # require 'queueing_rabbit/tasks'
2
+ # will give you the queueing_rabbit tasks
3
+
4
+ namespace :queueing_rabbit do
5
+ task :setup
6
+
7
+ desc "Start a queueing rabbit worker"
8
+ task :work => :setup do
9
+ require 'queueing_rabbit'
10
+
11
+ if ENV['PIDFILE'] && File.exists?(ENV['PIDFILE'])
12
+ abort "PID file already exists. Is the worker running?"
13
+ end
14
+
15
+ jobs = (ENV['JOBS'] || ENV['JOB']).to_s.split(',')
16
+
17
+ begin
18
+ worker = QueueingRabbit::Worker.new(*jobs)
19
+ rescue QueueingRabbit::NoJobError
20
+ abort "set JOB env var, e.g. $ JOB=ExportDataJob,CompressFileJob " \
21
+ "rake queueing_rabbit:work"
22
+ end
23
+
24
+ if ENV['BACKGROUND']
25
+ unless Process.respond_to?('daemon')
26
+ abort "env var BACKGROUND is set, which requires ruby >= 1.9"
27
+ end
28
+ Process.daemon(true)
29
+ end
30
+
31
+ worker.use_pidfile(ENV['PIDFILE']) if ENV['PIDFILE']
32
+
33
+ worker.info "starting a new queueing_rabbit worker #{worker}"
34
+
35
+ worker.work!
36
+ end
37
+ end
@@ -0,0 +1,3 @@
1
+ module QueueingRabbit
2
+ VERSION = "0.1.0.rc1"
3
+ end
@@ -0,0 +1,96 @@
1
+ module QueueingRabbit
2
+ class Worker
3
+ include QueueingRabbit::Logging
4
+
5
+ attr_accessor :jobs
6
+
7
+ def initialize(*jobs)
8
+ self.jobs = jobs.map { |job| job.to_s.strip }
9
+
10
+ sync_stdio
11
+ validate_jobs
12
+ constantize_jobs
13
+ use_asynchronous_client
14
+ end
15
+
16
+ def work
17
+ conn = QueueingRabbit.connection
18
+ trap_signals(conn)
19
+
20
+ jobs.each { |job| run_job(conn, job) }
21
+
22
+ QueueingRabbit.trigger_event(:consuming_started)
23
+ end
24
+
25
+ def work!
26
+ work
27
+ QueueingRabbit::Client::AMQP.join_event_machine_thread
28
+ end
29
+
30
+ def use_pidfile(filename)
31
+ File.open(@pidfile = filename, 'w') { |f| f << pid }
32
+ end
33
+
34
+ def remove_pidfile
35
+ File.delete(@pidfile) if @pidfile && File.exists?(@pidfile)
36
+ end
37
+
38
+ def pid
39
+ Process.pid
40
+ end
41
+
42
+ def to_s
43
+ "PID=#{pid}, JOBS=#{jobs.join(',')}"
44
+ end
45
+
46
+ private
47
+
48
+ def use_asynchronous_client
49
+ QueueingRabbit.client = QueueingRabbit::Client::AMQP
50
+ end
51
+
52
+ def validate_jobs
53
+ if jobs.nil? || jobs.empty?
54
+ fatal "no jobs specified to work on."
55
+ raise JobNotPresentError.new("No jobs specified to work on.")
56
+ end
57
+ end
58
+
59
+ def constantize_jobs
60
+ self.jobs = jobs.map do |job|
61
+ begin
62
+ Kernel.const_get(job)
63
+ rescue NameError
64
+ fatal "job #{job} doesn't exist."
65
+ raise JobNotFoundError.new("Job #{job} doesn't exist.")
66
+ end
67
+ end
68
+ end
69
+
70
+ def run_job(conn, job)
71
+ conn.open_channel(job.channel_options) do |channel, _|
72
+ conn.listen_queue(channel, job.queue_name, job.queue_options) do |args|
73
+ info "performing job #{job} with arguments #{args.inspect}"
74
+ job.perform(args)
75
+ end
76
+ end
77
+ end
78
+
79
+ def sync_stdio
80
+ $stdout.sync = true
81
+ $stderr.sync = true
82
+ end
83
+
84
+ def trap_signals(connection)
85
+ handler = Proc.new do
86
+ connection.disconnect {
87
+ QueueingRabbit.trigger_event(:consuming_done)
88
+ remove_pidfile
89
+ }
90
+ end
91
+
92
+ Signal.trap("TERM", &handler)
93
+ Signal.trap("INT", &handler)
94
+ end
95
+ end
96
+ end