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 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: