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 +4 -4
- data/.coveralls.yml +1 -0
- data/.gitignore +2 -0
- data/.travis.yml +1 -2
- data/Gemfile +6 -1
- data/Gemfile.lock +30 -3
- data/README.md +26 -7
- data/examples/consumer_group.rb +33 -0
- data/lib/poseidon/consumer_group.rb +126 -14
- data/poseidon_cluster.gemspec +2 -1
- data/spec/integration/poseidon/consumer_group_spec.rb +35 -12
- data/spec/lib/poseidon/consumer_group_spec.rb +66 -9
- data/spec/spec_helper.rb +5 -16
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3a9e9d18567496651c7c390954320166410d315f
|
4
|
+
data.tar.gz: a17c4b5192bcd360bd7fe2eda5da3b6d9afcf0a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c241fca61a6a6c6b59b32d023250665c939fb3f4c34cdddd4bb9cfbb125a4bab8bc87f405e0e496e2a14e6a8d9d6c6ef9278d85d369ddcde30afbad844fd0536
|
7
|
+
data.tar.gz: 170438cf674768bfc7f4002332f16f1d89d9a0669278e6f801d1dc10e6e0bfd1a9093fcd382bf3ba9adb88491bf11ba348d36e84e803e5a3bb0c2f80079cee2e
|
data/.coveralls.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
service_name: travis-ci
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -7,18 +7,29 @@ GIT
|
|
7
7
|
PATH
|
8
8
|
remote: .
|
9
9
|
specs:
|
10
|
-
poseidon_cluster (0.0.
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
36
|
+
```ruby
|
31
37
|
consumer.offset(0) # => 320 - current offset from partition 0
|
38
|
+
```
|
32
39
|
|
33
|
-
|
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
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
228
|
-
|
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
|
-
|
231
|
-
|
232
|
-
|
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
|
data/poseidon_cluster.gemspec
CHANGED
@@ -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.
|
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
|
-
|
10
|
-
|
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
|
-
|
16
|
-
|
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
|
-
|
57
|
-
|
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,
|
86
|
+
b = in_thread(4002, 40, q)
|
68
87
|
c = in_thread(4003, 120, q)
|
69
|
-
d = in_thread(4004,
|
88
|
+
d = in_thread(4004, 50, q)
|
70
89
|
e = in_thread(4005, 400, q)
|
71
|
-
vals = [a, b, c, d, e].map
|
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'
|
52
|
-
$KAFKA_PID = spawn kafka_bin.to_s, kafka_cfg.to_s, out: '/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.
|
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
|