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 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