poseidon_cluster 0.0.2 → 0.0.3

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