queueing_rabbit 0.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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