pebbles-river 0.2.6 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 810c3766a5dca1d09e230dd2fa33b2cfc2f17a6e
4
- data.tar.gz: 907a549065e1ae8013cfa6738466c92dbaa5e4ef
3
+ metadata.gz: e2e7d4334cd98eebaa0bb87f18ff27cb94c373bf
4
+ data.tar.gz: 21845c17a52a8c30aa620cf77e7d872fda365751
5
5
  SHA512:
6
- metadata.gz: ea19b805fe8d0c09f826a0cd6f52bdf155173d2c530347fa04c487baecd7a2924a1aac866b1dfb0fccfbde1c2de18790631c416566157ff6125b431256aee347
7
- data.tar.gz: 117fcfcf720e0de40ed98f6f37f0b1a9bf037748fcb9e1e8de84039dc3e9e9d0a21ef791b5fa8ef66626523471607f4ceced93cbce692ad47f7b3dd347b82df3
6
+ metadata.gz: b9757df6f8aa96fafca5646fd75dc0b31741d80b6c49a5713b70c6a16c2a26a6fa5ec9c5a1e12bf2b6d619b99a891923f0671364f6372f83b43568b56e27bbc7
7
+ data.tar.gz: 734552e446937b0016bf4aded20221f477fc6083f6e4f378c0c633bd74f40cf7cbe38e23e203146176af684f7a374ffa62c06836d245868d449f2aeabbef89b3
data/lib/pebbles/river.rb CHANGED
@@ -9,7 +9,6 @@ require_relative "river/version"
9
9
  require_relative "river/errors"
10
10
  require_relative "river/message"
11
11
  require_relative "river/worker"
12
- require_relative "river/subscription"
13
12
  require_relative "river/supervisor"
14
13
  require_relative "river/routing"
15
14
  require_relative "river/river"
@@ -8,12 +8,18 @@ module Pebbles
8
8
  attr_reader :session
9
9
  attr_reader :channel
10
10
  attr_reader :prefetch
11
+ attr_reader :exchange_name
11
12
 
12
13
  def initialize(options = {})
13
14
  options = {environment: options} if options.is_a?(String) # Backwards compatibility
14
15
 
15
16
  @environment = (options[:environment] || ENV['RACK_ENV'] || 'development').dup.freeze
17
+
18
+ @exchange_name = 'pebblebed.river'
19
+ @exchange_name << ".#{environment}" if @environment != 'production'
20
+
16
21
  @last_connect_attempt = nil
22
+
17
23
  @prefetch = options[:prefetch]
18
24
  end
19
25
 
@@ -25,15 +31,13 @@ module Pebbles
25
31
  unless @session and @channel and @exchange
26
32
  disconnect
27
33
  handle_session_error do
28
- session = Bunny::Session.new(::Pebbles::River.rabbitmq_options)
29
- session.start
30
-
31
- channel = session.create_channel
32
- channel.prefetch(@prefetch) if @prefetch
34
+ @session = Bunny::Session.new(::Pebbles::River.rabbitmq_options)
35
+ @session.start
33
36
 
34
- exchange = channel.exchange(exchange_name, EXCHANGE_OPTIONS.dup)
37
+ @channel = @session.create_channel
38
+ @channel.prefetch(@prefetch) if @prefetch
35
39
 
36
- @session, @channel, @exchange = session, channel, exchange
40
+ @exchange = @channel.exchange(@exchange_name, EXCHANGE_OPTIONS.dup)
37
41
  end
38
42
  end
39
43
  end
@@ -70,16 +74,27 @@ module Pebbles
70
74
  end
71
75
 
72
76
  def queue(options = {})
77
+ options.assert_valid_keys(:name, :ttl, :event, :path, :klass,
78
+ :dead_letter_routing_key, :routing_key)
79
+
73
80
  raise ArgumentError.new 'Queue must be named' unless options[:name]
74
81
 
75
- queue_opts = {durable: true}
82
+ queue_args = {}
76
83
  if (ttl = options[:ttl])
77
- queue_opts[:arguments] = {'x-message-ttl' => ttl}
84
+ queue_args['x-message-ttl'] = ttl
85
+ end
86
+ if (dead_letter_routing_key = options[:dead_letter_routing_key])
87
+ queue_args['x-dead-letter-exchange'] = @exchange_name
88
+ queue_args['x-dead-letter-routing-key'] = dead_letter_routing_key
78
89
  end
90
+ queue_opts = {durable: true, arguments: queue_args}
79
91
 
80
92
  connect
81
93
  queue = @channel.queue(options[:name], queue_opts)
82
- Subscription.new(options).queries.each do |key|
94
+ if (routing_key = options[:routing_key])
95
+ queue.bind(exchange.name, key: routing_key)
96
+ end
97
+ Routing.binding_routing_keys_for(options.slice(:event, :class, :path)).each do |key|
83
98
  queue.bind(exchange.name, key: key)
84
99
  end
85
100
  queue
@@ -87,20 +102,10 @@ module Pebbles
87
102
 
