message_queue 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +87 -2
  3. data/examples/{publisher.rb → producer.rb} +4 -3
  4. data/examples/producible_consumable.rb +28 -0
  5. data/lib/message_queue/adapter.rb +4 -0
  6. data/lib/message_queue/adapters/bunny/connection.rb +26 -28
  7. data/lib/message_queue/adapters/bunny/consumer.rb +21 -11
  8. data/lib/message_queue/adapters/bunny/{publisher.rb → producer.rb} +12 -12
  9. data/lib/message_queue/adapters/memory/connection.rb +12 -0
  10. data/lib/message_queue/adapters/memory/consumer.rb +34 -0
  11. data/lib/message_queue/adapters/memory/producer.rb +11 -0
  12. data/lib/message_queue/adapters/memory.rb +11 -0
  13. data/lib/message_queue/connection.rb +64 -0
  14. data/lib/message_queue/consumable.rb +83 -0
  15. data/lib/message_queue/consumable_runner.rb +28 -0
  16. data/lib/message_queue/consumer.rb +18 -0
  17. data/lib/message_queue/error_handlers/airbrake.rb +19 -0
  18. data/lib/message_queue/error_handlers/logger.rb +16 -0
  19. data/lib/message_queue/logging.rb +30 -0
  20. data/lib/message_queue/message.rb +14 -0
  21. data/lib/message_queue/options_helper.rb +26 -0
  22. data/lib/message_queue/producer.rb +29 -0
  23. data/lib/message_queue/producible.rb +49 -0
  24. data/lib/message_queue/rails.rb +19 -0
  25. data/lib/message_queue/serializer.rb +4 -0
  26. data/lib/message_queue/serializers/json.rb +4 -0
  27. data/lib/message_queue/serializers/message_pack.rb +4 -0
  28. data/lib/message_queue/serializers/plain.rb +4 -0
  29. data/lib/message_queue/version.rb +1 -1
  30. data/lib/message_queue.rb +118 -1
  31. data/test/adapters/bunny_test.rb +47 -32
  32. data/test/adapters/memory_test.rb +26 -0
  33. data/test/consumable_test.rb +45 -0
  34. data/test/message_queue_test.rb +22 -0
  35. data/test/options_helper_test.rb +23 -0
  36. data/test/producible_test.rb +41 -0
  37. data/test/support/message_queue.yml +1 -0
  38. metadata +29 -4
