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 +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 [](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
|