vx-consumer 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.travis.yml +14 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/lib/vx/consumer.rb +124 -0
- data/lib/vx/consumer/ack.rb +40 -0
- data/lib/vx/consumer/configuration.rb +64 -0
- data/lib/vx/consumer/error.rb +8 -0
- data/lib/vx/consumer/instrument.rb +21 -0
- data/lib/vx/consumer/params.rb +68 -0
- data/lib/vx/consumer/publish.rb +45 -0
- data/lib/vx/consumer/serializer.rb +88 -0
- data/lib/vx/consumer/session.rb +119 -0
- data/lib/vx/consumer/subscribe.rb +77 -0
- data/lib/vx/consumer/subscriber.rb +26 -0
- data/lib/vx/consumer/testing.rb +47 -0
- data/lib/vx/consumer/version.rb +5 -0
- data/spec/lib/consumer_spec.rb +116 -0
- data/spec/lib/serializer_spec.rb +43 -0
- data/spec/lib/session_spec.rb +29 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/support/beefcake_test_message.rb +8 -0
- data/spec/support/test_consumers.rb +46 -0
- data/vx-consumer.gemspec +27 -0
- metadata +148 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ac03dcb9650a50486b30d3894d974a034e51db69
|
4
|
+
data.tar.gz: da8ffda3715e6d70cdc955508e954bad26ba3c0d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ae6f10ae48f6e94efe9dc47663ad9acaef54e78457068b03b1fd1411ae8d043bd7026b27a9e0651decb9c8f47f98e853cf6bfcc09df3009abdfd9f9535b15bec
|
7
|
+
data.tar.gz: aa8694409352d12979039a539771ee5a8c8d50a2509f8b196f91bd336626122df29c54965cdb8883e44d02f36621324106ee6c9003d8b2c88c9bc10f6686d93b
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Dmitry Galinsky
|
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,29 @@
|
|
1
|
+
# Vx::Consumer
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'vx-consumer'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install vx-consumer
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it ( http://github.com/<my-github-username>/vx-consumer/fork )
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/vx/consumer.rb
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
%w{
|
2
|
+
version
|
3
|
+
error
|
4
|
+
configuration
|
5
|
+
instrument
|
6
|
+
session
|
7
|
+
params
|
8
|
+
serializer
|
9
|
+
subscriber
|
10
|
+
publish
|
11
|
+
subscribe
|
12
|
+
ack
|
13
|
+
}.each do |f|
|
14
|
+
require File.expand_path("../consumer/#{f}", __FILE__)
|
15
|
+
end
|
16
|
+
|
17
|
+
module Vx
|
18
|
+
module Consumer
|
19
|
+
|
20
|
+
attr_accessor :properties
|
21
|
+
attr_accessor :delivery_info
|
22
|
+
attr_accessor :channel
|
23
|
+
|
24
|
+
def self.included(base)
|
25
|
+
base.extend ClassMethods
|
26
|
+
base.extend Instrument
|
27
|
+
base.extend Publish
|
28
|
+
base.extend Subscribe
|
29
|
+
base.send :include, Ack
|
30
|
+
base.send :include, Instrument
|
31
|
+
end
|
32
|
+
|
33
|
+
module ClassMethods
|
34
|
+
def params
|
35
|
+
@params ||= Params.new(self.name)
|
36
|
+
end
|
37
|
+
|
38
|
+
def exchange(*args)
|
39
|
+
params.exchange_options = args.last.is_a?(Hash) ? args.pop : nil
|
40
|
+
params.exchange_name = args.first
|
41
|
+
end
|
42
|
+
|
43
|
+
def fanout
|
44
|
+
params.exchange_type = :fanout
|
45
|
+
end
|
46
|
+
|
47
|
+
def topic
|
48
|
+
params.exchange_type = :topic
|
49
|
+
end
|
50
|
+
|
51
|
+
def queue(*args)
|
52
|
+
params.queue_options = args.last.is_a?(Hash) ? args.pop : nil
|
53
|
+
params.queue_name = args.first
|
54
|
+
end
|
55
|
+
|
56
|
+
def routing_key(name)
|
57
|
+
params.routing_key = name
|
58
|
+
end
|
59
|
+
|
60
|
+
def headers(value)
|
61
|
+
params.headers = value
|
62
|
+
end
|
63
|
+
|
64
|
+
def content_type(value)
|
65
|
+
params.content_type = value
|
66
|
+
end
|
67
|
+
|
68
|
+
def ack
|
69
|
+
params.ack = true
|
70
|
+
end
|
71
|
+
|
72
|
+
def model(value)
|
73
|
+
params.model = value
|
74
|
+
end
|
75
|
+
|
76
|
+
def session
|
77
|
+
Consumer.session
|
78
|
+
end
|
79
|
+
|
80
|
+
def configuration
|
81
|
+
Consumer.configuration
|
82
|
+
end
|
83
|
+
|
84
|
+
def with_middlewares(name, env, &block)
|
85
|
+
Consumer.configuration.builders[name].to_app(block).call(env)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
extend self
|
90
|
+
|
91
|
+
@@session = Session.new
|
92
|
+
@@configuration = Configuration.new
|
93
|
+
|
94
|
+
def shutdown
|
95
|
+
session.shutdown
|
96
|
+
end
|
97
|
+
|
98
|
+
def shutdown?
|
99
|
+
session.shutdown?
|
100
|
+
end
|
101
|
+
|
102
|
+
def configure
|
103
|
+
yield configuration
|
104
|
+
end
|
105
|
+
|
106
|
+
def configuration
|
107
|
+
@@configuration
|
108
|
+
end
|
109
|
+
|
110
|
+
def session
|
111
|
+
@@session
|
112
|
+
end
|
113
|
+
|
114
|
+
def exception_handler(e, env)
|
115
|
+
$stderr.puts "#{e.class}: #{e.message}, env: #{env.inspect}"
|
116
|
+
$stderr.puts e.backtrace.map{|b| "\t#{b}" }.join("\n")
|
117
|
+
unless env.is_a?(Hash)
|
118
|
+
env = {env: env}
|
119
|
+
end
|
120
|
+
configuration.on_error.call(e, env)
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Vx
|
2
|
+
module Consumer
|
3
|
+
module Ack
|
4
|
+
|
5
|
+
def ack(multiple = false)
|
6
|
+
instrumentation = {
|
7
|
+
consumer: self.class.params.consumer_name,
|
8
|
+
properties: properties,
|
9
|
+
multiple: multiple,
|
10
|
+
}
|
11
|
+
if channel.open?
|
12
|
+
channel.ack delivery_info.delivery_tag, multiple
|
13
|
+
instrument("ack", instrumentation)
|
14
|
+
true
|
15
|
+
else
|
16
|
+
instrument("ack_failed", instrumentation)
|
17
|
+
false
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def nack(multiple = false, requeue = false)
|
22
|
+
instrumentation = {
|
23
|
+
consumer: self.class.params.consumer_name,
|
24
|
+
properties: properties,
|
25
|
+
multiple: multiple,
|
26
|
+
requeue: requeue,
|
27
|
+
}
|
28
|
+
if channel.open?
|
29
|
+
channel.ack delivery_info.delivery_tag, multiple, requeue
|
30
|
+
instrument("nack", instrumentation)
|
31
|
+
true
|
32
|
+
else
|
33
|
+
instrument("nack_failed", instrumentation)
|
34
|
+
false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'vx/common/rack/builder'
|
2
|
+
|
3
|
+
module Vx
|
4
|
+
module Consumer
|
5
|
+
class Configuration
|
6
|
+
|
7
|
+
attr_accessor :default_exchange_options, :default_queue_options,
|
8
|
+
:default_publish_options, :default_exchange_type, :pool_timeout,
|
9
|
+
:heartbeat, :spawn_attempts, :content_type, :instrumenter, :debug,
|
10
|
+
:on_error, :builders, :prefetch
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
reset!
|
14
|
+
end
|
15
|
+
|
16
|
+
def debug?
|
17
|
+
ENV['VX_CONSUMER_DEBUG']
|
18
|
+
end
|
19
|
+
|
20
|
+
def use(target, middleware, *args)
|
21
|
+
@builders[target].use middleware, *args
|
22
|
+
end
|
23
|
+
|
24
|
+
def on_error(&block)
|
25
|
+
@on_error = block if block
|
26
|
+
@on_error
|
27
|
+
end
|
28
|
+
|
29
|
+
def reset!
|
30
|
+
@default_exchange_type = :topic
|
31
|
+
@pool_timeout = 0.5
|
32
|
+
@heartbeat = :server
|
33
|
+
|
34
|
+
@spawn_attempts = 1
|
35
|
+
|
36
|
+
@content_type = 'application/json'
|
37
|
+
@prefetch = 1
|
38
|
+
|
39
|
+
@instrumenter = nil
|
40
|
+
@on_error = ->(e, env){ nil }
|
41
|
+
|
42
|
+
@builders = {
|
43
|
+
pub: Vx::Common::Rack::Builder.new,
|
44
|
+
sub: Vx::Common::Rack::Builder.new
|
45
|
+
}
|
46
|
+
|
47
|
+
@default_exchange_options = {
|
48
|
+
durable: true,
|
49
|
+
auto_delete: false
|
50
|
+
}
|
51
|
+
|
52
|
+
@default_queue_options = {
|
53
|
+
durable: true,
|
54
|
+
auto_delete: false,
|
55
|
+
exclusive: false
|
56
|
+
}
|
57
|
+
|
58
|
+
@default_publish_options = {
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Vx
|
2
|
+
module Consumer
|
3
|
+
module Instrument
|
4
|
+
|
5
|
+
def instrument(name, payload, &block)
|
6
|
+
name = "#{name}.consumer.vx"
|
7
|
+
|
8
|
+
if Consumer.configuration.debug?
|
9
|
+
$stdout.puts " --> #{name}: #{payload}"
|
10
|
+
end
|
11
|
+
|
12
|
+
if Consumer.configuration.instrumenter
|
13
|
+
Consumer.configuration.instrumenter.instrument(name, payload, &block)
|
14
|
+
else
|
15
|
+
yield if block_given?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Vx
|
2
|
+
module Consumer
|
3
|
+
Params = Struct.new(:consumer_class) do
|
4
|
+
|
5
|
+
attr_accessor :exchange_name, :exchange_options
|
6
|
+
attr_accessor :queue_name, :queue_options
|
7
|
+
attr_accessor :routing_key, :headers
|
8
|
+
attr_accessor :content_type
|
9
|
+
attr_accessor :ack
|
10
|
+
attr_accessor :exchange_type
|
11
|
+
attr_accessor :model
|
12
|
+
|
13
|
+
def exchange_name
|
14
|
+
@exchange_name || default_exchange_name
|
15
|
+
end
|
16
|
+
|
17
|
+
def queue_name
|
18
|
+
@queue_name || ""
|
19
|
+
end
|
20
|
+
|
21
|
+
def ack
|
22
|
+
!!@ack
|
23
|
+
end
|
24
|
+
|
25
|
+
def content_type
|
26
|
+
@content_type || config.content_type
|
27
|
+
end
|
28
|
+
|
29
|
+
def exchange_type
|
30
|
+
@exchange_type || config.default_exchange_type
|
31
|
+
end
|
32
|
+
|
33
|
+
def exchange_options
|
34
|
+
(@exchange_options || config.default_exchange_options).merge(type: exchange_type)
|
35
|
+
end
|
36
|
+
|
37
|
+
def queue_options
|
38
|
+
@queue_options || config.default_queue_options
|
39
|
+
end
|
40
|
+
|
41
|
+
def publish_options
|
42
|
+
config.default_publish_options
|
43
|
+
end
|
44
|
+
|
45
|
+
def bind_options
|
46
|
+
opts = { }
|
47
|
+
opts.merge!(routing_key: routing_key) if routing_key
|
48
|
+
opts.merge!(headers: headers) if headers
|
49
|
+
opts
|
50
|
+
end
|
51
|
+
|
52
|
+
def consumer_name
|
53
|
+
consumer_class.to_s
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def config
|
59
|
+
Consumer.configuration
|
60
|
+
end
|
61
|
+
|
62
|
+
def default_exchange_name
|
63
|
+
"amq.#{exchange_type}"
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module Vx
|
4
|
+
module Consumer
|
5
|
+
module Publish
|
6
|
+
|
7
|
+
def publish(payload, options = {})
|
8
|
+
session.open
|
9
|
+
|
10
|
+
options ||= {}
|
11
|
+
options[:routing_key] = params.routing_key if params.routing_key && !options.key?(:routing_key)
|
12
|
+
options[:headers] = params.headers if params.headers && !options.key?(:headers)
|
13
|
+
|
14
|
+
options[:content_type] ||= params.content_type || configuration.content_type
|
15
|
+
options[:message_id] ||= SecureRandom.uuid
|
16
|
+
|
17
|
+
name = params.exchange_name
|
18
|
+
|
19
|
+
instrumentation = {
|
20
|
+
payload: payload,
|
21
|
+
exchange: name,
|
22
|
+
consumer: params.consumer_name,
|
23
|
+
properties: options,
|
24
|
+
}
|
25
|
+
|
26
|
+
with_middlewares :pub, instrumentation do
|
27
|
+
instrument("process_publishing", instrumentation) do
|
28
|
+
session.with_channel do |ch|
|
29
|
+
encoded = encode_payload(payload, options[:content_type])
|
30
|
+
x = session.declare_exchange ch, name, params.exchange_options
|
31
|
+
x.publish encoded, options
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def encode_payload(payload, content_type)
|
40
|
+
Serializer.pack(content_type, payload)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Vx
|
2
|
+
module Consumer
|
3
|
+
class Serializer
|
4
|
+
@@types = {}
|
5
|
+
|
6
|
+
Type = Struct.new(:content_type) do
|
7
|
+
def pack(&block)
|
8
|
+
@pack = block if block_given?
|
9
|
+
@pack
|
10
|
+
end
|
11
|
+
|
12
|
+
def unpack(&block)
|
13
|
+
@unpack = block if block_given?
|
14
|
+
@unpack
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class << self
|
19
|
+
def types
|
20
|
+
@@types
|
21
|
+
end
|
22
|
+
|
23
|
+
def define(content_type, &block)
|
24
|
+
fmt = Type.new content_type
|
25
|
+
fmt.instance_eval(&block)
|
26
|
+
types.merge! content_type => fmt
|
27
|
+
end
|
28
|
+
|
29
|
+
def lookup(content_type)
|
30
|
+
types[content_type]
|
31
|
+
end
|
32
|
+
|
33
|
+
def pack(content_type, body)
|
34
|
+
if fmt = lookup(content_type)
|
35
|
+
fmt.pack.call(body)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def unpack(content_type, body, model)
|
40
|
+
if fmt = lookup(content_type)
|
41
|
+
fmt.unpack.call(body, model)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
define 'text/plain' do
|
47
|
+
pack do |body|
|
48
|
+
body.to_s
|
49
|
+
end
|
50
|
+
|
51
|
+
unpack do |body, _|
|
52
|
+
body
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
define 'application/json' do
|
57
|
+
pack do |body|
|
58
|
+
if body.is_a?(String)
|
59
|
+
body
|
60
|
+
else
|
61
|
+
::JSON.dump body
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
unpack do |payload, model|
|
66
|
+
if model && model.respond_to?(:from_json)
|
67
|
+
model.from_json payload
|
68
|
+
else
|
69
|
+
::JSON.parse(payload)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
define 'application/x-protobuf' do
|
75
|
+
|
76
|
+
pack do |object|
|
77
|
+
object.encode.to_s
|
78
|
+
end
|
79
|
+
|
80
|
+
unpack do |payload, model|
|
81
|
+
raise ModelIsNotDefined unless model
|
82
|
+
model.decode payload
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'bunny'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module Vx
|
5
|
+
module Consumer
|
6
|
+
class Session
|
7
|
+
|
8
|
+
include Instrument
|
9
|
+
|
10
|
+
@@session_lock = Mutex.new
|
11
|
+
|
12
|
+
attr_reader :conn
|
13
|
+
|
14
|
+
def shutdown
|
15
|
+
@shutdown = true
|
16
|
+
end
|
17
|
+
|
18
|
+
def shutdown?
|
19
|
+
!!@shutdown
|
20
|
+
end
|
21
|
+
|
22
|
+
def resume
|
23
|
+
@shutdown = false
|
24
|
+
end
|
25
|
+
|
26
|
+
def close
|
27
|
+
if open?
|
28
|
+
@@session_lock.synchronize do
|
29
|
+
instrument("closing_collection", info: conn_info)
|
30
|
+
|
31
|
+
instrument("close_collection", info: conn_info) do
|
32
|
+
begin
|
33
|
+
conn.close
|
34
|
+
while conn.status != :closed
|
35
|
+
sleep 0.01
|
36
|
+
end
|
37
|
+
rescue Bunny::ChannelError, Bunny::ClientTimeout => e
|
38
|
+
$stderr.puts "got #{e.class} #{e.message} in Vx::Consumer::Session#close"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
@conn = nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def open(options = {})
|
47
|
+
return self if open?
|
48
|
+
|
49
|
+
@@session_lock.synchronize do
|
50
|
+
unless open?
|
51
|
+
resume
|
52
|
+
|
53
|
+
@conn ||= Bunny.new(
|
54
|
+
nil, # from ENV['RABBITMQ_URL']
|
55
|
+
heartbeat: Consumer.configuration.heartbeat,
|
56
|
+
automatically_recover: false
|
57
|
+
)
|
58
|
+
|
59
|
+
instrumentation = { info: conn_info }.merge(options)
|
60
|
+
|
61
|
+
instrument("start_connecting", instrumentation)
|
62
|
+
|
63
|
+
instrument("connect", instrumentation) do
|
64
|
+
conn.start
|
65
|
+
while conn.connecting?
|
66
|
+
sleep 0.01
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
def open?
|
76
|
+
conn && conn.open? && conn.status == :open
|
77
|
+
end
|
78
|
+
|
79
|
+
def conn_info
|
80
|
+
if conn
|
81
|
+
"amqp://#{conn.user}@#{conn.host}:#{conn.port}/#{conn.vhost}"
|
82
|
+
else
|
83
|
+
"not connected"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def with_channel
|
88
|
+
assert_connection_is_open
|
89
|
+
|
90
|
+
conn.with_channel { |ch| yield ch }
|
91
|
+
end
|
92
|
+
|
93
|
+
def declare_exchange(ch, name, options = nil)
|
94
|
+
assert_connection_is_open
|
95
|
+
|
96
|
+
options ||= {}
|
97
|
+
ch.exchange name, options
|
98
|
+
end
|
99
|
+
|
100
|
+
def declare_queue(ch, name, options = nil)
|
101
|
+
assert_connection_is_open
|
102
|
+
|
103
|
+
options ||= {}
|
104
|
+
ch.queue name, options
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def assert_connection_is_open
|
110
|
+
open? || raise(ConnectionDoesNotExistError)
|
111
|
+
end
|
112
|
+
|
113
|
+
def config
|
114
|
+
Consumer.configuration
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Vx
|
2
|
+
module Consumer
|
3
|
+
module Subscribe
|
4
|
+
|
5
|
+
def subscribe
|
6
|
+
ch, q = bind
|
7
|
+
bunny_consumer = q.subscribe(block: false, ack: params.ack) do |delivery_info, properties, payload|
|
8
|
+
payload = decode_payload properties, payload
|
9
|
+
|
10
|
+
instrumentation = {
|
11
|
+
consumer: params.consumer_name,
|
12
|
+
payload: payload,
|
13
|
+
properties: properties,
|
14
|
+
}
|
15
|
+
|
16
|
+
with_middlewares :sub, instrumentation do
|
17
|
+
instrument("start_processing", instrumentation)
|
18
|
+
instrument("process", instrumentation) do
|
19
|
+
run_instance delivery_info, properties, payload, ch
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
Subscriber.new(bunny_consumer)
|
25
|
+
end
|
26
|
+
|
27
|
+
def run_instance(delivery_info, properties, payload, channel)
|
28
|
+
new.tap do |inst|
|
29
|
+
inst.properties = properties
|
30
|
+
inst.delivery_info = delivery_info
|
31
|
+
inst.channel = channel
|
32
|
+
end.perform payload
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def decode_payload(properties, payload)
|
38
|
+
Serializer.unpack(properties[:content_type], payload, params.model)
|
39
|
+
end
|
40
|
+
|
41
|
+
def bind
|
42
|
+
|
43
|
+
instrumentation = {
|
44
|
+
consumer: params.consumer_name
|
45
|
+
}
|
46
|
+
|
47
|
+
session.open
|
48
|
+
|
49
|
+
ch = session.conn.create_channel
|
50
|
+
assign_error_handlers_to_channel(ch)
|
51
|
+
ch.prefetch configuration.prefetch
|
52
|
+
|
53
|
+
x = session.declare_exchange ch, params.exchange_name, params.exchange_options
|
54
|
+
q = session.declare_queue ch, params.queue_name, params.queue_options
|
55
|
+
|
56
|
+
instrumentation.merge!(
|
57
|
+
exchange: x.name,
|
58
|
+
queue: q.name,
|
59
|
+
queue_options: params.queue_options,
|
60
|
+
exchange_options: params.exchange_options,
|
61
|
+
bind: params.bind_options
|
62
|
+
)
|
63
|
+
instrument("bind_queue", instrumentation) do
|
64
|
+
q.bind(x, params.bind_options)
|
65
|
+
end
|
66
|
+
|
67
|
+
[ch, q]
|
68
|
+
end
|
69
|
+
|
70
|
+
def assign_error_handlers_to_channel(ch)
|
71
|
+
ch.on_uncaught_exception {|e, c| Consumer.exception_handler(e, consumer: c) }
|
72
|
+
ch.on_error {|e, c| Consumer.exception_handler(e, consumer: c) }
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Vx
|
2
|
+
module Consumer
|
3
|
+
Subscriber = Struct.new(:consumer) do
|
4
|
+
|
5
|
+
def cancel
|
6
|
+
consumer.cancel
|
7
|
+
consumer.channel.close unless consumer.channel.closed?
|
8
|
+
end
|
9
|
+
|
10
|
+
def join
|
11
|
+
consumer.channel.work_pool.join
|
12
|
+
end
|
13
|
+
|
14
|
+
def wait
|
15
|
+
loop do
|
16
|
+
if Consumer.shutdown?
|
17
|
+
cancel
|
18
|
+
break
|
19
|
+
end
|
20
|
+
sleep Consumer.configuration.pool_timeout
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require File.expand_path("../../consumer", __FILE__)
|
2
|
+
|
3
|
+
module Vx
|
4
|
+
module Consumer
|
5
|
+
|
6
|
+
module Testing
|
7
|
+
|
8
|
+
extend self
|
9
|
+
|
10
|
+
@@messages = Hash.new { |h,k| h[k] = [] }
|
11
|
+
@@messages_and_options = Hash.new { |h,k| h[k] = [] }
|
12
|
+
|
13
|
+
def messages
|
14
|
+
@@messages
|
15
|
+
end
|
16
|
+
|
17
|
+
def messages_and_options
|
18
|
+
@@messages_and_options
|
19
|
+
end
|
20
|
+
|
21
|
+
def clear
|
22
|
+
messages.clear
|
23
|
+
messages_and_options.clear
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module Consumer::Publish
|
28
|
+
|
29
|
+
def publish(message, options = nil)
|
30
|
+
options ||= {}
|
31
|
+
Testing.messages[params.exchange_name] << message
|
32
|
+
Testing.messages_and_options[params.exchange_name] << [message, options]
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def messages
|
37
|
+
Testing.messages[params.exchange_name]
|
38
|
+
end
|
39
|
+
|
40
|
+
def messages_and_options
|
41
|
+
Testing.messages_and_options[params.exchange_name]
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'timeout'
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Vx::Consumer do
|
6
|
+
|
7
|
+
context "test consumer declaration" do
|
8
|
+
context "alice" do
|
9
|
+
subject { Alice.params }
|
10
|
+
its(:exchange_name) { should eq 'amq.fanout' }
|
11
|
+
its(:exchange_options) { should eq(durable: true, auto_delete: false, type: :fanout) }
|
12
|
+
its(:queue_name) { should eq '' }
|
13
|
+
its(:queue_options) { should eq(exclusive: false, durable: true, auto_delete: false) }
|
14
|
+
its(:ack) { should be_false }
|
15
|
+
its(:routing_key) { should eq 'mykey' }
|
16
|
+
its(:headers) { should be_nil }
|
17
|
+
its(:content_type) { should eq 'text/plain' }
|
18
|
+
end
|
19
|
+
|
20
|
+
context "bob" do
|
21
|
+
subject { Bob.params }
|
22
|
+
its(:exchange_name) { should eq 'bob_exch' }
|
23
|
+
its(:exchange_options) { should eq(durable: false, auto_delete: true, type: :topic) }
|
24
|
+
its(:queue_name) { should eq 'bob_queue' }
|
25
|
+
its(:queue_options) { should eq(exclusive: true, durable: false) }
|
26
|
+
its(:ack) { should be_true }
|
27
|
+
its(:routing_key) { should be_nil }
|
28
|
+
its(:content_type) { should eq 'application/json' }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it "simple pub/sub" do
|
33
|
+
consumer = Bob.subscribe
|
34
|
+
sleep 1
|
35
|
+
3.times {|n| Bob.publish("a" => n) }
|
36
|
+
|
37
|
+
Timeout.timeout(3) do
|
38
|
+
loop do
|
39
|
+
break if Bob._collected.size == 3
|
40
|
+
sleep 0.1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
consumer.cancel
|
44
|
+
|
45
|
+
expect(Bob._collected).to eq([{"a"=>0}, {"a"=>1}, {"a"=>2}])
|
46
|
+
end
|
47
|
+
|
48
|
+
it "pub/sub in multithreaded environment" do
|
49
|
+
handle_errors do
|
50
|
+
cns = []
|
51
|
+
30.times do
|
52
|
+
cns << Bob.subscribe
|
53
|
+
end
|
54
|
+
|
55
|
+
90.times do |n|
|
56
|
+
Thread.new do
|
57
|
+
Bob.publish("a" => n)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
Timeout.timeout(10) do
|
62
|
+
loop do
|
63
|
+
break if Bob._collected.size == 90
|
64
|
+
sleep 0.1
|
65
|
+
end
|
66
|
+
end
|
67
|
+
cns.map(&:cancel)
|
68
|
+
|
69
|
+
expect(Bob._collected.map{|c| c["a"] }.sort).to eq((0...90).to_a)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should catch errors" do
|
74
|
+
error = nil
|
75
|
+
Vx::Consumer.configure do |c|
|
76
|
+
c.on_error do |e, env|
|
77
|
+
error = [e, env]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
consumer = Bob.subscribe
|
81
|
+
sleep 0.1
|
82
|
+
Bob.publish "not json"
|
83
|
+
|
84
|
+
sleep 0.1
|
85
|
+
consumer.cancel
|
86
|
+
|
87
|
+
expect(error[0]).to be_an_instance_of(JSON::ParserError)
|
88
|
+
expect(error[1][:consumer]).to_not be_nil
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should wait shutdown" do
|
92
|
+
consumer = Bob.subscribe
|
93
|
+
Bob.publish a: 1
|
94
|
+
|
95
|
+
th = Thread.new {
|
96
|
+
consumer.wait
|
97
|
+
}
|
98
|
+
sleep 1
|
99
|
+
Vx::Consumer.shutdown
|
100
|
+
|
101
|
+
Timeout.timeout(1) do
|
102
|
+
th.join
|
103
|
+
end
|
104
|
+
|
105
|
+
expect(Bob._collected).to eq(["a" => 1])
|
106
|
+
end
|
107
|
+
|
108
|
+
def handle_errors
|
109
|
+
begin
|
110
|
+
yield
|
111
|
+
rescue Exception => e
|
112
|
+
Vx::Consumer.exception_handler(e, {})
|
113
|
+
raise e
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'beefcake'
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Vx::Consumer::Serializer do
|
6
|
+
let(:s) { described_class }
|
7
|
+
|
8
|
+
context "text/plain" do
|
9
|
+
let(:payload) { 'payload' }
|
10
|
+
|
11
|
+
it "should pack payload" do
|
12
|
+
expect(s.pack('text/plain', payload)).to eq 'payload'
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should unpack payload" do
|
16
|
+
expect(s.unpack('text/plain', payload, nil)).to eq 'payload'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context "application/json" do
|
21
|
+
let(:payload) { {a: 1} }
|
22
|
+
|
23
|
+
it "should pack payload" do
|
24
|
+
expect(s.pack('application/json', payload)).to eq "{\"a\":1}"
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should unpack payload" do
|
28
|
+
expect(s.unpack('application/json', payload.to_json, nil)).to eq("a"=>1)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context "application/x-protobuf" do
|
33
|
+
let(:payload) { BeefcakeTestMessage.new(x: 1, y: 2) }
|
34
|
+
|
35
|
+
it "should pack payload" do
|
36
|
+
expect(s.pack('application/x-protobuf', payload)).to eq payload.encode.to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should unpack payload" do
|
40
|
+
expect(s.unpack('application/x-protobuf', payload.encode.to_s, BeefcakeTestMessage)).to eq payload
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Vx::Consumer::Session do
|
4
|
+
let(:sess) { described_class.new }
|
5
|
+
subject { sess }
|
6
|
+
|
7
|
+
after do
|
8
|
+
sess.close
|
9
|
+
end
|
10
|
+
|
11
|
+
it { should be }
|
12
|
+
|
13
|
+
it "should successfuly open connection" do
|
14
|
+
expect {
|
15
|
+
sess.open
|
16
|
+
}.to change(sess, :open?).to(true)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should successfuly open connection in multithread environment" do
|
20
|
+
(0..10).map do |n|
|
21
|
+
Thread.new do
|
22
|
+
sess.open
|
23
|
+
end
|
24
|
+
end.map(&:value)
|
25
|
+
|
26
|
+
expect(sess).to be_open
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require File.expand_path '../../lib/vx/consumer', __FILE__
|
2
|
+
|
3
|
+
require 'rspec/autorun'
|
4
|
+
|
5
|
+
Dir[File.expand_path("../..", __FILE__) + "/spec/support/**.rb"].each {|f| require f}
|
6
|
+
|
7
|
+
ENV['VX_CONSUMER_DEBUG'] = '1'
|
8
|
+
|
9
|
+
RSpec.configure do |config|
|
10
|
+
|
11
|
+
config.before(:each) do
|
12
|
+
Vx::Consumer.configuration.reset!
|
13
|
+
end
|
14
|
+
|
15
|
+
config.after(:each) do
|
16
|
+
Vx::Consumer.session.close
|
17
|
+
Bob._reset
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
class Alice
|
4
|
+
include Vx::Consumer
|
5
|
+
|
6
|
+
content_type 'text/plain'
|
7
|
+
routing_key 'mykey'
|
8
|
+
|
9
|
+
fanout
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
class Bob
|
14
|
+
include Vx::Consumer
|
15
|
+
|
16
|
+
exchange 'bob_exch', durable: false, auto_delete: true
|
17
|
+
queue 'bob_queue', exclusive: true, durable: false
|
18
|
+
ack
|
19
|
+
|
20
|
+
@@m = Mutex.new
|
21
|
+
@@collected = []
|
22
|
+
|
23
|
+
class << self
|
24
|
+
def _collected
|
25
|
+
@@collected
|
26
|
+
end
|
27
|
+
|
28
|
+
def _reset
|
29
|
+
@@m.synchronize do
|
30
|
+
@@collected = []
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def _save(payload)
|
35
|
+
@@m.synchronize do
|
36
|
+
@@collected << payload
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def perform(payload)
|
42
|
+
self.class._save payload
|
43
|
+
sleep 0.1
|
44
|
+
ack
|
45
|
+
end
|
46
|
+
end
|
data/vx-consumer.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'vx/consumer/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "vx-consumer"
|
8
|
+
spec.version = Vx::Consumer::VERSION
|
9
|
+
spec.authors = ["Dmitry Galinsky"]
|
10
|
+
spec.email = ["dima.exe@gmail.com"]
|
11
|
+
spec.summary = %q{ summary }
|
12
|
+
spec.description = %q{ description }
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_runtime_dependency 'bunny', '= 1.1.1'
|
22
|
+
spec.add_runtime_dependency 'vx-common-rack-builder', '>= 0.0.2'
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
25
|
+
spec.add_development_dependency "rake"
|
26
|
+
spec.add_development_dependency "rspec"
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: vx-consumer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dmitry Galinsky
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-02-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bunny
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.1.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.1.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: vx-common-rack-builder
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.0.2
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.0.2
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.5'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.5'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: ' description '
|
84
|
+
email:
|
85
|
+
- dima.exe@gmail.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- .gitignore
|
91
|
+
- .rspec
|
92
|
+
- .travis.yml
|
93
|
+
- Gemfile
|
94
|
+
- LICENSE.txt
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- lib/vx/consumer.rb
|
98
|
+
- lib/vx/consumer/ack.rb
|
99
|
+
- lib/vx/consumer/configuration.rb
|
100
|
+
- lib/vx/consumer/error.rb
|
101
|
+
- lib/vx/consumer/instrument.rb
|
102
|
+
- lib/vx/consumer/params.rb
|
103
|
+
- lib/vx/consumer/publish.rb
|
104
|
+
- lib/vx/consumer/serializer.rb
|
105
|
+
- lib/vx/consumer/session.rb
|
106
|
+
- lib/vx/consumer/subscribe.rb
|
107
|
+
- lib/vx/consumer/subscriber.rb
|
108
|
+
- lib/vx/consumer/testing.rb
|
109
|
+
- lib/vx/consumer/version.rb
|
110
|
+
- spec/lib/consumer_spec.rb
|
111
|
+
- spec/lib/serializer_spec.rb
|
112
|
+
- spec/lib/session_spec.rb
|
113
|
+
- spec/spec_helper.rb
|
114
|
+
- spec/support/beefcake_test_message.rb
|
115
|
+
- spec/support/test_consumers.rb
|
116
|
+
- vx-consumer.gemspec
|
117
|
+
homepage: ''
|
118
|
+
licenses:
|
119
|
+
- MIT
|
120
|
+
metadata: {}
|
121
|
+
post_install_message:
|
122
|
+
rdoc_options: []
|
123
|
+
require_paths:
|
124
|
+
- lib
|
125
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - '>='
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0'
|
130
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
131
|
+
requirements:
|
132
|
+
- - '>='
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
requirements: []
|
136
|
+
rubyforge_project:
|
137
|
+
rubygems_version: 2.0.14
|
138
|
+
signing_key:
|
139
|
+
specification_version: 4
|
140
|
+
summary: summary
|
141
|
+
test_files:
|
142
|
+
- spec/lib/consumer_spec.rb
|
143
|
+
- spec/lib/serializer_spec.rb
|
144
|
+
- spec/lib/session_spec.rb
|
145
|
+
- spec/spec_helper.rb
|
146
|
+
- spec/support/beefcake_test_message.rb
|
147
|
+
- spec/support/test_consumers.rb
|
148
|
+
has_rdoc:
|