@@ -0,0 +1,16 @@
1
+ module MessageQueue
2
+ module ErrorHandlers
3
+ class Logger
4
+ include Logging
5
+
6
+ def handle(message, consumer, ex)
7
+ prefix = "Message(#{message.message_id || '-'}): "
8
+ logger.error prefix + "error in consumer '#{consumer}'"
9
+ logger.error prefix + "#{ex.class} - #{ex.message}"
10
+ logger.error (['backtrace:'] + ex.backtrace).join("\n")
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ MessageQueue.register_error_handler MessageQueue::ErrorHandlers::Logger.new
@@ -0,0 +1,30 @@
1
+ require "logger"
2
+ require "time"
3
+
4
+ module MessageQueue
5
+ module Logging
6
+ class Formatter < Logger::Formatter
7
+ def call(severity, time, program_name, message)
8
+ "#{time.utc.iso8601} #{Process.pid} #{severity} -- #{message}\n"
9
+ end
10
+ end
11
+
12
+ def self.setup_logger(target = $stdout)
13
+ @logger = Logger.new(target)
14
+ @logger.formatter = Formatter.new
15
+ @logger
16
+ end
17
+
18
+ def self.logger
19
+ @logger || setup_logger
20
+ end
21
+
22
+ def self.logger=(logger)
23
+ @logger = logger
24
+ end
25
+
26
+ def logger
27
+ Logging.logger
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,14 @@
1
+ module MessageQueue
2
+ class Message
3
+ attr_reader :attributes, :message_id, :type, :payload, :timestamp, :routing_key
4
+
5
+ def initialize(attributes = {})
6
+ @attributes = attributes
7
+ @message_id = attributes[:message_id]
8
+ @type = attributes[:type]
9
+ @payload = attributes[:payload]
10
+ @timestamp = attributes[:timestamp]
11
+ @routing_key = attributes[:routing_key]
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,26 @@
1
+ module OptionsHelper
2
+ # Internal: Deep clone a Hash. Compute the values in the Hash if responding to :call
3
+ #
4
+ # options - The Hash options to clone
5
+ #
6
+ # Returns the cloned Hash with values computed if responding to :call
7
+ def deep_clone(options = {})
8
+ compute_values(options)
9
+ Marshal.load(Marshal.dump(options)) # deep cloning options
10
+ end
11
+
12
+ # Internal: Recursively compute the value of a Hash if it responds to :call
13
+ #
14
+ # options - The Hash options to compute value for
15
+ #
16
+ # Returns the Hash with values computed if responding to :call
17
+ def compute_values(options = {})
18
+ options.each do |k, v|
19
+ if v.is_a?(Hash)
20
+ compute_values(v)
21
+ else
22
+ options[k] = v.call if v.respond_to?(:call)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,29 @@
1
+ require "securerandom"
2
+ require "message_queue/options_helper"
3
+
4
+ module MessageQueue
5
+ class Producer
6
+ include OptionsHelper
7
+
8
+ attr_reader :connection, :options
9
+
10
+ def initialize(connection, options = {})
11
+ @connection = connection
12
+ @options = deep_clone(options)
13
+ end
14
+
15
+ def dump_object(object)
16
+ connection.serializer.dump(object)
17
+ end
18
+
19
+ def default_options
20
+ { :content_type => connection.serializer.content_type, :timestamp => Time.now.utc.to_i, :message_id => generate_id }
21
+ end
22
+
23
+ private
24
+
25
+ def generate_id
26
+ SecureRandom.uuid
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,49 @@
1
+ require "message_queue/logging"
2
+
3
+ module MessageQueue
4
+ # A module to mix in a producer class, for example:
5
+ #
6
+ # class Producer
7
+ # include MessageQueue::Producible
8
+ #
9
+ # exchange :name => "time", :type => :topic
10
+ # message :routing_key => "time.now", :mandatory => true
11
+ # end
12
+ #
13
+ # Producer.new.publish(Time.now.to_s)
14
+ module Producible
15
+ include Logging
16
+
17
+ def self.included(base)
18
+ base.extend(ClassMethods)
19
+ end
20
+
21
+ module ClassMethods
22
+ def exchange(options = {})
23
+ exchange_options.merge!(options)
24
+ end
25
+
26
+ def message(options = {})
27
+ message_options.merge!(options)
28
+ end
29
+
30
+ def exchange_options
31
+ @exchange_options ||= {}
32
+ end
33
+
34
+ def message_options
35
+ @message_options ||= {}
36
+ end
37
+ end
38
+
39
+ def initialize
40
+ @producer = MessageQueue.new_producer(:exchange => self.class.exchange_options, :message => self.class.message_options)
41
+ end
42
+
43
+ def publish(object, options = {})
44
+ logger.info "Publishing #{object.inspect} with options #{options.inspect}"
45
+
46
+ @producer.publish(object, options)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,19 @@
1
+ module MessageQueue
2
+ def self.hook_rails!
3
+ MessageQueue::Logging.logger = ::Rails.logger
4
+
5
+ config_file = ::Rails.root.join("config", "message_queue.yml")
6
+ config = if config_file.exist?
7
+ HashWithIndifferentAccess.new YAML.load_file(config_file)[::Rails.env]
8
+ else
9
+ { :adapter => :memory, :serializer => :json }
10
+ end
11
+ MessageQueue.connect(config)
12
+ end
13
+
14
+ class Rails < ::Rails::Engine
15
+ initializer "message_queue" do
16
+ MessageQueue.hook_rails!
17
+ end
18
+ end if defined?(::Rails)
19
+ end
@@ -13,6 +13,10 @@ module MessageQueue
13
13
  def dump(object, options = {})
14
14
  instance.dump(object, options)
15
15
  end
16
+
17
+ def content_type
18
+ instance.content_type
19
+ end
16
20
  end
17
21
  end
18
22
  end
@@ -10,6 +10,10 @@ module MessageQueue
10
10
  def dump(object, options = {})
11
11
  ::MultiJson.dump(object, options)
12
12
  end
13
+
14
+ def content_type
15
+ "application/json"
16
+ end
13
17
  end
14
18
  end
15
19
  end
@@ -10,6 +10,10 @@ module MessageQueue
10
10
  def dump(object, options = {})
11
11
  ::MessagePack.pack(object)
12
12
  end
13
+
14
+ def content_type
15
+ "application/x-msgpack"
16
+ end
13
17
  end
14
18
  end
15
19
  end
@@ -8,6 +8,10 @@ module MessageQueue
8
8
  def dump(object, options = {})
9
9
  object
10
10
  end
11
+
12
+ def content_type
13
+ "text/plain"
14
+ end
11
15
  end
12
16
  end
13
17
  end
@@ -1,3 +1,3 @@
1
1
  module MessageQueue
2
- VERSION = "0.0.4"
2
+ VERSION = "0.1.0"
3
3
  end
data/lib/message_queue.rb CHANGED
@@ -1,13 +1,69 @@
1
1
  require "message_queue/version"
2
2
  require "message_queue/adapter"
3
+ require "message_queue/connection"
3
4
  require "message_queue/serializer"
5
+ require "message_queue/message"
6
+ require "message_queue/logging"
4
7
 
5
8
  module MessageQueue
6
9
  extend self
7
10
 
8
- ADAPTERS = [:bunny]
11
+ attr_reader :connection, :settings
12
+
13
+ ADAPTERS = [:memory, :bunny]
9
14
  SERIALIZERS = [:plain, :message_pack, :json]
10
15
 
16
+ # Public: Connect to the message queue.
17
+ #
18
+ # It either reads options from a Hash or the path to the Yaml settings file.
19
+ # After connecting, it stores the connection instance locally.
20
+ #
21
+ # file_or_options - The Hash options or the String Yaml settings file
22
+ # Detail Hash options see the new_connection method.
23
+ #
24
+ # Returns the connection for the specified message queue.
25
+ # Raises a RuntimeError if an adapter can't be found.
26
+ def connect(file_or_options = {})
27
+ if file_or_options.is_a?(String)
28
+ require "yaml"
29
+ file_or_options = YAML.load_file(file_or_options).inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
30
+ end
31
+
32
+ @settings = file_or_options
33
+ @connection = new_connection(@settings)
34
+ @connection.connect
35
+ end
36
+
37
+ # Public: Disconnect from the message queue if it's connected
38
+ #
39
+ # It clears out the stored connection.
40
+ #
41
+ # Returns true if it disconnects successfully
42
+ def disconnect
43
+ if @connection
44
+ @connection.disconnect
45
+ @connection = nil
46
+ return true
47
+ end
48
+
49
+ false
50
+ end
51
+
52
+ # Public: Reconnect to the message queue if it's disconnected by using the previous connection settings
53
+ #
54
+ # Returns the new connection if it reconnects successfully
55
+ def reconnect
56
+ disconnect if connected?
57
+ connect(settings)
58
+ end
59
+
60
+ # Public: Check if it's connected to the message queue
61
+ #
62
+ # Returns true if it's connected
63
+ def connected?
64
+ connection.connected? if connection
65
+ end
66
+
11
67
  # Public: Initialize a connection to a message queue.
12
68
  #
13
69
  # options - The Hash options used to initialize a connection
@@ -42,6 +98,60 @@ module MessageQueue
42
98
  connection.with_connection(&block)
43
99
  end
44
100
 
