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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 54b28361ea6e2d74ad75503560e29230f928cf87
4
- data.tar.gz: dce02dfed81efe6a59c210391d07c74cf3781083
3
+ metadata.gz: 70266dd2ea6c22a6e57d802e8a30ad722228b2ad
4
+ data.tar.gz: 75eaf7ea9e37bb9c74ef7c5291b3fb28aa074096
5
5
  SHA512:
6
- metadata.gz: ac690a44445aa513d2131339f8a7bb06dfe0e442d569e1389e1aa6959c01b80b5224dee4c8733aff41d2629803a847335a4a794ae8adb59ca5bd2f6c4a0b15cc
7
- data.tar.gz: 117990bd5fd6badf2bdc9286ecba6d1ec0eaaca0b87ebaaf6fb385fae371d4669adc2527593d177ffe28caecf210f4af4049873e5d0e3ef1480ac9069405b17d
6
+ metadata.gz: fbb88da9cdb10569defc933e51bcde385b17813408f57d443562b531a851f046b1868f34f59e9ccb682ba0c9223c58e9fbca998cbf166ad9832bfd69723cc127
7
+ data.tar.gz: 1f89ca21c4d5aac75c757069c26a7a0e8d1cec32993ccd9dd7402e35096b7c65f1450d7739d3ad94fd5e1792405eca7bc7e4ead246ef954dfec3f673e2afa678
data/.gitignore CHANGED
@@ -5,3 +5,4 @@ doc/
5
5
  .bundle/
6
6
  pkg/
7
7
  coverage/
8
+ *.log
@@ -1,9 +1,7 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.1.0
4
3
  - 2.0.0
5
- - 1.9.3
6
- - jruby-19mode
7
- env:
8
- - SLOW=1
9
- script: bundle exec rake spec:coveralls
4
+ - 2.1.5
5
+ script: bundle exec rake spec:coveralls scenario
6
+ matrix:
7
+ fast_finish: true
data/Gemfile CHANGED
@@ -1,7 +1,6 @@
1
1
  source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
- gem "poseidon", git: "https://github.com/dim/poseidon.git"
5
4
 
6
5
  platform :jruby do
7
6
  gem 'slyphon-log4j', '= 1.2.15'
@@ -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.1.1)
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.7.0)
18
- multi_json (~> 1.3)
19
- rest-client
20
- simplecov (>= 0.7)
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.2)
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.7.2)
26
+ logging (1.8.2)
27
27
  little-plugger (>= 1.1.3)
28
- mime-types (2.1)
29
- multi_json (1.8.4)
30
- rake (10.1.1)
31
- rest-client (1.6.7)
32
- mime-types (>= 1.16)
33
- rspec (2.14.1)
34
- rspec-core (~> 2.14.0)
35
- rspec-expectations (~> 2.14.0)
36
- rspec-mocks (~> 2.14.0)
37
- rspec-core (2.14.7)
38
- rspec-expectations (2.14.4)
39
- diff-lcs (>= 1.1.3, < 2.0)
40
- rspec-mocks (2.14.4)
41
- simplecov (0.8.2)
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
- multi_json
44
- simplecov-html (~> 0.8.0)
45
- simplecov-html (0.8.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.2.2)
49
- tins (~> 0.8)
50
- thor (0.18.1)
51
- tins (0.13.1)
52
- yard (0.8.7.3)
53
- zk (1.9.3)
54
- logging (~> 1.7.2)
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.8)
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
@@ -13,4 +13,9 @@ namespace :spec do
13
13
  task coveralls: [:spec, 'coveralls:push']
14
14
  end
15
15
 
16
+ desc "Run full integration test scenario"
17
+ task :scenario do
18
+ load File.expand_path("../scenario/run.rb", __FILE__)
19
+ end
20
+
16
21
  task default: :spec
