kafka-consumer 0.1.1 → 0.1.2

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: 7e0b4f66c8e873f254c213a4d12ec89a2a46134a
4
- data.tar.gz: 0dcc6b6cdce939337a51acebc95b9c7d8ac6b757
3
+ metadata.gz: fd211d48c462dd372ab59425ef6f36b79460abd0
4
+ data.tar.gz: b13cb4c3d7fcbba2c6c7703eb6ea4e6d72fc030d
5
5
  SHA512:
6
- metadata.gz: 22f95d7e30a7c4e8c83b1e2ddea5c962a88b1655e19b68df9eadc670286f5a5ae98376b95e7eac9fd85e5f6c13d78e74f009787b5e0eaeb64f4217656a7df104
7
- data.tar.gz: f4b831cb109d4ee7b05efa2c61cb3ba00874088e62b03d83af981c10ce411a4dc10535f56a163bc7058f0f5a7d27cac286dff4ef4ad4b359126f757f843583a0
6
+ metadata.gz: 48d39d21d7370f509b644667ae428c0d8c7ef27883d336ef98e102e4efb2f7f75909959acff27e8b75cfba56b681a1df5f2337f3031e4e6fdfd1bd7ae094536c
7
+ data.tar.gz: 2de3c13aa95faeb7ed27d7a558ac03c22f5098fb47d28f76125d276608a55d4b6c9e6c1bdba8030601e8bd42f4df43f8a956b24b6690622299f4237dd0db8b74
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ sudo: false
3
+
4
+ rvm:
5
+ - "2.0"
6
+ - "2.1"
7
+ - "2.2"
8
+
9
+ cache:
10
+ - bundler
data/README.md CHANGED
@@ -11,7 +11,7 @@ instance at a time. It uses Zookeeper watches to be notified of new consumer ins
11
11
  online or going offline, which will trigger a redistribition of all the partitions that are consumed.
12
12
 
13
13
  Periodically, it will commit the last processed offset of every partition to Zookeeper. Whenever a
14
- new consumer starts, it will resume consumingevery partition at the last committed offset. This implements
14
+ new consumer starts, it will resume consuming every partition at the last committed offset. This implements
15
15
  an **at least once guarantee**, so it is possible that you end up consuming the same message more than once.
16
16
  It's your responsibility to deal with this if that is a problem for you, e.g. by using idempotent operations.
17
17
 
@@ -34,6 +34,14 @@ consumer.each do |message|
34
34
  end
