message_queue 0.0.4 → 0.1.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 (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