poseidon_cluster 0.0.2 → 0.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bd4a2f60912c1cf42f542630df36bff3fda31f36
4
- data.tar.gz: 892927b055dca81c8805ef204cfadab0be1b4d88
3
+ metadata.gz: 3a9e9d18567496651c7c390954320166410d315f
4
+ data.tar.gz: a17c4b5192bcd360bd7fe2eda5da3b6d9afcf0a9
5
5
  SHA512:
6
- metadata.gz: 72d72af7e3c642c93cad4d98c67ae628895a8344497466d7299a5a763c97d95754bb2c7750bd5666d39d79584ba989ec07d57c5767a1a61ff46775778bde8fd0
7
- data.tar.gz: 5637f76ec6318cb52b2a353f02324f2c7f1c7e2fd57213ad1c39d6e87f724471a6932a2fc1e9253a21ced77fb01f1273e3fe61ce50ac5807d0727ab5b886cd17
6
+ metadata.gz: c241fca61a6a6c6b59b32d023250665c939fb3f4c34cdddd4bb9cfbb125a4bab8bc87f405e0e496e2a14e6a8d9d6c6ef9278d85d369ddcde30afbad844fd0536
7
+ data.tar.gz: 170438cf674768bfc7f4002332f16f1d89d9a0669278e6f801d1dc10e6e0bfd1a9093fcd382bf3ba9adb88491bf11ba348d36e84e803e5a3bb0c2f80079cee2e
data/.coveralls.yml ADDED
@@ -0,0 +1 @@
1
+ service_name: travis-ci
data/.gitignore CHANGED
@@ -3,3 +3,5 @@ kafka*/
3
3
  doc/
4
4
  .yardoc/
5
5
  .bundle/
6
+ pkg/
7
+ coverage/
data/.travis.yml CHANGED
@@ -1,9 +1,8 @@
1
1
  language: ruby
2
-
3
2
  rvm:
4
3
  - 2.1.0
5
4
  - 2.0.0
6
5
  - 1.9.3
6
+ - jruby-19mode
7
7
  env:
8
8
  - SLOW=1
9
- - SLOW=0
data/Gemfile CHANGED
@@ -2,4 +2,9 @@ source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
4
  gem "poseidon", git: "https://github.com/dim/poseidon.git"
5
- gem "snappy"
5
+ gem "coveralls", require: false
6
+
7
+ platform :jruby do
8
+ gem 'slyphon-log4j', '= 1.2.15'
9
+ gem 'slyphon-zookeeper_jar', '= 3.3.5'
10
+ end
data/Gemfile.lock CHANGED
@@ -7,18 +7,29 @@ GIT
7
7
  PATH
8
8
  remote: .
9
9
  specs:
10
- poseidon_cluster (0.0.2)
10
+ poseidon_cluster (0.0.3)
11
11
  poseidon
12
12
  zk
13
13
 
14
14
  GEM
15
15
  remote: https://rubygems.org/
16
16
  specs:
17
+ coveralls (0.7.0)
18
+ multi_json (~> 1.3)
19
+ rest-client
20
+ simplecov (>= 0.7)
21
+ term-ansicolor
22
+ thor
17
23
  diff-lcs (1.2.5)
24
+ docile (1.1.2)
18
25
  little-plugger (1.1.3)
19
26
  logging (1.7.2)
20
27
  little-plugger (>= 1.1.3)
28
+ mime-types (2.0)
29
+ multi_json (1.8.4)
21
30
  rake (10.1.1)
31
+ rest-client (1.6.7)
32
+ mime-types (>= 1.16)
22
33
  rspec (2.14.1)
23
34
  rspec-core (~> 2.14.0)
24
35
  rspec-expectations (~> 2.14.0)
@@ -27,21 +38,37 @@ GEM
27
38
  rspec-expectations (2.14.4)
28
39
  diff-lcs (>= 1.1.3, < 2.0)
29
40
  rspec-mocks (2.14.4)
