rack-rabbit 0.5.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.
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,5 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.pattern = "test/unit/**/test_*.rb"
5
+ end
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+ #
4
+ # Run a RackRabbit server.
5
+ #
6
+
7
+ #==============================================================================
8
+ # PARSE COMMAND LINE OPTIONS
9
+ #==============================================================================
10
+
11
+ require 'optparse'
12
+
13
+ action = :run
14
+ options = { :rabbit => {} }
15
+
16
+ config_help = "provide options using a rack-rabbit configuration file"
17
+ queue_help = "subscribe to a queue for incoming requests"
18
+ exchange_help = "subscribe to an exchange for incoming requests"
19
+ exchange_type_help = "subscribe to an exchange for incoming requests - type (e.g. :direct, :fanout, :topic)"
20
+ routing_key_help = "subscribe to an exchange for incoming requests - routing key"
21
+ app_id_help = "an app_id for this application server"
22
+ host_help = "the rabbitMQ broker IP address (default: 127.0.0.1)"
23
+ port_help = "the rabbitMQ broker port (default: 5672)"
24
+
25
+ workers_help = "the number of worker processes (default: 1)"
26
+ daemonize_help = "run daemonized in the background (default: false)"
27
+ pidfile_help = "the pid filename (default when daemonized: /var/run/<app_id>.pid)"
28
+ logfile_help = "the log filename (default when daemonized: /var/log/<app_id>.log)"
29
+ log_level_help = "the log level for rack rabbit output (default: info)"
30
+ preload_help = "preload the rack app before forking worker processes (default: false)"
31
+ include_help = "an additional $LOAD_PATH (may be used more than once)"
32
+ debug_help = "set $DEBUG to true"
33
+ warn_help = "enable warnings"
34
+
35
+ op = OptionParser.new
36
+ op.banner = "A load balanced rack server for hosting rabbitMQ consumer processes."
37
+ op.separator ""
38
+ op.separator "Usage: rack-rabbit [options] rack-file"
39
+ op.separator ""
40
+ op.separator "Examples:"
41
+ op.separator ""
42
+ op.separator " rack-rabbit -h broker -q my.queue # subscribe to a named queue"
43
+ op.separator " rack-rabbit -h broker -e my.exchange -t fanout # subscribe to a fanout exchange"
44
+ op.separator " rack-rabbit -h broker -e my.exchange -t topic -r my.topic # subscribe to a topic exchange with a routing key"
45
+ op.separator " rack-rabbit -c rack-rabbit.conf # subscribe with advanced options provided by a config file"
46
+
47
+ op.separator ""
48
+ op.separator "RackRabbit options:"
49
+ op.on("-c", "--config CONFIG", config_help) { |value| options[:config_file] = value }
50
+ op.on("-q", "--queue QUEUE", queue_help) { |value| options[:queue] = value }
51
+ op.on("-e", "--exchange EXCHANGE", exchange_help) { |value| options[:exchange] = value }
52
+ op.on("-t", "--type TYPE", exchange_type_help) { |value| options[:exchange_type] = value }
53
+ op.on("-r", "--route ROUTE", routing_key_help) { |value| options[:routing_key] = value }
54
+ op.on("-a", "--app_id ID", app_id_help) { |value| options[:app_id] = value }
55
+ op.on( "--host HOST", host_help) { |value| options[:rabbit][:host] = value }
56
+ op.on( "--port PORT", port_help) { |value| options[:rabbit][:port] = value }
57
+
58
+ op.separator ""
59
+ op.separator "Process options:"
60
+ op.on("-w", "--workers COUNT", workers_help) { |value| options[:workers] = value.to_i }
61
+ op.on("-d", "--daemonize", daemonize_help) { options[:daemonize] = true }
62
+ op.on("-p", "--pid PIDFILE", pidfile_help) { |value| options[:pidfile] = value }
63
+ op.on("-l", "--log LOGFILE", logfile_help) { |value| options[:logfile] = value }
64
+ op.on( "--log-level LEVEL", log_level_help) { |value| options[:log_level] = value }
65
+ op.on( "--preload", preload_help) { options[:preload_app] = true }
66
+
67
+ op.separator ""
68
+ op.separator "Ruby options:"
69
+ op.on("-I", "--include PATH", include_help) { |value| $LOAD_PATH.unshift(*value.split(":").map{|v| File.expand_path(v)}) }
70
+ op.on( "--debug", debug_help) { $DEBUG = true }
71
+ op.on( "--warn", warn_help) { $-w = true }
72
+
73
+ op.separator ""
74
+ op.separator "Common options:"
75
+ op.on("-h", "--help") { action = :help }
76
+ op.on("-v", "--version") { action = :version }
77
+
78
+ op.separator ""
79
+ op.parse!(ARGV)
80
+
81
+ options[:rack_file] = ARGV[0] unless ARGV.empty?
82
+
83
+ #==============================================================================
84
+ # EXECUTE script
85
+ #==============================================================================
86
+
87
+ require 'rack-rabbit'
88
+
89
+ case action
90
+ when :help then puts op.to_s
91
+ when :version then puts RackRabbit::VERSION
92
+ else
93
+ RackRabbit.run!(options)
94
+ end
95
+
96
+ #==============================================================================
data/bin/rr ADDED
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ #==============================================================================
5
+ # PARSE COMMAND LINE OPTIONS
6
+ #==============================================================================
7
+
8
+ require 'optparse'
9
+
10
+ COMMANDS = [ :request, :enqueue, :publish ]
11
+ METHODS = [ :get, :post, :put, :delete ]
12
+ HOST_HELP = "the rabbitMQ broker IP address (default: 127.0.0.1)"
13
+ PORT_HELP = "the rabbitMQ broker port (default: 5672)"
14
+ QUEUE_HELP = "a queue for publishing outgoing requests"
15
+ EXCHANGE_HELP = "publish to a non-default exchange - name"
16
+ EXCHANGE_TYPE_HELP = "publish to a non-default exchange - type (e.g. :direct, :fanout, :topic)"
17
+ ROUTING_KEY_HELP = "a routing key when publishing to a non-default exchange"
18
+ INCLUDE_HELP = "specify an additional $LOAD_PATH (may be used more than once)"
19
+ DEBUG_HELP = "set $DEBUG to true"
20
+ WARN_HELP = "enable warnings"
21
+
22
+ options = { :rabbit => {} }
23
+ parser = OptionParser.new
24
+
25
+ parser.banner = "Make a request to a RackRabbit service."
26
+ parser.separator ""
27
+ parser.separator "Usage: rr <command> [options] [METHOD] [PATH] [BODY]"
28
+ parser.separator ""
29
+ parser.separator "list of commands:"
30
+ parser.separator ""
31
+ parser.separator " request make a synchronous request to a rabbitMQ queue and wait for a reply"
32
+ parser.separator " enqueue make an asynchronous request to a rabbitMQ queue and continue"
33
+ parser.separator " publish make an asynchronous request to a rabbitMQ exchange with a routing key"
34
+ parser.separator " help show help for a given topic or a help overview"
35
+ parser.separator " version show version"
36
+ parser.separator ""
37
+ parser.separator "Examples:"
38
+ parser.separator ""
39
+ parser.separator " rr request -q queue GET /hello # submit GET to queue and WAIT for reply"
40
+ parser.separator " rr request -q queue POST /submit 'data' # submit POST to queue and WAIT for reply"
41
+ parser.separator " rr enqueue -q queue POST /submit 'data' # submit POST to queue and CONTINUE"
42
+ parser.separator " rr enqueue -q queue DELETE /resource # submit DELETE to queue and CONTINUE"
43
+ parser.separator " rr publish -e ex -t fanout POST /event # submit POST to a fanout exchange and CONTINUE"
44
+ parser.separator " rr publish -e ex -t topic -r foo POST /submit 'data' # submit POST to a topic exchange with routing key and CONTINUE"
45
+ parser.separator ""
46
+ parser.separator "RackRabbit options:"
47
+ parser.on( "--host HOST", HOST_HELP) { |value| options[:rabbit][:host] = value }
48
+ parser.on( "--port PORT", PORT_HELP) { |value| options[:rabbit][:port] = value }
49
+ parser.on("-q", "--queue QUEUE", QUEUE_HELP) { |value| options[:queue] = value }
50
+ parser.on("-e", "--exchange EXCHANGE", EXCHANGE_HELP) { |value| options[:exchange] = value }
51
+ parser.on("-t", "--type TYPE", EXCHANGE_TYPE_HELP) { |value| options[:exchange_type] = value }
52
+ parser.on("-r", "--route ROUTE", ROUTING_KEY_HELP) { |value| options[:routing_key] = value }
53
+ parser.separator ""
54
+ parser.separator "Ruby options:"
55
+ parser.on("-I", "--include PATH", INCLUDE_HELP) { |value| $LOAD_PATH.unshift(*value.split(":")) }
56
+ parser.on( "--debug", DEBUG_HELP) { $DEBUG = true }
57
+ parser.on( "--warn", WARN_HELP) { $-w = true }
58
+ parser.separator ""
59
+ parser.separator "Common options:"
60
+ parser.on("-h", "--help") { options[:help] = true }
61
+ parser.on("-v", "--version") { options[:version] = true }
62
+ parser.separator ""
63
+
64
+ def pluck(args, values, default = nil)
65
+ if value = values.find{|v| v.to_s.downcase.to_sym == args[0].to_s.downcase.to_sym }
66
+ args.delete_at(0)
67
+ value
68
+ else
69
+ default
70
+ end
71
+ end
72
+
73
+ parser.parse!(ARGV)
74
+
75
+ options[:command] = pluck(ARGV, COMMANDS, :request)
76
+ options[:method] = pluck(ARGV, METHODS, :get)
77
+ options[:path] = ARGV.shift
78
+ options[:body] = ARGV.shift
79
+
80
+ #==============================================================================
81
+ # EXECUTE script
82
+ #==============================================================================
83
+
84
+ require 'rack-rabbit/client'
85
+
86
+ case
87
+ when options[:help] then puts parser.to_s
88
+ when options[:version] then puts RackRabbit::VERSION
89
+ else
90
+ case options[:command]
91
+ when :request then puts RR.request(options[:queue], options[:path], options[:body], options)
92
+ when :enqueue then puts RR.enqueue(options[:queue], options[:path], options[:body], options)
93
+ when :publish then puts RR.publish(options[:exchange], options[:path], options[:body], options)
94
+ else
95
+ puts "Invalid command"
96
+ end
97
+ end
98
+
99
+ #==============================================================================
@@ -0,0 +1,63 @@
1
+ module RackRabbit
2
+
3
+ #============================================================================
4
+ # CONSTANTS
5
+ #============================================================================
6
+
7
+ VERSION = "0.5.0"
8
+ SUMMARY = "A Unicorn-style forking, rack-based server for hosting rabbitMQ consumer processes"
9
+
10
+ DEFAULT_RABBIT = {
11
+ :host => "127.0.0.1",
12
+ :port => "5672",
13
+ :adapter => "bunny"
14
+ }.freeze
15
+
16
+ #----------------------------------------------------------------------------
17
+
18
+ module HEADER
19
+ METHOD = "Request-Method"
20
+ PATH = "Request-Path"
21
+ STATUS = "Status-Code"
22
+ CONTENT_TYPE = "Content-Type"
23
+ CONTENT_ENCODING = "Content-Encoding"
24
+ end
25
+
26
+ #----------------------------------------------------------------------------
27
+
28
+ module STATUS
29
+ SUCCESS = 200 # re-purpose common HTTP status codes
30
+ CREATED = 201 # ...
31
+ ACCEPTED = 202 # ...
32
+ BAD_REQUEST = 400 # ...
33
+ NOT_FOUND = 404 # ...
34
+ FAILED = 500 # ...
35
+ end
36
+
37
+ #============================================================================
38
+ # ENTRY POINT
39
+ #============================================================================
40
+
41
+ def self.run!(options)
42
+ require 'rack-rabbit/server'
43
+ Server.new(options).run
44
+ end
45
+
46
+ #============================================================================
47
+ # HELPER METHODS
48
+ #============================================================================
49
+
50
+ def self.friendly_signal(sig)
51
+ case sig
52
+ when :QUIT then "QUIT"
53
+ when :INT then "INTERRUPT"
54
+ when :TERM then "TERMINATE"
55
+ else
56
+ sig
57
+ end
58
+ end
59
+
60
+ #----------------------------------------------------------------------------
61
+
62
+ end
63
+
@@ -0,0 +1,85 @@
1
+ require 'rack-rabbit/message'
2
+
3
+ module RackRabbit
4
+ class Adapter
5
+
6
+ #--------------------------------------------------------------------------
7
+
8
+ def self.load(options)
9
+ adapter = options.delete(:adapter) || :bunny
10
+ if adapter.is_a?(Symbol) || adapter.is_a?(String)
11
+ adapter = case adapter.to_s.downcase.to_sym
12
+ when :bunny
13
+ require 'rack-rabbit/adapter/bunny'
14
+ RackRabbit::Adapter::Bunny
15
+ when :amqp
16
+ require 'rack-rabbit/adapter/amqp'
17
+ RackRabbit::Adapter::AMQP
18
+ when :mock
19
+ require 'rack-rabbit/adapter/mock'
20
+ RackRabbit::Adapter::Mock
21
+ else
22
+ raise ArgumentError, "unknown rabbitMQ adapter #{adapter}"
23
+ end
24
+ end
25
+ adapter.new(options)
26
+ end
27
+
28
+ #--------------------------------------------------------------------------
29
+
30
+ attr_reader :connection_options
31
+
32
+ def initialize(options)
33
+ @connection_options = options
34
+ end
35
+
36
+ def startup
37
+ # derived classes optionally override this (e.g. to startup EventMachine)
38
+ end
39
+
40
+ def shutdown
41
+ # derived classes optionally override this (e.g. to shutdown EventMachine)
42
+ end
43
+
44
+ def started?
45
+ true # derived classes optionally override this (e.g. if running inside EventMachine)
46
+ end
47
+
48
+ def connect
49
+ raise NotImplementedError, "derived classes must implement this"
50
+ end
51
+
52
+ def disconnect
53
+ raise NotImplementedError, "derived classes must implement this"
54
+ end
55
+
56
+ def connected?
57
+ raise NotImplementedError, "derived classes must implement this"
58
+ end
59
+
60
+ def subscribe(options = {}, &block)
61
+ raise NotImplementedError, "derived classes must implement this"
62
+ end
63
+
64
+ def publish(payload, properties)
65
+ raise NotImplementedError, "derived classes must implement this"
66
+ end
67
+
68
+ def with_reply_queue(&block)
69
+ raise NotImplementedError, "derived classes must implement this"
70
+ end
71
+
72
+ #--------------------------------------------------------------------------
73
+
74
+ def ack(delivery_tag)
75
+ raise NotImplementedError, "derived classes must implement this"
76
+ end
77
+
78
+ def reject(delivery_tag)
79
+ raise NotImplementedError, "derived classes must implement this"
80
+ end
81
+
82
+ #--------------------------------------------------------------------------
83
+
84
+ end
85
+ end
@@ -0,0 +1,114 @@
1
+ begin
2
+ require 'amqp'
3
+ rescue LoadError
4
+ abort "missing 'amqp' gem"
5
+ end
6
+
7
+ module RackRabbit
8
+ class Adapter
9
+ class AMQP < RackRabbit::Adapter
10
+
11
+ attr_accessor :connection, :channel
12
+
13
+ def startup
14
+ startup_eventmachine
15
+ end
16
+
17
+ def shutdown
18
+ shutdown_eventmachine
19
+ end
20
+
21
+ def started?
22
+ !@thread.nil?
23
+ end
24
+
25
+ def connect
26
+ return if connected?
27
+ @connection = ::AMQP.connect(connection_options)
28
+ @channel = ::AMQP::Channel.new(connection)
29
+ channel.prefetch(1)
30
+ end
31
+
32
+ def disconnect
33
+ channel.close unless channel.nil?
34
+ connection.close unless connection.nil?
35
+ end
36
+
37
+ def connected?
38
+ !@connection.nil?
39
+ end
40
+
41
+ def subscribe(options = {}, &block)
42
+ queue = get_queue(options.delete(:queue)) || channel.queue("", :exclusive => true)
43
+ exchange = get_exchange(options.delete(:exchange), options.delete(:exchange_type))
44
+ if exchange
45
+ queue.bind(exchange, :routing_key => options.delete(:routing_key))
46
+ end
47
+ queue.subscribe(options) do |properties, payload|
48
+ yield Message.new(properties.delivery_tag, properties, payload, self)
49
+ end
50
+ end
51
+
52
+ def publish(payload, properties)
53
+ exchange = get_exchange(properties.delete(:exchange), properties.delete(:exchange_type))
54
+ exchange ||= channel.default_exchange
55
+ exchange.publish(payload || "", properties)
56
+ end
57
+
58
+ def with_reply_queue
59
+ channel.queue("", :exclusive => true, :auto_delete => true) do |reply_queue, declare_ok|
60
+ yield reply_queue
61
+ end
62
+ end
63
+
64
+ def ack(delivery_tag)
65
+ channel.acknowledge(delivery_tag, false)
66
+ end
67
+
68
+ def reject(delivery_tag)
69
+ channel.reject(delivery_tag, false)
70
+ end
71
+
72
+ #========================================================================
73
+ # PRIVATE IMPLEMENTATION
74
+ #========================================================================
75
+
76
+ private
77
+
78
+ def startup_eventmachine
79
+ raise RuntimeError, "already started" if started?
80
+ ready = false
81
+ @thread = Thread.new { EventMachine.run { ready = true } }
82
+ sleep(1) until ready
83
+ sleep(1) # warmup
84
+ end
85
+
86
+ def shutdown_eventmachine
87
+ sleep(1) # warmdown
88
+ EventMachine.stop
89
+ @thread = nil
90
+ end
91
+
92
+ def get_exchange(ex = :default, type = :direct)
93
+ case ex
94
+ when ::AMQP::Exchange then ex
95
+ when Symbol, String then channel.send(type || :direct, ex) unless ex.to_s.downcase.to_sym == :default
96
+ else
97
+ nil
98
+ end
99
+ end
100
+
101
+ def get_queue(q)
102
+ case q
103
+ when ::AMQP::Queue then q
104
+ when Symbol, String then channel.queue(q)
105
+ else
106
+ nil
107
+ end
108
+ end
109
+
110
+ #------------------------------------------------------------------------
111
+
112
+ end
113
+ end
114
+ end