@@ -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 = 10
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, ::Socket.gethostname, ::Process.pid, ::Time.now.to_i, ::Poseidon::Cluster.inc!].join("-")
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.partitions.sort_by do |part|
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
- return false if @rebalancing
230
+ consumer = nil
231
+ commit = @mutex.synchronize do
232
+ consumer = @consumers.shift
233
+ return false unless consumer
224
234
 
225
- consumer = @consumers.shift
226
- return false unless consumer
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 || result == 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
- @mutex.synchronize { perform_rebalance! }
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
- sleep(0.1) while zk.create(path, id, ephemeral: true, ignore: :node_exists).nil?
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
@@ -1,11 +1,11 @@
1
1
  Gem::Specification.new do |s|
2
- s.required_ruby_version = '>= 1.9.1'
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.1.1"
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
 
@@ -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
@@ -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
@@ -12,4 +12,8 @@ describe Poseidon::Cluster do
12
12
  (described_class.inc! - num).should == 502
13
13
  end
14
14
 
15
+ it 'should generate GUIDs' do
16
+ described_class.guid.should match(/\A[\w\-\.]+?\-\d{1,5}\-\d{10}\-\d{1,3}\z/)
17
+ end
18
+
15
19
  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(TOPIC_NAME, [pfr])
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, TOPIC_NAME, partitions)) ]
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
- subject { new_group }
42
- before do
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
- after do
49
- subject.zk.rm_rf "/consumers/#{subject.name}"
50
- groups.each(&:close)
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 == TOPIC_NAME }
51
+ its(:topic) { should == "mytopic" }
56
52
  its(:pool) { should be_instance_of(Poseidon::BrokerPool) }
57
- its(:id) { should match(/\Amy-group\-[\w\-\.]+?\-\d{1,5}\-\d{10}\-\d{1,3}\z/) }
58
- its(:zk) { should be_instance_of(ZK::Client::Threaded) }
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/my-topic",
66
- offset: "/consumers/my-group/offsets/my-topic",
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
- subject.zk.children("/consumers/my-group/ids").should include(subject.id)
74
- stat = subject.zk.stat("/consumers/my-group/ids")
75
- stat.ephemeral_owner.should be(0)
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
- data, stat = subject.zk.get("/consumers/my-group/ids/#{subject.id}")
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
- subject.offset(1).should == 0
98
- subject.offset(2).should == 0
99
- subject.fetch {|*| true }
100
- subject.offset(0).should == 0
101
- subject.offset(1).should == 5
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 be_true
113
- subject.checkout {|c| c.partition.should == 0 }.should be_true
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
- subject.should_receive(:rebalance!).twice.and_call_original
134
- new_group.should_receive(:rebalance!).once.and_call_original
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
- b = new_group
142
- wait_for { subject.claimed.size > 0 }
143
- wait_for { b.claimed.size > 0 }
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
- b = new_group
161
- wait_for { subject.claimed.size > 0 }
162
- wait_for { b.claimed.size > 0 }
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
- subject.claimed.should == [1]
172
- b.claimed.should == []
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 owned partitions" do
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 be_true
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 be_true
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 be_true
191
+ end.should be_truthy
195
192
  end
196
193
 
197
194
  it "should auto-commit fetched offset" do
198
- -> {
199
- subject.fetch {|n, _| n.should == 1 }
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
- subject.fetch(commit: false) {|n, _| n.should == 1 }
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
- subject.fetch {|n, _| n.should == 1; false }
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 be_false
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 be_true
219
+ subject.fetch {|*| }.should be_truthy
226
220
  end
227
221
 
228
222
  end
@@ -1,64 +1,14 @@
1
1
  require 'poseidon_cluster'
2
2
  require 'rspec'
3
- require 'fileutils'
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.include Poseidon::SpecHelper
25
- c.filter_run_excluding slow: true unless ENV["SLOW"] == "1"
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
- c.after :suite do
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.1.1
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: 2014-02-05 00:00:00.000000000 Z
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: '0'
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: '0'
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
- - spec/integration/poseidon/consumer_group_spec.rb
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: 1.9.1
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.2.0.rc.1
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