kafka-consumer 0.1.1 → 0.1.2

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