kazoo-ruby 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +8 -0
- data/Makefile +53 -0
- data/lib/kazoo.rb +5 -0
- data/lib/kazoo/cli.rb +80 -3
- data/lib/kazoo/cluster.rb +52 -0
- data/lib/kazoo/consumergroup.rb +75 -38
- data/lib/kazoo/partition.rb +26 -0
- data/lib/kazoo/topic.rb +117 -2
- data/lib/kazoo/version.rb +1 -1
- data/test/functional/functional_consumergroup_test.rb +93 -0
- data/test/functional/functional_topic_management_test.rb +22 -0
- data/test/partition_test.rb +6 -0
- data/test/topic_test.rb +41 -0
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 87213b930132989503c56e226e6755b8c2f51559
|
4
|
+
data.tar.gz: 5c62c5a979ad421634272a4839b746777ed63cc6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d3a2b95c9e360fdf62c98a68ef752b220e35ddd4d9f8e05081a4eb95176a6e08d753125c47354a98ad171490458d3596199adf8eaea0edfa4768203c244302ab
|
7
|
+
data.tar.gz: e5af8de385e6946a53aa4a276c2708fb772d611cbf6afb99135c8af4c8f3a61969a52baf9c9fca8ea1f75b70c8c602689ec00024b163b748702b4e7411f47dbf
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/Makefile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
.PHONY: confluent/kafka/* confluent/zookeeper/* confluent/registry/* confluent/start confluent/stop
|
2
|
+
|
3
|
+
|
4
|
+
# Confluent platform tasks
|
5
|
+
|
6
|
+
confluent/start: confluent/rest/start
|
7
|
+
|
8
|
+
confluent/stop: confluent/rest/stop confluent/registry/stop confluent/kafka/stop confluent/zookeeper/stop
|
9
|
+
|
10
|
+
# Download & extract tasks
|
11
|
+
|
12
|
+
confluent/confluent.tgz:
|
13
|
+
mkdir -p confluent && wget http://packages.confluent.io/archive/1.0/confluent-1.0-2.10.4.tar.gz -O confluent/confluent.tgz
|
14
|
+
|
15
|
+
confluent/EXTRACTED: confluent/confluent.tgz
|
16
|
+
tar xzf confluent/confluent.tgz -C confluent --strip-components 1 && mkdir confluent/logs && touch confluent/EXTRACTED
|
17
|
+
echo "delete.topic.enable=true" >> confluent/etc/kafka/server.properties
|
18
|
+
|
19
|
+
# Zookeeper tasks
|
20
|
+
|
21
|
+
confluent/zookeeper/start: confluent/EXTRACTED
|
22
|
+
nohup confluent/bin/zookeeper-server-start confluent/etc/kafka/zookeeper.properties 2> confluent/logs/zookeeper.err > confluent/logs/zookeeper.out < /dev/null &
|
23
|
+
while ! nc localhost 2181 </dev/null; do echo "Waiting for zookeeper..."; sleep 1; done
|
24
|
+
|
25
|
+
confluent/zookeeper/stop: confluent/EXTRACTED
|
26
|
+
confluent/bin/zookeeper-server-stop
|
27
|
+
|
28
|
+
# Kafka tasks
|
29
|
+
|
30
|
+
confluent/kafka/start: confluent/zookeeper/start confluent/EXTRACTED
|
31
|
+
nohup confluent/bin/kafka-server-start confluent/etc/kafka/server.properties 2> confluent/logs/kafka.err > confluent/logs/kafka.out < /dev/null &
|
32
|
+
while ! nc localhost 9092 </dev/null; do echo "Waiting for Kafka..."; sleep 1; done
|
33
|
+
|
34
|
+
confluent/kafka/stop: confluent/EXTRACTED
|
35
|
+
confluent/bin/kafka-server-stop
|
36
|
+
|
37
|
+
# schema-registry tasks
|
38
|
+
|
39
|
+
confluent/registry/start: confluent/kafka/start confluent/EXTRACTED
|
40
|
+
nohup confluent/bin/schema-registry-start confluent/etc/schema-registry/schema-registry.properties 2> confluent/logs/schema-registry.err > confluent/logs/schema-registry.out < /dev/null &
|
41
|
+
while ! nc localhost 8081 </dev/null; do echo "Waiting for schema registry..."; sleep 1; done
|
42
|
+
|
43
|
+
confluent/registry/stop: confluent/EXTRACTED
|
44
|
+
confluent/bin/kafka-server-stop
|
45
|
+
|
46
|
+
# REST proxy tasks
|
47
|
+
|
48
|
+
confluent/rest/start: confluent/registry/start confluent/EXTRACTED
|
49
|
+
nohup confluent/bin/kafka-rest-start confluent/etc/kafka-rest/kafka-rest.properties 2> confluent/logs/kafka-rest.err > confluent/logs/kafka-rest.out < /dev/null &
|
50
|
+
while ! nc localhost 8082 </dev/null; do echo "Waiting for REST proxy..."; sleep 1; done
|
51
|
+
|
52
|
+
confluent/rest/stop: confluent/EXTRACTED
|
53
|
+
confluent/bin/kafka-rest-stop
|
data/lib/kazoo.rb
CHANGED
@@ -7,10 +7,15 @@ require 'securerandom'
|
|
7
7
|
module Kazoo
|
8
8
|
Error = Class.new(StandardError)
|
9
9
|
|
10
|
+
ValidationError = Class.new(Kazoo::Error)
|
10
11
|
NoClusterRegistered = Class.new(Kazoo::Error)
|
11
12
|
ConsumerInstanceRegistrationFailed = Class.new(Kazoo::Error)
|
12
13
|
PartitionAlreadyClaimed = Class.new(Kazoo::Error)
|
13
14
|
ReleasePartitionFailure = Class.new(Kazoo::Error)
|
15
|
+
|
16
|
+
def self.connect(zookeeper)
|
17
|
+
Kazoo::Cluster.new(zookeeper)
|
18
|
+
end
|
14
19
|
end
|
15
20
|
|
16
21
|
require 'kazoo/cluster'
|
data/lib/kazoo/cli.rb
CHANGED
@@ -3,7 +3,7 @@ require 'thor'
|
|
3
3
|
|
4
4
|
module Kazoo
|
5
5
|
class CLI < Thor
|
6
|
-
class_option :zookeeper, :
|
6
|
+
class_option :zookeeper, type: :string, default: ENV['ZOOKEEPER']
|
7
7
|
|
8
8
|
desc "cluster", "Describes the Kafka cluster as registered in Zookeeper"
|
9
9
|
def cluster
|
@@ -23,7 +23,25 @@ module Kazoo
|
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
-
|
26
|
+
desc "create_topic", "Creates a new topic"
|
27
|
+
option :name, type: :string, required: true
|
28
|
+
option :partitions, type: :numeric, required: true
|
29
|
+
option :replication_factor, type: :numeric, required: true
|
30
|
+
def create_topic
|
31
|
+
validate_class_options!
|
32
|
+
|
33
|
+
kafka_cluster.create_topic(options[:name], partitions: options[:partitions], replication_factor: options[:replication_factor])
|
34
|
+
end
|
35
|
+
|
36
|
+
desc "delete_topic", "Removes a topic"
|
37
|
+
option :name, type: :string, required: true
|
38
|
+
def delete_topic
|
39
|
+
validate_class_options!
|
40
|
+
|
41
|
+
kafka_cluster.topics.fetch(options[:name]).destroy
|
42
|
+
end
|
43
|
+
|
44
|
+
option :topic, type: :string
|
27
45
|
desc "partitions", "Lists partitions"
|
28
46
|
def partitions
|
29
47
|
validate_class_options!
|
@@ -39,7 +57,7 @@ module Kazoo
|
|
39
57
|
end
|
40
58
|
end
|
41
59
|
|
42
|
-
option :replicas, :
|
60
|
+
option :replicas, type: :numeric, default: 1
|
43
61
|
desc "critical <broker>", "Determine whether a broker is critical"
|
44
62
|
def critical(broker_name)
|
45
63
|
validate_class_options!
|
@@ -52,6 +70,65 @@ module Kazoo
|
|
52
70
|
end
|
53
71
|
|
54
72
|
|
73
|
+
desc "consumergroups", "Lists the consumergroups registered for this Kafka cluster"
|
74
|
+
def consumergroups
|
75
|
+
validate_class_options!
|
76
|
+
|
77
|
+
kafka_cluster.consumergroups.each do |group|
|
78
|
+
instances = group.instances
|
79
|
+
if instances.length == 0
|
80
|
+
puts "- #{group.name}: inactive"
|
81
|
+
else
|
82
|
+
puts "- #{group.name}: #{instances.length} running instances"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
desc "consumergroup", "Prints information about a consumer group"
|
88
|
+
option :name, type: :string, required: true
|
89
|
+
def consumergroup
|
90
|
+
validate_class_options!
|
91
|
+
|
92
|
+
cg = kafka_cluster.consumergroup(options[:name])
|
93
|
+
raise Kazoo::Error, "Consumergroup #{options[:name]} is not registered in Zookeeper" unless cg.exists?
|
94
|
+
|
95
|
+
puts "Consumer group: #{cg.name}\n"
|
96
|
+
|
97
|
+
if cg.active?
|
98
|
+
puts "Running instances:"
|
99
|
+
cg.instances.each do |instance|
|
100
|
+
puts "- #{instance.id}"
|
101
|
+
end
|
102
|
+
else
|
103
|
+
puts "This consumer group is inactive."
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
desc "delete_consumergroup", "Removes a consumer group from Zookeeper"
|
108
|
+
option :name, type: :string, required: true
|
109
|
+
def delete_consumergroup
|
110
|
+
validate_class_options!
|
111
|
+
|
112
|
+
cg = kafka_cluster.consumergroup(options[:name])
|
113
|
+
raise Kazoo::Error, "Consumergroup #{options[:name]} is not registered in Zookeeper" unless cg.exists?
|
114
|
+
raise Kazoo::Error, "Cannot remove consumergroup #{cg.name} because it's still active" if cg.active?
|
115
|
+
|
116
|
+
cg.destroy
|
117
|
+
end
|
118
|
+
|
119
|
+
desc "reset_consumergroup", "Resets all the offsets stored for a consumergroup"
|
120
|
+
option :name, type: :string, required: true
|
121
|
+
def reset_consumergroup
|
122
|
+
validate_class_options!
|
123
|
+
|
124
|
+
cg = kafka_cluster.consumergroup(options[:name])
|
125
|
+
raise Kazoo::Error, "Consumergroup #{options[:name]} is not registered in Zookeeper" unless cg.exists?
|
126
|
+
raise Kazoo::Error, "Cannot remove consumergroup #{cg.name} because it's still active" if cg.active?
|
127
|
+
|
128
|
+
cg.reset_all_offsets
|
129
|
+
end
|
130
|
+
|
131
|
+
|
55
132
|
private
|
56
133
|
|
57
134
|
def validate_class_options!
|
data/lib/kazoo/cluster.rb
CHANGED
@@ -47,6 +47,10 @@ module Kazoo
|
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
50
|
+
def consumergroup(name)
|
51
|
+
Kazoo::Consumergroup.new(self, name)
|
52
|
+
end
|
53
|
+
|
50
54
|
def topics
|
51
55
|
@topics_mutex.synchronize do
|
52
56
|
@topics ||= begin
|
@@ -70,6 +74,17 @@ module Kazoo
|
|
70
74
|
end
|
71
75
|
end
|
72
76
|
|
77
|
+
def topic(name)
|
78
|
+
Kazoo::Topic.new(self, name)
|
79
|
+
end
|
80
|
+
|
81
|
+
def create_topic(name, partitions: nil, replication_factor: nil)
|
82
|
+
raise ArgumentError, "partitions must be a positive integer" if Integer(partitions) <= 0
|
83
|
+
raise ArgumentError, "replication_factor must be a positive integer" if Integer(replication_factor) <= 0
|
84
|
+
|
85
|
+
Kazoo::Topic.create(self, name, partitions: Integer(partitions), replication_factor: Integer(replication_factor))
|
86
|
+
end
|
87
|
+
|
73
88
|
def partitions
|
74
89
|
topics.values.flat_map(&:partitions)
|
75
90
|
end
|
@@ -85,5 +100,42 @@ module Kazoo
|
|
85
100
|
def close
|
86
101
|
zk.close
|
87
102
|
end
|
103
|
+
|
104
|
+
protected
|
105
|
+
|
106
|
+
def recursive_create(path: nil)
|
107
|
+
raise ArgumentError, "path is a required argument" if path.nil?
|
108
|
+
|
109
|
+
result = zk.stat(path: path)
|
110
|
+
case result.fetch(:rc)
|
111
|
+
when Zookeeper::Constants::ZOK
|
112
|
+
return
|
113
|
+
when Zookeeper::Constants::ZNONODE
|
114
|
+
recursive_create(path: File.dirname(path))
|
115
|
+
result = zk.create(path: path)
|
116
|
+
raise Kazoo::Error, "Failed to create node #{path}. Result code: #{result.fetch(:rc)}" unless result.fetch(:rc) == Zookeeper::Constants::ZOK
|
117
|
+
else
|
118
|
+
raise Kazoo::Error, "Failed to create node #{path}. Result code: #{result.fetch(:rc)}"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def recursive_delete(path: nil)
|
123
|
+
raise ArgumentError, "path is a required argument" if path.nil?
|
124
|
+
|
125
|
+
result = zk.get_children(path: path)
|
126
|
+
raise Kazoo::Error, "Failed to list children of #{path} to delete them. Result code: #{result.fetch(:rc)}" if result.fetch(:rc) != Zookeeper::Constants::ZOK
|
127
|
+
|
128
|
+
threads = []
|
129
|
+
result.fetch(:children).each do |name|
|
130
|
+
threads << Thread.new do
|
131
|
+
Thread.abort_on_exception = true
|
132
|
+
recursive_delete(path: File.join(path, name))
|
133
|
+
end
|
134
|
+
threads.each(&:join)
|
135
|
+
end
|
136
|
+
|
137
|
+
result = zk.delete(path: path)
|
138
|
+
raise Kazoo::Error, "Failed to delete node #{path}. Result code: #{result.fetch(:rc)}" if result.fetch(:rc) != Zookeeper::Constants::ZOK
|
139
|
+
end
|
88
140
|
end
|
89
141
|
end
|
data/lib/kazoo/consumergroup.rb
CHANGED
@@ -7,10 +7,12 @@ module Kazoo
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def create
|
10
|
-
cluster.
|
11
|
-
cluster.
|
12
|
-
|
13
|
-
|
10
|
+
cluster.send(:recursive_create, path: "/consumers/#{name}/ids")
|
11
|
+
cluster.send(:recursive_create, path: "/consumers/#{name}/owners")
|
12
|
+
end
|
13
|
+
|
14
|
+
def destroy
|
15
|
+
cluster.send(:recursive_delete, path: "/consumers/#{name}")
|
14
16
|
end
|
15
17
|
|
16
18
|
def exists?
|
@@ -23,9 +25,20 @@ module Kazoo
|
|
23
25
|
Instance.new(self, id: id)
|
24
26
|
end
|
25
27
|
|
28
|
+
def active?
|
29
|
+
instances.length > 0
|
30
|
+
end
|
31
|
+
|
26
32
|
def instances
|
27
|
-
|
28
|
-
|
33
|
+
result = cluster.zk.get_children(path: "/consumers/#{name}/ids")
|
34
|
+
case result.fetch(:rc)
|
35
|
+
when Zookeeper::Constants::ZOK
|
36
|
+
result.fetch(:children).map { |id| Instance.new(self, id: id) }
|
37
|
+
when Zookeeper::Constants::ZNONODE
|
38
|
+
[]
|
39
|
+
else
|
40
|
+
raise Kazoo::Error, "Failed getting a list of runniong instances for #{name}. Error code: #{result.fetch(:rc)}"
|
41
|
+
end
|
29
42
|
end
|
30
43
|
|
31
44
|
def watch_instances(&block)
|
@@ -52,58 +65,82 @@ module Kazoo
|
|
52
65
|
when Zookeeper::Constants::ZOK
|
53
66
|
[Kazoo::Consumergroup::Instance.new(self, id: result.fetch(:data)), cb]
|
54
67
|
else
|
55
|
-
raise Kazoo::Error, "Failed set watch for partition claim of #{partition.topic.name}/#{partition.id}. Error code: #{result.fetch(:rc)}"
|
68
|
+
raise Kazoo::Error, "Failed to set watch for partition claim of #{partition.topic.name}/#{partition.id}. Error code: #{result.fetch(:rc)}"
|
56
69
|
end
|
57
70
|
end
|
58
71
|
|
59
72
|
def retrieve_offset(partition)
|
60
73
|
result = cluster.zk.get(path: "/consumers/#{name}/offsets/#{partition.topic.name}/#{partition.id}")
|
61
74
|
case result.fetch(:rc)
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
75
|
+
when Zookeeper::Constants::ZOK;
|
76
|
+
result.fetch(:data).to_i
|
77
|
+
when Zookeeper::Constants::ZNONODE;
|
78
|
+
nil
|
79
|
+
else
|
80
|
+
raise Kazoo::Error, "Failed to retrieve offset for partition #{partition.topic.name}/#{partition.id}. Error code: #{result.fetch(:rc)}"
|
68
81
|
end
|
69
82
|
end
|
70
83
|
|
71
|
-
def
|
72
|
-
|
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
|
84
|
+
def retrieve_all_offsets
|
85
|
+
offsets, threads, mutex = {}, [], Mutex.new
|
80
86
|
|
81
|
-
|
87
|
+
topic_result = cluster.zk.get_children(path: "/consumers/#{name}/offsets")
|
88
|
+
case topic_result.fetch(:rc)
|
89
|
+
when Zookeeper::Constants::ZOK;
|
90
|
+
# continue
|
91
|
+
when Zookeeper::Constants::ZNONODE;
|
92
|
+
return offsets
|
93
|
+
else
|
94
|
+
raise Kazoo::Error, "Failed to retrieve offset for partition #{partition.topic.name}/#{partition.id}. Error code: #{topic_result.fetch(:rc)}"
|
82
95
|
end
|
83
96
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
97
|
+
topic_result.fetch(:children).each do |topic_name|
|
98
|
+
threads << Thread.new do
|
99
|
+
Thread.abort_on_exception = true
|
100
|
+
|
101
|
+
topic = Kazoo::Topic.new(cluster, topic_name)
|
102
|
+
partition_result = cluster.zk.get_children(path: "/consumers/#{name}/offsets/#{topic.name}")
|
103
|
+
raise Kazoo::Error, "Failed to retrieve offsets. Error code: #{partition_result.fetch(:rc)}" if partition_result.fetch(:rc) != Zookeeper::Constants::ZOK
|
88
104
|
|
89
|
-
|
90
|
-
|
91
|
-
|
105
|
+
partition_threads = []
|
106
|
+
partition_result.fetch(:children).each do |partition_id|
|
107
|
+
partition_threads << Thread.new do
|
108
|
+
Thread.abort_on_exception = true
|
92
109
|
|
93
|
-
|
94
|
-
|
95
|
-
|
110
|
+
partition = topic.partition(partition_id.to_i)
|
111
|
+
offset_result = cluster.zk.get(path: "/consumers/#{name}/offsets/#{topic.name}/#{partition.id}")
|
112
|
+
raise Kazoo::Error, "Failed to retrieve offsets. Error code: #{offset_result.fetch(:rc)}" if offset_result.fetch(:rc) != Zookeeper::Constants::ZOK
|
96
113
|
|
97
|
-
|
98
|
-
|
99
|
-
|
114
|
+
mutex.synchronize { offsets[partition] = offset_result.fetch(:data).to_i }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
partition_threads.each(&:join)
|
100
118
|
end
|
119
|
+
end
|
120
|
+
|
121
|
+
threads.each(&:join)
|
122
|
+
return offsets
|
123
|
+
end
|
101
124
|
|
102
|
-
|
103
|
-
|
125
|
+
def commit_offset(partition, offset)
|
126
|
+
partition_offset_path = "/consumers/#{name}/offsets/#{partition.topic.name}/#{partition.id}"
|
127
|
+
next_offset_data = (offset + 1).to_s
|
128
|
+
|
129
|
+
result = cluster.zk.set(path: partition_offset_path, data: next_offset_data)
|
130
|
+
if result.fetch(:rc) == Zookeeper::Constants::ZNONODE
|
131
|
+
cluster.send(:recursive_create, path: File.dirname(partition_offset_path))
|
132
|
+
result = cluster.zk.create(path: partition_offset_path, data: next_offset_data)
|
133
|
+
end
|
134
|
+
|
135
|
+
if result.fetch(:rc) != Zookeeper::Constants::ZOK
|
136
|
+
raise Kazoo::Error, "Failed to commit offset #{offset} for partition #{partition.topic.name}/#{partition.id}. Error code: #{result.fetch(:rc)}"
|
104
137
|
end
|
105
138
|
end
|
106
139
|
|
140
|
+
def reset_all_offsets
|
141
|
+
cluster.send(:recursive_delete, path: "/consumers/#{name}/offsets")
|
142
|
+
end
|
143
|
+
|
107
144
|
def inspect
|
108
145
|
"#<Kazoo::Consumergroup name=#{name}>"
|
109
146
|
end
|
data/lib/kazoo/partition.rb
CHANGED
@@ -33,6 +33,19 @@ module Kazoo
|
|
33
33
|
isr.length < replication_factor
|
34
34
|
end
|
35
35
|
|
36
|
+
def validate
|
37
|
+
raise Kazoo::ValidationError, "No replicas defined for #{topic.name}/#{id}" if replicas.length == 0
|
38
|
+
raise Kazoo::ValidationError, "The replicas of #{topic.name}/#{id} should be assigned to different brokers" if replicas.length > replicas.uniq.length
|
39
|
+
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
def valid?
|
44
|
+
validate
|
45
|
+
rescue Kazoo::ValidationError
|
46
|
+
false
|
47
|
+
end
|
48
|
+
|
36
49
|
def inspect
|
37
50
|
"#<Kazoo::Partition #{topic.name}/#{id}>"
|
38
51
|
end
|
@@ -47,6 +60,19 @@ module Kazoo
|
|
47
60
|
[topic, id].hash
|
48
61
|
end
|
49
62
|
|
63
|
+
def wait_for_leader
|
64
|
+
current_leader = nil
|
65
|
+
while current_leader.nil?
|
66
|
+
current_leader = begin
|
67
|
+
leader
|
68
|
+
rescue Kazoo::Error
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
|
72
|
+
sleep(0.1) if current_leader.nil?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
50
76
|
protected
|
51
77
|
|
52
78
|
def refresh_state
|
data/lib/kazoo/topic.rb
CHANGED
@@ -1,11 +1,14 @@
|
|
1
1
|
module Kazoo
|
2
2
|
class Topic
|
3
|
+
VALID_TOPIC_NAMES = %r{\A[a-zA-Z0-9\\._\\-]+\z}
|
4
|
+
BLACKLISTED_TOPIC_NAMES = %r{\A\.\.?\z}
|
3
5
|
|
4
6
|
attr_reader :cluster, :name
|
5
7
|
attr_accessor :partitions
|
6
8
|
|
7
|
-
def initialize(cluster, name
|
8
|
-
@cluster, @name
|
9
|
+
def initialize(cluster, name)
|
10
|
+
@cluster, @name = cluster, name
|
11
|
+
@partitions = []
|
9
12
|
end
|
10
13
|
|
11
14
|
def self.from_json(cluster, name, json)
|
@@ -42,5 +45,117 @@ module Kazoo
|
|
42
45
|
def hash
|
43
46
|
[cluster, name].hash
|
44
47
|
end
|
48
|
+
|
49
|
+
def exists?
|
50
|
+
stat = cluster.zk.stat(path: "/brokers/topics/#{name}")
|
51
|
+
stat.fetch(:stat).exists?
|
52
|
+
end
|
53
|
+
|
54
|
+
def validate
|
55
|
+
raise Kazoo::ValidationError, "#{name} is not a valid topic name" if VALID_TOPIC_NAMES !~ name
|
56
|
+
raise Kazoo::ValidationError, "#{name} is not a valid topic name" if BLACKLISTED_TOPIC_NAMES =~ name
|
57
|
+
raise Kazoo::ValidationError, "#{name} is too long" if name.length > 255
|
58
|
+
raise Kazoo::ValidationError, "The topic has no partitions defined" if partitions.length == 0
|
59
|
+
partitions.each(&:validate)
|
60
|
+
|
61
|
+
true
|
62
|
+
end
|
63
|
+
|
64
|
+
def valid?
|
65
|
+
validate
|
66
|
+
rescue Kazoo::ValidationError
|
67
|
+
false
|
68
|
+
end
|
69
|
+
|
70
|
+
def create
|
71
|
+
raise Kazoo::Error, "The topic #{name} already exists!" if exists?
|
72
|
+
validate
|
73
|
+
|
74
|
+
result = cluster.zk.create(
|
75
|
+
path: "/brokers/topics/#{name}",
|
76
|
+
data: JSON.dump(version: 1, partitions: partitions_as_json)
|
77
|
+
)
|
78
|
+
|
79
|
+
if result.fetch(:rc) != Zookeeper::Constants::ZOK
|
80
|
+
raise Kazoo::Error, "Failed to create topic #{name}. Error code: #{result.fetch(:rc)}"
|
81
|
+
end
|
82
|
+
|
83
|
+
wait_for_partitions
|
84
|
+
end
|
85
|
+
|
86
|
+
def destroy
|
87
|
+
t = Thread.current
|
88
|
+
cb = Zookeeper::Callbacks::WatcherCallback.create do |event|
|
89
|
+
case event.type
|
90
|
+
when Zookeeper::Constants::ZOO_DELETED_EVENT
|
91
|
+
t.run if t.status == 'sleep'
|
92
|
+
else
|
93
|
+
raise Kazoo::Error, "Unexpected Zookeeper event: #{event.type}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
result = cluster.zk.stat(path: "/brokers/topics/#{name}", watcher: cb)
|
98
|
+
case result.fetch(:rc)
|
99
|
+
when Zookeeper::Constants::ZOK
|
100
|
+
# continue
|
101
|
+
when Zookeeper::Constants::ZNONODE
|
102
|
+
raise Kazoo::Error, "Topic #{name} does not exist!"
|
103
|
+
else
|
104
|
+
raise Kazoo::Error, "Failed to monitor topic"
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
result = cluster.zk.create(path: "/admin/delete_topics/#{name}")
|
109
|
+
case result.fetch(:rc)
|
110
|
+
when Zookeeper::Constants::ZOK
|
111
|
+
Thread.stop unless cb.completed?
|
112
|
+
when Zookeeper::Constants::ZNODEEXISTS
|
113
|
+
raise Kazoo::Error, "The topic #{name} is already marked for deletion!"
|
114
|
+
else
|
115
|
+
raise Kazoo::Error, "Failed to delete topic #{name}. Error code: #{result.fetch(:rc)}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.create(cluster, name, partitions: nil, replication_factor: nil)
|
120
|
+
topic = new(cluster, name)
|
121
|
+
topic.send(:sequentially_assign_partitions, partitions, replication_factor)
|
122
|
+
topic.create
|
123
|
+
topic
|
124
|
+
end
|
125
|
+
|
126
|
+
protected
|
127
|
+
|
128
|
+
def wait_for_partitions
|
129
|
+
threads = []
|
130
|
+
partitions.each do |partition|
|
131
|
+
threads << Thread.new do
|
132
|
+
Thread.abort_on_exception = true
|
133
|
+
partition.wait_for_leader
|
134
|
+
end
|
135
|
+
end
|
136
|
+
threads.each(&:join)
|
137
|
+
end
|
138
|
+
|
139
|
+
def sequentially_assign_partitions(partition_count, replication_factor, brokers: nil)
|
140
|
+
brokers = cluster.brokers.values if brokers.nil?
|
141
|
+
raise ArgumentError, "replication_factor should be smaller or equal to the number of brokers" if replication_factor > brokers.length
|
142
|
+
|
143
|
+
# Sequentially assign replicas to brokers. There might be a better way.
|
144
|
+
@partitions = 0.upto(partition_count - 1).map do |partition_index|
|
145
|
+
replicas = 0.upto(replication_factor - 1).map do |replica_index|
|
146
|
+
broker_index = (partition_index + replica_index) % brokers.length
|
147
|
+
brokers[broker_index]
|
148
|
+
end
|
149
|
+
|
150
|
+
self.partition(partition_index, replicas: replicas)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def partitions_as_json
|
155
|
+
partitions.inject({}) do |hash, partition|
|
156
|
+
hash[partition.id] = partition.replicas.map(&:id)
|
157
|
+
hash
|
158
|
+
end
|
159
|
+
end
|
45
160
|
end
|
46
161
|
end
|
data/lib/kazoo/version.rb
CHANGED
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class FunctionalConsumergroupTest < Minitest::Test
|
4
|
+
def setup
|
5
|
+
zookeeper = ENV["ZOOKEEPER_PEERS"] || "127.0.0.1:2181"
|
6
|
+
@cluster = Kazoo.connect(zookeeper)
|
7
|
+
@cg = Kazoo::Consumergroup.new(@cluster, 'test.kazoo')
|
8
|
+
@cg.create
|
9
|
+
end
|
10
|
+
|
11
|
+
def teardown
|
12
|
+
cg = Kazoo::Consumergroup.new(@cluster, 'test.kazoo')
|
13
|
+
cg.destroy if cg.exists?
|
14
|
+
|
15
|
+
@cluster.close
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_create_and_destroy_consumergroup
|
19
|
+
cg = Kazoo::Consumergroup.new(@cluster, 'test.kazoo.2')
|
20
|
+
refute cg.exists?
|
21
|
+
|
22
|
+
cg.create
|
23
|
+
assert cg.exists?
|
24
|
+
|
25
|
+
cg.destroy
|
26
|
+
refute cg.exists?
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_retrieve_and_commit_offsets
|
30
|
+
topic = Kazoo::Topic.new(@cluster, 'test.1')
|
31
|
+
partition = Kazoo::Partition.new(topic, 0)
|
32
|
+
|
33
|
+
assert_nil @cg.retrieve_offset(partition)
|
34
|
+
|
35
|
+
@cg.commit_offset(partition, 1234)
|
36
|
+
|
37
|
+
assert_equal 1234 + 1, @cg.retrieve_offset(partition)
|
38
|
+
|
39
|
+
@cg.reset_all_offsets
|
40
|
+
assert_nil @cg.retrieve_offset(partition)
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_retrieve_all_offsets
|
44
|
+
topic1 = Kazoo::Topic.new(@cluster, 'test.1')
|
45
|
+
partition10 = Kazoo::Partition.new(topic1, 0)
|
46
|
+
|
47
|
+
topic4 = Kazoo::Topic.new(@cluster, 'test.4')
|
48
|
+
partition40 = Kazoo::Partition.new(topic4, 0)
|
49
|
+
partition41 = Kazoo::Partition.new(topic4, 1)
|
50
|
+
partition42 = Kazoo::Partition.new(topic4, 2)
|
51
|
+
partition43 = Kazoo::Partition.new(topic4, 3)
|
52
|
+
|
53
|
+
assert_equal Hash.new, @cg.retrieve_all_offsets
|
54
|
+
|
55
|
+
@cg.commit_offset(partition10, 10)
|
56
|
+
@cg.commit_offset(partition40, 40)
|
57
|
+
@cg.commit_offset(partition41, 41)
|
58
|
+
@cg.commit_offset(partition42, 42)
|
59
|
+
@cg.commit_offset(partition43, 43)
|
60
|
+
|
61
|
+
offsets = @cg.retrieve_all_offsets
|
62
|
+
|
63
|
+
assert_equal 5, offsets.length
|
64
|
+
assert_equal 11, offsets[partition10]
|
65
|
+
assert_equal 41, offsets[partition40]
|
66
|
+
assert_equal 42, offsets[partition41]
|
67
|
+
assert_equal 43, offsets[partition42]
|
68
|
+
assert_equal 44, offsets[partition43]
|
69
|
+
|
70
|
+
@cg.reset_all_offsets
|
71
|
+
assert_equal Hash.new, @cg.retrieve_all_offsets
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_watch_instances
|
75
|
+
topic = Kazoo::Topic.new(@cluster, 'test.1')
|
76
|
+
|
77
|
+
instance1 = @cg.instantiate
|
78
|
+
instance1.register([topic])
|
79
|
+
instance2 = @cg.instantiate
|
80
|
+
instance2.register([topic])
|
81
|
+
|
82
|
+
t = Thread.current
|
83
|
+
instances, cb = @cg.watch_instances { t.run if t.status == 'sleep' }
|
84
|
+
assert_equal Set[instance1, instance2], Set.new(instances)
|
85
|
+
|
86
|
+
Thread.new { instance2.deregister }
|
87
|
+
|
88
|
+
Thread.stop unless cb.completed?
|
89
|
+
|
90
|
+
assert assert_equal Set[instance1], Set.new(@cg.instances)
|
91
|
+
instance1.deregister
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class FunctionalTopicManagementTest < Minitest::Test
|
4
|
+
def setup
|
5
|
+
zookeeper = ENV["ZOOKEEPER_PEERS"] || "127.0.0.1:2181"
|
6
|
+
@cluster = Kazoo.connect(zookeeper)
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_create_and_delete_topic
|
10
|
+
topic = @cluster.create_topic('test.kazoo', partitions: 8, replication_factor: 1)
|
11
|
+
|
12
|
+
assert @cluster.topics.key?(topic.name)
|
13
|
+
assert topic.partitions.all? { |partition| @cluster.brokers.values.include?(partition.leader) }
|
14
|
+
assert_equal 8, topic.partitions.length
|
15
|
+
|
16
|
+
topic.destroy
|
17
|
+
@cluster.reset_metadata
|
18
|
+
|
19
|
+
refute topic.exists?
|
20
|
+
refute @cluster.topics.key?(topic.name)
|
21
|
+
end
|
22
|
+
end
|
data/test/partition_test.rb
CHANGED
@@ -37,4 +37,10 @@ class PartitionTest < Minitest::Test
|
|
37
37
|
assert p1 != Kazoo::Partition.new(@cluster.topics['test.4'], 0)
|
38
38
|
assert_equal p1.hash, p2.hash
|
39
39
|
end
|
40
|
+
|
41
|
+
def test_validate
|
42
|
+
partition = Kazoo::Partition.new(@cluster.topics['test.1'], 1, replicas: [@cluster.brokers[1], @cluster.brokers[1]])
|
43
|
+
refute partition.valid?
|
44
|
+
assert_raises(Kazoo::ValidationError) { partition.validate }
|
45
|
+
end
|
40
46
|
end
|
data/test/topic_test.rb
CHANGED
@@ -50,4 +50,45 @@ class TopicTest < Minitest::Test
|
|
50
50
|
assert t1 != Kazoo::Topic.new(@cluster, 'test.2')
|
51
51
|
assert_equal t1.hash, t2.hash
|
52
52
|
end
|
53
|
+
|
54
|
+
def test_validate
|
55
|
+
t = Kazoo::Topic.new(@cluster, "normal")
|
56
|
+
t.partitions = [t.partition(0, replicas: [@cluster.brokers[1]])]
|
57
|
+
assert t.valid?
|
58
|
+
|
59
|
+
t = Kazoo::Topic.new(@cluster, "invalid/character")
|
60
|
+
t.partitions = [t.partition(0, replicas: [@cluster.brokers[1]])]
|
61
|
+
refute t.valid?
|
62
|
+
|
63
|
+
t = Kazoo::Topic.new(@cluster, "..")
|
64
|
+
t.partitions = [t.partition(0, replicas: [@cluster.brokers[1]])]
|
65
|
+
refute t.valid?
|
66
|
+
|
67
|
+
t = Kazoo::Topic.new(@cluster, "l#{'o' * 253}ng")
|
68
|
+
t.partitions = [t.partition(0, replicas: [@cluster.brokers[1]])]
|
69
|
+
refute t.valid?
|
70
|
+
|
71
|
+
t = Kazoo::Topic.new(@cluster, "normal")
|
72
|
+
t.partitions = [t.partition(0, replicas: [])]
|
73
|
+
refute t.valid?
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_sequentially_assign_partitions
|
77
|
+
topic = Kazoo::Topic.new(@cluster, 'test.new')
|
78
|
+
|
79
|
+
assert_raises(ArgumentError) { topic.send(:sequentially_assign_partitions, 4, 100) }
|
80
|
+
|
81
|
+
topic.send(:sequentially_assign_partitions, 4, 3)
|
82
|
+
|
83
|
+
assert_equal 4, topic.partitions.length
|
84
|
+
assert_equal 3, topic.replication_factor
|
85
|
+
assert topic.partitions.all? { |p| p.replicas.length == 3 }
|
86
|
+
assert topic.valid?
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_partitions_as_json
|
90
|
+
assignment = @cluster.topics['test.1'].send(:partitions_as_json)
|
91
|
+
assert_equal 1, assignment.length
|
92
|
+
assert_equal [1,2], assignment[0]
|
93
|
+
end
|
53
94
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kazoo-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
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-
|
11
|
+
date: 2015-08-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -106,6 +106,7 @@ files:
|
|
106
106
|
- .travis.yml
|
107
107
|
- Gemfile
|
108
108
|
- LICENSE.txt
|
109
|
+
- Makefile
|
109
110
|
- README.md
|
110
111
|
- Rakefile
|
111
112
|
- bin/kazoo
|
@@ -120,6 +121,8 @@ files:
|
|
120
121
|
- lib/kazoo/version.rb
|
121
122
|
- test/broker_test.rb
|
122
123
|
- test/cluster_test.rb
|
124
|
+
- test/functional/functional_consumergroup_test.rb
|
125
|
+
- test/functional/functional_topic_management_test.rb
|
123
126
|
- test/partition_test.rb
|
124
127
|
- test/test_helper.rb
|
125
128
|
- test/topic_test.rb
|
@@ -150,6 +153,8 @@ summary: Library to access and manipulate Kafka metadata in Zookeeper
|
|
150
153
|
test_files:
|
151
154
|
- test/broker_test.rb
|
152
155
|
- test/cluster_test.rb
|
156
|
+
- test/functional/functional_consumergroup_test.rb
|
157
|
+
- test/functional/functional_topic_management_test.rb
|
153
158
|
- test/partition_test.rb
|
154
159
|
- test/test_helper.rb
|
155
160
|
- test/topic_test.rb
|