101
+ # Public: Initialize a producer using current connection to a message queue.
102
+ #
103
+ # Details options see a particular adapter.
104
+ #
105
+ # Returns a new producer
106
+ def new_producer(options = {})
107
+ connection.new_producer(options)
108
+ end
109
+
110
+ # Public: Initialize a consumer using current connection to a message queue.
111
+ #
112
+ # Details options see a particular adapter.
113
+ #
114
+ # Returns a new consumer
115
+ def new_consumer(options = {})
116
+ connection.new_consumer(options)
117
+ end
118
+
119
+ def logger
120
+ Logging.logger
121
+ end
122
+
123
+ def run_consumables(options = {})
124
+ MessageQueue::ConsumableRunner.new(consumables).run(options)
125
+ end
126
+
127
+ # Internal: Register a consumable.
128
+ #
129
+ # Returns the registered consumables.
130
+ def register_consumable(consumable)
131
+ consumables << consumable
132
+ end
133
+
134
+ # Internal: Register a error handler.
135
+ #
136
+ # Returns the registered error handlers.
137
+ def register_error_handler(error_handler)
138
+ error_handlers << error_handler
139
+ end
140
+
141
+ # Internal: Get the list of error handlers.
142
+ #
143
+ # Returns the list of error handlers.
144
+ def error_handlers
145
+ @error_handlers ||= []
146
+ end
147
+
148
+ # Internal: Get the list of consumables.
149
+ #
150
+ # Returns the list of consumables.
151
+ def consumables
152
+ @consumables ||= []
153
+ end
154
+
45
155
  # Internal: Load an adapter by name
46
156
  #
47
157
  # Returns the adapter or nil if it can't find it
@@ -79,3 +189,10 @@ module MessageQueue
79
189
  name.to_s.split("_").map(&:capitalize) * ""
80
190
  end
81
191
  end
192
+
193
+ require "message_queue/producible"
194
+ require "message_queue/consumable"
195
+ require "message_queue/consumable_runner"
196
+ require "message_queue/error_handlers/logger"
197
+ require "message_queue/error_handlers/airbrake"
198
+ require "message_queue/rails" if defined?(::Rails::Engine)
@@ -12,71 +12,86 @@ class BunnyTest < Test::Unit::TestCase
12
12
  assert_equal ["path"], connection.settings[:tls_certificates]
13
13
 
14
14
  connection = MessageQueue::Adapters::Bunny.new_connection MessageQueue::Serializers::Plain
15
- bunny = connection.connect
16
- assert bunny.open?
15
+ connection.connect
16
+ assert connection.connected?
17
17
 
18
18
  connection.disconnect
19
- assert bunny.closed?
19
+ assert !connection.connected?
20
20
  end
21
21
 
22
- def test_new_publisher
22
+ def test_new_producer
23
23
  connection = MessageQueue::Adapters::Bunny.new_connection MessageQueue::Serializers::Plain
24
24
  connection.with_connection do |conn|