35
35
  ```
36
36
 
37
+ ## Notes
38
+
39
+ - It will spawn a manager thread and two threads per partition. However, your code
40
+ doesn't have to be thread-safe because every message is yielded to `each`
41
+ sequentially using a mutex.
42
+ - On my Macbook Pro, I can consume around ~10,000 messages per second from a
43
+ topic with 64 partitions on a production Kafka cluster.
44
+
37
45
  ## Contributing
38
46
 
39
47
  1. Fork it ( https://github.com/wvanbergen/kafka-consumer/fork )
data/Rakefile CHANGED
@@ -30,14 +30,6 @@ namespace :kafka do
30
30
  puts
31
31
  puts "%d messages consumed in %0.3fs (%0.3f msg/s)" % [counter, duration, counter.to_f / duration]
32
32
  end
33
-
34
- namespace :consumer do
35
- task :reset do
36
- zookeeper = ENV["ZOOKEEPER"] or raise "Specify the ZOOKEEPER connection string."
37
- name = ENV["NAME"] or raise "Specify NAME to name the consumergroup."
38
-
39
- consumer = Kafka::Consumer.new(name, [], zookeeper: zookeeper)
40
- consumer.group.reset_offsets
41
- end
42
- end
43
33
  end
34
+
35
+ task default: :test
@@ -24,5 +24,5 @@ Gem::Specification.new do |spec|
24
24
  spec.add_development_dependency "mocha", "~> 1.0"
25
25
 
26
26
  spec.add_runtime_dependency "poseidon", "~> 0.0.5"
27
- spec.add_runtime_dependency "zookeeper", "~> 1.4"
27
+ spec.add_runtime_dependency "kazoo-ruby", "~> 0.4.0"
28
28
  end
@@ -0,0 +1 @@
1
+ require 'kafka/consumer'
@@ -9,8 +9,6 @@ require "kafka/consumer/version"
9
9
 
10
10
  module Kafka
11
11
  class Consumer
12
- BACKPRESSURE_MESSAGE_LIMIT = 1000
13
-
14
12
  include Enumerable
15
13
 
16
14
  attr_reader :subscription,
@@ -18,17 +16,19 @@ module Kafka
18
16
  :max_wait_ms, :initial_offset,
19
17
  :logger
20
18
 
21
- def initialize(name, subscription, zookeeper: [], max_wait_ms: 200, initial_offset: :latest_offset, logger: nil)
22
- @name, @subscription = name, subscription
19
+ def initialize(name, subscription, zookeeper: nil, max_wait_ms: 200, initial_offset: :latest_offset, logger: nil)
20
+ raise ArgumentError, "The consumer's name cannot be empty" if name.nil? || name.empty?
21
+ raise ArgumentError, "You have to specify a zookeeper connection string" if zookeeper.nil? || zookeeper.empty?
22
+
23
+ @name = name
23
24
  @max_wait_ms, @initial_offset = max_wait_ms, initial_offset
24
25
  @logger = logger || Logger.new($stdout)
25
26
 
26
27
  @cluster = Kazoo::Cluster.new(zookeeper)
27
28
  @group = Kazoo::Consumergroup.new(@cluster, name)
28
- @group.create unless @group.exists?
29
29
 
30
- @instance = @group.instantiate
31
- @instance.register(topics)
30
+ @group.create unless @group.exists?
31
+ @instance = @group.instantiate(subscription: Kazoo::Subscription.build(subscription)).register
32
32
  end
33
33
 
34
34
  def name
@@ -39,15 +39,12 @@ module Kafka
39
39
  instance.id
40
40
  end
41
41
 
42
- def topics
43
- @topics ||= begin
44
- topic_names = Array(subscription)
45
- topic_names.map { |topic_name| cluster.topics.fetch(topic_name) }
46
- end
42
+ def subscription
43
+ instance.subscription
47
44
  end
48
45
 
49
46
  def partitions
50
- topics.flat_map(&:partitions).sort_by { |partition| [partition.leader.id, partition.topic.name, partition.id] }
47
+ subscription.partitions(@cluster).sort_by { |partition| [partition.preferred_leader.id, partition.topic.name, partition.id] }
51
48
  end
52
49
 
53
50
  def interrupt
@@ -83,9 +80,7 @@ module Kafka
83
80
  mutex = Mutex.new
84
81
 
85
82
  handler = lambda do |message|
86
- mutex.synchronize do
87
- block.call(message)
88
- end
83
+ mutex.synchronize { block.call(message) }
89
84
  end
90
85
 
91
86
  @consumer_manager = Thread.new do
@@ -1,5 +1,5 @@
1
1
  module Kafka
2
2
  class Consumer
3
- VERSION = "0.1.1"
3
+ VERSION = "0.1.2"
4
4
  end
5
5
  end
@@ -1,48 +1,4 @@
1
1
  require 'minitest/autorun'
2
2
  require 'kafka/consumer'
3
- require 'kazoo'
4
3
  require 'mocha/mini_test'
5
4
  require 'pp'
6
-
7
- module MockCluster
8
- def mock_cluster
9
- cluster = Kazoo::Cluster.new('example.com:2181')
10
- cluster.stubs(:zk).returns(mock)
11
-
12
- cluster.stubs(:brokers).returns(
13
- 1 => Kazoo::Broker.new(cluster, 1, 'example.com', 9091),
14
- 2 => Kazoo::Broker.new(cluster, 2, 'example.com', 9092),
15
- 3 => Kazoo::Broker.new(cluster, 3, 'example.com', 9093),
16
- )
17
-
18
- cluster.stubs(:topics).returns(
19
- 'test.1' => topic_1 = Kazoo::Topic.new(cluster, 'test.1'),
20
- 'test.4' => topic_4 = Kazoo::Topic.new(cluster, 'test.4')
21
- )
22
-
23
- topic_1.stubs(:partitions).returns([
24
- Kazoo::Partition.new(topic_1, 0, replicas: [cluster.brokers[1], cluster.brokers[2]]),
25
- ])
26
-
27
- topic_4.stubs(:partitions).returns([
28
- Kazoo::Partition.new(topic_4, 0, replicas: [cluster.brokers[2], cluster.brokers[1]]),
29
- Kazoo::Partition.new(topic_4, 1, replicas: [cluster.brokers[2], cluster.brokers[3]]),
30
- Kazoo::Partition.new(topic_4, 2, replicas: [cluster.brokers[1], cluster.brokers[3]]),
31
- Kazoo::Partition.new(topic_4, 3, replicas: [cluster.brokers[3], cluster.brokers[2]]),
32
- ])
33
-
34
- topic_1.partitions[0].stubs(:isr).returns([cluster.brokers[1], cluster.brokers[2]])
35
- topic_4.partitions[0].stubs(:isr).returns([cluster.brokers[2], cluster.brokers[1]])
36
- topic_4.partitions[1].stubs(:isr).returns([cluster.brokers[2], cluster.brokers[3]])
37
- topic_4.partitions[2].stubs(:isr).returns([cluster.brokers[1], cluster.brokers[3]])
38
- topic_4.partitions[3].stubs(:isr).returns([cluster.brokers[3], cluster.brokers[2]])
39
-
40
- topic_1.partitions[0].stubs(:leader).returns(cluster.brokers[1])
41
- topic_4.partitions[0].stubs(:leader).returns(cluster.brokers[2])
42
- topic_4.partitions[1].stubs(:leader).returns(cluster.brokers[2])
43
- topic_4.partitions[2].stubs(:leader).returns(cluster.brokers[1])
44
- topic_4.partitions[3].stubs(:leader).returns(cluster.brokers[3])
45
-
46
- return cluster
47
- end
48
- end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kafka-consumer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Willem van Bergen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-08-09 00:00:00.000000000 Z
11
+ date: 2015-10-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -81,53 +81,41 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: 0.0.5
83
83
  - !ruby/object:Gem::Dependency
84
- name: zookeeper
84
+ name: kazoo-ruby
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - ~>
88
88
  - !ruby/object:Gem::Version
89
- version: '1.4'
89
+ version: 0.4.0
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - ~>
95
95
  - !ruby/object:Gem::Version
96
- version: '1.4'
96
+ version: 0.4.0
97
97
  description: High-level consumer for Kafka. Implements the Zookeeper-backed consumer
98
98
  implementation that offers offset management, load balancing and automatic failovers.
99
99
  email:
100
100
  - willem@vanbergen.org
101
- executables:
102
- - kazoo
101
+ executables: []
103
102
  extensions: []
104
103
  extra_rdoc_files: []
105
104
  files:
106
105
  - .gitignore
106
+ - .travis.yml
107
107
  - Gemfile
108
108
  - LICENSE.txt
109
109
  - README.md
110
110
  - Rakefile
111
- - bin/kazoo
112
111
  - kafka-consumer.gemspec
112
+ - lib/kafka-consumer.rb
113
113
  - lib/kafka/consumer.rb
114
114
  - lib/kafka/consumer/message.rb
115
115
  - lib/kafka/consumer/partition_consumer.rb
116
116
  - lib/kafka/consumer/version.rb
117
- - lib/kazoo.rb
118
- - lib/kazoo/broker.rb
119
- - lib/kazoo/cli.rb
120
- - lib/kazoo/cluster.rb
121
- - lib/kazoo/consumergroup.rb
122
- - lib/kazoo/partition.rb
123
- - lib/kazoo/topic.rb
124
- - lib/kazoo/version.rb
125
- - test/broker_test.rb
126
- - test/cluster_test.rb
127
117
  - test/partition_distribution_test.rb
128
- - test/partition_test.rb
129
118
  - test/test_helper.rb
130
- - test/topic_test.rb
131
119
  homepage: https://github.com/wvanbergen/kafka-consumer
132
120
  licenses:
133
121
  - MIT
@@ -153,9 +141,5 @@ signing_key:
153
141
  specification_version: 4
154
142
  summary: High-level consumer for Kafka
155
143
  test_files:
156
- - test/broker_test.rb
157
- - test/cluster_test.rb
158
144
  - test/partition_distribution_test.rb
159
- - test/partition_test.rb
160
145
  - test/test_helper.rb
161
- - test/topic_test.rb
data/bin/kazoo DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env ruby
2
- $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
3
- require 'kazoo/cli'
4
-
5
- begin
6
- ENV["THOR_DEBUG"] = "1"
7
- Kazoo::CLI.start(ARGV)
8
- rescue Thor::UndefinedCommandError, Thor::UnknownArgumentError, Thor::AmbiguousCommandError, Thor::InvocationError => e
9
- $stderr.puts(e.message)
10
- exit(64)
11
- rescue Thor::Error => e
12
- $stderr.puts(e.message)
13
- exit(1)
14
- end
@@ -1,19 +0,0 @@
1
- require 'zookeeper'
2
- require 'json'
3
- require 'thread'
4
- require 'socket'
5
- require 'securerandom'
6
-
7
- module Kazoo
8
- Error = Class.new(StandardError)
9
- ConsumerInstanceRegistrationFailed = Class.new(Kazoo::Error)
10
- PartitionAlreadyClaimed = Class.new(Kazoo::Error)
11
- ReleasePartitionFailure = Class.new(Kazoo::Error)
12
- end
13
-
14
- require 'kazoo/cluster'
15
- require 'kazoo/broker'
16
- require 'kazoo/topic'
17
- require 'kazoo/partition'
18
- require 'kazoo/consumergroup'
19
- require 'kazoo/version'
@@ -1,68 +0,0 @@
1
- module Kazoo
2
- class Broker
3
- attr_reader :cluster, :id, :host, :port, :jmx_port
4
-
5
- def initialize(cluster, id, host, port, jmx_port: nil)
6
- @cluster = cluster
7
- @id, @host, @port = id, host, port
8
- @jmx_port = jmx_port
9
- end
10
-
11
- def led_partitions
12
- result, threads, mutex = [], ThreadGroup.new, Mutex.new
13
- cluster.partitions.each do |partition|
14
- t = Thread.new do
15
- select = partition.leader == self
16
- mutex.synchronize { result << partition } if select
17
- end
18
- threads.add(t)
19
- end
20
- threads.list.each(&:join)
21
- result
22
- end
23
-
24
- def replicated_partitions
25
- result, threads, mutex = [], ThreadGroup.new, Mutex.new
26
- cluster.partitions.each do |partition|
27
- t = Thread.new do
28
- select = partition.replicas.include?(self)
29
- mutex.synchronize { result << partition } if select
30
- end
31
- threads.add(t)
32
- end
33
- threads.list.each(&:join)
34
- result
35
- end
36
-
37
- def critical?(replicas: 1)
38
- result, threads, mutex = false, ThreadGroup.new, Mutex.new
39
- replicated_partitions.each do |partition|
40
- t = Thread.new do
41
- isr = partition.isr.reject { |r| r == self }
42
- mutex.synchronize { result = true if isr.length < replicas }
43
- end
44
- threads.add(t)
45
- end
46
- threads.list.each(&:join)
47
- result
48
- end
49
-
50
- def addr
51
- "#{host}:#{port}"
52
- end
53
-
54
- def eql?(other)
55
- other.is_a?(Kazoo::Broker) && other.cluster == self.cluster && other.id == self.id
56
- end
57
-
58
- alias_method :==, :eql?
59
-
60
- def hash
61
- [self.cluster, self.id].hash
62
- end
63
-
64
- def self.from_json(cluster, id, json)
65
- new(cluster, id.to_i, json.fetch('host'), json.fetch('port'), jmx_port: json.fetch('jmx_port', nil))
66
- end
67
- end
68
- end
@@ -1,78 +0,0 @@
1
- require 'kazoo'
2
- require 'thor'
3
-
4
- module Kazoo
5
- class CLI < Thor
6
- class_option :zookeeper, :type => :string, :default => ENV['ZOOKEEPER']
7
-
8
- desc "cluster", "Describes the Kafka cluster as registered in Zookeeper"
9
- def cluster
10
- validate_class_options!
11
-
12
- kafka_cluster.brokers.values.sort_by(&:id).each do |broker|
13
- $stdout.puts "#{broker.id}:\t#{broker.addr}\t(hosts #{broker.replicated_partitions.length} partitions, leads #{broker.led_partitions.length})"
14
- end
15
- end
16
-
17
- desc "topics", "Lists all topics in the cluster"
18
- def topics
19
- validate_class_options!
20
-
21
- kafka_cluster.topics.values.sort_by(&:name).each do |topic|
22
- $stdout.puts topic.name
23
- end
24
- end
25
-
26
- option :topic, :type => :string
27
- desc "partitions", "Lists partitions"
28
- def partitions
29
- validate_class_options!
30
-
31
- topics = kafka_cluster.topics.values
32
- topics.select! { |t| t.name == options[:topic] } if options[:topic]
33
- topics.sort_by!(&:name)
34
-
35
- topics.each do |topic|
36
- topic.partitions.each do |partition|
37
- $stdout.puts "#{partition.topic.name}/#{partition.id}\tReplicas: #{partition.replicas.map(&:id).join(",")}"
38
- end
39
- end
40
- end
41
-
42
- option :replicas, :type => :numeric, :default => 1
43
- desc "critical <broker>", "Determine whether a broker is critical"
44
- def critical(broker_name)
45
- validate_class_options!
46
-
47
- if broker(broker_name).critical?(replicas: options[:replicas])
48
- raise Thor::Error, "WARNING: broker #{broker_name} is critical and cannot be stopped safely!"
49
- else
50
- $stdout.puts "Broker #{broker_name} is non-critical and can be stopped safely."
51
- end
52
- end
53
-
54
-
55
- private
56
-
57
- def validate_class_options!
58
- if options[:zookeeper].nil? || options[:zookeeper] == ''
59
- raise Thor::InvocationError, "Please supply --zookeeper argument, or set the ZOOKEEPER_PEERS environment variable"
60
- end
61
- end
62
-
63
- def broker(name_or_id)
64
- broker = if name_or_id =~ /\A\d+\z/
65
- kafka_cluster.brokers[name_or_id.to_i]
66
- else
67
- kafka_cluster.brokers.values.detect { |b| b.addr == name_or_id } || cluster.brokers.values.detect { |b| b.host == name_or_id }
68
- end
69
-
70
- raise Thor::InvocationError, "Broker #{name_or_id.inspect} not found!" if broker.nil?
71
- broker
72
- end
73
-
74
- def kafka_cluster
75
- @kafka_cluster ||= Kazoo::Cluster.new(options[:zookeeper])
76
- end
77
- end
78
- end
@@ -1,78 +0,0 @@
1
- module Kazoo
2
- class Cluster
3
-
4
- attr_reader :zookeeper
5
-
6
- def initialize(zookeeper)
7
- @zookeeper = zookeeper
8
- @zk_mutex, @brokers_mutex, @topics_mutex, @consumergroups_mutex = Mutex.new, Mutex.new, Mutex.new, Mutex.new
9
- end
10
-
11
- def zk
12
- @zk_mutex.synchronize do
13
- @zk ||= Zookeeper.new(zookeeper)
14
- end
15
- end
16
-
17
- def brokers
18
- @brokers_mutex.synchronize do
19
- @brokers ||= begin
20
- brokers = zk.get_children(path: "/brokers/ids")
21
- result, threads, mutex = {}, ThreadGroup.new, Mutex.new
22
- brokers.fetch(:children).map do |id|
23
- t = Thread.new do
24
- broker_info = zk.get(path: "/brokers/ids/#{id}")
25
- broker = Kazoo::Broker.from_json(self, id, JSON.parse(broker_info.fetch(:data)))
26
- mutex.synchronize { result[id.to_i] = broker }
27
- end
28
- threads.add(t)
29
- end
30
- threads.list.each(&:join)
31
- result
32
- end
33
- end
34
- end
35
-
36
- def consumergroups
37
- @consumergroups ||= begin
38
- consumers = zk.get_children(path: "/consumers")
39
- consumers.fetch(:children).map { |name| Kazoo::Consumergroup.new(self, name) }
40
- end
41
- end
42
-
43
- def topics
44
- @topics_mutex.synchronize do
45
- @topics ||= begin
46
- topics = zk.get_children(path: "/brokers/topics")
47
- result, threads, mutex = {}, ThreadGroup.new, Mutex.new
48
- topics.fetch(:children).each do |name|
49
- t = Thread.new do
50
- topic_info = zk.get(path: "/brokers/topics/#{name}")
51
- topic = Kazoo::Topic.from_json(self, name, JSON.parse(topic_info.fetch(:data)))
52
- mutex.synchronize { result[name] = topic }
53
- end
54
- threads.add(t)
55
- end
56
- threads.list.each(&:join)
57
- result
58
- end
59
- end
60
- end
61
-
62
- def partitions
63
- topics.values.flat_map(&:partitions)
64
- end
65
-
66
- def reset_metadata
67
- @topics, @brokers = nil, nil
68
- end
69
-
70
- def under_replicated?
71
- partitions.any?(&:under_replicated?)
72
- end
73
-
74
- def close
75
- zk.close
76
- end
77
- end
78
- end
@@ -1,215 +0,0 @@
1
- module Kazoo
2
- class Consumergroup
3
- attr_reader :cluster, :name
4
-
5
- def initialize(cluster, name)
6
- @cluster, @name = cluster, name
7
- end
8
-
9
- def create
10
- cluster.zk.create(path: "/consumers/#{name}")
11
- cluster.zk.create(path: "/consumers/#{name}/ids")
12
- cluster.zk.create(path: "/consumers/#{name}/owners")
13
- cluster.zk.create(path: "/consumers/#{name}/offsets")
14
- end
15
-
16
- def exists?
17
- stat = cluster.zk.stat(path: "/consumers/#{name}")
18
- stat.fetch(:stat).exists?
19
- end
20
-
21
-
22
- def instantiate(id: nil)
23
- Instance.new(self, id: id)
24
- end
25
-
26
- def instances
27
- instances = cluster.zk.get_children(path: "/consumers/#{name}/ids")
28
- instances.fetch(:children).map { |id| Instance.new(self, id: id) }
29
- end
30
-
31
- def watch_instances(&block)
32
- cb = Zookeeper::Callbacks::WatcherCallback.create(&block)
33
- result = cluster.zk.get_children(path: "/consumers/#{name}/ids", watcher: cb)
34
-
35
- if result.fetch(:rc) != Zookeeper::Constants::ZOK
36
- raise Kazoo::Error, "Failed to watch instances. Error code result[:rc]"
37
- end
38
-
39
- instances = result.fetch(:children).map { |id| Instance.new(self, id: id) }
40
- [instances, cb]
41
- end
42
-
43
-
44
- def watch_partition_claim(partition, &block)
45
- cb = Zookeeper::Callbacks::WatcherCallback.create(&block)
46
-
47
- result = cluster.zk.get(path: "/consumers/#{name}/owners/#{partition.topic.name}/#{partition.id}", watcher: cb)
48
-
49
- case result.fetch(:rc)
50
- when Zookeeper::Constants::ZNONODE # Nobody is claiming this partition yet
51
- [nil, nil]
52
- when Zookeeper::Constants::ZOK
53
- [Kazoo::Consumergroup::Instance.new(self, id: result.fetch(:data)), cb]
54
- else
55
- raise Kazoo::Error, "Failed set watch for partition claim of #{partition.topic.name}/#{partition.id}. Error code: #{result.fetch(:rc)}"
56
- end
57
- end
58
-
59
- def retrieve_offset(partition)
60
- result = cluster.zk.get(path: "/consumers/#{name}/offsets/#{partition.topic.name}/#{partition.id}")
61
- case result.fetch(:rc)
62
- when Zookeeper::Constants::ZOK;
63
- result.fetch(:data).to_i
64
- when Zookeeper::Constants::ZNONODE;
65
- nil
66
- else
67
- raise Kazoo::Error, "Failed to retrieve offset for partition #{partition.topic.name}/#{partition.id}. Error code: #{result.fetch(:rc)}"
68
- end
69
- end
70
-
71
- def commit_offset(partition, offset)
72
- result = cluster.zk.set(path: "/consumers/#{name}/offsets/#{partition.topic.name}/#{partition.id}", data: (offset + 1).to_s)
73
- if result.fetch(:rc) == Zookeeper::Constants::ZNONODE
74
- result = cluster.zk.create(path: "/consumers/#{name}/offsets/#{partition.topic.name}")
75
- case result.fetch(:rc)
76
- when Zookeeper::Constants::ZOK, Zookeeper::Constants::ZNODEEXISTS
77
- else
78
- raise Kazoo::Error, "Failed to commit offset #{offset} for partition #{partition.topic.name}/#{partition.id}. Error code: #{result.fetch(:rc)}"
79
- end
80
-
81
- result = cluster.zk.create(path: "/consumers/#{name}/offsets/#{partition.topic.name}/#{partition.id}", data: (offset + 1).to_s)
82
- end
83
-
84
- if result.fetch(:rc) != Zookeeper::Constants::ZOK
85
- raise Kazoo::Error, "Failed to commit offset #{offset} for partition #{partition.topic.name}/#{partition.id}. Error code: #{result.fetch(:rc)}"
86
- end
87
- end
88
-
89
- def reset_offsets
90
- result = cluster.zk.get_children(path: "/consumers/#{name}/offsets")
91
- raise Kazoo::Error unless result.fetch(:rc) == Zookeeper::Constants::ZOK
92
-
93
- result.fetch(:children).each do |topic|
94
- result = cluster.zk.get_children(path: "/consumers/#{name}/offsets/#{topic}")
95
- raise Kazoo::Error unless result.fetch(:rc) == Zookeeper::Constants::ZOK
96
-
97
- result.fetch(:children).each do |partition|
98
- cluster.zk.delete(path: "/consumers/#{name}/offsets/#{topic}/#{partition}")
99
- raise Kazoo::Error unless result.fetch(:rc) == Zookeeper::Constants::ZOK
100
- end
101
-
102
- cluster.zk.delete(path: "/consumers/#{name}/offsets/#{topic}")
103
- raise Kazoo::Error unless result.fetch(:rc) == Zookeeper::Constants::ZOK
104
- end
105
- end
106
-
107
- def inspect
108
- "#<Kazoo::Consumergroup name=#{name}>"
109
- end
110
-
111
- def eql?(other)
112
- other.kind_of?(Kazoo::Consumergroup) && cluster == other.cluster && name == other.name
113
- end
114
-
115
- alias_method :==, :eql?
116
-
117
- def hash
118
- [cluster, name].hash
119
- end
120
-
121
- class Instance
122
-
123
- def self.generate_id
124
- "#{Socket.gethostname}:#{SecureRandom.uuid}"
125
- end
126
-
127
- attr_reader :group, :id
128
-
129
- def initialize(group, id: nil)
130
- @group = group
131
- @id = id || self.class.generate_id
132
- end
133
-
134
- def registered?
135
- stat = cluster.zk.stat(path: "/consumers/#{group.name}/ids/#{id}")
136
- stat.fetch(:stat).exists?
137
- end
138
-
139
- def register(subscription)
140
- result = cluster.zk.create(
141
- path: "/consumers/#{group.name}/ids/#{id}",
142
- ephemeral: true,
143
- data: JSON.generate({
144
- version: 1,
145
- timestamp: Time.now.to_i,
146
- pattern: "static",
147
- subscription: Hash[*subscription.flat_map { |topic| [topic.name, 1] } ]
148
- })
149
- )
150
-
151
- if result.fetch(:rc) != Zookeeper::Constants::ZOK
152
- raise Kazoo::ConsumerInstanceRegistrationFailed, "Failed to register instance #{id} for consumer group #{group.name}! Error code: #{result.fetch(:rc)}"
153
- end
154
-
155
- subscription.each do |topic|
156
- stat = cluster.zk.stat(path: "/consumers/#{group.name}/owners/#{topic.name}")
157
- unless stat.fetch(:stat).exists?
158
- result = cluster.zk.create(path: "/consumers/#{group.name}/owners/#{topic.name}")
159
- if result.fetch(:rc) != Zookeeper::Constants::ZOK
160
- raise Kazoo::ConsumerInstanceRegistrationFailed, "Failed to register subscription of #{topic.name} for consumer group #{group.name}! Error code: #{result.fetch(:rc)}"
161
- end
162
- end
163
- end
164
- end
165
-
166
- def deregister
167
- cluster.zk.delete(path: "/consumers/#{group.name}/ids/#{id}")
168
- end
169
-
170
- def claim_partition(partition)
171
- result = cluster.zk.create(
172
- path: "/consumers/#{group.name}/owners/#{partition.topic.name}/#{partition.id}",
173
- ephemeral: true,
174
- data: id,
175
- )
176
-
177
- case result.fetch(:rc)
178
- when Zookeeper::Constants::ZOK
179
- return true
180
- when Zookeeper::Constants::ZNODEEXISTS
181
- raise Kazoo::PartitionAlreadyClaimed, "Partition #{partition.topic.name}/#{partition.id} is already claimed!"
182
- else
183
- raise Kazoo::Error, "Failed to claim partition #{partition.topic.name}/#{partition.id}. Error code: #{result.fetch(:rc)}"
184
- end
185
- end
186
-
187
- def release_partition(partition)
188
- result = cluster.zk.delete(path: "/consumers/#{group.name}/owners/#{partition.topic.name}/#{partition.id}")
189
- if result.fetch(:rc) != Zookeeper::Constants::ZOK
190
- raise Kazoo::Error, "Failed to release partition #{partition.topic.name}/#{partition.id}. Error code: #{result.fetch(:rc)}"
191
- end
192
- end
193
-
194
- def inspect
195
- "#<Kazoo::Consumergroup::Instance group=#{group.name} id=#{id}>"
196
- end
197
-
198
- def hash
199
- [group, id].hash
200
- end
201
-
202
- def eql?(other)
203
- other.kind_of?(Kazoo::Consumergroup::Instance) && group == other.group && id == other.id
204
- end
205
-
206
- alias_method :==, :eql?
207
-
208
- private
209
-
210
- def cluster
211
- group.cluster
212
- end
213
- end
214
- end
215
- end
@@ -1,62 +0,0 @@
1
- module Kazoo
2
- class Partition
3
- attr_reader :topic, :id, :replicas
4
-
5
- def initialize(topic, id, replicas: nil)
6
- @topic, @id, @replicas = topic, id, replicas
7
- @mutex = Mutex.new
8
- end
9
-
10
- def cluster
11
- topic.cluster
12
- end
13
-
14
- def replication_factor
15
- replicas.length
16
- end
17
-
18
- def leader
19
- @mutex.synchronize do
20
- refresh_state if @leader.nil?
21
- @leader
22
- end
23
- end
24
-
25
- def isr
26
- @mutex.synchronize do
27
- refresh_state if @isr.nil?
28
- @isr
29
- end
30
- end
31
-
32
- def under_replicated?
33
- isr.length < replication_factor
34
- end
35
-
36
- def inspect
37
- "#<Kazoo::Partition #{topic.name}/#{id}>"
38
- end
39
-
40
- def eql?(other)
41
- other.kind_of?(Kazoo::Partition) && topic == other.topic && id == other.id
42
- end
43
-
44
- alias_method :==, :eql?
45
-
46
- def hash
47
- [topic, id].hash
48
- end
49
-
50
- protected
51
-
52
- def refresh_state
53
- state_json = cluster.zk.get(path: "/brokers/topics/#{topic.name}/partitions/#{id}/state")
54
- set_state(JSON.parse(state_json.fetch(:data)))
55
- end
56
-
57
- def set_state(json)
58
- @leader = cluster.brokers.fetch(json.fetch('leader'))
59
- @isr = json.fetch('isr').map { |r| cluster.brokers.fetch(r) }
60
- end
61
- end
62
- end
@@ -1,46 +0,0 @@
1
- module Kazoo
2
- class Topic
3
-
4
- attr_reader :cluster, :name
5
- attr_accessor :partitions
6
-
7
- def initialize(cluster, name, partitions: nil)
8
- @cluster, @name, @partitions = cluster, name, partitions
9
- end
10
-
11
- def self.from_json(cluster, name, json)
12
- topic = new(cluster, name)
13
- topic.partitions = json.fetch('partitions').map do |(id, replicas)|
14
- topic.partition(id.to_i, replicas: replicas.map { |b| cluster.brokers[b] })
15
- end.sort_by(&:id)
16
-
17
- return topic
18
- end
19
-
20
- def partition(*args)
21
- Kazoo::Partition.new(self, *args)
22
- end
23
-
24
- def replication_factor
25
- partitions.map(&:replication_factor).min
26
- end
27
-
28
- def under_replicated?
29
- partitions.any?(:under_replicated?)
30
- end
31
-
32
- def inspect
33
- "#<Kazoo::Topic #{name}>"
34
- end
35
-
36
- def eql?(other)
37
- other.kind_of?(Kazoo::Topic) && cluster == other.cluster && name == other.name
38
- end
39
-
40
- alias_method :==, :eql?
41
-
42
- def hash
43
- [cluster, name].hash
44
- end
45
- end
46
- end
@@ -1,3 +0,0 @@
1
- module Kazoo
2
- VERSION = "0.1.1"
3
- end
@@ -1,45 +0,0 @@
1
- require 'test_helper'
2
-
3
- class BrokerTest < Minitest::Test
4
- include MockCluster
5
-
6
- def setup
7
- @cluster = mock_cluster
8
- end
9
-
10
- def test_broker_critical?
11
- refute @cluster.brokers[1].critical?(replicas: 1), "We have 2 in-sync replicas for everything so we can lose 1."
12
- assert @cluster.brokers[2].critical?(replicas: 2), "We only have 2 replicas so we can never lose 2."
13
-
14
- # Simulate losing a broker from the ISR for a partition.
15
- # This partition lives on broker 1 and 3
16
- @cluster.topics['test.4'].partitions[2].expects(:isr).returns([@cluster.brokers[1]])
17
-
18
- assert @cluster.brokers[1].critical?(replicas: 1), "Final remaining broker for this partition's ISR set, cannot lose"
19
- refute @cluster.brokers[2].critical?(replicas: 1), "Not related to the under-replicated partitions"
20
- refute @cluster.brokers[3].critical?(replicas: 1), "Already down, so not critical"
21
- end
22
-
23
- def test_from_json
24
- json_payload = '{"jmx_port":9999,"timestamp":"1431719964125","host":"kafka03.example.com","version":1,"port":9092}'
25
- broker = Kazoo::Broker.from_json(mock('cluster'), 3, JSON.parse(json_payload))
26
-
27
- assert_equal 3, broker.id
28
- assert_equal 'kafka03.example.com', broker.host
29
- assert_equal 9092, broker.port
30
- assert_equal 9999, broker.jmx_port
31
- assert_equal "kafka03.example.com:9092", broker.addr
32
- end
33
-
34
- def test_replicated_partitions
35
- assert_equal 3, @cluster.brokers[1].replicated_partitions.length
36
- assert_equal 4, @cluster.brokers[2].replicated_partitions.length
37
- assert_equal 3, @cluster.brokers[3].replicated_partitions.length
38
- end
39
-
40
- def test_led_partitions
41
- assert_equal 2, @cluster.brokers[1].led_partitions.length
42
- assert_equal 2, @cluster.brokers[2].led_partitions.length
43
- assert_equal 1, @cluster.brokers[3].led_partitions.length
44
- end
45
- end
@@ -1,16 +0,0 @@
1
- require 'test_helper'
2
-
3
- class ClusterTest < Minitest::Test
4
- include MockCluster
5
-
6
- def setup
7
- @cluster = mock_cluster
8
- end
9
-
10
- def test_cluster_under_replicated?
11
- refute @cluster.under_replicated?
12
-
13
- @cluster.topics['test.4'].partitions[2].expects(:isr).returns([@cluster.brokers[1]])
14
- assert @cluster.under_replicated?
15
- end
16
- end
@@ -1,25 +0,0 @@
1
- require 'test_helper'
2
-
3
- class PartitionTest < Minitest::Test
4
- include MockCluster
5
-
6
- def setup
7
- @cluster = mock_cluster
8
- end
9
-
10
- def test_replication_factor
11
- assert_equal 2, @cluster.topics['test.1'].partitions[0].replication_factor
12
- end
13
-
14
- def test_state
15
- partition = @cluster.topics['test.1'].partitions[0]
16
- partition.unstub(:leader)
17
- partition.unstub(:isr)
18
-
19
- json_payload = '{"controller_epoch":157,"leader":1,"version":1,"leader_epoch":8,"isr":[3,2,1]}'
20
- @cluster.zk.expects(:get).with(path: "/brokers/topics/test.1/partitions/0/state").returns(data: json_payload)
21
-
22
- assert_equal 1, partition.leader.id
23
- assert_equal [3,2,1], partition.isr.map(&:id)
24
- end
25
- end
@@ -1,40 +0,0 @@
1
- require 'test_helper'
2
-
3
- class TopicTest < Minitest::Test
4
- include MockCluster
5
-
6
- def setup
7
- @cluster = mock_cluster
8
- end
9
-
10
- def test_from_json
11
- json_payload = '{"version":1,"partitions":{"2":[1,2,3],"1":[3,1,2],"3":[2,3,1],"0":[3,2,1]}}'
12
- topic = Kazoo::Topic.from_json(@cluster, 'test.4', JSON.parse(json_payload))
13
-
14
- assert_equal 4, topic.partitions.length
15
- assert_equal [3,2,1], topic.partitions[0].replicas.map(&:id)
16
- assert_equal [3,1,2], topic.partitions[1].replicas.map(&:id)
17
- assert_equal [1,2,3], topic.partitions[2].replicas.map(&:id)
18
- assert_equal [2,3,1], topic.partitions[3].replicas.map(&:id)
19
- end
20
-
21
- def test_replication_factor
22
- json_payload = '{"version":1,"partitions":{"2":[1,2,3],"1":[3,1,2],"3":[2,3,1],"0":[3,2,1]}}'
23
- topic = Kazoo::Topic.from_json(@cluster, 'test.4', JSON.parse(json_payload))
24
- assert_equal 3, topic.replication_factor
25
-
26
- json_payload = '{"version":1,"partitions":{"2":[2,3],"1":[2],"3":[2,3,1],"0":[3,2,1]}}'
27
- topic = Kazoo::Topic.from_json(@cluster, 'test.4', JSON.parse(json_payload))
28
- assert_equal 1, topic.replication_factor
29
- end
30
-
31
- def tets_topic_under_replicated?
32
- refute @cluster.topics['test.1'].under_replicated?
33
- refute @cluster.topics['test.1'].partitions[0].under_replicated?
34
-
35
- @cluster.topics['test.1'].partitions[0].expects(:isr).returns([@cluster.brokers[1]])
36
-
37
- assert @cluster.topics['test.1'].partitions[0].under_replicated?
38
- assert @cluster.topics['test.1'].under_replicated?
39
- end
40
- end