88
103
  private
89
104
 
90
- def exchange_name
91
- return @exchange_name ||= format_exchange_name
92
- end
93
-
94
- def format_exchange_name
95
- name = 'pebblebed.river'
96
- name << ".#{environment}" if @environment != 'production'
97
- name
98
- end
99
-
100
105
  def handle_session_error(exception_klass = ConnectFailure, &block)
101
106
  last_exception = nil
102
107
  Timeout.timeout(MAX_RETRY_TIMEOUT) do
103
- retry_until, retry_count = nil, 0
108
+ retry_count = 0
104
109
  begin
105
110
  yield
106
111
  rescue *CONNECTION_EXCEPTIONS => exception
@@ -112,6 +117,9 @@ module Pebbles
112
117
  end
113
118
  end
114
119
  rescue Timeout::Error => timeout_exception
120
+ # Timeouts can screw up the connection, so forcibly close it
121
+ disconnect
122
+
115
123
  if last_exception
116
124
  raise exception_klass.new(last_exception.message, last_exception)
117
125
  else
@@ -4,7 +4,6 @@ module Pebbles
4
4
 
5
5
  def self.routing_key_for(options)
6
6
  options.assert_valid_keys(:uid, :event)
7
-
8
7
  raise ArgumentError.new(':event is required') unless options[:event]
9
8
  raise ArgumentError.new(':uid is required') unless options[:uid]
10
9
 
@@ -13,6 +12,40 @@ module Pebbles
13
12
  key.join('._.')
14
13
  end
15
14
 