25
- publisher = conn.new_publisher(
25
+ producer = conn.new_producer(
26
26
  :exchange => {
27
- :name => "test",
28
- :type => :direct
27
+ :name => "test_producer",
28
+ :type => :direct,
29
+ :auto_delete => true
29
30
  },
30
31
  :message => {
31
- :routing_key => "test"
32
+ :routing_key => "test_producer"
32
33
  }
33
34
  )
34
35
 
35
- assert_equal "test", publisher.exchange_name
36
- assert_equal :direct, publisher.exchange_type
37
- assert_equal "test", publisher.message_options[:routing_key]
36
+ assert_equal "test_producer", producer.exchange_name
37
+ assert_equal :direct, producer.exchange_type
38
+ assert_equal "test_producer", producer.message_options[:routing_key]
39
+
40
+ ch = connection.connection.create_channel
41
+ queue = ch.queue("test_producer", :auto_delete => true).bind("test_producer", :routing_key => "test_producer")
42
+
43
+ @payload = nil
44
+ queue.subscribe do |_, _, payload|
45
+ @payload = payload
46
+ end
38
47
 
39
48
  msg = Time.now.to_s
40
- publisher.publish msg
49
+ producer.publish msg
41
50
 
42
- ch = connection.connection.create_channel
43
- queue = ch.queue("test")
44
- _, _, m = queue.pop
51
+ sleep 1
45
52
 
46
- assert_equal msg, m
53
+ assert_equal msg, @payload
47
54
  end
48
55
  end
49
56
 
50
57
  def test_new_consumer
51
58
  connection = MessageQueue::Adapters::Bunny.new_connection MessageQueue::Serializers::Plain
52
59
  connection.with_connection do |conn|
60
+ producer = conn.new_producer(
61
+ :exchange => {
62
+ :name => "test_consumer",
63
+ :type => :direct,
64
+ :auto_delete => true
65
+ },
66
+ :message => {
67
+ :routing_key => "test_consumer"
68
+ }
69
+ )
70
+
53
71
  consumer = conn.new_consumer(
54
72
  :queue => {
55
- :name => "test"
73
+ :name => "test_consumer",
74
+ :auto_delete => true
56
75
  },
57
76
  :exchange => {
58
- :name => "test"
77
+ :name => "test_consumer"
59
78
  }
60
79
  )
61
80
 
62
- assert_equal "test", consumer.queue_name
63
- assert_equal "test", consumer.exchange_name
81
+ assert_equal "test_consumer", consumer.queue_name
82
+ assert_equal "test_consumer", consumer.exchange_name
64
83
 
65
- publisher = conn.new_publisher(
66
- :exchange => {
67
- :name => "test",
68
- :type => :direct
69
- },
70
- :message => {
71
- :routing_key => "test"
72
- }
73
- )
84
+ @payload = nil
85
+ consumer.subscribe do |message|
86
+ @payload = message.payload
87
+ end
74
88
 
75
89
  msg = Time.now.to_s
76
- publisher.publish msg
90
+ producer.publish msg, :type => :foo
91
+
92
+ sleep 1
77
93
 
78
- _, _, m = consumer.queue.pop
79
- assert_equal msg, m
94
+ assert_equal msg, @payload
80
95
  end
81
96
  end
82
97
  end
@@ -0,0 +1,26 @@
1
+ require_relative "../test_helper"
2
+ require_relative "../../lib/message_queue/serializers/plain"
3
+ require_relative "../../lib/message_queue/adapters/memory"
4
+
5
+ class MemoryTest < Test::Unit::TestCase
6
+ def test_connected?
7
+ connection = MessageQueue::Adapters::Memory.new_connection MessageQueue::Serializers::Plain
8
+ assert !connection.connected?
9
+ end
10
+
11
+ def test_pub_sub
12
+ connection = MessageQueue::Adapters::Memory.new_connection MessageQueue::Serializers::Plain
13
+ connection.with_connection do |conn|
14
+ producer = conn.new_producer
15
+ consumer = conn.new_consumer
16
+ consumer.subscribe(:producer => producer)
17
+
18
+ msg = Time.now.to_s
19
+ producer.publish msg, :type => :time
20
+
21
+ message = consumer.queue.pop
22
+ assert_equal :time, message.type
23
+ assert_equal msg, message.payload
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,45 @@
1
+ require_relative "test_helper"
2
+
3
+ class ConsumableTest < Test::Unit::TestCase
4
+ class Consumer
5
+ attr_reader :message
6
+
7
+ include MessageQueue::Consumable
8
+ end
9
+
10
+ def setup
11
+ MessageQueue.connect(:adapter => :bunny, :serializer => :plain)
12
+ end
13
+
14
+ def teardown
15
+ MessageQueue.disconnect
16
+ end
17
+
18
+ def test_consumable
19
+ producer = MessageQueue.new_producer(
20
+ :exchange => {
21
+ :name => "test_consumable",
22
+ :type => :direct,
23
+ :auto_delete => true
24
+ },
25
+ :message => {
26
+ :routing_key => "test_consumable"
27
+ }
28
+ )
29
+
30
+ Consumer.queue :name => "test_consumable", :auto_delete => true
31
+ Consumer.exchange :name => "test_consumable"
32
+ Consumer.send(:define_method, :process) do |message|
33
+ @message = message
34
+ end
35
+ consumer = Consumer.new
36
+ consumer.subscribe
37
+
38
+ msg = Time.now.to_s
39
+ producer.publish msg
40
+
41
+ sleep 1
42
+
43
+ assert_equal msg, consumer.message.payload
44
+ end
45
+ end
@@ -27,4 +27,26 @@ class MessageQueueTest < Test::Unit::TestCase
27
27
  :uri => "amqp://user:pass@host/vhost")
