rack-rabbit 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/EXAMPLES.md +212 -0
  4. data/Gemfile +14 -0
  5. data/Gemfile.lock +42 -0
  6. data/LICENSE +21 -0
  7. data/README.md +412 -0
  8. data/Rakefile +5 -0
  9. data/bin/rack-rabbit +96 -0
  10. data/bin/rr +99 -0
  11. data/lib/rack-rabbit.rb +63 -0
  12. data/lib/rack-rabbit/adapter.rb +85 -0
  13. data/lib/rack-rabbit/adapter/amqp.rb +114 -0
  14. data/lib/rack-rabbit/adapter/bunny.rb +87 -0
  15. data/lib/rack-rabbit/adapter/mock.rb +92 -0
  16. data/lib/rack-rabbit/client.rb +181 -0
  17. data/lib/rack-rabbit/config.rb +260 -0
  18. data/lib/rack-rabbit/handler.rb +44 -0
  19. data/lib/rack-rabbit/message.rb +95 -0
  20. data/lib/rack-rabbit/middleware/program_name.rb +34 -0
  21. data/lib/rack-rabbit/response.rb +43 -0
  22. data/lib/rack-rabbit/server.rb +263 -0
  23. data/lib/rack-rabbit/signals.rb +62 -0
  24. data/lib/rack-rabbit/subscriber.rb +77 -0
  25. data/lib/rack-rabbit/worker.rb +84 -0
  26. data/rack-rabbit.gemspec +26 -0
  27. data/test/apps/config.ru +7 -0
  28. data/test/apps/custom.conf +27 -0
  29. data/test/apps/custom.ru +7 -0
  30. data/test/apps/empty.conf +1 -0
  31. data/test/apps/error.ru +7 -0
  32. data/test/apps/mirror.ru +19 -0
  33. data/test/apps/sinatra.ru +37 -0
  34. data/test/apps/sleep.ru +21 -0
  35. data/test/test_case.rb +154 -0
  36. data/test/unit/middleware/test_program_name.rb +32 -0
  37. data/test/unit/test_client.rb +275 -0
  38. data/test/unit/test_config.rb +403 -0
  39. data/test/unit/test_handler.rb +92 -0
  40. data/test/unit/test_message.rb +213 -0
  41. data/test/unit/test_response.rb +59 -0
  42. data/test/unit/test_signals.rb +45 -0
  43. data/test/unit/test_subscriber.rb +140 -0
  44. metadata +91 -0