30
- snappy (0.0.10)
41
+ simplecov (0.8.2)
42
+ docile (~> 1.1.0)
43
+ multi_json
44
+ simplecov-html (~> 0.8.0)
45
+ simplecov-html (0.8.0)
46
+ slyphon-log4j (1.2.15)
47
+ slyphon-zookeeper_jar (3.3.5-java)
48
+ term-ansicolor (1.2.2)
49
+ tins (~> 0.8)
50
+ thor (0.18.1)
51
+ tins (0.13.1)
31
52
  yard (0.8.7.3)
32
53
  zk (1.9.3)
33
54
  logging (~> 1.7.2)
34
55
  zookeeper (~> 1.4.0)
35
56
  zookeeper (1.4.8)
57
+ zookeeper (1.4.8-java)
58
+ slyphon-log4j (= 1.2.15)
59
+ slyphon-zookeeper_jar (= 3.3.5)
36
60
 
37
61
  PLATFORMS
62
+ java
38
63
  ruby
39
64
 
40
65
  DEPENDENCIES
41
66
  bundler
67
+ coveralls
42
68
  poseidon!
43
69
  poseidon_cluster!
44
70
  rake
45
71
  rspec
46
- snappy
72
+ slyphon-log4j (= 1.2.15)
73
+ slyphon-zookeeper_jar (= 3.3.5)
47
74
  yard
data/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # Poseidon Cluster [![Build Status](https://travis-ci.org/bsm/poseidon_cluster.png?branch=master)](https://travis-ci.org/bsm/poseidon_cluster)
2
2
 
