pebbles-river 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: 0738174b6c9733edbe9524576e2ecf48ca2ad56e
4
+ data.tar.gz: bdf513a26a16bd44be5a02dec8af1ac9b11f467f
5
+ SHA512:
6
+ metadata.gz: 9068b048fa0c91a0640d15d77756dc742c3add7aa0a3106fd50178bf86e58717caa0d917497879b92394b596d74353ae8ca7a9b2ee3ceda8a68455e0d4a6689b
7
+ data.tar.gz: 8f0747a995edcd03ca44e64122ef63187904263fe248a22417860a44f854da54ad40c4da1629a448868d0fca7f30ca1595648c64d70a1bb8788f488672fbef19
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/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pebbles-worker.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Alexander Staubo
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,3 @@
1
+ # River
2
+
3
+ An extension to [Pebblebed](//github.com/bengler/pebblebed) that provides a simple framework for publishing and consuming asynchronous events via RabbitMQ.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,11 @@
1
+ unless defined?(Pebblebed::River)
2
+
3
+ module Pebblebed
4
+
5
+ # For backward compatibility with existing projects.
6
+ class River < Pebbles::River::River
7
+ end
8
+
9
+ end
10
+
11
+ end
@@ -0,0 +1,57 @@
1
+ module Pebbles
2
+ module River
3
+
4
+ class InvalidPayloadError < StandardError
5
+
6
+ attr_reader :payload
7
+
8
+ def initalize(message, payload)
9
+ super(message)
10
+ @payload = payload
11
+ end
12
+
13
+ end
14
+
15
+ class Message
16
+
17
+ attr_reader :payload
18
+ attr_reader :queue
19
+
20
+ def self.deserialize_payload(payload)
21
+ if payload
22
+ begin
23
+ return JSON.parse(payload)
24
+ rescue => e
25
+ raise InvalidPayloadError.new(e.message, payload)
26
+ end
27
+ end
28
+ end
29
+
30
+ def initialize(raw_message, queue = nil)
31
+ @queue = queue
32
+ @raw_message = raw_message
33
+ @payload = self.class.deserialize_payload(raw_message[:payload])
34
+ end
35
+
36
+ def ==(other)
37
+ other &&
38
+ other.is_a?(Message) &&
39
+ other.payload == @payload
40
+ end
41
+
42
+ def delivery_tag
43
+ @raw_message[:delivery_details][:delivery_tag]
44
+ end
45
+
46
+ def ack
47
+ @queue.ack(delivery_tag: delivery_tag)
48
+ end
49
+
50
+ def nack
51
+ @queue.nack(delivery_tag: delivery_tag)
52
+ end
53
+
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,114 @@
1
+ module Pebbles
2
+ module River
3
+
4
+ class SendFailure < StandardError
5
+
6
+ attr_reader :connection_exception
7
+
8
+ def initialize(message, connection_exception = nil)
9
+ super(message)
10
+ @connection_exception = connection_exception
11
+ end
12
+
13
+ end
14
+
15
+ class River
16
+
17
+ attr_reader :environment
18
+
19
+ def initialize(options = {})
20
+ options = {environment: options} if options.is_a?(String) # Backwards compatibility
21
+
22
+ @environment = (options[:environment] || ENV['RACK_ENV'] || 'development').dup.freeze
23
+ end
24
+
25
+ def connected?
26
+ bunny.connected?
27
+ end
28
+
29
+ def connect
30
+ unless connected?
31
+ bunny.start
32
+ bunny.qos
33
+ end
34
+ end
35
+
36
+ def disconnect
37
+ bunny.stop if connected?
38
+ end
39
+
40
+ def publish(options = {})
41
+ connect
42
+ handle_connection_error do
43
+ exchange.publish(options.to_json,
44
+ persistent: options.fetch(:persistent, true),
45
+ key: Routing.routing_key_for(options.slice(:event, :uid)))
46
+ end
47
+ end
48
+
49
+ def queue(options = {})
50
+ raise ArgumentError.new 'Queue must be named' unless options[:name]
51
+
52
+ connect
53
+
54
+ queue = bunny.queue(options[:name], QUEUE_OPTIONS.dup)
55
+ Subscription.new(options).queries.each do |key|
56
+ queue.bind(exchange.name, key: key)
57
+ end
58
+ queue
59
+ end
60
+
61
+ def exchange_name
62
+ return @exchange_name ||= format_exchange_name
63
+ end
64
+
65
+ private
66
+
67
+ def bunny
68
+ @bunny ||= Bunny.new
69
+ end
70
+
71
+ def format_exchange_name
72
+ name = 'pebblebed.river'
73
+ name << ".#{environment}" if @environment != 'production'
74
+ name
75
+ end
76
+
77
+ def exchange
78
+ connect
79
+ @exchange ||= bunny.exchange(exchange_name, EXCHANGE_OPTIONS.dup)
80
+ end
81
+
82
+ def handle_connection_error(&block)
83
+ retry_until = nil
84
+ begin
85
+ yield
86
+ rescue *CONNECTION_EXCEPTIONS => exception
87
+ retry_until ||= Time.now + 4
88
+ if Time.now < retry_until
89
+ sleep(0.5)
90
+ retry
91
+ else
92
+ raise SendFailure.new(exception.message, exception)
93
+ end
94
+ end
95
+ end
96
+
97
+ QUEUE_OPTIONS = {durable: true}.freeze
98
+
99
+ EXCHANGE_OPTIONS = {type: :topic, durable: :true}.freeze
100
+
101
+ CONNECTION_EXCEPTIONS = [
102
+ Bunny::ConnectionError,
103
+ Bunny::ForcedChannelCloseError,
104
+ Bunny::ForcedConnectionCloseError,
105
+ Bunny::ServerDownError,
106
+ Bunny::ProtocolError,
107
+ # These should be caught by Bunny, but apparently aren't.
108
+ Errno::ECONNRESET
109
+ ].freeze
110
+
111
+ end
112
+
113
+ end
114
+ end
@@ -0,0 +1,18 @@
1
+ module Pebbles
2
+ module River
3
+ module Routing
4
+
5
+ def self.routing_key_for(options)
6
+ options.assert_valid_keys(:uid, :event)
7
+
8
+ raise ArgumentError.new(':event is required') unless options[:event]
9
+ raise ArgumentError.new(':uid is required') unless options[:uid]
10
+
11
+ uid = Pebblebed::Uid.new(options[:uid])
12
+ key = [options[:event], uid.klass, uid.path].compact
13
+ key.join('._.')
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,44 @@
1
+ module Pebbles
2
+ module River
3
+
4
+ class Subscription
5
+
6
+ attr_reader :events, :klasses, :paths
7
+
8
+ def initialize(options = {})
9
+ @events = querify(options[:event]).split('|')
10
+ @paths = querify(options[:path]).split('|')
11
+ @klasses = querify(options[:klass]).split('|')
12
+ end
13
+
14
+ def queries
15
+ qx = []
16
+ # If we add more than one more level,
17
+ # it's probably time to go recursive.
18
+ events.each do |event|
19
+ klasses.each do |klass|
20
+ paths.each do |pathspec|
21
+ pathify(pathspec).each do |path|
22
+ qx << [event, klass, path].join('._.')
23
+ end
24
+ end
25
+ end
26
+ end
27
+ qx
28
+ end
29
+
30
+ def querify(query)
31
+ (query || '#').gsub('**', '#')
32
+ end
33
+
34
+ def pathify(s)
35
+ required, optional = s.split('^').map {|s| s.split('.')}
36
+ required = Array(required.join('.'))
37
+ optional ||= []
38
+ (0..optional.length).map {|i| required + optional[0,i]}.map {|p| p.join('.')}
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,63 @@
1
+ module Pebbles
2
+ module River
3
+
4
+ class Supervisor < Servolux::Server
5
+
6
+ def initialize(name, options = {})
7
+ super(name, {interval: 5}.merge(options.slice(:logger, :pid_file)))
8
+
9
+ options.assert_valid_keys(:logger, :pid_file, :worker_count, :worker)
10
+
11
+ @worker_count = options[:worker_count] || 1
12
+ @worker = options[:worker]
13
+
14
+ worker = @worker
15
+ @pool = Servolux::Prefork.new(min_workers: @worker_count) do
16
+ $0 = "#{name}: worker"
17
+ trap('TERM') { worker.stop }
18
+ worker.run
19
+ end
20
+ end
21
+
22
+ def before_starting
23
+ $0 = "#{name}: master"
24
+
25
+ logger.info "Starting workers"
26
+ @pool.start(1)
27
+ end
28
+
29
+ def after_stopping
30
+ shutdown_workers
31
+ end
32
+
33
+ def usr2
34
+ shutdown_workers
35
+ end
36
+
37
+ def run
38
+ @pool.ensure_worker_pool_size
39
+ rescue => e
40
+ if logger.respond_to? :exception
41
+ logger.exception(e)
42
+ else
43
+ logger.error(e.inspect)
44
+ logger.error(e.backtrace.join("\n"))
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def shutdown_workers
51
+ logger.info "Shutting down all workers"
52
+ @pool.stop
53
+ loop do
54
+ break if @pool.live_worker_count <= 0
55
+ logger.info "Waiting for workers to quit"
56
+ sleep 0.25
57
+ end
58
+ end
59
+
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,5 @@
1
+ module Pebbles
2
+ module River
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,204 @@
1
+ module Pebbles
2
+ module River
3
+
4
+ # Implements a queue worker.
5
+ class Worker
6
+
7
+ class << self
8
+ def run(handler, options = {})
9
+ Worker.new(handler, options).run
10
+ end
11
+ end
12
+
13
+ attr_reader :queue_options
14
+ attr_reader :handler
15
+ attr_reader :river
16
+
17
+ # Initializes worker with a handler. Options:
18
+ #
19
+ # * `queue`: Same queue options as `River#queue`.
20
+ # * `on_exception`: If provided, called when a message could not be handled
21
+ # due to an exception.
22
+ # * `on_connection_error`: If provided, call on recovered connection errors.
23
+ # Uses `on_exception` if not implemented.
24
+ # * `managed_acking`: If true, ack/nack handling is automatic; every message
25
+ # is automatically acked unless the handler returns false or the handler
26
+ # raises an exception, in which case it's nacked. If false, the handler
27
+ # must do the ack/nacking. Defaults to true.
28
+ #
29
+ # The handler must implement `call(payload, extra)`, where the payload is
30
+ # the message payload, and the extra argument contains message metadata as
31
+ # a hash. If the handler returns false, it is considered rejected, and will
32
+ # be nacked. Otherwise, the message with be acked.
33
+ #
34
+ def initialize(handler, options = {})
35
+ options.assert_valid_keys(
36
+ :queue,
37
+ :on_exception,
38
+ :on_connection_error,
39
+ :managed_acking)
40
+
41
+ unless handler.respond_to?(:call)
42
+ raise ArgumentError.new('Handler must implement #call protocool')
43
+ end
44
+
45
+ @queue_options = (options[:queue] || {}).freeze
46
+ @managed_acking = !!options.fetch(:managed_acking, true)
47
+ @on_exception = options[:on_exception] || ->(e) { }
48
+ @on_connection_error = options[:on_connection_error] || @on_exception
49
+ @handler = handler
50
+ @river = River.new
51
+ @next_event_time = Time.now
52
+ end
53
+
54
+ # Runs the handler once.
55
+ def run_once
56
+ with_exceptions do
57
+ now = Time.now
58
+
59
+ if @next_event_time > now
60
+ sleep(@next_event_time - now)
61
+ now = Time.now
62
+ end
63
+
64
+ if should_run?
65
+ if process_next
66
+ @next_event_time = now
67
+ else
68
+ if @handler.respond_to?(:on_idle)
69
+ with_exceptions do
70
+ @handler.on_idle
71
+ end
72
+ end
73
+ @next_event_time = now + 1
74
+ end
75
+ else
76
+ @next_event_time = now + 5
77
+ end
78
+ end
79
+ nil
80
+ end
81
+
82
+ # Runs the handler. This will process the queue indefinitely.
83
+ def run
84
+ @enabled = true
85
+ while enabled? do
86
+ run_once
87
+ end
88
+ end
89
+
90
+ # Stops any concurrent run.
91
+ def stop
92
+ @enabled = false
93
+ end
94
+
95
+ # Are we enabled?
96
+ def enabled?
97
+ @enabled
98
+ end
99
+
100
+ private
101
+
102
+ def should_run?
103
+ if @handler.respond_to?(:should_run?)
104
+ @handler.should_run?
105
+ else
106
+ true
107
+ end
108
+ end
109
+
110
+ def queue
111
+ @river.connect unless @river.connected?
112
+ return @queue ||= @river.queue(@queue_options)
113
+ end
114
+
115
+ def process_next
116
+ with_exceptions do
117
+ with_connection_error_handling do
118
+ queue.pop(auto_ack: false, ack: true) do |raw_message|
119
+ if raw_message[:payload] != :queue_empty
120
+ process_message(raw_message)
121
+ return true
122
+ else
123
+ return false
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ def process_message(raw_message)
131
+ begin
132
+ message = Message.new(raw_message, queue)
133
+ rescue => exception
134
+ ignore_exceptions do
135
+ queue.nack(delivery_tag: message[:delivery_details][:delivery_tag])
136
+ end
137
+ raise exception
138
+ else
139
+ begin
140
+ result = @handler.call(message)
141
+ rescue *CONNECTION_EXCEPTIONS
142
+ raise
143
+ rescue => exception
144
+ if @managed_acking
145
+ ignore_exceptions do
146
+ message.nack
147
+ end
148
+ end
149
+ raise exception
150
+ else
151
+ if result != false and @managed_acking
152
+ message.ack
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ def with_connection_error_handling(&block)
159
+ yield
160
+ rescue *CONNECTION_EXCEPTIONS => exception
161
+ if @queue
162
+ ignore_exceptions do
163
+ @queue.close
164
+ end
165
+ @queue = nil
166
+ end
167
+
168
+ @river.disconnect
169
+
170
+ ignore_exceptions do
171
+ @on_connection_error.call(exception)
172
+ end
173
+ end
174
+
175
+ def with_exceptions(&block)
176
+ begin
177
+ yield
178
+ rescue => exception
179
+ ignore_exceptions do
180
+ @on_exception.call(exception)
181
+ end
182
+ end
183
+ end
184
+
185
+ def ignore_exceptions(&block)
186
+ yield
187
+ rescue
188
+ # Ignore
189
+ end
190
+
191
+ CONNECTION_EXCEPTIONS = [
192
+ Bunny::ConnectionError,
193
+ Bunny::ForcedChannelCloseError,
194
+ Bunny::ForcedConnectionCloseError,
195
+ Bunny::ServerDownError,
196
+ Bunny::ProtocolError,
197
+ # These should be caught by Bunny, but apparently aren't.
198
+ Errno::ECONNRESET
199
+ ].freeze
200
+
201
+ end
202
+
203
+ end
204
+ end
@@ -0,0 +1,15 @@
1
+ require 'active_support/core_ext/hash/keys'
2
+ require 'active_support/core_ext/hash/slice'
3
+ require 'json'
4
+ require 'bunny'
5
+ require 'pebblebed/uid'
6
+ require 'servolux'
7
+
8
+ require_relative "river/version"
9
+ require_relative "river/message"
10
+ require_relative "river/worker"
11
+ require_relative "river/subscription"
12
+ require_relative "river/supervisor"
13
+ require_relative "river/routing"
14
+ require_relative "river/river"
15
+ require_relative "river/compatibility"
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require 'pebbles/river/version'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = "pebbles-river"
10
+ spec.version = Pebbles::River::VERSION
11
+ spec.authors = ["Alexander Staubo", "Simen Svale Skogsrud"]
12
+ spec.email = ["alex@bengler.no"]
13
+ spec.summary =
14
+ spec.description = %q{Implements an event river mechanism for Pebblebed.}
15
+ spec.homepage = ""
16
+ spec.license = "MIT"
17
+
18
+ spec.files = `git ls-files -z`.split("\x0")
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_runtime_dependency 'pebblebed', '>= 0.1.3'
24
+ spec.add_runtime_dependency 'bunny', '~> 0.8.0'
25
+ spec.add_runtime_dependency 'activesupport', '>= 3.0'
26
+ spec.add_runtime_dependency 'servolux', '~> 0.10'
27
+
28
+ spec.add_development_dependency 'rspec'
29
+ spec.add_development_dependency "bundler", "~> 1.5"
30
+ spec.add_development_dependency "rake"
31
+ spec.add_development_dependency "simplecov"
32
+ end
@@ -0,0 +1,167 @@
1
+ require 'spec_helper'
2
+
3
+ # Note to readers. This is verbose and ugly
4
+ # because I'm trying to understand what I'm doing.
5
+ # When I do understand it, I'll clean up the tests.
6
+ # Until then, please just bear with me.
7
+ # Or explain it to me :)
8
+ describe Pebbles::River::River do
9
+
10
+ subject do
11
+ Pebbles::River::River.new('whatever')
12
+ end
13
+
14
+ CONNECTION_EXCEPTIONS = [
15
+ Bunny::ConnectionError,
16
+ Bunny::ForcedChannelCloseError,
17
+ Bunny::ForcedConnectionCloseError,
18
+ Bunny::ServerDownError,
19
+ Bunny::ProtocolError,
20
+ Errno::ECONNRESET
21
+ ]
22
+
23
+ after(:each) do
24
+ subject.send(:bunny).queues.each do |name, queue|
25
+ queue.purge
26
+ # If you don't delete the queue, the subscription will not
27
+ # change, even if you give it a new one.
28
+ queue.delete
29
+ end
30
+ subject.disconnect
31
+ end
32
+
33
+ it { subject.should_not be_connected }
34
+
35
+ it "gets the name right" do
36
+ subject.exchange_name.should eq('pebblebed.river.whatever')
37
+ end
38
+
39
+ context "in production" do
40
+ subject { Pebbles::River::River.new('production') }
41
+
42
+ it "doesn't append the thing" do
43
+ subject.exchange_name.should eq('pebblebed.river')
44
+ end
45
+ end
46
+
47
+ it "connects" do
48
+ subject.connect
49
+ subject.should be_connected
50
+ end
51
+
52
+ it "disconnects" do
53
+ subject.connect
54
+ subject.should be_connected
55
+ subject.disconnect
56
+ subject.should_not be_connected
57
+ end
58
+
59
+ it "connects if you try to publish something" do
60
+ subject.should_not be_connected
61
+ subject.publish(:event => :test, :uid => 'klass:path$123', :attributes => {:a => 'b'})
62
+ subject.should be_connected
63
+ end
64
+
65
+ it "connects if you try to talk to the exchange" do
66
+ subject.should_not be_connected
67
+ subject.send(:exchange)
68
+ subject.should be_connected
69
+ end
70
+
71
+ describe "publishing" do
72
+
73
+ it "gets selected messages" do
74
+ queue = subject.queue(:name => 'thingivore', :path => 'rspec', :klass => 'thing')
75
+
76
+ queue.message_count.should eq(0)
77
+ subject.publish(:event => 'smile', :uid => 'thing:rspec$1', :attributes => {:a => 'b'})
78
+ subject.publish(:event => 'frown', :uid => 'thing:rspec$2', :attributes => {:a => 'b'})
79
+ subject.publish(:event => 'laugh', :uid => 'thing:testunit$3', :attributes => {:a => 'b'})
80
+ sleep(0.1)
81
+ queue.message_count.should eq(2)
82
+ end
83
+
84
+ it "gets everything if it connects without a key" do
85
+ queue = subject.queue(:name => 'omnivore')
86
+
87
+ queue.message_count.should eq(0)
88
+ subject.publish(:event => 'smile', :uid => 'thing:rspec$1', :attributes => {:a => 'b'})
89
+ subject.publish(:event => 'frown', :uid => 'thing:rspec$2', :attributes => {:a => 'b'})
90
+ subject.publish(:event => 'laugh', :uid => 'testunit:rspec$3', :attributes => {:a => 'b'})
91
+ sleep(0.1)
92
+ queue.message_count.should eq(3)
93
+ end
94
+
95
+ it "sends messages as json" do
96
+ queue = subject.queue(:name => 'eatseverything')
97
+ subject.publish(:event => 'smile', :source => 'rspec', :uid => 'klass:path$1', :attributes => {:a => 'b'})
98
+ sleep(0.1)
99
+ JSON.parse(queue.pop[:payload])['uid'].should eq('klass:path$1')
100
+ end
101
+
102
+ CONNECTION_EXCEPTIONS.each do |exception_class|
103
+ context "on temporary failure with #{exception_class}" do
104
+ it "retries sending until success" do
105
+ exchange = double('exchange')
106
+
107
+ count = 0
108
+ exchange.stub(:publish) do
109
+ count += 1
110
+ if count < 3
111
+ raise exception_class.new
112
+ end
113
+ end
114
+
115
+ subject.stub(:exchange) { exchange }
116
+ subject.stub(:sleep) { }
117
+
118
+ expect(subject).to receive(:sleep).at_least(2).times
119
+
120
+ expect(exchange).to receive(:publish).at_least(2).times
121
+
122
+ subject.publish({event: 'explode', uid: 'thing:rspec$1'})
123
+ end
124
+ end
125
+ end
126
+
127
+ CONNECTION_EXCEPTIONS.each do |exception_class|
128
+ context "on permanent failure with #{exception_class}" do
129
+ it "re-raises #{exception_class} wrapped in SendFailure exception" do
130
+ exchange = double('exchange')
131
+ exchange.stub(:publish) do
132
+ raise exception_class.new
133
+ end
134
+
135
+ subject.stub(:exchange) { exchange }
136
+ subject.stub(:sleep) { }
137
+
138
+ expect(-> { subject.publish({event: 'explode', uid: 'thing:rspec$1'})}).to raise_error(
139
+ Pebbles::River::SendFailure)
140
+ end
141
+ end
142
+ end
143
+
144
+ end
145
+
146
+ it "subscribes" do
147
+ queue = subject.queue(:name => 'alltestivore', :path => 'area51.rspec|area51.testunit|area52.*|area53.**', :klass => 'thing', :event => 'smile')
148
+
149
+ queue.message_count.should eq(0)
150
+ subject.publish(:event => 'smile', :uid => 'thing:area51.rspec$1', :attributes => {:a => 'b'})
151
+ subject.publish(:event => 'smile', :uid => 'thing:area51.testunit$2', :attributes => {:a => 'b'})
152
+ subject.publish(:event => 'smile', :uid => 'thing:area51.whatever$3', :attributes => {:a => 'b'}) # doesn't match path
153
+ subject.publish(:event => 'frown', :uid => 'thing:area51.rspec$4', :attributes => {:a => 'b'}) # doesn't match event
154
+ subject.publish(:event => 'smile', :uid => 'thing:area52.one.two.three$5', :attributes => {:a => 'b'}) # doesn't match wildcard path
155
+ subject.publish(:event => 'smile', :uid => 'thing:area52.one$6', :attributes => {:a => 'b'}) # matches wildcard path
156
+ subject.publish(:event => 'smile', :uid => 'thing:area53.one.two.three$7', :attributes => {:a => 'b'}) # matches wildcard path
157
+
158
+ sleep(0.1)
159
+ queue.message_count.should eq(4)
160
+ end
161
+
162
+ it "is a durable queue" do
163
+ queue = subject.queue(:name => 'adurablequeue', :path => 'katrina')
164
+ subject.publish(:event => 'test', :uid => 'person:katrina$1', :attributes => {:a => rand(1000)}, :persistent => false)
165
+ end
166
+
167
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe Pebbles::River::Routing do
4
+
5
+ subject do
6
+ Pebbles::River::Routing
7
+ end
8
+
9
+ describe "routing keys" do
10
+
11
+ specify do
12
+ options = {:event => 'created', :uid => 'post.awesome.event:feeds.bagera.whatevs$123'}
13
+ subject.routing_key_for(options).should eq('created._.post.awesome.event._.feeds.bagera.whatevs')
14
+ end
15
+
16
+ specify "event is required" do
17
+ ->{ subject.routing_key_for(:uid => 'whatevs') }.should raise_error ArgumentError
18
+ end
19
+
20
+ specify "uid is required" do
21
+ ->{ subject.routing_key_for(:event => 'whatevs') }.should raise_error ArgumentError
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+
3
+ include Pebbles::River
4
+
5
+ describe Subscription do
6
+
7
+ specify 'simple, direct match' do
8
+ options = {:event => 'create', :klass => 'post.event', :path => 'feed.bagera'}
9
+ subscription = Subscription.new(options)
10
+ subscription.queries.should eq(['create._.post.event._.feed.bagera'])
11
+ end
12
+
13
+ specify 'simple wildcard match' do
14
+ options = {:event => '*.create', :klass => 'post.*', :path => '*.bagera.*'}
15
+ Subscription.new(options).queries.should eq(['*.create._.post.*._.*.bagera.*'])
16
+ end
17
+
18
+ describe "anything matchers" do
19
+
20
+ specify 'match anything (duh)' do
21
+ options = {:event => '**', :klass => '**', :path => '**'}
22
+ Subscription.new(options).queries.should eq(['#._.#._.#'])
23
+ end
24
+
25
+ specify 'match anything if not specified' do
26
+ Subscription.new.queries.should eq(['#._.#._.#'])
27
+ end
28
+
29
+ end
30
+
31
+ it 'handles "or" queries' do
32
+ options = {:event => 'create|delete', :klass => 'post', :path => 'bagera|bandwagon'}
33
+ expected = ['create._.post._.bagera', 'delete._.post._.bagera', 'create._.post._.bandwagon', 'delete._.post._.bandwagon'].sort
34
+ Subscription.new(options).queries.sort.should eq(expected)
35
+ end
36
+
37
+ describe "optional paths" do
38
+ it { Subscription.new.pathify('a.b').should eq(['a.b']) }
39
+ it { Subscription.new.pathify('a.^b.c').should eq(%w(a a.b a.b.c)) }
40
+ end
41
+
42
+ it "handles optional queries" do
43
+ options = {:event => 'create', :klass => 'post', :path => 'feeds.bagera.^fb.concerts'}
44
+ expected = ['create._.post._.feeds.bagera', 'create._.post._.feeds.bagera.fb', 'create._.post._.feeds.bagera.fb.concerts'].sort
45
+ Subscription.new(options).queries.sort.should eq(expected)
46
+ end
47
+
48
+ it "combines all kinds of weird stuff" do
49
+ options = {:event => 'create', :klass => 'post', :path => 'a.^b.c|x.^y.z'}
50
+ expected = [
51
+ 'create._.post._.a',
52
+ 'create._.post._.a.b',
53
+ 'create._.post._.a.b.c',
54
+ 'create._.post._.x',
55
+ 'create._.post._.x.y',
56
+ 'create._.post._.x.y.z',
57
+ ].sort
58
+ Subscription.new(options).queries.sort.should eq(expected)
59
+ end
60
+
61
+ end
@@ -0,0 +1,268 @@
1
+ require 'spec_helper'
2
+
3
+ include Pebbles::River
4
+
5
+ describe Worker do
6
+
7
+ subject do
8
+ Worker
9
+ end
10
+
11
+ let :invalid_handler do
12
+ Class.new.new
13
+ end
14
+
15
+ let :null_handler do
16
+ handler = double('null_handler')
17
+ handler.stub(:call) { }
18
+ handler
19
+ end
20
+
21
+ let :io_error do
22
+ IOError.new("This is not the exception you are looking for")
23
+ end
24
+
25
+ let :io_error_raising_handler do
26
+ handler = double('io_error_raising_handler')
27
+ handler.stub(:call).and_raise(io_error)
28
+ handler
29
+ end
30
+
31
+ let :connection_exception do
32
+ Bunny::ConnectionError.new("Fail!")
33
+ end
34
+
35
+ let :payload do
36
+ {'answer' => 42}
37
+ end
38
+
39
+ let :raw_message do
40
+ {
41
+ header: 'someheader',
42
+ payload: JSON.dump(payload),
43
+ delivery_details: {delivery_tag: 'foo'}
44
+ }
45
+ end
46
+
47
+ let :queue do
48
+ queue = double('Bunny::Queue')
49
+ queue.stub(:close) { nil }
50
+ queue.stub(:pop) { |&block|
51
+ block.call(raw_message)
52
+ }
53
+ queue.stub(:ack) { }
54
+ queue.stub(:nack) { }
55
+ queue
56
+ end
57
+
58
+ let :message do
59
+ Message.new(raw_message, queue)
60
+ end
61
+
62
+ let :river do
63
+ river = double('Pebbles::River::River')
64
+ river.stub(:connected?) { true }
65
+ river.stub(:queue).and_return(queue)
66
+ river.stub(:connect).and_return(nil)
67
+ river
68
+ end
69
+
70
+ before do
71
+ River.stub(:new).and_return(river)
72
+ end
73
+
74
+ describe '#initialize' do
75
+ it 'accepts a handler' do
76
+ worker = subject.new(null_handler)
77
+ expect(worker.handler).to eq null_handler
78
+ end
79
+
80
+ it 'requires that handler implement #call' do
81
+ expect(-> {
82
+ subject.new(invalid_handler)
83
+ }).to raise_error(ArgumentError)
84
+ end
85
+
86
+ it 'accepts queue options' do
87
+ worker = subject.new(null_handler, queue: {event: 'foo'})
88
+ expect(worker.queue_options).to eq({event: 'foo'})
89
+ end
90
+ end
91
+
92
+ describe '#run_once' do
93
+
94
+ it 'creates a queue and runs worker with it' do
95
+ expect(queue).to receive(:pop).at_least(1).times
96
+ expect(queue).to receive(:ack).at_least(1).times
97
+ expect(queue).to_not receive(:nack)
98
+
99
+ expect(null_handler).to receive(:call).with(message)
100
+
101
+ expect(river).to receive(:connected?).with(no_args).at_least(1).times
102
+ expect(river).to_not receive(:connect)
103
+ expect(river).to receive(:queue).with({name: 'foo'})
104
+
105
+ subject.new(null_handler, queue: {name: 'foo'}).run_once
106
+ end
107
+
108
+ context 'when queue is empty' do
109
+ it 'does nothing' do
110
+ queue.stub(:pop) { |&block|
111
+ block.call({payload: :queue_empty, delivery_details: {}})
112
+ }
113
+
114
+ expect(queue).to receive(:pop).at_least(1).times
115
+ expect(queue).to_not receive(:ack)
116
+ expect(queue).to_not receive(:nack)
117
+
118
+ expect(null_handler).not_to receive(:call)
119
+
120
+ subject.new(null_handler, queue: {name: 'foo'}).run_once
121
+ end
122
+
123
+ it 'calls #on_idle if implemented' do
124
+ queue.stub(:pop) { |&block|
125
+ block.call({payload: :queue_empty, delivery_details: {}})
126
+ }
127
+
128
+ null_handler.stub(:on_idle) { }
129
+ expect(null_handler).to receive(:on_idle)
130
+
131
+ subject.new(null_handler, queue: {name: 'foo'}).run_once
132
+ end
133
+ end
134
+
135
+ context 'when handler is successful' do
136
+ it 'acks the message' do
137
+ expect(queue).to receive(:ack).at_least(1).times
138
+ expect(queue).to_not receive(:nack)
139
+ expect(queue).to_not receive(:close)
140
+
141
+ expect(river).to receive(:connected?).with(no_args).at_least(1).times
142
+ expect(river).to_not receive(:connect)
143
+
144
+ subject.new(null_handler, queue: {name: 'foo'}).run_once
145
+ end
146
+ end
147
+
148
+ context 'when handler throws exception' do
149
+
150
+ let :on_exception_callback do
151
+ on_exception_callback = double('on_exception')
152
+ on_exception_callback.stub(:call) { }
153
+ on_exception_callback
154
+ end
155
+
156
+ it 'nacks the message' do
157
+ expect(queue).to receive(:nack).at_least(1).times
158
+ expect(queue).to_not receive(:close)
159
+
160
+ subject.new(io_error_raising_handler, queue: {name: 'foo'}).run_once
161
+ end
162
+
163
+ [
164
+ Bunny::ConnectionError,
165
+ Bunny::ForcedChannelCloseError,
166
+ Bunny::ForcedConnectionCloseError,
167
+ Bunny::ServerDownError,
168
+ Bunny::ProtocolError,
169
+ Errno::ECONNRESET
170
+ ].each do |exception_class|
171
+ it "performs connection reset on #{exception_class}" do
172
+ expect(queue).to receive(:close).at_least(1).times
173
+
174
+ handler = double('handler')
175
+ handler.stub(:call).and_return {
176
+ raise exception_class.new("Dangit")
177
+ }
178
+ expect(handler).to receive(:call).with(message)
179
+
180
+ expect(river).to receive(:connected?).with(no_args).at_least(1).times
181
+ expect(river).to_not receive(:connect)
182
+ expect(river).to receive(:queue).with({name: 'foo'})
183
+ expect(river).to receive(:disconnect).at_least(1).times
184
+
185
+ subject.new(handler, queue: {name: 'foo'}).run_once
186
+ end
187
+ end
188
+
189
+ it "calls #on_connection_error if it's implemented" do
190
+ connection_error_handler = double('on_connection_error')
191
+ connection_error_handler.stub(:call) { }
192
+ expect(connection_error_handler).to receive(:call).with(connection_exception)
193
+
194
+ expect(queue).to receive(:close).at_least(1).times
195
+
196
+ erroring_handler = double('handler')
197
+ erroring_handler.stub(:call).and_return {
198
+ raise connection_exception
199
+ }
200
+ erroring_handler.stub(:on_connection_error).and_return(nil)
201
+ expect(erroring_handler).to receive(:call).with(message)
202
+
203
+ expect(river).to receive(:connected?).with(no_args).at_least(1).times
204
+ expect(river).to_not receive(:connect)
205
+ expect(river).to receive(:disconnect).at_least(1).times
206
+
207
+ subject.new(erroring_handler,
208
+ queue: {name: 'foo'},
209
+ on_connection_error: connection_error_handler).run_once
210
+ end
211
+
212
+ it "calls #on_exception for non-connection errors" do
213
+ expect(queue).to_not receive(:close)
214
+ expect(on_exception_callback).to receive(:call).with(io_error)
215
+
216
+ subject.new(io_error_raising_handler,
217
+ queue: {name: 'foo'},
218
+ on_exception: on_exception_callback).run_once
219
+ end
220
+
221
+ end
222
+
223
+ end
224
+
225
+ describe '#run' do
226
+ it 'runs indefinitely until #enabled? returns false' do
227
+ count = 0
228
+
229
+ handler = double('handler')
230
+ handler.stub(:call) { }
231
+
232
+ expect(handler).to receive(:call).at_least(10).times
233
+
234
+ worker = subject.new(handler, queue: {name: 'foo'})
235
+ worker.stub(:enabled?) {
236
+ count += 1
237
+ count <= 10
238
+ }
239
+ worker.run
240
+ end
241
+
242
+ context 'when queue is empty' do
243
+ it 'calls #sleep to delay polling a bit' do
244
+ queue.stub(:pop) { |&block|
245
+ block.call({payload: :queue_empty, delivery_details: {}})
246
+ }
247
+
248
+ count = 0
249
+ worker = subject.new(null_handler, queue: {name: 'foo'})
250
+ worker.stub(:sleep) { }
251
+ expect(worker).to receive(:sleep).at_least(9).times
252
+ worker.stub(:enabled?) {
253
+ count += 1
254
+ count <= 10
255
+ }
256
+ worker.run
257
+ end
258
+ end
259
+
260
+ it 'continues on exception'
261
+ it 'calls #should_run? on handler'
262
+ end
263
+
264
+ describe 'Worker.run' do
265
+ it 'wraps Worker#run'
266
+ end
267
+
268
+ end
@@ -0,0 +1,13 @@
1
+ require 'simplecov'
2
+ require 'rspec'
3
+ require 'rspec/mocks'
4
+
5
+ SimpleCov.add_filter 'spec'
6
+ SimpleCov.add_filter 'config'
7
+ SimpleCov.start
8
+
9
+ require_relative '../lib/pebbles/river'
10
+
11
+ RSpec.configure do |c|
12
+ c.mock_with :rspec
13
+ end
metadata ADDED
@@ -0,0 +1,183 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pebbles-river
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alexander Staubo
8
+ - Simen Svale Skogsrud
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-04-15 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: pebblebed
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - '>='
19
+ - !ruby/object:Gem::Version
20
+ version: 0.1.3
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - '>='
26
+ - !ruby/object:Gem::Version
27
+ version: 0.1.3
28
+ - !ruby/object:Gem::Dependency
29
+ name: bunny
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ~>
33
+ - !ruby/object:Gem::Version
34
+ version: 0.8.0
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ~>
40
+ - !ruby/object:Gem::Version
41
+ version: 0.8.0
42
+ - !ruby/object:Gem::Dependency
43
+ name: activesupport
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '3.0'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '3.0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: servolux
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ~>
61
+ - !ruby/object:Gem::Version
62
+ version: '0.10'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: '0.10'
70
+ - !ruby/object:Gem::Dependency
71
+ name: rspec
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: bundler
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ~>
89
+ - !ruby/object:Gem::Version
90
+ version: '1.5'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ~>
96
+ - !ruby/object:Gem::Version
97
+ version: '1.5'
98
+ - !ruby/object:Gem::Dependency
99
+ name: rake
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - '>='
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - '>='
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ - !ruby/object:Gem::Dependency
113
+ name: simplecov
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - '>='
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: Implements an event river mechanism for Pebblebed.
127
+ email:
128
+ - alex@bengler.no
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - .gitignore
134
+ - Gemfile
135
+ - LICENSE.txt
136
+ - README.md
137
+ - Rakefile
138
+ - lib/pebbles/river.rb
139
+ - lib/pebbles/river/compatibility.rb
140
+ - lib/pebbles/river/message.rb
141
+ - lib/pebbles/river/river.rb
142
+ - lib/pebbles/river/routing.rb
143
+ - lib/pebbles/river/subscription.rb
144
+ - lib/pebbles/river/supervisor.rb
145
+ - lib/pebbles/river/version.rb
146
+ - lib/pebbles/river/worker.rb
147
+ - pebbles-river.gemspec
148
+ - spec/lib/river_spec.rb
149
+ - spec/lib/routing_spec.rb
150
+ - spec/lib/subscription_spec.rb
151
+ - spec/lib/worker_spec.rb
152
+ - spec/spec_helper.rb
153
+ homepage: ''
154
+ licenses:
155
+ - MIT
156
+ metadata: {}
157
+ post_install_message:
158
+ rdoc_options: []
159
+ require_paths:
160
+ - lib
161
+ required_ruby_version: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - '>='
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ required_rubygems_version: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - '>='
169
+ - !ruby/object:Gem::Version
170
+ version: '0'
171
+ requirements: []
172
+ rubyforge_project:
173
+ rubygems_version: 2.0.3
174
+ signing_key:
175
+ specification_version: 4
176
+ summary: Implements an event river mechanism for Pebblebed.
177
+ test_files:
178
+ - spec/lib/river_spec.rb
179
+ - spec/lib/routing_spec.rb
180
+ - spec/lib/subscription_spec.rb
181
+ - spec/lib/worker_spec.rb
182
+ - spec/spec_helper.rb
183
+ has_rdoc: