vx-consumer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ -fd
3
+ --order=rand
data/.travis.yml ADDED
@@ -0,0 +1,14 @@
1
+ services:
2
+ - rabbitmq
3
+
4
+ rvm:
5
+ - 1.9.3
6
+ - 2.0.0
7
+ - 2.1.0
8
+ - jruby
9
+
10
+ matrix:
11
+ allow_failures:
12
+ - rvm: jruby
13
+
14
+ script: bundle exec rspec spec/
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in vx-consumer.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'beefcake'
8
+ end
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"
@@ -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,8 @@
1
+ module Vx
2
+ module Consumer
3
+
4
+ class ConnectionDoesNotExistError < StandardError ; end
5
+ class ModelIsNotdefined < StandardError ; end
6
+
7
+ end
8
+ 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,5 @@
1
+ module Vx
2
+ module Consumer
3
+ VERSION = "0.0.1"
4
+ end
5
+ 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
@@ -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,8 @@
1
+ require 'beefcake'
2
+
3
+ class BeefcakeTestMessage
4
+ include Beefcake::Message
5
+
6
+ required :x, :int32, 1
7
+ required :y, :int32, 2
8
+ 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
@@ -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: