poseidon_cluster 0.1.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +4 -6
- data/Gemfile +0 -1
- data/Gemfile.lock +59 -42
- data/README.md +1 -1
- data/Rakefile +5 -0
- data/lib/poseidon/cluster.rb +6 -0
- data/lib/poseidon/consumer_group.rb +49 -33
- data/poseidon_cluster.gemspec +5 -4
- data/scenario/.gitignore +1 -0
- data/scenario/consumer.rb +17 -0
- data/scenario/producer.rb +23 -0
- data/scenario/run.rb +35 -0
- data/scenario/scenario.rb +134 -0
- data/spec/lib/poseidon/cluster_spec.rb +4 -0
- data/spec/lib/poseidon/consumer_group_spec.rb +81 -87
- data/spec/spec_helper.rb +5 -55
- metadata +31 -7
- data/spec/integration/poseidon/consumer_group_spec.rb +0 -168
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 70266dd2ea6c22a6e57d802e8a30ad722228b2ad
|
4
|
+
data.tar.gz: 75eaf7ea9e37bb9c74ef7c5291b3fb28aa074096
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fbb88da9cdb10569defc933e51bcde385b17813408f57d443562b531a851f046b1868f34f59e9ccb682ba0c9223c58e9fbca998cbf166ad9832bfd69723cc127
|
7
|
+
data.tar.gz: 1f89ca21c4d5aac75c757069c26a7a0e8d1cec32993ccd9dd7402e35096b7c65f1450d7739d3ad94fd5e1792405eca7bc7e4ead246ef954dfec3f673e2afa678
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,59 +1,76 @@
|
|
1
|
-
GIT
|
2
|
-
remote: https://github.com/dim/poseidon.git
|
3
|
-
revision: f853e8f07a6f2ff4a520d3f7e1dac030453a18ed
|
4
|
-
specs:
|
5
|
-
poseidon (0.0.4)
|
6
|
-
|
7
1
|
PATH
|
8
2
|
remote: .
|
9
3
|
specs:
|
10
|
-
poseidon_cluster (0.
|
11
|
-
poseidon
|
4
|
+
poseidon_cluster (0.3.0)
|
5
|
+
poseidon (>= 0.0.5, < 0.1.0)
|
12
6
|
zk
|
13
7
|
|
14
8
|
GEM
|
15
9
|
remote: https://rubygems.org/
|
16
10
|
specs:
|
17
|
-
coveralls (0.
|
18
|
-
|
19
|
-
rest-client
|
20
|
-
simplecov (
|
21
|
-
term-ansicolor
|
22
|
-
thor
|
11
|
+
coveralls (0.8.1)
|
12
|
+
json (~> 1.8)
|
13
|
+
rest-client (>= 1.6.8, < 2)
|
14
|
+
simplecov (~> 0.10.0)
|
15
|
+
term-ansicolor (~> 1.3)
|
16
|
+
thor (~> 0.19.1)
|
23
17
|
diff-lcs (1.2.5)
|
24
|
-
docile (1.1.
|
18
|
+
docile (1.1.5)
|
19
|
+
domain_name (0.5.24)
|
20
|
+
unf (>= 0.0.5, < 1.0.0)
|
21
|
+
http-cookie (1.0.2)
|
22
|
+
domain_name (~> 0.5)
|
23
|
+
json (1.8.3)
|
24
|
+
json (1.8.3-java)
|
25
25
|
little-plugger (1.1.3)
|
26
|
-
logging (1.
|
26
|
+
logging (1.8.2)
|
27
27
|
little-plugger (>= 1.1.3)
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
rspec
|
39
|
-
|
40
|
-
|
41
|
-
|
28
|
+
multi_json (>= 1.8.4)
|
29
|
+
mime-types (2.6.1)
|
30
|
+
multi_json (1.11.0)
|
31
|
+
netrc (0.10.3)
|
32
|
+
poseidon (0.0.5)
|
33
|
+
rake (10.4.2)
|
34
|
+
rest-client (1.8.0)
|
35
|
+
http-cookie (>= 1.0.2, < 2.0)
|
36
|
+
mime-types (>= 1.16, < 3.0)
|
37
|
+
netrc (~> 0.7)
|
38
|
+
rspec (3.2.0)
|
39
|
+
rspec-core (~> 3.2.0)
|
40
|
+
rspec-expectations (~> 3.2.0)
|
41
|
+
rspec-mocks (~> 3.2.0)
|
42
|
+
rspec-core (3.2.3)
|
43
|
+
rspec-support (~> 3.2.0)
|
44
|
+
rspec-expectations (3.2.1)
|
45
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
46
|
+
rspec-support (~> 3.2.0)
|
47
|
+
rspec-its (1.2.0)
|
48
|
+
rspec-core (>= 3.0.0)
|
49
|
+
rspec-expectations (>= 3.0.0)
|
50
|
+
rspec-mocks (3.2.1)
|
51
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
52
|
+
rspec-support (~> 3.2.0)
|
53
|
+
rspec-support (3.2.2)
|
54
|
+
simplecov (0.10.0)
|
42
55
|
docile (~> 1.1.0)
|
43
|
-
|
44
|
-
simplecov-html (~> 0.
|
45
|
-
simplecov-html (0.
|
56
|
+
json (~> 1.8)
|
57
|
+
simplecov-html (~> 0.10.0)
|
58
|
+
simplecov-html (0.10.0)
|
46
59
|
slyphon-log4j (1.2.15)
|
47
60
|
slyphon-zookeeper_jar (3.3.5-java)
|
48
|
-
term-ansicolor (1.
|
49
|
-
tins (~> 0
|
50
|
-
thor (0.
|
51
|
-
tins (
|
52
|
-
|
53
|
-
|
54
|
-
|
61
|
+
term-ansicolor (1.3.0)
|
62
|
+
tins (~> 1.0)
|
63
|
+
thor (0.19.1)
|
64
|
+
tins (1.5.2)
|
65
|
+
unf (0.1.4)
|
66
|
+
unf_ext
|
67
|
+
unf (0.1.4-java)
|
68
|
+
unf_ext (0.0.7.1)
|
69
|
+
yard (0.8.7.6)
|
70
|
+
zk (1.9.5)
|
71
|
+
logging (~> 1.8.2)
|
55
72
|
zookeeper (~> 1.4.0)
|
56
|
-
zookeeper (1.4.
|
73
|
+
zookeeper (1.4.10)
|
57
74
|
|
58
75
|
PLATFORMS
|
59
76
|
java
|
@@ -62,10 +79,10 @@ PLATFORMS
|
|
62
79
|
DEPENDENCIES
|
63
80
|
bundler
|
64
81
|
coveralls
|
65
|
-
poseidon!
|
66
82
|
poseidon_cluster!
|
67
83
|
rake
|
68
84
|
rspec
|
85
|
+
rspec-its
|
69
86
|
slyphon-log4j (= 1.2.15)
|
70
87
|
slyphon-zookeeper_jar (= 3.3.5)
|
71
88
|
yard
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Poseidon Cluster [![Build Status](https://travis-ci.org/bsm/poseidon_cluster.png?branch=master)](https://travis-ci.org/bsm/poseidon_cluster) [![Coverage Status](https://coveralls.io/repos/bsm/poseidon_cluster/badge.png?branch=master)](https://coveralls.io/r/bsm/poseidon_cluster?branch=master)
|
2
2
|
|
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.
|
3
|
+
Poseidon Cluster is a cluster extension of 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
|
|
data/Rakefile
CHANGED
data/lib/poseidon/cluster.rb
CHANGED
@@ -15,6 +15,12 @@ module Poseidon::Cluster
|
|
15
15
|
@@sem.synchronize { @@inc += 1; @@inc = 1 if @@inc > MAX_INT32; @@inc }
|
16
16
|
end
|
17
17
|
|
18
|
+
# @return [String] an globally unique identifier
|
19
|
+
# @api private
|
20
|
+
def self.guid
|
21
|
+
[::Socket.gethostname, ::Process.pid, ::Time.now.to_i, inc!].join("-")
|
22
|
+
end
|
23
|
+
|
18
24
|
end
|
19
25
|
|
20
26
|
%w|consumer_group|.each do |name|
|
@@ -21,7 +21,7 @@
|
|
21
21
|
#
|
22
22
|
# @api public
|
23
23
|
class Poseidon::ConsumerGroup
|
24
|
-
DEFAULT_CLAIM_TIMEOUT =
|
24
|
+
DEFAULT_CLAIM_TIMEOUT = 30
|
25
25
|
DEFAULT_LOOP_DELAY = 1
|
26
26
|
|
27
27
|
# Poseidon::ConsumerGroup::Consumer is internally used by Poseidon::ConsumerGroup.
|
@@ -37,6 +37,8 @@ class Poseidon::ConsumerGroup
|
|
37
37
|
def initialize(group, partition, options = {})
|
38
38
|
broker = group.leader(partition)
|
39
39
|
offset = group.offset(partition)
|
40
|
+
offset = (options[:trail] ? :latest_offset : :earliest_offset) if offset == 0
|
41
|
+
options.delete(:trail)
|
40
42
|
super group.id, broker.host, broker.port, group.topic, partition, offset, options
|
41
43
|
end
|
42
44
|
|
@@ -87,16 +89,21 @@ class Poseidon::ConsumerGroup
|
|
87
89
|
# @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)
|
88
90
|
# @option options [Integer] :claim_timeout Maximum number of seconds to wait for a partition claim. Default: 10
|
89
91
|
# @option options [Integer] :loop_delay Number of seconds to delay the next fetch (in #fetch_loop) if nothing was returned. Default: 1
|
92
|
+
# @option options [Integer] :socket_timeout_ms broker connection wait timeout in ms. Default: 10000
|
90
93
|
# @option options [Boolean] :register Automatically register instance and start consuming. Default: true
|
94
|
+
# @option options [Boolean] :trail Starts reading messages from the latest partitions offsets and skips 'old' messages . Default: false
|
91
95
|
#
|
92
96
|
# @api public
|
93
97
|
def initialize(name, brokers, zookeepers, topic, options = {})
|
94
98
|
@name = name
|
95
99
|
@topic = topic
|
96
100
|
@zk = ::ZK.new(zookeepers.join(","))
|
101
|
+
# Poseidon::BrokerPool doesn't provide default value for this option
|
102
|
+
# Configuring default value like this isn't beautiful, though.. by kssminus
|
103
|
+
options[:socket_timeout_ms] ||= 10000
|
97
104
|
@options = options
|
98
105
|
@consumers = []
|
99
|
-
@pool = ::Poseidon::BrokerPool.new(id, brokers)
|
106
|
+
@pool = ::Poseidon::BrokerPool.new(id, brokers, options[:socket_timeout_ms])
|
100
107
|
@mutex = Mutex.new
|
101
108
|
@registered = false
|
102
109
|
|
@@ -105,7 +112,7 @@ class Poseidon::ConsumerGroup
|
|
105
112
|
|
106
113
|
# @return [String] a globally unique identifier
|
107
114
|
def id
|
108
|
-
@id ||= [name,
|
115
|
+
@id ||= [name, Poseidon::Cluster.guid].join("-")
|
109
116
|
end
|
110
117
|
|
111
118
|
# @return [Hash<Symbol,String>] registry paths
|
@@ -158,7 +165,7 @@ class Poseidon::ConsumerGroup
|
|
158
165
|
# Closes the consumer group gracefully, only really useful in tests
|
159
166
|
# @api private
|
160
167
|
def close
|
161
|
-
release_all!
|
168
|
+
@mutex.synchronize { release_all! }
|
162
169
|
zk.close
|
163
170
|
end
|
164
171
|
|
@@ -189,7 +196,7 @@ class Poseidon::ConsumerGroup
|
|
189
196
|
def partitions
|
190
197
|
return [] unless topic_metadata
|
191
198
|
|
192
|
-
topic_metadata.
|
199
|
+
topic_metadata.available_partitions.sort_by do |part|
|
193
200
|
broker = metadata.brokers[part.leader]
|
194
201
|
[broker.host, broker.port].join(":")
|
195
202
|
end
|
@@ -220,15 +227,16 @@ class Poseidon::ConsumerGroup
|
|
220
227
|
#
|
221
228
|
# @api public
|
222
229
|
def checkout(opts = {})
|
223
|
-
|
230
|
+
consumer = nil
|
231
|
+
commit = @mutex.synchronize do
|
232
|
+
consumer = @consumers.shift
|
233
|
+
return false unless consumer
|
224
234
|
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
@consumers.push(consumer)
|
229
|
-
result = yield(consumer)
|
235
|
+
@consumers.push consumer
|
236
|
+
yield consumer
|
237
|
+
end
|
230
238
|
|
231
|
-
unless opts[:commit] == false ||
|
239
|
+
unless opts[:commit] == false || commit == false
|
232
240
|
commit consumer.partition, consumer.offset
|
233
241
|
end
|
234
242
|
true
|
@@ -358,7 +366,29 @@ class Poseidon::ConsumerGroup
|
|
358
366
|
# * let POS be our index position in CG and let N = size(PT)/size(CG)
|
359
367
|
# * assign partitions from POS*N to (POS+1)*N-1
|
360
368
|
def rebalance!
|
361
|
-
|
369
|
+
return if @pending
|
370
|
+
|
371
|
+
@pending = true
|
372
|
+
@mutex.synchronize do
|
373
|
+
@pending = nil
|
374
|
+
|
375
|
+
release_all!
|
376
|
+
reload
|
377
|
+
|
378
|
+
ids = zk.children(registries[:consumer], watch: true)
|
379
|
+
pms = partitions
|
380
|
+
rng = self.class.pick(pms.size, ids, id)
|
381
|
+
|
382
|
+
pms[rng].each do |pm|
|
383
|
+
if @pending
|
384
|
+
release_all!
|
385
|
+
break
|
386
|
+
end
|
387
|
+
|
388
|
+
consumer = claim!(pm.id)
|
389
|
+
@consumers.push(consumer) if consumer
|
390
|
+
end if rng
|
391
|
+
end
|
362
392
|
end
|
363
393
|
|
364
394
|
# Release all consumer claims
|
@@ -367,12 +397,17 @@ class Poseidon::ConsumerGroup
|
|
367
397
|
@consumers.clear
|
368
398
|
end
|
369
399
|
|
400
|
+
private
|
401
|
+
|
370
402
|
# Claim the ownership of the partition for this consumer
|
371
403
|
# @raise [Timeout::Error]
|
372
404
|
def claim!(partition)
|
373
405
|
path = claim_path(partition)
|
374
406
|
Timeout.timeout options[:claim_timout] || DEFAULT_CLAIM_TIMEOUT do
|
375
|
-
|
407
|
+
while zk.create(path, id, ephemeral: true, ignore: :node_exists).nil?
|
408
|
+
return if @pending
|
409
|
+
sleep(0.1)
|
410
|
+
end
|
376
411
|
end
|
377
412
|
Consumer.new self, partition, options.dup
|
378
413
|
end
|
@@ -382,8 +417,6 @@ class Poseidon::ConsumerGroup
|
|
382
417
|
zk.delete claim_path(partition), ignore: :no_node
|
383
418
|
end
|
384
419
|
|
385
|
-
private
|
386
|
-
|
387
420
|
# @return [String] zookeeper ownership claim path
|
388
421
|
def claim_path(partition)
|
389
422
|
"#{registries[:owner]}/#{partition}"
|
@@ -399,21 +432,4 @@ class Poseidon::ConsumerGroup
|
|
399
432
|
"#{registries[:consumer]}/#{id}"
|
400
433
|
end
|
401
434
|
|
402
|
-
def perform_rebalance!
|
403
|
-
@rebalancing = true
|
404
|
-
reload
|
405
|
-
release_all!
|
406
|
-
|
407
|
-
ids = zk.children(registries[:consumer], watch: true)
|
408
|
-
pms = partitions
|
409
|
-
rng = self.class.pick(pms.size, ids, id)
|
410
|
-
|
411
|
-
pms[rng].each do |pm|
|
412
|
-
consumer = claim!(pm.id)
|
413
|
-
@consumers.push(consumer)
|
414
|
-
end if rng
|
415
|
-
ensure
|
416
|
-
@rebalancing = nil
|
417
|
-
end
|
418
|
-
|
419
435
|
end
|
data/poseidon_cluster.gemspec
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
|
-
s.required_ruby_version = '>=
|
2
|
+
s.required_ruby_version = '>= 2.0.0'
|
3
3
|
s.required_rubygems_version = ">= 1.8.0"
|
4
4
|
|
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.
|
8
|
+
s.version = "0.3.0"
|
9
9
|
|
10
10
|
s.authors = ["Black Square Media"]
|
11
11
|
s.email = "info@blacksquaremedia.com"
|
@@ -13,14 +13,15 @@ Gem::Specification.new do |s|
|
|
13
13
|
|
14
14
|
s.require_path = 'lib'
|
15
15
|
s.files = `git ls-files`.split("\n")
|
16
|
-
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features,scenario}/*`.split("\n")
|
17
17
|
|
18
|
-
s.add_dependency "poseidon"
|
18
|
+
s.add_dependency "poseidon", ">= 0.0.5", "<0.1.0"
|
19
19
|
s.add_dependency "zk"
|
20
20
|
|
21
21
|
s.add_development_dependency "rake"
|
22
22
|
s.add_development_dependency "bundler"
|
23
23
|
s.add_development_dependency "rspec"
|
24
|
+
s.add_development_dependency "rspec-its"
|
24
25
|
s.add_development_dependency "yard"
|
25
26
|
s.add_development_dependency "coveralls"
|
26
27
|
|
data/scenario/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
output.txt
|
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'poseidon_cluster'
|
4
|
+
|
5
|
+
name = ARGV[0].to_s
|
6
|
+
output = File.open(ARGV[1], "a")
|
7
|
+
output.sync = true
|
8
|
+
|
9
|
+
total = 0
|
10
|
+
consumer = Poseidon::ConsumerGroup.new "my-group", ["localhost:29092"], ["localhost:22181"], "my-topic", max_bytes: 256*1024
|
11
|
+
consumer.fetch_loop do |n, messages|
|
12
|
+
break if name[0] > 'Q' && total > 0
|
13
|
+
messages.each do |m|
|
14
|
+
output.write "#{name},#{n},#{m.value}\n"
|
15
|
+
end
|
16
|
+
total += messages.size
|
17
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'poseidon'
|
4
|
+
|
5
|
+
limit, offset = ARGV[0].to_i, ARGV[1].to_i
|
6
|
+
producer = Poseidon::Producer.new ["localhost:29092"], "poseidon-producer"
|
7
|
+
|
8
|
+
while limit > 0 do
|
9
|
+
batch = limit > 10000 ? 10000 : limit
|
10
|
+
limit -= batch
|
11
|
+
|
12
|
+
messages = (0...batch).map do
|
13
|
+
num = offset.to_s.rjust(8, "0")
|
14
|
+
offset += 1
|
15
|
+
Poseidon::MessageToSend.new "my-topic", num, Time.now.to_s+num
|
16
|
+
end
|
17
|
+
|
18
|
+
10.times do
|
19
|
+
ok = producer.send_messages messages
|
20
|
+
break if ok
|
21
|
+
sleep(1)
|
22
|
+
end
|
23
|
+
end
|
data/scenario/run.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'timeout'
|
5
|
+
require File.expand_path("../scenario", __FILE__)
|
6
|
+
|
7
|
+
# Start Zookeeper & Kafka
|
8
|
+
Scenario.run do
|
9
|
+
5.times do
|
10
|
+
produce 1000
|
11
|
+
end
|
12
|
+
consume "A"
|
13
|
+
consume "B"
|
14
|
+
consume "C"
|
15
|
+
checkpoint!
|
16
|
+
|
17
|
+
15.times { produce 1000 }
|
18
|
+
consume "D"
|
19
|
+
10.times { produce 1000 }
|
20
|
+
consume "X"
|
21
|
+
10.times { produce 1000 }
|
22
|
+
checkpoint!
|
23
|
+
|
24
|
+
20.times { produce 1000 }
|
25
|
+
consume "E"
|
26
|
+
consume "F"
|
27
|
+
15.times { produce 1000 }
|
28
|
+
consume "Y"
|
29
|
+
50.times { produce 100 }
|
30
|
+
20.times { produce 1000 }
|
31
|
+
|
32
|
+
checkpoint!
|
33
|
+
end
|
34
|
+
|
35
|
+
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
module Scenario
|
5
|
+
extend self
|
6
|
+
|
7
|
+
ROOT = Pathname.new(File.expand_path("../", __FILE__))
|
8
|
+
VERSION = "0.8.1.1"
|
9
|
+
SERVER = ROOT.join "kafka_2.10-#{VERSION}"
|
10
|
+
|
11
|
+
TOPIC_NAME = "my-topic"
|
12
|
+
KAFKA_BIN = SERVER.join("bin", "kafka-server-start.sh")
|
13
|
+
KAFKA_CFG = SERVER.join("config", "server-poseidon.properties")
|
14
|
+
KAFKA_TMP = "/tmp/kafka-logs-poseidon"
|
15
|
+
ZOOKP_BIN = SERVER.join("bin", "zookeeper-server-start.sh")
|
16
|
+
ZOOKP_CFG = SERVER.join("config", "zookeeper-poseidon.properties")
|
17
|
+
ZOOKP_TMP = "/tmp/zookeeper-poseidon"
|
18
|
+
LOG4J_CFG = SERVER.join("config", "log4j.properties")
|
19
|
+
OUTPUT = Scenario::ROOT.join("output.txt")
|
20
|
+
|
21
|
+
@@pids = {}
|
22
|
+
@@total = 0
|
23
|
+
|
24
|
+
def run(&block)
|
25
|
+
setup
|
26
|
+
instance_eval(&block)
|
27
|
+
rescue => e
|
28
|
+
abort [e, *e.backtrace[0,20]].join("\n")
|
29
|
+
ensure
|
30
|
+
teardown
|
31
|
+
end
|
32
|
+
|
33
|
+
def setup
|
34
|
+
FileUtils.rm_rf OUTPUT.to_s
|
35
|
+
configure
|
36
|
+
|
37
|
+
# Ensure all required files are present
|
38
|
+
[KAFKA_BIN, ZOOKP_BIN, KAFKA_CFG, ZOOKP_CFG].each do |path|
|
39
|
+
abort "Unable to locate #{path}. File does not exist!" unless path.file?
|
40
|
+
end
|
41
|
+
|
42
|
+
Signal.trap("INT") { teardown }
|
43
|
+
|
44
|
+
spawn KAFKA_BIN, KAFKA_CFG
|
45
|
+
spawn ZOOKP_BIN, ZOOKP_CFG
|
46
|
+
sleep(2)
|
47
|
+
end
|
48
|
+
|
49
|
+
def teardown
|
50
|
+
@@pids.each do |_, pid|
|
51
|
+
Process.kill :TERM, pid
|
52
|
+
end
|
53
|
+
sleep(1)
|
54
|
+
FileUtils.rm_rf KAFKA_TMP.to_s
|
55
|
+
FileUtils.rm_rf ZOOKP_TMP.to_s
|
56
|
+
|
57
|
+
fail! unless numlines == @@total
|
58
|
+
end
|
59
|
+
|
60
|
+
def configure
|
61
|
+
download
|
62
|
+
|
63
|
+
KAFKA_CFG.open("w") do |f|
|
64
|
+
f.write SERVER.join("config", "server.properties").read.
|
65
|
+
sub("=9092", "=29092").
|
66
|
+
sub(":2181", ":22181").
|
67
|
+
sub("num.partitions=2", "num.partitions=12").
|
68
|
+
sub("log.flush.interval.ms=1000", "log.flush.interval.ms=10").
|
69
|
+
sub("/tmp/kafka-logs", KAFKA_TMP)
|
70
|
+
end
|
71
|
+
ZOOKP_CFG.open("w") do |f|
|
72
|
+
f.write SERVER.join("config", "zookeeper.properties").read.
|
73
|
+
sub("/tmp/zookeeper", ZOOKP_TMP).
|
74
|
+
sub("=2181", "=22181")
|
75
|
+
end
|
76
|
+
content = LOG4J_CFG.read
|
77
|
+
LOG4J_CFG.open("w") do |f|
|
78
|
+
f.write content.gsub("INFO", "FATAL")
|
79
|
+
end if content.include?("INFO")
|
80
|
+
end
|
81
|
+
|
82
|
+
def download
|
83
|
+
return if SERVER.directory?
|
84
|
+
sh "cd #{ROOT} && curl http://www.mirrorservice.org/sites/ftp.apache.org/kafka/#{VERSION}/kafka_2.10-#{VERSION}.tgz | tar xz"
|
85
|
+
end
|
86
|
+
|
87
|
+
def checkpoint!(timeout = 10)
|
88
|
+
puts "--> Verifying #{@@total}"
|
89
|
+
timeout.times do
|
90
|
+
if numlines > @@total
|
91
|
+
break
|
92
|
+
elsif numlines < @@total
|
93
|
+
sleep(1)
|
94
|
+
else
|
95
|
+
return
|
96
|
+
end
|
97
|
+
end
|
98
|
+
fail!
|
99
|
+
end
|
100
|
+
|
101
|
+
def consume(name)
|
102
|
+
puts "--> Launching consumer #{name}"
|
103
|
+
spawn ROOT.join("consumer.rb"), name, OUTPUT
|
104
|
+
end
|
105
|
+
|
106
|
+
def produce(count)
|
107
|
+
puts "--> Producing messages #{@@total}-#{@@total+count-1}"
|
108
|
+
sh ROOT.join("producer.rb"), count, @@total
|
109
|
+
@@total += count
|
110
|
+
end
|
111
|
+
|
112
|
+
def numlines
|
113
|
+
`wc -l #{OUTPUT} 2> /dev/null`.to_i
|
114
|
+
end
|
115
|
+
|
116
|
+
def abort(message)
|
117
|
+
Kernel.abort "ERROR: #{message}"
|
118
|
+
end
|
119
|
+
|
120
|
+
def fail!
|
121
|
+
Kernel.abort "FAILED: expected #{@@total} but was #{numlines}"
|
122
|
+
end
|
123
|
+
|
124
|
+
def sh(*bits)
|
125
|
+
cmd = bits.join(" ")
|
126
|
+
system(cmd) || abort(cmd)
|
127
|
+
end
|
128
|
+
|
129
|
+
def spawn(*args)
|
130
|
+
cmd = args.join(" ")
|
131
|
+
@@pids[cmd] = Process.spawn(cmd)
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
@@ -2,24 +2,14 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Poseidon::ConsumerGroup do
|
4
4
|
|
5
|
-
def new_group
|
6
|
-
group = described_class.new "my-group", ["localhost:29092", "localhost:29091"], ["localhost:22181"], TOPIC_NAME
|
7
|
-
groups.push(group)
|
8
|
-
group
|
9
|
-
end
|
10
|
-
|
11
5
|
def fetch_response(n)
|
12
6
|
set = Poseidon::MessageSet.new
|
13
7
|
n.times {|i| set << Poseidon::Message.new(value: "value", key: "key", offset: i) }
|
14
8
|
pfr = Poseidon::Protocol::PartitionFetchResponse.new(0, 0, 100, set)
|
15
|
-
tfr = Poseidon::Protocol::TopicFetchResponse.new(
|
9
|
+
tfr = Poseidon::Protocol::TopicFetchResponse.new("mytopic", [pfr])
|
16
10
|
Poseidon::Protocol::FetchResponse.new(nil, [tfr])
|
17
11
|
end
|
18
12
|
|
19
|
-
let :groups do
|
20
|
-
[]
|
21
|
-
end
|
22
|
-
|
23
13
|
let :brokers do
|
24
14
|
[ Poseidon::Protocol::Broker.new(1, "localhost", 29092), # id,host,port
|
25
15
|
Poseidon::Protocol::Broker.new(2, "localhost", 29091), ]
|
@@ -31,53 +21,59 @@ describe Poseidon::ConsumerGroup do
|
|
31
21
|
end
|
32
22
|
|
33
23
|
let :topics do
|
34
|
-
[ Poseidon::TopicMetadata.new(Poseidon::Protocol::TopicMetadataStruct.new(0,
|
24
|
+
[ Poseidon::TopicMetadata.new(Poseidon::Protocol::TopicMetadataStruct.new(0, "mytopic", partitions)) ]
|
35
25
|
end
|
36
26
|
|
37
27
|
let :metadata do
|
38
28
|
Poseidon::Protocol::MetadataResponse.new nil, brokers.dup, topics.dup
|
39
29
|
end
|
40
30
|
|
41
|
-
|
42
|
-
|
43
|
-
Poseidon::ConsumerGroup.any_instance.stub(:sleep)
|
44
|
-
Poseidon::BrokerPool.any_instance.stub(:fetch_metadata_from_broker).and_return(metadata)
|
45
|
-
Poseidon::Connection.any_instance.stub(:fetch).with{|_, _, req| req[0].partition_fetches[0].partition == 0 }.and_return(fetch_response(10))
|
46
|
-
Poseidon::Connection.any_instance.stub(:fetch).with{|_, _, req| req[0].partition_fetches[0].partition == 1 }.and_return(fetch_response(5))
|
31
|
+
let :zk_client do
|
32
|
+
double "ZK", mkdir_p: nil, get: nil, set: nil, delete: nil, create: "/path", register: nil, children: ["my-group-UNIQUEID"], close: nil
|
47
33
|
end
|
48
|
-
|
49
|
-
|
50
|
-
|
34
|
+
|
35
|
+
let(:group) { described_class.new "my-group", ["localhost:29092", "localhost:29091"], ["localhost:22181"], "mytopic" }
|
36
|
+
subject { group }
|
37
|
+
|
38
|
+
before do
|
39
|
+
allow(ZK).to receive_messages(new: zk_client)
|
40
|
+
allow(Poseidon::Cluster).to receive_messages(guid: "UNIQUEID")
|
41
|
+
allow_any_instance_of(Poseidon::ConsumerGroup).to receive(:sleep)
|
42
|
+
allow_any_instance_of(Poseidon::PartitionConsumer).to receive_messages(resolve_offset_if_necessary: 0)
|
43
|
+
allow_any_instance_of(Poseidon::BrokerPool).to receive_messages(fetch_metadata_from_broker: metadata)
|
44
|
+
|
45
|
+
allow_any_instance_of(Poseidon::Connection).to receive(:fetch).with(10000, 1, ->req { req[0].partition_fetches[0].partition == 0 }).and_return(fetch_response(10))
|
46
|
+
allow_any_instance_of(Poseidon::Connection).to receive(:fetch).with(10000, 1, ->req { req[0].partition_fetches[0].partition == 1 }).and_return(fetch_response(5))
|
51
47
|
end
|
52
48
|
|
53
49
|
it { should be_registered }
|
54
50
|
its(:name) { should == "my-group" }
|
55
|
-
its(:topic) { should ==
|
51
|
+
its(:topic) { should == "mytopic" }
|
56
52
|
its(:pool) { should be_instance_of(Poseidon::BrokerPool) }
|
57
|
-
its(:id) { should
|
58
|
-
its(:zk) { should
|
53
|
+
its(:id) { should == "my-group-UNIQUEID" }
|
54
|
+
its(:zk) { should be(zk_client) }
|
59
55
|
|
60
56
|
its(:claimed) { should == [0, 1] }
|
61
57
|
its(:metadata) { should be_instance_of(Poseidon::ClusterMetadata) }
|
62
58
|
its(:topic_metadata) { should be_instance_of(Poseidon::TopicMetadata) }
|
63
59
|
its(:registries) { should == {
|
64
60
|
consumer: "/consumers/my-group/ids",
|
65
|
-
owner: "/consumers/my-group/owners/
|
66
|
-
offset: "/consumers/my-group/offsets/
|
61
|
+
owner: "/consumers/my-group/owners/mytopic",
|
62
|
+
offset: "/consumers/my-group/offsets/mytopic",
|
67
63
|
}}
|
68
64
|
|
69
65
|
its("metadata.brokers.keys") { should =~ [1,2] }
|
70
66
|
its("topic_metadata.partition_count") { should == 2 }
|
71
67
|
|
72
|
-
it "should register with zookeeper" do
|
73
|
-
|
74
|
-
|
75
|
-
|
68
|
+
it "should register with zookeeper and rebalance" do
|
69
|
+
zk_client.should_receive(:mkdir_p).with("/consumers/my-group/ids")
|
70
|
+
zk_client.should_receive(:mkdir_p).with("/consumers/my-group/owners/mytopic")
|
71
|
+
zk_client.should_receive(:mkdir_p).with("/consumers/my-group/offsets/mytopic")
|
72
|
+
zk_client.should_receive(:create).with("/consumers/my-group/ids/my-group-UNIQUEID", "{}", ephemeral: true)
|
73
|
+
zk_client.should_receive(:register).with("/consumers/my-group/ids")
|
74
|
+
described_class.any_instance.should_receive :rebalance!
|
76
75
|
|
77
|
-
|
78
|
-
data.should == "{}"
|
79
|
-
stat.num_children.should == 0
|
80
|
-
stat.ephemeral_owner.should > 0
|
76
|
+
subject
|
81
77
|
end
|
82
78
|
|
83
79
|
it "should sort partitions by leader address" do
|
@@ -93,12 +89,13 @@ describe Poseidon::ConsumerGroup do
|
|
93
89
|
end
|
94
90
|
|
95
91
|
it "should return the offset for each partition" do
|
92
|
+
zk_client.should_receive(:get).with("/consumers/my-group/offsets/mytopic/0", ignore: :no_node).and_return([nil])
|
96
93
|
subject.offset(0).should == 0
|
97
|
-
|
98
|
-
|
99
|
-
subject.
|
100
|
-
|
101
|
-
|
94
|
+
|
95
|
+
zk_client.should_receive(:get).with("/consumers/my-group/offsets/mytopic/1", ignore: :no_node).and_return(["21", nil])
|
96
|
+
subject.offset(1).should == 21
|
97
|
+
|
98
|
+
zk_client.should_receive(:get).with("/consumers/my-group/offsets/mytopic/2", ignore: :no_node).and_return(["0", nil])
|
102
99
|
subject.offset(2).should == 0
|
103
100
|
end
|
104
101
|
|
@@ -109,8 +106,8 @@ describe Poseidon::ConsumerGroup do
|
|
109
106
|
end
|
110
107
|
|
111
108
|
it "should checkout individual partition consumers (atomically)" do
|
112
|
-
subject.checkout {|c| c.partition.should == 1 }.should
|
113
|
-
subject.checkout {|c| c.partition.should == 0 }.should
|
109
|
+
subject.checkout {|c| c.partition.should == 1 }.should be_truthy
|
110
|
+
subject.checkout {|c| c.partition.should == 0 }.should be_truthy
|
114
111
|
|
115
112
|
n = 0
|
116
113
|
a = Thread.new do
|
@@ -127,89 +124,86 @@ describe Poseidon::ConsumerGroup do
|
|
127
124
|
n.should == 400
|
128
125
|
end
|
129
126
|
|
127
|
+
describe "consumer" do
|
128
|
+
subject { described_class::Consumer.new group, 1 }
|
129
|
+
before { group.stub(:offset).with(1).and_return(432) }
|
130
|
+
|
131
|
+
it { should be_a(Poseidon::PartitionConsumer) }
|
132
|
+
its(:offset) { should == 432 }
|
133
|
+
|
134
|
+
it 'should start with the earliest offset if none stored' do
|
135
|
+
group.unstub(:offset)
|
136
|
+
subject.offset.should == :earliest_offset
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'should start with the latest offset if none stored and in trailing mode' do
|
140
|
+
group.unstub(:offset)
|
141
|
+
trailing_consumer = described_class::Consumer.new group, 1, {trail: true}
|
142
|
+
trailing_consumer.offset.should == :latest_offset
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
130
147
|
describe "rebalance" do
|
131
148
|
|
132
149
|
it "should watch out for new consumers joining/leaving" do
|
133
|
-
|
134
|
-
|
135
|
-
new_group
|
150
|
+
described_class.any_instance.should_receive(:rebalance!)
|
151
|
+
subject
|
136
152
|
end
|
137
153
|
|
138
154
|
it "should distribute available partitions between consumers" do
|
139
155
|
subject.claimed.should == [0, 1]
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
subject.claimed.should == [1]
|
145
|
-
b.claimed.should == [0]
|
146
|
-
|
147
|
-
c = new_group
|
148
|
-
b.close
|
149
|
-
wait_for { b.claimed.size < 0 }
|
150
|
-
wait_for { c.claimed.size > 0 }
|
151
|
-
|
152
|
-
subject.claimed.should == [1]
|
153
|
-
b.claimed.should == []
|
154
|
-
c.claimed.should == [0]
|
156
|
+
zk_client.stub children: ["my-group-UNIQUEID", "my-group-OTHERID"]
|
157
|
+
-> { subject.send :rebalance! }.should change { subject.claimed }.to([0])
|
158
|
+
zk_client.stub children: ["my-group-UNIQUEID", "my-group-OTHERID", "my-group-THIRDID"]
|
159
|
+
-> { subject.send :rebalance! }.should change { subject.claimed }.to([])
|
155
160
|
end
|
156
161
|
|
157
162
|
it "should allocate partitions correctly" do
|
158
163
|
subject.claimed.should == [0, 1]
|
159
164
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
subject.claimed.should == [1]
|
164
|
-
b.claimed.should == [0]
|
165
|
-
|
166
|
-
c = new_group
|
167
|
-
b.close
|
168
|
-
wait_for { b.claimed.size < 0 }
|
169
|
-
wait_for { c.claimed.size > 0 }
|
165
|
+
zk_client.stub children: ["my-group-UNIQUEID", "my-group-ZID"]
|
166
|
+
zk_client.should_receive(:delete).with("/consumers/my-group/owners/mytopic/1", ignore: :no_node)
|
167
|
+
-> { subject.send :rebalance! }.should change { subject.claimed }.to([1])
|
170
168
|
|
171
|
-
|
172
|
-
|
173
|
-
c.claimed.should == [0]
|
169
|
+
zk_client.stub children: ["my-group-UNIQUEID", "my-group-ZID", "my-group-AID"]
|
170
|
+
-> { subject.send :rebalance! }.should change { subject.claimed }.to([0])
|
174
171
|
end
|
175
172
|
|
176
173
|
end
|
177
174
|
|
178
175
|
describe "fetch" do
|
179
176
|
|
180
|
-
it "should return messages from
|
177
|
+
it "should return messages from claimed partitions" do
|
181
178
|
subject.fetch do |n, msg|
|
182
179
|
n.should == 1
|
183
180
|
msg.size.should == 5
|
184
|
-
end.should
|
181
|
+
end.should be_truthy
|
185
182
|
|
186
183
|
subject.fetch do |n, msg|
|
187
184
|
n.should == 0
|
188
185
|
msg.size.should == 10
|
189
|
-
end.should
|
186
|
+
end.should be_truthy
|
190
187
|
|
191
188
|
subject.fetch do |n, msg|
|
192
189
|
n.should == 1
|
193
190
|
msg.size.should == 5
|
194
|
-
end.should
|
191
|
+
end.should be_truthy
|
195
192
|
end
|
196
193
|
|
197
194
|
it "should auto-commit fetched offset" do
|
198
|
-
|
199
|
-
|
200
|
-
}.should change { subject.offset(1) }.from(0).to(5)
|
195
|
+
zk_client.should_receive(:set).with("/consumers/my-group/offsets/mytopic/1", "5")
|
196
|
+
subject.fetch {|n, _| n.should == 1 }
|
201
197
|
end
|
202
198
|
|
203
199
|
it "should skip auto-commits if requested" do
|
204
|
-
|
205
|
-
|
206
|
-
}.should_not change { subject.offset(1) }
|
200
|
+
zk_client.should_not_receive(:set)
|
201
|
+
subject.fetch(commit: false) {|n, _| n.should == 1 }
|
207
202
|
end
|
208
203
|
|
209
204
|
it "should skip auto-commits if block results in false" do
|
210
|
-
|
211
|
-
|
212
|
-
}.should_not change { subject.offset(1) }
|
205
|
+
zk_client.should_not_receive(:set)
|
206
|
+
subject.fetch {|n, _| n.should == 1; false }
|
213
207
|
end
|
214
208
|
|
215
209
|
it "should return false when trying to fetch messages without a claim" do
|
@@ -217,12 +211,12 @@ describe Poseidon::ConsumerGroup do
|
|
217
211
|
Poseidon::BrokerPool.any_instance.stub fetch_metadata_from_broker: no_topics
|
218
212
|
|
219
213
|
subject.claimed.should == []
|
220
|
-
subject.fetch {|*| }.should
|
214
|
+
subject.fetch {|*| }.should be_falsey
|
221
215
|
end
|
222
216
|
|
223
217
|
it "should return true even when no messages were fetched" do
|
224
218
|
Poseidon::Connection.any_instance.stub fetch: fetch_response(0)
|
225
|
-
subject.fetch {|*| }.should
|
219
|
+
subject.fetch {|*| }.should be_truthy
|
226
220
|
end
|
227
221
|
|
228
222
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,64 +1,14 @@
|
|
1
1
|
require 'poseidon_cluster'
|
2
2
|
require 'rspec'
|
3
|
-
require '
|
4
|
-
require 'pathname'
|
3
|
+
require 'rspec/its'
|
5
4
|
require 'coveralls'
|
6
5
|
Coveralls.wear_merged!
|
7
6
|
|
8
|
-
TOPIC_NAME = "my-topic"
|
9
|
-
KAFKA_LOCAL = File.expand_path("../kafka_2.8.0-0.8.0", __FILE__)
|
10
|
-
KAFKA_ROOT = Pathname.new(ENV["KAFKA_ROOT"] || KAFKA_LOCAL)
|
11
|
-
|
12
|
-
module Poseidon::SpecHelper
|
13
|
-
|
14
|
-
def wait_for(&truth)
|
15
|
-
100.times do
|
16
|
-
break if truth.call
|
17
|
-
sleep(0.01)
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
end
|
22
|
-
|
23
7
|
RSpec.configure do |c|
|
24
|
-
c.
|
25
|
-
|
26
|
-
c.filter_run_excluding java: false if RUBY_PLATFORM == "java"
|
27
|
-
|
28
|
-
c.before :suite do
|
29
|
-
kafka_bin = KAFKA_ROOT.join("bin", "kafka-server-start.sh")
|
30
|
-
kafka_cfg = KAFKA_ROOT.join("config", "server-poseidon.properties")
|
31
|
-
zookp_bin = KAFKA_ROOT.join("bin", "zookeeper-server-start.sh")
|
32
|
-
zookp_cfg = KAFKA_ROOT.join("config", "zookeeper-poseidon.properties")
|
33
|
-
|
34
|
-
if KAFKA_ROOT.to_s == KAFKA_LOCAL && !kafka_bin.file?
|
35
|
-
puts "---> Downloading Kafka"
|
36
|
-
target = Pathname.new(File.expand_path("../", __FILE__))
|
37
|
-
system("cd #{target} && curl http://www.us.apache.org/dist/kafka/0.8.0/kafka_2.8.0-0.8.0.tar.gz | tar xz") ||
|
38
|
-
raise("Unable to download Kafka")
|
39
|
-
|
40
|
-
kafka_cfg.open("w") do |f|
|
41
|
-
f.write KAFKA_ROOT.join("config", "server.properties").read.sub("=9092", "=29092").sub(":2181", ":22181").sub("/tmp/kafka-logs", "/tmp/kafka-logs-poseidon")
|
42
|
-
end
|
43
|
-
zookp_cfg.open("w") do |f|
|
44
|
-
f.write KAFKA_ROOT.join("config", "zookeeper.properties").read.sub("=2181", "=22181")
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
# Ensure all required files are present
|
49
|
-
[kafka_bin, zookp_bin, kafka_cfg, zookp_cfg].each do |path|
|
50
|
-
raise "Unable to locate #{path}. File does not exist!" unless path.file?
|
51
|
-
end
|
52
|
-
|
53
|
-
# Start Zookeeper & Kafka
|
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'
|
8
|
+
c.expect_with :rspec do |c|
|
9
|
+
c.syntax = [:expect, :should]
|
56
10
|
end
|
57
|
-
|
58
|
-
|
59
|
-
Process.kill :TERM, $KAFKA_PID if $KAFKA_PID
|
60
|
-
Process.kill :TERM, $ZOOKP_PID if $ZOOKP_PID
|
61
|
-
FileUtils.rm_rf "/tmp/kafka-logs-poseidon"
|
11
|
+
c.mock_with :rspec do |c|
|
12
|
+
c.syntax = [:expect, :should]
|
62
13
|
end
|
63
|
-
|
64
14
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: poseidon_cluster
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Black Square Media
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-06-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: poseidon
|
@@ -16,14 +16,20 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 0.0.5
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.1.0
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
27
|
- - ">="
|
25
28
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
29
|
+
version: 0.0.5
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.1.0
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: zk
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +86,20 @@ dependencies:
|
|
80
86
|
- - ">="
|
81
87
|
- !ruby/object:Gem::Version
|
82
88
|
version: '0'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: rspec-its
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
83
103
|
- !ruby/object:Gem::Dependency
|
84
104
|
name: yard
|
85
105
|
requirement: !ruby/object:Gem::Requirement
|
@@ -127,7 +147,11 @@ files:
|
|
127
147
|
- lib/poseidon/consumer_group.rb
|
128
148
|
- lib/poseidon_cluster.rb
|
129
149
|
- poseidon_cluster.gemspec
|
130
|
-
-
|
150
|
+
- scenario/.gitignore
|
151
|
+
- scenario/consumer.rb
|
152
|
+
- scenario/producer.rb
|
153
|
+
- scenario/run.rb
|
154
|
+
- scenario/scenario.rb
|
131
155
|
- spec/lib/poseidon/cluster_spec.rb
|
132
156
|
- spec/lib/poseidon/consumer_group_spec.rb
|
133
157
|
- spec/spec_helper.rb
|
@@ -142,7 +166,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
142
166
|
requirements:
|
143
167
|
- - ">="
|
144
168
|
- !ruby/object:Gem::Version
|
145
|
-
version:
|
169
|
+
version: 2.0.0
|
146
170
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
147
171
|
requirements:
|
148
172
|
- - ">="
|
@@ -150,7 +174,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
150
174
|
version: 1.8.0
|
151
175
|
requirements: []
|
152
176
|
rubyforge_project:
|
153
|
-
rubygems_version: 2.
|
177
|
+
rubygems_version: 2.4.7
|
154
178
|
signing_key:
|
155
179
|
specification_version: 4
|
156
180
|
summary: Poseidon cluster extensions
|
@@ -1,168 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe Poseidon::ConsumerGroup, integration: true do
|
4
|
-
|
5
|
-
def new_group(max_bytes = 1024*8, name = TOPIC_NAME)
|
6
|
-
described_class.new "my-group", ["localhost:29092"], ["localhost:22181"], name, max_bytes: max_bytes
|
7
|
-
end
|
8
|
-
|
9
|
-
def stored_offsets
|
10
|
-
{ 0 => subject.offset(0), 1 => subject.offset(1) }
|
11
|
-
end
|
12
|
-
|
13
|
-
subject { new_group }
|
14
|
-
let(:consumed) { Hash.new(0) }
|
15
|
-
let(:zookeeper) { ::ZK.new("localhost:22181") }
|
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"
|
34
|
-
end
|
35
|
-
|
36
|
-
describe "small batches" do
|
37
|
-
|
38
|
-
it "should consume messages from all partitions" do
|
39
|
-
5.times do
|
40
|
-
subject.fetch {|n, msgs| consumed[n] += msgs.size }
|
41
|
-
end
|
42
|
-
consumed.values.inject(0, :+).should < 676
|
43
|
-
|
44
|
-
5.times do
|
45
|
-
subject.fetch {|n, msgs| consumed[n] += msgs.size }
|
46
|
-
end
|
47
|
-
consumed.keys.should =~ [0, 1]
|
48
|
-
consumed.values.inject(0, :+).should == 676
|
49
|
-
consumed.should == stored_offsets
|
50
|
-
end
|
51
|
-
|
52
|
-
end
|
53
|
-
|
54
|
-
describe "large batches" do
|
55
|
-
subject { new_group 1024 * 1024 * 10 }
|
56
|
-
|
57
|
-
it "should consume messages from all partitions" do
|
58
|
-
5.times do
|
59
|
-
subject.fetch {|n, msgs| consumed[n] += msgs.size }
|
60
|
-
end
|
61
|
-
consumed.keys.should =~ [0, 1]
|
62
|
-
consumed.values.inject(0, :+).should == 676
|
63
|
-
consumed.should == stored_offsets
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
describe "multi-thread fuzzing", slow: true do
|
68
|
-
|
69
|
-
def in_thread(batch_size, target, qu)
|
70
|
-
Thread.new do
|
71
|
-
sum = 0
|
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 }
|
77
|
-
end
|
78
|
-
group.close
|
79
|
-
sum
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
it "should consume from multiple sources" do
|
84
|
-
q = Queue.new
|
85
|
-
a = in_thread(4001, 200, q)
|
86
|
-
b = in_thread(4002, 40, q)
|
87
|
-
c = in_thread(4003, 120, q)
|
88
|
-
d = in_thread(4004, 50, q)
|
89
|
-
e = in_thread(4005, 400, q)
|
90
|
-
vals = [a, b, c, d, e].map(&:value)
|
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
|
96
|
-
end
|
97
|
-
|
98
|
-
end
|
99
|
-
|
100
|
-
describe "multi-process fuzzing", slow: true, java: false do
|
101
|
-
before do
|
102
|
-
producer = Poseidon::Producer.new(["localhost:29092"], "my-producer")
|
103
|
-
payload = "data" * 10
|
104
|
-
100.times do
|
105
|
-
messages = (0...1000).map do |i|
|
106
|
-
Poseidon::MessageToSend.new("slow-topic", payload, i.to_s)
|
107
|
-
end
|
108
|
-
producer.send_messages(messages)
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
it 'should consume correctly' do
|
113
|
-
read, write = IO.pipe
|
114
|
-
pid1 = fork do
|
115
|
-
group = new_group(64*1024, "slow-topic")
|
116
|
-
10.times do
|
117
|
-
5.times { group.fetch {|_, m| write.write "1:#{m.size}\n" }}
|
118
|
-
sleep(1)
|
119
|
-
end
|
120
|
-
end
|
121
|
-
pid2 = fork do
|
122
|
-
group = new_group(32*1024, "slow-topic")
|
123
|
-
5.times do
|
124
|
-
10.times { group.fetch {|_, m| write.write "2:#{m.size}\n" }}
|
125
|
-
sleep(1)
|
126
|
-
end
|
127
|
-
end
|
128
|
-
pid3 = fork do
|
129
|
-
group = new_group(8*1024, "slow-topic")
|
130
|
-
5.times do
|
131
|
-
50.times { group.fetch {|_, m| write.write "3:#{m.size}\n" }}
|
132
|
-
end
|
133
|
-
end
|
134
|
-
Process.wait(pid2)
|
135
|
-
|
136
|
-
pid4 = fork do
|
137
|
-
group = new_group(8*1024, "slow-topic")
|
138
|
-
5.times do
|
139
|
-
50.times { group.fetch {|_, m| write.write "4:#{m.size}\n" }}
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
pid5 = fork do
|
144
|
-
group = new_group(32*1024, "slow-topic")
|
145
|
-
10.times do
|
146
|
-
50.times { group.fetch {|_, m| write.write "5:#{m.size}\n" }}
|
147
|
-
sleep(3)
|
148
|
-
end
|
149
|
-
end
|
150
|
-
Process.wait(pid1)
|
151
|
-
Process.wait(pid3)
|
152
|
-
Process.wait(pid4)
|
153
|
-
Process.wait(pid5)
|
154
|
-
write.close
|
155
|
-
raw = read.read
|
156
|
-
read.close
|
157
|
-
|
158
|
-
stats = raw.lines.inject(Hash.new(0)) do |res, line|
|
159
|
-
pid, count = line.chomp.split(":")
|
160
|
-
res[pid.to_i] += count.to_i
|
161
|
-
res
|
162
|
-
end
|
163
|
-
stats.keys.size.should be_within(1).of(4)
|
164
|
-
stats.values.inject(0, :+).should == 100_000
|
165
|
-
end
|
166
|
-
|
167
|
-
end
|
168
|
-
end
|