28
28
  assert_equal "MessageQueue::Adapters::Bunny::Connection", connection.class.to_s
29
29
  end
30
+
31
+ def test_connection
32
+ config_file = File.join File.expand_path(File.dirname(__FILE__)), "support", "message_queue.yml"
33
+ MessageQueue.connect(config_file)
34
+
35
+ assert_equal "bunny", MessageQueue.settings[:adapter]
36
+ assert_equal "json", MessageQueue.settings[:serializer]
37
+ assert MessageQueue.connected?
38
+
39
+ connection = MessageQueue.connection
40
+ assert_equal "MessageQueue::Adapters::Bunny::Connection", connection.class.to_s
41
+
42
+ result = MessageQueue.disconnect
43
+ assert result
44
+ assert !MessageQueue.connected?
45
+ assert_nil MessageQueue.connection
46
+
47
+ MessageQueue.reconnect
48
+ assert MessageQueue.connected?
49
+
50
+ MessageQueue.disconnect
51
+ end
30
52
  end
@@ -0,0 +1,23 @@
1
+ require_relative "test_helper"
2
+ require "message_queue/options_helper"
3
+
4
+ class OptionsHelperTest < Test::Unit::TestCase
5
+ class TestClass
6
+ include OptionsHelper
7
+ end
8
+
9
+ def test_deep_clone
10
+ hash = {:foo => :bar}
11
+
12
+ obj = TestClass.new
13
+ new_hash = obj.deep_clone(hash)
14
+ assert_equal new_hash, hash
15
+
16
+ hash[:foo] = :baz
17
+ assert_not_equal new_hash, hash
18
+
19
+ hash_with_block = {:foo => { :bar => ->() {:baz} } }
20
+ new_hash = obj.deep_clone(hash_with_block)
21
+ assert_equal new_hash, {:foo => { :bar => :baz }}
22
+ end
23
+ end
@@ -0,0 +1,41 @@
1
+ require_relative "test_helper"
2
+
3
+ class ProducibleTest < Test::Unit::TestCase
4
+ class Producer
5
+ include MessageQueue::Producible
6
+ end
7
+
8
+ def setup
9
+ MessageQueue.connect(:adapter => :bunny, :serializer => :plain)
10
+ end
11
+
12
+ def teardown
13
+ MessageQueue.disconnect
14
+ end
15
+
16
+ def test_producible
17
+ Producer.exchange :name => "test_producible", :type => :direct, :auto_delete => true
18
+ Producer.message :routing_key => "test_producible"
19
+
20
+ assert_equal "test_producible", Producer.exchange_options[:name]
21
+ assert_equal :direct, Producer.exchange_options[:type]
22
+ assert_equal "test_producible", Producer.message_options[:routing_key]
23
+
24
+ producer = Producer.new
25
+
26
+ ch = MessageQueue.connection.connection.create_channel
27
+ queue = ch.queue("test_producible", :auto_delete => true).bind("test_producible", :routing_key => "test_producible")
28
+
29
+ @payload = nil
30
+ queue.subscribe do |_, _, payload|
31
+ @payload = payload
32
+ end
33
+
34
+ msg = Time.now.to_s
35
+ producer.publish msg
36
+
37
+ sleep 1
38
+
39
+ assert_equal msg, @payload
40
+ end
41
+ end
@@ -1,2 +1,3 @@
1
1
  adapter: bunny
2
+ serializer: json
2
3
  url: amqp://user:pass@host/vhost