@@ -0,0 +1,62 @@
1
+ module RackRabbit
2
+ class Signals
3
+
4
+ # The RackRabbit server process has a single primary thread, but it doesn't
5
+ # actually need to do any work once it has spun up the worker processes. I
6
+ # need it to hibernate until one (or more) signal event's occurs.
7
+ #
8
+ # Originally I tried to use the standard Thread::Queue, it uses a Mutex to
9
+ # perform a blocking pop on the Queue. However Ruby (2.x) won't let the last
10
+ # active thread hibernate (it's overly aggressive at deadlock prevention)
11
+ #
12
+ # So, instead of using a Mutex based Queue, I can use a blocking select
13
+ # on an IO pipe, then when the signal handler pushes into the Queue
14
+ # it can also write to the pipe in order to "awaken" the primary thread.
15
+ #
16
+ # FYI: this is the same underlying idea that is used by the Unicorn master
17
+ # process, I've just encapsulated it in a Signals class
18
+ #
19
+
20
+ def initialize
21
+ @reader, @writer = IO.pipe
22
+ @queue = []
23
+ end
24
+
25
+ def close
26
+ @reader.close
27
+ @writer.close
28
+ @reader = @writer = nil
29
+ end
30
+
31
+ def closed?
32
+ @reader.nil?
33
+ end
34
+
35
+ def push(item)
36
+ raise RuntimeError, "closed" if closed?
37
+ @queue << item
38
+ awaken
39
+ end
40
+
41
+ def pop(options = {})
42
+ raise RuntimeError, "closed" if closed?
43
+ if @queue.empty? && (:timeout == hibernate(options[:timeout]))
44
+ :timeout
45
+ else
46
+ @queue.shift
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def awaken
53
+ @writer.write '.'
54
+ end
55
+
56
+ def hibernate(seconds = nil)
57
+ return :timeout unless IO.select([@reader], nil, nil, seconds)
58
+ @reader.readchar
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,77 @@
1
+ require 'rack-rabbit/adapter'
2
+ require 'rack-rabbit/handler'
3
+
4
+ module RackRabbit
5
+ class Subscriber
6
+
7
+ #--------------------------------------------------------------------------
8
+
9
+ attr_reader :config, # configuration options provided by the Server
10
+ :logger, # convenience for config.logger
11
+ :rabbit, # rabbitMQ adapter constructed by this class
12
+ :handler, # actually does the work of handling the RACK request/response
13
+ :lock # mutex to synchronize with Worker#shutdown for graceful QUIT handling
14
+
15
+ #--------------------------------------------------------------------------
16
+
17
+ def initialize(rabbit, handler, lock, config)
18
+ @rabbit = rabbit
19
+ @handler = handler
20
+ @lock = lock
21
+ @config = config
22
+ @logger = config.logger
23
+ end
24
+
25
+ #--------------------------------------------------------------------------
26
+
27
+ def subscribe
28
+ rabbit.startup
29
+ rabbit.connect
30
+ rabbit.subscribe(:queue => config.queue,
31
+ :exchange => config.exchange,
32
+ :exchange_type => config.exchange_type,
33
+ :routing_key => config.routing_key,
34
+ :ack => config.ack) do |message|
35
+ lock.synchronize do
36
+ start = Time.now
37
+ response = handle(message)
38
+ finish = Time.now
39
+ logger.info "\"#{message.method} #{message.path}\" [#{response.status}] - #{"%.4f" % (finish - start)}"
40
+ end
41
+ end
42
+ end
43
+
44
+ def unsubscribe
45
+ rabbit.disconnect
46
+ rabbit.shutdown
47
+ end
48
+
49
+ #==========================================================================
50
+ # PRIVATE IMPLEMENTATION
51
+ #==========================================================================
52
+
53
+ private
54
+
55
+ def handle(message)
56
+
57
+ response = handler.handle(message) # does all the Rack related work
58
+
59
+ if message.should_reply?
60
+ rabbit.publish(response.body, message.get_reply_properties(response, config))
61
+ end
62
+
63
+ if config.ack && !message.acknowledged? && !message.rejected?
64
+ if response.succeeded?
65
+ message.ack
66
+ else
67
+ message.reject
68
+ end
69
+ end
70
+
71
+ response
72
+ end
73
+
74
+ #--------------------------------------------------------------------------
75
+
76
+ end # class Subscriber
77
+ end # module RackRabbit
@@ -0,0 +1,84 @@
1
+ require 'rack-rabbit/signals'
2
+ require 'rack-rabbit/subscriber'
3
+ require 'rack-rabbit/handler'
4
+ require 'rack-rabbit/adapter'
5
+
6
+ module RackRabbit
7
+ class Worker
8
+
9
+ #--------------------------------------------------------------------------
10
+
11
+ attr_reader :config, # provided by the Server
12
+ :logger, # convenience for config.logger
13
+ :signals, # blocking Q for signal handling
14
+ :lock, # mutex to synchronise with Subscriber#subscribe for graceful QUIT handling
15
+ :rabbit, # interface to rabbit MQ
16
+ :subscriber, # actually does the work of subscribing to the rabbit queue
17
+ :handler # actually does the work of handling the rack request/response
18
+
19
+ #--------------------------------------------------------------------------
20
+
21
+ def initialize(config, app)
22
+ @config = config
23
+ @logger = config.logger
24
+ @signals = Signals.new
25
+ @lock = Mutex.new
26
+ @rabbit = Adapter.load(config.rabbit)
27
+ @handler = Handler.new(app, config)
28
+ @subscriber = Subscriber.new(rabbit, handler, lock, config)
29
+ end
30
+
31
+ #--------------------------------------------------------------------------
32
+
33
+ def run
34
+
35
+ logger.info "STARTED a new worker with PID #{Process.pid}"
36
+
37
+ trap_signals
38
+
39
+ subscriber.subscribe
40
+
41
+ while true
42
+ sig = signals.pop # BLOCKS until there is a signal
43
+ case sig
44
+ when :INT then shutdown(:INT)
45
+ when :QUIT then shutdown(:QUIT)
46
+ when :TERM then shutdown(:TERM)
47
+ else
48
+ raise RuntimeError, "unknown signal #{sig}"
49
+ end
50
+ end
51
+
52
+ ensure
53
+ subscriber.unsubscribe
54
+
55
+ end
56
+
57
+ #--------------------------------------------------------------------------
58
+
59
+ def shutdown(sig)
60
+ lock.lock if sig == :QUIT # graceful shutdown should wait for any pending message handler to finish
61
+ logger.info "#{RackRabbit.friendly_signal(sig)} worker #{Process.pid}"
62
+ exit
63
+ end
64
+
65
+ #--------------------------------------------------------------------------
66
+
67
+ def trap_signals # overwrite the handlers inherited from the server process
68
+
69
+ [:QUIT, :TERM, :INT].each do |sig|
70
+ trap(sig) do
71
+ signals.push(sig)
72
+ end
73
+ end
74
+
75
+ trap(:CHLD, :DEFAULT)
76
+ trap(:TTIN, nil)
77
+ trap(:TTOU, nil)
78
+
79
+ end
80
+
81
+ #--------------------------------------------------------------------------
82
+
83
+ end
84
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ LIB = File.expand_path("lib", File.dirname(__FILE__))
3
+ $LOAD_PATH.unshift LIB unless $LOAD_PATH.include?(LIB)
4
+
5
+ require 'rack-rabbit'
6
+
7
+ Gem::Specification.new do |s|
8
+
9
+ s.name = "rack-rabbit"
10
+ s.version = RackRabbit::VERSION
11
+ s.platform = Gem::Platform::RUBY
12
+ s.authors = ["Jake Gordon"]
13
+ s.email = ["jake@codeincomplete.com"]
14
+ s.homepage = "https://github.com/jakesgordon/rack-rabbit"
15
+ s.summary = RackRabbit::SUMMARY
16
+
17
+ s.has_rdoc = false
18
+ s.extra_rdoc_files = ["README.md"]
19
+ s.rdoc_options = ["--charset=UTF-8"]
20
+ s.files = `git ls-files `.split("\n")
21
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
23
+ s.require_paths = ["lib"]
24
+ s.licenses = ["MIT"]
25
+
26
+ end
@@ -0,0 +1,7 @@
1
+ class DefaultApp
2
+ def self.call(env)
3
+ [ 200, {}, [ "Hello World" ] ]
4
+ end
5
+ end
6
+
7
+ run DefaultApp
@@ -0,0 +1,27 @@
1
+
2
+ rack_file "custom.ru"
3
+
4
+ rabbit :host => "10.10.10.10",
5
+ :port => "1234",
6
+ :adapter => "amqp"
7
+
8
+ queue "myqueue"
9
+ exchange "myexchange"
10
+ exchange_type "topic"
11
+ routing_key "myroute"
12
+ app_id "myapp"
13
+ workers 7
14
+ min_workers 3
15
+ max_workers 42
16
+ ack true
17
+ preload_app true
18
+ daemonize true
19
+ logfile "myapp.log"
20
+ pidfile "myapp.pid"
21
+ log_level :fatal
22
+
23
+ class ::MyLogger < Logger
24
+ end
25
+
26
+ logger MyLogger.new($stderr)
27
+
@@ -0,0 +1,7 @@
1
+ class CustomApp
2
+ def self.call(env)
3
+ [ 200, {}, [ "Custom App" ] ]
4
+ end
5
+ end
6
+
7
+ run CustomApp
@@ -0,0 +1 @@
1
+ # empty rack-rabbit configuration file
@@ -0,0 +1,7 @@
1
+ class ErrorApp
2
+ def self.call(env)
3
+ raise "wtf"
4
+ end
5
+ end
6
+
7
+ run ErrorApp
@@ -0,0 +1,19 @@
1
+ require 'json'
2
+
3
+ class MirrorApp
4
+ def self.call(env)
5
+
6
+ request = Rack::Request.new env
7
+ result = {
8
+ :method => request.request_method,
9
+ :path => request.path_info,
10
+ :params => request.params,
11
+ :body => request.body.read
12
+ }
13
+
14
+ [ 200, {}, [ result.to_json ] ]
15
+
16
+ end
17
+ end
18
+
19
+ run MirrorApp
@@ -0,0 +1,37 @@
1
+ require 'sinatra/base'
2
+
3
+ class MyApp < Sinatra::Base
4
+
5
+ set :logging, nil # skip sinatra logging middleware, use the env['rack.logger'] provided by the rack-rabbit server
6
+
7
+ get "/hello" do
8
+ "Hello World"
9
+ end
10
+
11
+ post "/submit" do
12
+ "Submitted #{request.body.read}"
13
+ end
14
+
15
+ get "/error" do
16
+ raise "uh oh"
17
+ end
18
+
19
+ get "/sleep/:seconds" do
20
+ slumber params[:seconds].to_i
21
+ end
22
+
23
+ post "/sleep/:seconds" do
24
+ slumber params[:seconds].to_i
25
+ end
26
+
27
+ def slumber(seconds)
28
+ seconds.times do |i|
29
+ logger.info "#{request.path_info} - #{i}"
30
+ sleep(1)
31
+ end
32
+ "Slept for #{seconds}"
33
+ end
34
+
35
+ end
36
+
37
+ run MyApp
@@ -0,0 +1,21 @@
1
+ class SleepApp
2
+
3
+ def self.call(env)
4
+
5
+ request = Rack::Request.new env
6
+ logger = request.logger
7
+ path = request.path_info.to_s
8
+
9
+ duration = path.split("/").last.to_i
10
+ duration.times do |n|
11
+ logger.info "#{path} - #{n}"
12
+ sleep(1)
13
+ end
14
+
15
+ [ 200, {}, [ "Slept for: #{duration}" ] ]
16
+
17
+ end
18
+
19
+ end
20
+
21
+ run SleepApp
@@ -0,0 +1,154 @@
1
+ require 'minitest/autorun'
2
+ require 'mocha/mini_test'
3
+ require 'rack'
4
+ require 'rack/builder'
5
+ require 'ostruct'
6
+ require 'timecop'
7
+ require 'pp'
8
+
9
+ require 'rack-rabbit' # top level module
10
+
11
+ require 'rack-rabbit/subscriber' # subscribes to rabbit queue/exchange and passes messages on to a handler
12
+ require 'rack-rabbit/handler' # converts rabbit messages to rack environments and calls a rack app to handle the request
13
+ require 'rack-rabbit/adapter' # abstract interface to rabbitMQ
14
+ require 'rack-rabbit/message' # a rabbitMQ message
15
+ require 'rack-rabbit/response' # a rack response
16
+
17
+ require 'rack-rabbit/client' # client code for making requests
18
+ require 'rack-rabbit/worker' # worker process
19
+ require 'rack-rabbit/server' # server process
20
+ require 'rack-rabbit/config' # server configuration
21
+ require 'rack-rabbit/signals' # process signal queue
22
+
23
+ module RackRabbit
24
+ class TestCase < Minitest::Unit::TestCase
25
+
26
+ #--------------------------------------------------------------------------
27
+
28
+ EMPTY_CONFIG = File.expand_path("apps/empty.conf", File.dirname(__FILE__))
29
+ CUSTOM_CONFIG = File.expand_path("apps/custom.conf", File.dirname(__FILE__))
30
+
31
+ #--------------------------------------------------------------------------
32
+
33
+ DEFAULT_RACK_APP = File.expand_path("apps/config.ru", File.dirname(__FILE__))
34
+ CUSTOM_RACK_APP = File.expand_path("apps/custom.ru", File.dirname(__FILE__))
35
+ ERROR_RACK_APP = File.expand_path("apps/error.ru", File.dirname(__FILE__))
36
+ MIRROR_RACK_APP = File.expand_path("apps/mirror.ru", File.dirname(__FILE__))
37
+
38
+ #--------------------------------------------------------------------------
39
+
40
+ APP_ID = "app.id"
41
+ DELIVERY_TAG = "delivery.tag"
42
+ REPLY_TO = "reply.queue"
43
+ CORRELATION_ID = "correlation.id"
44
+ QUEUE = "my.queue"
45
+ REPLY_QUEUE = "reply.queue"
46
+ EXCHANGE = "my.exchange"
47
+ ROUTE = "my.route"
48
+ BODY = "body"
49
+ PATH = "/foo/bar"
50
+ QUERY = "a=b&c=d"
51
+ URI = "#{PATH}?#{QUERY}"
52
+ PRIORITY = 7
53
+
54
+ module CONTENT
55
+ ASCII = "iso-8859-1"
56
+ UTF8 = "utf-8"
57
+ PLAIN_TEXT = "text/plain"
58
+ PLAIN_TEXT_UTF8 = "text/plain; charset=\"utf-8\""
59
+ FORM_URLENCODED = "application/x-www-form-urlencoded"
60
+ FORM_URLENCODED_UTF8 = "application/x-www-form-urlencoded; charset=\"utf-8\""
61
+ JSON = "application/json"
62
+ JSON_UTF8 = "application/json; charset=\"utf-8\""
63
+ JSON_ASCII = "application/json; charset=\"iso-8859-1\""
64
+ end
65
+
66
+ #--------------------------------------------------------------------------
67
+
68
+ NullLogger = Rack::NullLogger.new($stdout)
69
+
70
+ #--------------------------------------------------------------------------
71
+
72
+ def default_config
73
+ build_config( :rabbit => nil, :logger => nil ) # special case for select tests that want TRUE defaults (not the :mock adapter or NullLogger needed in 80% of other tests)
74
+ end
75
+
76
+ def build_config(options = {})
77
+ Config.new({
78
+ :validate => false, # skip validation for most tests
79
+ :rack_file => DEFAULT_RACK_APP, # required - so default to sample app
80
+ :rabbit => { :adapter => :mock }, # use RackRabbit::Adapter::Mock to mock out rabbit MQ
81
+ :logger => NullLogger # suppress logging during tests
82
+ }.merge(options))
83
+ end
84
+
85
+ def build_client(options = {})
86
+ Client.new({ :adapter => :mock }.merge(options))
87
+ end
88
+
89
+ def build_message(options = {})
90
+ options[:headers] ||= {}
91
+ options[:headers][RackRabbit::HEADER::METHOD] ||= options.delete(:method) # convenience to make calling code a little more compact
92
+ options[:headers][RackRabbit::HEADER::PATH] ||= options.delete(:path) # (ditto)
93
+ options[:headers][RackRabbit::HEADER::STATUS] ||= options.delete(:status) # (ditto)
94
+ Message.new(options[:delivery_tag], OpenStruct.new(options), options[:body], options[:rabbit] || build_rabbit)
95
+ end
96
+
97
+ def build_response(status, body, headers = {})
98
+ headers ||= {}
99
+ headers[RackRabbit::HEADER::CONTENT_TYPE] ||= headers.delete(:content_type) # convenience to make calling code a little more compact
100
+ headers[RackRabbit::HEADER::CONTENT_ENCODING] ||= headers.delete(:content_encoding) # (ditto)
101
+ Response.new(status, headers, body)
102
+ end
103
+
104
+ def build_app(rack_file)
105
+ Rack::Builder.parse_file(rack_file)[0]
106
+ end
107
+
108
+ def build_handler(options = {})
109
+ config = options[:config] || build_config(options)
110
+ app = options[:app] || build_app(config.rack_file)
111
+ Handler.new(app, config)
112
+ end
113
+
114
+ def build_subscriber(options = {})
115
+ rabbit = options[:rabbit] || build_rabbit(options)
116
+ config = options[:config] || build_config(options)
117
+ handler = options[:handler] || build_handler(options.merge(:config => config))
118
+ Subscriber.new(rabbit, handler, Mutex.new, handler.config)
119
+ end
120
+
121
+ def build_rabbit(options = {})
122
+ Adapter.load({ :adapter => :mock }.merge(options))
123
+ end
124
+
125
+ #--------------------------------------------------------------------------
126
+
127
+ def assert_raises_argument_error(message = nil, &block)
128
+ e = assert_raises(ArgumentError, &block)
129
+ assert_match(/#{message}/, e.message) unless message.nil?
130
+ end
131
+
132
+ #--------------------------------------------------------------------------
133
+
134
+ def measure
135
+ start = Time.now
136
+ yield
137
+ finish = Time.now
138
+ finish - start
139
+ end
140
+
141
+ #--------------------------------------------------------------------------
142
+
143
+ def with_program_name(name)
144
+ original = $PROGRAM_NAME
145
+ $PROGRAM_NAME = name
146
+ yield
147
+ ensure
148
+ $PROGRAM_NAME = original
149
+ end
150
+
151
+ #--------------------------------------------------------------------------
152
+
153
+ end
154
+ end