15
+ def self.binding_routing_keys_for(options)
16
+ options.assert_valid_keys(:path, :class, :event)
17
+ keys = []
18
+ if options[:event] or options[:path] or options[:class]
19
+ element_to_routing_key_parts(options[:event]).each do |event|
20
+ element_to_routing_key_parts(options[:class]).each do |klass|
21
+ element_to_routing_key_parts(options[:path]).each do |pathspec|
22
+ path_to_routing_key_parts(pathspec).each do |path|
23
+ keys << [event, klass, path].join('._.')
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ keys
30
+ end
31
+
32
+ private
33
+
34
+ def self.path_to_routing_key_parts(s)
35
+ required, optional = s.split('^').map { |p| p.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
+ def self.element_to_routing_key_parts(s)
42
+ s ||= '#'
43
+ if s.respond_to?(:to_a)
44
+ s = s.join('|')
45
+ end
46
+ s.gsub('**', '#').split('|')
47
+ end
48
+
16
49
  end
17
50
  end
18
- end
51
+ end
@@ -1,5 +1,5 @@
1
1
  module Pebbles
2
2
  module River
3
- VERSION = '0.2.6'
3
+ VERSION = '0.3.0'
4
4
  end
5
5
  end
@@ -50,6 +50,7 @@ module Pebbles
50
50
  else
51
51
  @managed_acking = true
52
52
  end
53
+ @dead_lettered = !!@queue_options[:dead_letter_routing_key]
53
54
  @on_exception = options[:on_exception] || ->(*args) { }
54
55
  @handler = handler
55
56
  @river = River.new(options.slice(:prefetch))
@@ -137,7 +138,7 @@ module Pebbles
137
138
  message = Message.new(content, delivery_info, queue)
138
139
  rescue => exception
139
140
  ignore_exceptions do
140
- queue.channel.nack(delivery_info, false, true)
141
+ reject(delivery_info)
141
142
  end
142
143
  raise exception
143
144
  else
@@ -148,7 +149,7 @@ module Pebbles
148
149
  rescue => exception
149
150
  if @managed_acking
150
151
  ignore_exceptions do
151
- message.nack
152
+ reject(delivery_info)
152
153
  end
153
154
  end
154
155
  raise exception
@@ -156,7 +157,7 @@ module Pebbles
156
157
  if @managed_acking
157
158
  case result
158
159
  when false
159
- message.nack
160
+ reject(delivery_info)
160
161
  else
161
162
  message.ack
162
163
  end
@@ -165,6 +166,14 @@ module Pebbles
165
166
  end
166
167
  end
167
168
 
169
+ def reject(delivery_info)
170
+ # Normally requeue, except if we are dead-lettering to another queue, where
171
+ # requeue = false means to bounce it to DLX.
172
+ requeue = !@dead_lettered
173
+
174
+ queue.channel.reject(delivery_info.delivery_tag.to_i, requeue)
175
+ end
176
+
168
177
  def with_exceptions(&block)
169
178
  begin
170
179
  yield
@@ -6,20 +6,72 @@ describe Pebbles::River::Routing do
6
6
  Pebbles::River::Routing
7
7
  end
8
8
 
9
- describe "routing keys" do
10
-
9
+ describe '#routing_key_for' do
11
10
  specify do
12
- options = {:event => 'created', :uid => 'post.awesome.event:feeds.bagera.whatevs$123'}
11
+ options = {event: 'created', :uid => 'post.awesome.event:feeds.bagera.whatevs$123'}
13
12
  subject.routing_key_for(options).should eq('created._.post.awesome.event._.feeds.bagera.whatevs')
14
13
  end
15
14
 
16
- specify "event is required" do
15
+ specify 'event is required' do
17
16
  ->{ subject.routing_key_for(:uid => 'whatevs') }.should raise_error ArgumentError
18
17
  end
19
18
 
20
- specify "uid is required" do
21
- ->{ subject.routing_key_for(:event => 'whatevs') }.should raise_error ArgumentError
19
+ specify 'uid is required' do
20
+ ->{ subject.routing_key_for(event: 'whatevs') }.should raise_error ArgumentError
21
+ end
22
+ end
23
+
24
+ describe '#binding_routing_keys_for' do
25
+ specify 'simple, direct match' do
26
+ expect(subject.binding_routing_keys_for(event: 'create', class: 'post.event', path: 'feed.bagera')).to eq [
27
+ 'create._.post.event._.feed.bagera'
28
+ ]
29
+ end
30
+
31
+ specify 'simple wildcard match' do
32
+ options = {event: '*.create', class: 'post.*', path: '*.bagera.*'}
33
+ expect(subject.binding_routing_keys_for(options)).to eq(['*.create._.post.*._.*.bagera.*'])
22
34
  end
23
35
 
36
+ describe "anything matchers" do
37
+ specify 'match anything (duh)' do
38
+ expect(subject.binding_routing_keys_for({event: '**', class: '**', path: '**'})).to eq(['#._.#._.#'])
39
+ end
40
+
41
+ specify 'match nothing if not specified' do
42
+ expect(subject.binding_routing_keys_for({})).to eq []
43
+ end
44
+ end
45
+
46
+ it 'handles "or" queries' do
47
+ options = {event: 'create|delete', class: 'post', path: 'bagera|bandwagon'}
48
+ expected = ['create._.post._.bagera', 'delete._.post._.bagera', 'create._.post._.bandwagon', 'delete._.post._.bandwagon'].sort
49
+ expect(subject.binding_routing_keys_for(options).sort).to eq(expected)
50
+ end
51
+
52
+ # FIXME
53
+ # describe "optional paths" do
54
+ # it { Subscription.new.pathify('a.b').should eq(['a.b']) }
55
+ # it { Subscription.new.pathify('a.^b.c').should eq(%w(a a.b a.b.c)) }
56
+ # end
57
+
58
+ it "handles optional queries" do
59
+ options = {event: 'create', class: 'post', path: 'feeds.bagera.^fb.concerts'}
60
+ expected = ['create._.post._.feeds.bagera', 'create._.post._.feeds.bagera.fb', 'create._.post._.feeds.bagera.fb.concerts'].sort
61
+ expect(subject.binding_routing_keys_for(options).sort).to eq(expected)
62
+ end
63
+
64
+ it "combines all kinds of weird stuff" do
65
+ options = {event: 'create', class: 'post', path: 'a.^b.c|x.^y.z'}
66
+ expected = [
67
+ 'create._.post._.a',
68
+ 'create._.post._.a.b',
69
+ 'create._.post._.a.b.c',
70
+ 'create._.post._.x',
71
+ 'create._.post._.x.y',
72
+ 'create._.post._.x.y.z',
73
+ ].sort
74
+ expect(subject.binding_routing_keys_for(options).sort).to eq(expected)
75
+ end
24
76
  end
25
77
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pebbles-river
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.6
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Staubo
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-08-02 00:00:00.000000000 Z
12
+ date: 2016-07-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: pebblebed
@@ -157,14 +157,12 @@ files:
157
157
  - lib/pebbles/river/rate_limiter.rb
158
158
  - lib/pebbles/river/river.rb
159
159
  - lib/pebbles/river/routing.rb
160
- - lib/pebbles/river/subscription.rb
161
160
  - lib/pebbles/river/supervisor.rb
162
161
  - lib/pebbles/river/version.rb
163
162
  - lib/pebbles/river/worker.rb
164
163
  - pebbles-river.gemspec
165
164
  - spec/lib/river_spec.rb
166
165
  - spec/lib/routing_spec.rb
167
- - spec/lib/subscription_spec.rb
168
166
  - spec/lib/worker_spec.rb
169
167
  - spec/spec_helper.rb
170
168
  homepage: ''
@@ -187,14 +185,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
187
185
  version: '0'
188
186
  requirements: []
189
187
  rubyforge_project:
190
- rubygems_version: 2.4.6
188
+ rubygems_version: 2.2.5
191
189
  signing_key:
192
190
  specification_version: 4
193
191
  summary: Implements an event river mechanism for Pebblebed.
194
192
  test_files:
195
193
  - spec/lib/river_spec.rb
196
194
  - spec/lib/routing_spec.rb
197
- - spec/lib/subscription_spec.rb
198
195
  - spec/lib/worker_spec.rb
199
196
  - spec/spec_helper.rb
200
197
  has_rdoc:
@@ -1,44 +0,0 @@
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
@@ -1,61 +0,0 @@
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