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 +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +3 -0
- data/Rakefile +1 -0
- data/lib/pebbles/river/compatibility.rb +11 -0
- data/lib/pebbles/river/message.rb +57 -0
- data/lib/pebbles/river/river.rb +114 -0
- data/lib/pebbles/river/routing.rb +18 -0
- data/lib/pebbles/river/subscription.rb +44 -0
- data/lib/pebbles/river/supervisor.rb +63 -0
- data/lib/pebbles/river/version.rb +5 -0
- data/lib/pebbles/river/worker.rb +204 -0
- data/lib/pebbles/river.rb +15 -0
- data/pebbles-river.gemspec +32 -0
- data/spec/lib/river_spec.rb +167 -0
- data/spec/lib/routing_spec.rb +25 -0
- data/spec/lib/subscription_spec.rb +61 -0
- data/spec/lib/worker_spec.rb +268 -0
- data/spec/spec_helper.rb +13 -0
- metadata +183 -0
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
data/Gemfile
ADDED
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
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -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,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
|
data/spec/spec_helper.rb
ADDED
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:
|