3
- Poseidon Cluster is a cluster extenstion the excellent [Poseidon](http://github.com/bpot/poseidon) Ruby client for Kafka 0.8+. It implements the distribution concept of self-rebalancing *Consumer Groups* and supports the consumption of a single topic from multiple instances.
3
+ Poseidon Cluster is a cluster extension the excellent [Poseidon](http://github.com/bpot/poseidon) Ruby client for Kafka 0.8+. It implements the distribution concept of self-rebalancing *Consumer Groups* and supports the consumption of a single topic from multiple instances.
4
4
 
5
5
  Consumer group instances share a common group name, and each message published to a topic is delivered to one instance within each subscribing consumer group. Consumer instances can be in separate processes or on separate machines.
6
6
 
7
7
  ## Usage
8
8
 
9
- ### Consuming messages
9
+ Launch a consumer group:
10
10
 
11
11
  ```ruby
12
12
  require 'poseidon_cluster'
@@ -18,19 +18,28 @@ consumer = Poseidon::ConsumerGroup.new(
18
18
  "my-topic") # Topic name
19
19
 
20
20
  consumer.partitions # => [0, 1, 2, 3] - all partitions of 'my-topic'
21
- consumer.claimed # => [0, 1] - partitions this instance is consuming
21
+ consumer.claimed # => [0, 1] - partitions this instance has claimed
22
+ ```
23
+
24
+ Fetch a bulk of messages, auto-commit the offset:
22
25
 
23
- # Fetch a bulk of messages, auto-commit the offset
26
+ ```ruby
24
27
  consumer.fetch do |partition, bulk|
25
28
  bulk.each do |m|
26
29
  puts "Fetched '#{m.value}' at #{m.offset} from #{partition}"
27
30
  end
28
31
  end
32
+ ```
33
+
34
+ Get the offset for a partition:
29
35
 
30
- # Get the offset for a partition
36
+ ```ruby
31
37
  consumer.offset(0) # => 320 - current offset from partition 0
38
+ ```
32
39
 
33
- # Fetch more, commit manually
40
+ Fetch more messages, commit manually:
41
+
42
+ ```ruby
34
43
  consumer.fetch commit: false do |partition, bulk|
35
44
  bulk.each do |m|
36
45
  puts "Fetched '#{m.value}' at #{m.offset} from #{partition}"
@@ -40,7 +49,17 @@ consumer.fetch commit: false do |partition, bulk|
40
49
  end
41
50
  ```
42
51
 
43
- For more details and information, please see the [Poseidon::ConsumerGroup](http://rubydoc.info/github/bsm/poseidon_cluster/Poseidon/ConsumerGroup) documentation.
52
+ Initiate a fetch-loop, consume indefinitely:
53
+
54
+ ```ruby
55
+ consumer.fetch_loop do |partition, bulk|
56
+ bulk.each do |m|
57
+ puts "Fetched '#{m.value}' at #{m.offset} from #{partition}"
58
+ end
59
+ end
60
+ ```
61
+
62
+ For more details and information, please see the [Poseidon::ConsumerGroup](http://rubydoc.info/github/bsm/poseidon_cluster/Poseidon/ConsumerGroup) documentation and the [Examples](https://github.com/bsm/poseidon_cluster/tree/master/examples).
44
63
 
45
64
  ## Running Tests
46
65
 
@@ -0,0 +1,33 @@
1
+ =begin
2
+
3
+ PLEASE NOTE
4
+
5
+ This example uses threads, but you could equally use fork or run your
6
+ consumer groups from completely separate process and from multiple machines.
7
+
8
+ =end
9
+ require 'poseidon_cluster'
10
+
11
+ # Create a consumer group
12
+ group1 = Poseidon::ConsumerGroup.new "my-group", ["host1:9092", "host2:9092"], ["host1:2181", "host2:2181"], "my-topic"
13
+
14
+ # Start consuming "my-topic" in a background thread
15
+ thread1 = Thread.new do
16
+ group1.fetch_loop do |partition, messages|
17
+ puts "Consumer #1 fetched #{messages.size} from #{partition}"
18
+ end
19
+ end
20
+
21
+ # Create a second consumer group
22
+ group2 = Poseidon::ConsumerGroup.new "my-group", ["host1:9092", "host2:9092"], ["host1:2181", "host2:2181"], "my-topic"
23
+
24
+ # Now consuming all partitions of "my-topic" in parallel
25
+ thread2 = Thread.new do
26
+ group2.fetch_loop do |partition, messages|
27
+ puts "Consumer #2 fetched #{messages.size} from #{partition}"
28
+ end
29
+ end
30
+
31
+ # Join threads, loop forever
32
+ [thread1, thread2].each(&:join)
33
+
@@ -22,6 +22,7 @@
22
22
  # @api public
23
23
  class Poseidon::ConsumerGroup
24
24
  DEFAULT_CLAIM_TIMEOUT = 10
25
+ DEFAULT_LOOP_DELAY = 1
25
26
 
26
27
  # Poseidon::ConsumerGroup::Consumer is internally used by Poseidon::ConsumerGroup.
27
28
  # Don't invoke it directly.
@@ -62,7 +63,12 @@ class Poseidon::ConsumerGroup
62
63
  # @param [Array<String>] brokers A list of known brokers, e.g. ["localhost:9092"]
63
64
  # @param [Array<String>] zookeepers A list of known zookeepers, e.g. ["localhost:2181"]
64
65
  # @param [String] topic Topic to operate on
65
- # @param [Hash] options Partition consumer options, see Poseidon::PartitionConsumer#initialize
66
+ # @param [Hash] options Consumer options
67
+ # @option options [Integer] :max_bytes Maximum number of bytes to fetch. Default: 1048576 (1MB)
68
+ # @option options [Integer] :max_wait_ms How long to block until the server sends us data. Default: 100 (100ms)
69
+ # @option options [Integer] :min_bytes Smallest amount of data the server should send us. Default: 0 (Send us data as soon as it is ready)
70
+ # @option options [Integer] :claim_timeout Maximum number of seconds to wait for a partition claim. Default: 10
71
+ # @option options [Integer] :loop_delay Number of seconds to delay the next fetch (in #fetch_loop) if nothing was returned. Default: 1
66
72
  #
67
73
  # @api public
68
74
  def initialize(name, brokers, zookeepers, topic, options = {})
@@ -150,6 +156,8 @@ class Poseidon::ConsumerGroup
150
156
  # Sorted partitions by broker address (so partitions on the same broker are clustered together)
151
157
  # @return [Array<Poseidon::Protocol::PartitionMetadata>] sorted partitions
152
158
  def partitions
159
+ return [] unless topic_metadata
160
+
153
161
  topic_metadata.partitions.sort_by do |part|
154
162
  broker = metadata.brokers[part.leader]
155
163
  [broker.host, broker.port].join(":")
@@ -170,12 +178,20 @@ class Poseidon::ConsumerGroup
170
178
  #
171
179
  # @param [Hash] opts
172
180
  # @option opts [Boolean] :commit Automatically commit consumer offset (default: true)
181
+ # @return [Boolean] true if a consumer was checked out, false if none could be claimed
182
+ #
183
+ # @example
184
+ #
185
+ # ok = group.checkout do |consumer|
186
+ # puts "Checked out consumer for partition #{consumer.partition}"
187
+ # end
188
+ # ok # => true if the block was run, false otherwise
173
189
  #
174
190
  # @api public
175
191
  def checkout(opts = {})
176
192
  @mutex.synchronize do
177
193
  consumer = @consumers.shift
178
- break unless consumer
194
+ break false unless consumer
179
195
 
180
196
  @consumers.push(consumer)
181
197
  result = yield(consumer)
@@ -183,8 +199,8 @@ class Poseidon::ConsumerGroup
183
199
  unless opts[:commit] == false || result == false
184
200
  commit consumer.partition, consumer.offset
185
201
  end
202
+ true
186
203
  end
187
- nil
188
204
  end
189
205
 
190
206
  # Convenience method to fetch messages from the broker.
@@ -193,10 +209,18 @@ class Poseidon::ConsumerGroup
193
209
  # @yield [partition, messages] The processing block
194
210
  # @yieldparam [Integer] partition The source partition
195
211
  # @yieldparam [Array<Message>] messages The fetched messages
196
- # @yieldreturn [Boolean] return false to stop commit
212
+ # @yieldreturn [Boolean] return false to prevent auto-commit
197
213
  #
198
214
  # @param [Hash] opts
199
215
  # @option opts [Boolean] :commit Automatically commit consumed offset (default: true)
216
+ # @return [Boolean] true if messages were fetched, false if none could be claimed
217
+ #
218
+ # @example
219
+ #
220
+ # ok = group.fetch do |n, messages|
221
+ # puts "Fetched #{messages.size} messages for partition #{n}"
222
+ # end
223
+ # ok # => true if the block was run, false otherwise
200
224
  #
201
225
  # @api public
202
226
  def fetch(opts = {})
@@ -205,6 +229,93 @@ class Poseidon::ConsumerGroup
205
229
  end
206
230
  end
207
231
 
232
+ # Initializes an infinite fetch loop. This method blocks!
233
+ #
234
+ # Will wait for `loop_delay` seconds after each failed fetch. This may happen when there is
235
+ # no new data or when the consumer hasn't claimed any partitions.
236
+ #
237
+ # SPECIAL ATTENTION:
238
+ # When 'breaking out' of the loop, you must do it before processing the messages, as the
239
+ # the last offset will not be committed. Please see examples below.
240
+ #
241
+ # @yield [partition, messages] The processing block
242
+ # @yieldparam [Integer] partition The source partition, may be -1 if no partitions are claimed
243
+ # @yieldparam [Array<Message>] messages The fetched messages
244
+ # @yieldreturn [Boolean] return false to prevent auto-commit
245
+ #
246
+ # @param [Hash] opts
247
+ # @option opts [Boolean] :commit Automatically commit consumed offset (default: true)
248
+ # @option opts [Boolean] :loop_delay Delay override in seconds after unsuccessful fetch.
249
+ #
250
+ # @example
251
+ #
252
+ # group.fetch_loop do |n, messages|
253
+ # puts "Fetched #{messages.size} messages for partition #{n}"
254
+ # end
255
+ # puts "Done" # => this code is never reached
256
+ #
257
+ # @example Stopping the loop (wrong)
258
+ #
259
+ # counts = Hash.new(0)
260
+ # group.fetch_loop do |n, messages|
261
+ # counts[n] += messages.size
262
+ # puts "Status: #{counts.inspect}"
263
+ # break if counts[0] > 100
264
+ # end
265
+ # puts "Result: #{counts.inspect}"
266
+ # puts "Offset: #{group.offset(0)}"
267
+ #
268
+ # # Output:
269
+ # # Status: {0=>30}
270
+ # # Status: {0=>60}
271
+ # # Status: {0=>90}
272
+ # # Status: {0=>120}
273
+ # # Result: {0=>120}
274
+ # # Offset: 90 # => Last offset was not committed!
275
+ #
276
+ # @example Stopping the loop (correct)
277
+ #
278
+ # counts = Hash.new(0)
279
+ # group.fetch_loop do |n, messages|
280
+ # break if counts[0] > 100
281
+ # counts[n] += messages.size
282
+ # puts "Status: #{counts.inspect}"
283
+ # end
284
+ # puts "Result: #{counts.inspect}"
285
+ # puts "Offset: #{group.offset(0)}"
286
+ #
287
+ # # Output:
288
+ # # Status: {0=>30}
289
+ # # Status: {0=>60}
290
+ # # Status: {0=>90}
291
+ # # Status: {0=>120}
292
+ # # Result: {0=>120}
293
+ # # Offset: 120
294
+ #
295
+ # @api public
296
+ def fetch_loop(opts = {})
297
+ delay = opts[:loop_delay] || options[:loop_delay] || DEFAULT_LOOP_DELAY
298
+
299
+ loop do
300
+ mp = false
301
+ ok = fetch(opts) do |n, messages|
302
+ mp = !messages.empty?
303
+ yield n, messages
304
+ end
305
+
306
+ # Yield over an empty array if nothing claimed,
307
+ # to allow user to e.g. break out of the loop
308
+ unless ok
309
+ yield -1, []
310
+ end
311
+
312
+ # Sleep if either not claimes or nothing returned
313
+ unless ok && mp
314
+ sleep delay
315
+ end
316
+ end
317
+ end
318
+
208
319
  protected
209
320
 
210
321
  # Rebalance algorithm:
@@ -218,18 +329,19 @@ class Poseidon::ConsumerGroup
218
329
  def rebalance!
219
330
  @mutex.synchronize do
220
331
  reload
221
- cg = zk.children(registries[:consumer], watch: true).sort
222
- pt = partitions
223
- pos = cg.index(id)
224
- n = pt.size / cg.size
225
- n = 1 if n < 1
332
+ release_all!
226
333
 
227
- first = pos*n
228
- last = (pos+1)*n-1
334
+ cmg = zk.children(registries[:consumer], watch: true).sort
335
+ ptm = partitions
336
+ pos = cmg.index(id)
337
+ break unless ptm.size > pos
229
338
 
230
- release_all!
231
- (pt[first..last] || []).each do |part|
232
- consumer = claim!(part.id)
339
+ num = ptm.size / cmg.size
340
+ num = 1 if num < 1
341
+ rng = pos*num..(pos+1)*num-1
342
+
343
+ (ptm[rng] || []).each do |pm|
344
+ consumer = claim!(pm.id)
233
345
  @consumers.push(consumer)
234
346
  end
235
347
  end
@@ -5,7 +5,7 @@ Gem::Specification.new do |s|
5
5
  s.name = File.basename(__FILE__, '.gemspec')
6
6
  s.summary = "Poseidon cluster extensions"
7
7
  s.description = "Cluster extensions for Poseidon, a producer and consumer implementation for Kafka >= 0.8"
8
- s.version = "0.0.2"
8
+ s.version = "0.0.3"
9
9
 
10
10
  s.authors = ["Black Square Media"]
11
11
  s.email = "info@blacksquaremedia.com"
@@ -22,4 +22,5 @@ Gem::Specification.new do |s|
22
22
  s.add_development_dependency "bundler"
23
23
  s.add_development_dependency "rspec"
24
24
  s.add_development_dependency "yard"
25
+
25
26
  end
@@ -6,14 +6,31 @@ describe Poseidon::ConsumerGroup, integration: true do
6
6
  described_class.new "my-group", ["localhost:29092"], ["localhost:22181"], name, max_bytes: max_bytes
7
7
  end
8
8
 
9
- subject { new_group }
10
- after { zookeeper.rm_rf "/consumers/#{subject.name}" }
9
+ def stored_offsets
10
+ { 0 => subject.offset(0), 1 => subject.offset(1) }
11
+ end
11
12
 
13
+ subject { new_group }
12
14
  let(:consumed) { Hash.new(0) }
13
15
  let(:zookeeper) { ::ZK.new("localhost:22181") }
14
16
 
15
- def stored_offsets
16
- { 0 => subject.offset(0), 1 => subject.offset(1) }
17
+ before :all do
18
+ producer = Poseidon::Producer.new(["localhost:29092"], "my-producer")
19
+ payload = "data" * 10
20
+ messages = ("aa".."zz").map do |key|
21
+ Poseidon::MessageToSend.new(TOPIC_NAME, [key, payload].join(":"), key)
22
+ end
23
+
24
+ ok = false
25
+ 100.times do
26
+ break if (ok = producer.send_messages(messages))
27
+ sleep(0.1)
28
+ end
29
+ pending "Unable to start Kafka instance." unless ok
30
+ end
31
+
32
+ after do
33
+ zookeeper.rm_rf "/consumers/my-group"
17
34
  end
18
35
 
19
36
  describe "small batches" do
@@ -47,14 +64,16 @@ describe Poseidon::ConsumerGroup, integration: true do
47
64
  end
48
65
  end
49
66
 
50
- describe "fuzzing" do
67
+ describe "multi-thread fuzzing", slow: true do
51
68
 
52
69
  def in_thread(batch_size, target, qu)
53
70
  Thread.new do
54
- group = new_group(batch_size)
55
71
  sum = 0
56
- while sum < target && qu.size < 676
57
- group.fetch {|_, m| sum += m.size; m.size.times { qu << true } }
72
+ group = new_group(batch_size)
73
+ group.fetch_loop do |n, m|
74
+ break if sum > target || qu.size >= 676
75
+ sum += m.size
76
+ m.size.times { qu << true }
58
77
  end
59
78
  group.close
60
79
  sum
@@ -64,17 +83,21 @@ describe Poseidon::ConsumerGroup, integration: true do
64
83
  it "should consume from multiple sources" do
65
84
  q = Queue.new
66
85
  a = in_thread(4001, 200, q)
67
- b = in_thread(4002, 50, q)
86
+ b = in_thread(4002, 40, q)
68
87
  c = in_thread(4003, 120, q)
69
- d = in_thread(4004, 40, q)
88
+ d = in_thread(4004, 50, q)
70
89
  e = in_thread(4005, 400, q)
71
- vals = [a, b, c, d, e].map &:value
90
+ vals = [a, b, c, d, e].map(&:value)
72
91
  vals.inject(0, :+).should == 676
92
+
93
+ o1, _ = zookeeper.get "/consumers/my-group/offsets/#{TOPIC_NAME}/0"
94
+ o2, _ = zookeeper.get "/consumers/my-group/offsets/#{TOPIC_NAME}/1"
95
+ (o1.to_i + o2.to_i).should == 676
73
96
  end
74
97
 
75
98
  end
76
99
 
77
- describe "multi-process fuzzing", slow: true do
100
+ describe "multi-process fuzzing", slow: true, java: false do
78
101
  before do
79
102
  producer = Poseidon::Producer.new(["localhost:29092"], "my-producer")
80
103
  payload = "data" * 10
@@ -40,6 +40,7 @@ describe Poseidon::ConsumerGroup do
40
40
 
41
41
  subject { new_group }
42
42
  before do
43
+ Poseidon::ConsumerGroup.any_instance.stub(:sleep)
43
44
  Poseidon::BrokerPool.any_instance.stub(:fetch_metadata_from_broker).and_return(metadata)
44
45
  Poseidon::Connection.any_instance.stub(:fetch).with{|_, _, req| req[0].partition_fetches[0].partition == 0 }.and_return(fetch_response(10))
45
46
  Poseidon::Connection.any_instance.stub(:fetch).with{|_, _, req| req[0].partition_fetches[0].partition == 1 }.and_return(fetch_response(5))
@@ -82,6 +83,14 @@ describe Poseidon::ConsumerGroup do
82
83
  subject.partitions.map(&:id).should == [1, 0]
83
84
  end
84
85
 
86
+ it "should not fail if topic doesn't exist" do
87
+ no_topics = Poseidon::Protocol::MetadataResponse.new nil, brokers.dup, []
88
+ Poseidon::BrokerPool.any_instance.stub(:fetch_metadata_from_broker).and_return(no_topics)
89
+
90
+ subject.partitions.should == []
91
+ subject.claimed.should == []
92
+ end
93
+
85
94
  it "should return the offset for each partition" do
86
95
  subject.offset(0).should == 0
87
96
  subject.offset(1).should == 0
@@ -99,8 +108,8 @@ describe Poseidon::ConsumerGroup do
99
108
  end
100
109
 
101
110
  it "should checkout individual partition consumers (atomically)" do
102
- subject.checkout {|c| c.partition.should == 1 }
103
- subject.checkout {|c| c.partition.should == 0 }
111
+ subject.checkout {|c| c.partition.should == 1 }.should be_true
112
+ subject.checkout {|c| c.partition.should == 0 }.should be_true
104
113
 
105
114
  n = 0
106
115
  a = Thread.new do
@@ -135,10 +144,6 @@ describe Poseidon::ConsumerGroup do
135
144
  b.claimed.should == [0]
136
145
 
137
146
  c = new_group
138
- subject.claimed.should == [1]
139
- b.claimed.should == [0]
140
- c.claimed.should == []
141
-
142
147
  b.close
143
148
  wait_for { b.claimed.size < 0 }
144
149
  wait_for { c.claimed.size > 0 }
@@ -156,15 +161,17 @@ describe Poseidon::ConsumerGroup do
156
161
  subject.fetch do |n, msg|
157
162
  n.should == 1
158
163
  msg.size.should == 5
159
- end
164
+ end.should be_true
165
+
160
166
  subject.fetch do |n, msg|
161
167
  n.should == 0
162
168
  msg.size.should == 10
163
- end
169
+ end.should be_true
170
+
164
171
  subject.fetch do |n, msg|
165
172
  n.should == 1
166
173
  msg.size.should == 5
167
- end
174
+ end.should be_true
168
175
  end
169
176
 
170
177
  it "should auto-commit fetched offset" do
@@ -185,6 +192,56 @@ describe Poseidon::ConsumerGroup do
185
192
  }.should_not change { subject.offset(1) }
186
193
  end
187
194
 
195
+ it "should return false when trying to fetch messages without a claim" do
196
+ no_topics = Poseidon::Protocol::MetadataResponse.new nil, brokers.dup, []
197
+ Poseidon::BrokerPool.any_instance.stub fetch_metadata_from_broker: no_topics
198
+
199
+ subject.claimed.should == []
200
+ subject.fetch {|*| }.should be_false
201
+ end
202
+
203
+ it "should return true even when no messages were fetched" do
204
+ Poseidon::Connection.any_instance.stub fetch: fetch_response(0)
205
+ subject.fetch {|*| }.should be_true
206
+ end
207
+
188
208
  end
189
209
 
210
+ describe "fetch_loop" do
211
+
212
+ it "should fetch indefinitely" do
213
+ total, cycles = 0, 0
214
+ subject.fetch_loop do |_, m|
215
+ total += m.size
216
+ break if (cycles+=1) > 2
217
+ end
218
+ total.should == 20
219
+ cycles.should == 3
220
+ end
221
+
222
+ it "should delay fetch was unsuccessful" do
223
+ subject.stub fetch: false
224
+
225
+ cycles = 0
226
+ subject.should_receive(:sleep).with(1)
227
+ subject.fetch_loop do |n, m|
228
+ n.should == -1
229
+ m.should == []
230
+ break if (cycles+=1) > 1
231
+ end
232
+ end
233
+
234
+ it "should delay fetch didn't yield any results" do
235
+ subject.stub(:fetch).and_yield(3, []).and_return(true)
236
+
237
+ cycles = 0
238
+ subject.should_receive(:sleep).with(1)
239
+ subject.fetch_loop do |n, m|
240
+ n.should == 3
241
+ m.should == []
242
+ break if (cycles+=1) > 1
243
+ end
244
+ end
245
+
246
+ end
190
247
  end
data/spec/spec_helper.rb CHANGED
@@ -2,6 +2,8 @@ require 'poseidon_cluster'
2
2
  require 'rspec'
3
3
  require 'fileutils'
4
4
  require 'pathname'
5
+ require 'coveralls'
6
+ Coveralls.wear!
5
7
 
6
8
  TOPIC_NAME = "my-topic"
7
9
  KAFKA_LOCAL = File.expand_path("../kafka_2.8.0-0.8.0", __FILE__)
@@ -21,6 +23,7 @@ end
21
23
  RSpec.configure do |c|
22
24
  c.include Poseidon::SpecHelper
23
25
  c.filter_run_excluding slow: true unless ENV["SLOW"] == "1"
26
+ c.filter_run_excluding java: false if RUBY_PLATFORM == "java"
24
27
 
25
28
  c.before :suite do
26
29
  kafka_bin = KAFKA_ROOT.join("bin", "kafka-server-start.sh")
@@ -48,22 +51,8 @@ RSpec.configure do |c|
48
51
  end
49
52
 
50
53
  # Start Zookeeper & Kafka
51
- $ZOOKP_PID = spawn zookp_bin.to_s, zookp_cfg.to_s, out: '/dev/null' # , err: '/dev/null'
52
- $KAFKA_PID = spawn kafka_bin.to_s, kafka_cfg.to_s, out: '/dev/null' #, err: '/dev/null'
53
-
54
- # Produce some fixtures
55
- producer = Poseidon::Producer.new(["localhost:29092"], "my-producer")
56
- payload = "data" * 10
57
- messages = ("aa".."zz").map do |key|
58
- Poseidon::MessageToSend.new(TOPIC_NAME, [key, payload].join(":"), key)
59
- end
60
-
61
- ok = false
62
- 100.times do
63
- break if (ok = producer.send_messages(messages))
64
- sleep(0.1)
65
- end
66
- raise "Unable to start Kafka instance." unless ok
54
+ $ZOOKP_PID = spawn zookp_bin.to_s, zookp_cfg.to_s, out: '/dev/null'
55
+ $KAFKA_PID = spawn kafka_bin.to_s, kafka_cfg.to_s, out: '/dev/null'
67
56
  end
68
57
 
69
58
  c.after :suite do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: poseidon_cluster
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Black Square Media
@@ -101,12 +101,14 @@ executables: []
101
101
  extensions: []
102
102
  extra_rdoc_files: []
103
103
  files:
104
+ - ".coveralls.yml"
104
105
  - ".gitignore"
105
106
  - ".travis.yml"
106
107
  - Gemfile
107
108
  - Gemfile.lock
108
109
  - README.md
109
110
  - Rakefile
111
+ - examples/consumer_group.rb
110
112
  - lib/poseidon/cluster.rb
111
113
  - lib/poseidon/consumer_group.rb
112
114
  - lib/poseidon_cluster.rb