poseidon_cluster 0.1.1 → 0.3.0
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/.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 [](https://travis-ci.org/bsm/poseidon_cluster) [](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
|