ktl 1.0.0-java

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.
@@ -0,0 +1,41 @@
1
+ # encoding: utf-8
2
+
3
+ module Ktl
4
+ class ClusterStatsTask
5
+ def initialize(zk_client, shell)
6
+ @zk_client = zk_client
7
+ @shell = shell
8
+ end
9
+
10
+ def execute
11
+ brokers = @zk_client.brokers
12
+ partitions = @zk_client.all_partitions
13
+ topics = extract_topics(partitions)
14
+ leaders = @zk_client.leader_and_isr_for(partitions)
15
+ ownership = broker_ownership(leaders)
16
+ @shell.say 'Cluster status:'
17
+ @shell.say ' topics: %d (%d partitions)' % [topics.size, partitions.size]
18
+ @shell.say ' brokers: %d' % [brokers.size]
19
+ brokers.foreach do |broker|
20
+ leader_for = ownership[broker.id]
21
+ share = leader_for.fdiv(partitions.size.to_f) * 100
22
+ @shell.say ' - %d leader for %d partitions (%.2f %%)' % [broker.id, leader_for, share]
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def extract_topics(partitions)
29
+ partitions.map(proc { |tp| tp.topic }, CanBuildFrom).to_seq
30
+ end
31
+
32
+ def broker_ownership(leaders)
33
+ result = Hash.new(0)
34
+ leaders.foreach do |item|
35
+ leader = item.last.leader_and_isr.leader
36
+ result[leader] += 1
37
+ end
38
+ result
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: utf-8
2
+
3
+ module Ktl
4
+ class Command < Thor
5
+
6
+ java_import 'java.io.ByteArrayOutputStream'
7
+
8
+ private
9
+
10
+ def with_zk_client
11
+ zk_client = ZookeeperClient.new(options.zookeeper).setup
12
+ yield zk_client
13
+ rescue => e
14
+ logger.error '%s (%s)' % [e.message, e.class.name]
15
+ logger.debug e.backtrace.join($/)
16
+ $stderr.puts '%s (%s)' % [e.message, e.class.name]
17
+ $stderr.puts e.backtrace.join($/)
18
+ ensure
19
+ zk_client.close if zk_client
20
+ end
21
+
22
+ def logger
23
+ @logger ||= Logger.new($stdout).tap do |log|
24
+ log.formatter = ShellFormater.new(shell)
25
+ end
26
+ end
27
+
28
+ def silence_scala(&block)
29
+ baos = ByteArrayOutputStream.new
30
+ Scala::Console.with_out(baos) { block.call }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,68 @@
1
+ # encoding: utf-8
2
+
3
+ module Ktl
4
+ class DecommissionPlan
5
+ def initialize(zk_client, broker_id)
6
+ @zk_client = zk_client
7
+ @broker_id = broker_id
8
+ @replicas_count = Hash.new(0)
9
+ @leaders_count = Hash.new(0)
10
+ end
11
+
12
+ def generate
13
+ plan = Scala::Collection::Map.empty
14
+ brokers = @zk_client.broker_ids
15
+ brokers = brokers - @broker_id
16
+ partitions = @zk_client.all_partitions
17
+ topics = topics_from(partitions)
18
+ assignments = @zk_client.replica_assignment_for_topics(topics)
19
+ count_leaders_and_replicas(assignments)
20
+ partitions = ScalaEnumerable.new(partitions).sort_by { |tp| tp.topic + tp.partition.to_s }
21
+ partitions.each do |tp|
22
+ replicas = assignments[tp]
23
+ if replicas.contains?(@broker_id)
24
+ if brokers.size >= replicas.size
25
+ brokers_diff = ScalaEnumerable.new(brokers.diff(replicas)).sort
26
+ broker_index = replicas.index_of(@broker_id)
27
+ new_broker = elect_new_broker(broker_index, brokers_diff)
28
+ new_replicas = replicas.updated(broker_index, new_broker, CanBuildFrom)
29
+ plan += Scala::Tuple.new(tp, new_replicas)
30
+ else
31
+ raise InsufficientBrokersRemainingError, %(#{brokers.size} remaining brokers, #{replicas.size} replicas needed)
32
+ end
33
+ end
34
+ end
35
+ plan
36
+ end
37
+
38
+ private
39
+
40
+ def elect_new_broker(broker_index, diff)
41
+ if broker_index.zero?
42
+ new_broker = diff.min_by { |broker| @leaders_count[broker] }
43
+ @leaders_count[new_broker] += 1
44
+ else
45
+ new_broker = diff.min_by { |broker| @replicas_count[broker] }
46
+ @replicas_count[new_broker] += 1
47
+ end
48
+ new_broker
49
+ end
50
+
51
+ def count_leaders_and_replicas(assignments)
52
+ assignments.foreach do |assignment|
53
+ replicas = ScalaEnumerable.new(assignment.last)
54
+ replicas.each_with_index do |broker, index|
55
+ if index.zero?
56
+ @leaders_count[broker] += 1
57
+ else
58
+ @replicas_count[broker] += 1
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ def topics_from(partitions)
65
+ partitions.map(proc { |tp| tp.topic }, CanBuildFrom).to_seq
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+
3
+ module Ktl
4
+ class MigrationPlan
5
+ def initialize(zk_client, old_leader, new_leader)
6
+ @zk_client = zk_client
7
+ @old_leader = old_leader.to_java
8
+ @new_leader = new_leader.to_java
9
+ end
10
+
11
+ def generate
12
+ plan = Scala::Collection::Map.empty
13
+ topics = @zk_client.all_topics
14
+ assignments = ScalaEnumerable.new(@zk_client.replica_assignment_for_topics(topics))
15
+ assignments.each do |item|
16
+ topic_partition = item.first
17
+ replicas = item.last
18
+ if replicas.contains?(@old_leader)
19
+ index = replicas.index_of(@old_leader)
20
+ new_replicas = replicas.updated(index, @new_leader, CanBuildFrom)
21
+ plan += Scala::Tuple.new(topic_partition, new_replicas)
22
+ end
23
+ end
24
+ plan
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,133 @@
1
+ # encoding: utf-8
2
+
3
+ module Ktl
4
+ class Reassigner
5
+ def initialize(zk_client, options={})
6
+ @zk_client = zk_client
7
+ @limit = options[:limit]
8
+ @overflow_path = '/ktl/overflow'
9
+ @state_path = '/ktl/reassign'
10
+ @logger = options[:logger] || NullLogger.new
11
+ @log_assignments = !!options[:log_assignments]
12
+ end
13
+
14
+ def reassignment_in_progress?
15
+ partitions = @zk_client.partitions_being_reassigned
16
+ partitions.size > 0
17
+ end
18
+
19
+ def overflow?
20
+ overflow_znodes = @zk_client.get_children(@overflow_path)
21
+ overflow_znodes.size > 0
22
+ rescue ZkClient::Exception::ZkNoNodeException
23
+ false
24
+ end
25
+
26
+ def load_overflow
27
+ overflow = Scala::Collection::Map.empty
28
+ overflow_nodes = @zk_client.get_children(@overflow_path)
29
+ overflow_nodes.foreach do |index|
30
+ overflow_json = @zk_client.read_data(overflow_path(index)).first
31
+ data = parse_reassignment_json(overflow_json)
32
+ overflow = overflow.send('++', data)
33
+ end
34
+ delete_previous_overflow
35
+ overflow
36
+ end
37
+
38
+ def execute(reassignment)
39
+ reassignments = split(reassignment, @limit)
40
+ actual_reassignment = reassignments.shift
41
+ if @log_assignments
42
+ Scala::Collection::JavaConversions.as_java_iterable(actual_reassignment).each do |pr|
43
+ topic_and_partition, replicas = pr.elements
44
+ brokers = Scala::Collection::JavaConversions.as_java_iterable(replicas).to_a
45
+ @logger.info "Assigning #{topic_and_partition.topic},#{topic_and_partition.partition} to #{brokers.join(',')}"
46
+ end
47
+ end
48
+ json = reassignment_json(actual_reassignment)
49
+ @zk_client.reassign_partitions(json)
50
+ manage_overflow(reassignments)
51
+ manage_progress_state(actual_reassignment)
52
+ end
53
+
54
+ private
55
+
56
+ JSON_MAX_SIZE = 1024**2
57
+
58
+ def manage_progress_state(reassignment)
59
+ delete_previous_state
60
+ json = reassignment_json(reassignment)
61
+ @zk_client.create_znode(@state_path, json)
62
+ end
63
+
64
+ def delete_previous_state
65
+ if @zk_client.exists?(@state_path)
66
+ @zk_client.delete_znode(@state_path)
67
+ end
68
+ end
69
+
70
+ def delete_previous_overflow
71
+ overflow = @zk_client.get_children(@overflow_path)
72
+ overflow.foreach do |index|
73
+ @zk_client.delete_znode(overflow_path(index))
74
+ end
75
+ rescue ZkClient::Exception::ZkNoNodeException
76
+ end
77
+
78
+ def manage_overflow(reassignments)
79
+ delete_previous_overflow
80
+ empty_map = Scala::Collection::Map.empty
81
+ overflow = reassignments.reduce(empty_map) do |acc, data|
82
+ acc.send('++', data)
83
+ end
84
+ if overflow.size > 0
85
+ write_overflow(split(overflow))
86
+ end
87
+ end
88
+
89
+ def overflow_path(index)
90
+ [@overflow_path, index].join('/')
91
+ end
92
+
93
+ def write_overflow(reassignments)
94
+ reassignments.each_with_index do |reassignment, index|
95
+ overflow_json = reassignment_json(reassignment)
96
+ @zk_client.create_znode(overflow_path(index), overflow_json)
97
+ end
98
+ end
99
+
100
+ def reassignment_json(reassignment)
101
+ zk_utils.format_as_reassignment_json(reassignment)
102
+ end
103
+
104
+ def parse_reassignment_json(json)
105
+ zk_utils.parse_partition_reassignment_data(json)
106
+ end
107
+
108
+ def zk_utils
109
+ @zk_utils ||= Kafka::Utils::ZkUtils.new(nil, nil, false)
110
+ end
111
+
112
+ def maybe_split_by_limit(reassignment, limit=nil)
113
+ if limit
114
+ splitted = ScalaEnumerable.new(reassignment.grouped(limit)).map(&:seq)
115
+ else
116
+ splitted = [reassignment]
117
+ end
118
+ end
119
+
120
+ def split(reassignment, limit=nil)
121
+ splitted = maybe_split_by_limit(reassignment, limit)
122
+ bytesize = reassignment_json(splitted.first).bytesize
123
+ while bytesize > JSON_MAX_SIZE do
124
+ splitted = splitted.flat_map do |s|
125
+ group_size = s.size.fdiv(2).round
126
+ ScalaEnumerable.new(s.grouped(group_size)).map(&:seq)
127
+ end
128
+ bytesize = reassignment_json(splitted.first).bytesize
129
+ end
130
+ splitted
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,57 @@
1
+ # encoding: utf-8
2
+
3
+ module Ktl
4
+ class ReassignmentProgress
5
+ def initialize(zk_client, options={})
6
+ @zk_client = zk_client
7
+ @utils = options[:utils] || Kafka::Utils::ZkUtils
8
+ @logger = options[:logger] || NullLogger.new
9
+ @state_path = '/ktl/reassign'
10
+ @options = options
11
+ end
12
+
13
+ def display(shell)
14
+ in_progress = reassignment_in_progress
15
+ original = original_reassignment
16
+ if in_progress && !in_progress.empty?
17
+ original_size, remaining_size = original.size, in_progress.size
18
+ done_percentage = (original_size - remaining_size).fdiv(original_size) * 100
19
+ @logger.info 'remaining partitions to reassign: %d (%.2f%% done)' % [remaining_size, done_percentage]
20
+ if @options[:verbose]
21
+ shell.print_table(table_data(in_progress), indent: 2 + 6)
22
+ end
23
+ else
24
+ @logger.info 'no partitions remaining to reassign'
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def table_data(reassignments)
31
+ topics = reassignments.group_by { |r| r['topic'] }
32
+ table = topics.map do |t, r|
33
+ reassignments = r.sort_by { |r| r['partition'] }
34
+ reassignments = reassignments.map { |r| '%d => %s' % [r['partition'], r['replicas'].inspect] }.join(', ')
35
+ [t, reassignments]
36
+ end.sort_by(&:first)
37
+ table.unshift(%w[topic assignments])
38
+ table
39
+ end
40
+
41
+ def reassignment_in_progress
42
+ read_json(@utils.reassign_partitions_path).fetch('partitions')
43
+ rescue ZkClient::Exception::ZkNoNodeException
44
+ {}
45
+ end
46
+
47
+ def original_reassignment
48
+ read_json(@state_path).fetch('partitions')
49
+ rescue ZkClient::Exception::ZkNoNodeException
50
+ {}
51
+ end
52
+
53
+ def read_json(path)
54
+ JSON.parse(@zk_client.read_data(path).first)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,46 @@
1
+ # encoding: utf-8
2
+
3
+ module Ktl
4
+ class ReassignmentTask
5
+ def initialize(reassigner, plan, shell, options={})
6
+ @reassigner = reassigner
7
+ @plan = plan
8
+ @shell = shell
9
+ @logger = options[:logger] || NullLogger.new
10
+ end
11
+
12
+ def execute(dryrun = false)
13
+ if @reassigner.reassignment_in_progress?
14
+ @logger.warn 'reassignment already in progress, exiting'
15
+ else
16
+ if use_overflow?
17
+ @logger.info 'loading overflow data'
18
+ reassignment = @reassigner.load_overflow
19
+ else
20
+ @logger.info 'generating a new reassignment plan'
21
+ reassignment = @plan.generate
22
+ end
23
+ if reassignment.size > 0
24
+ @logger.info 'reassigning %d partitions' % reassignment.size
25
+ if dryrun
26
+ @logger.info 'dryrun detected, skipping reassignment'
27
+ else
28
+ @reassigner.execute(reassignment)
29
+ end
30
+ else
31
+ @logger.warn 'empty reassignment, ignoring'
32
+ end
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def use_overflow?
39
+ if @reassigner.overflow?
40
+ @logger.info 'overflow from previous reassignment found, use? [y/n]'
41
+ @shell.yes? ' ' * 8 << '>'
42
+ end
43
+ end
44
+ end
45
+ end
46
+
@@ -0,0 +1,32 @@
1
+ # encoding: utf-8
2
+
3
+ module Ktl
4
+ class ShellFormater < Logger::Formatter
5
+ def initialize(shell)
6
+ @shell = shell
7
+ @padding = ' ' * 2
8
+ end
9
+
10
+ def call(severity, time, progname, msg)
11
+ severity_part = sprintf('%-5s', severity.downcase)
12
+ severity_part = @shell.set_color(severity_part, color_for[severity])
13
+ line = %(#{@padding}#{severity_part} #{msg2str(msg)})
14
+ line << NEWLINE unless line.end_with?(NEWLINE)
15
+ line
16
+ end
17
+
18
+ private
19
+
20
+ NEWLINE = "\n".freeze
21
+
22
+ def color_for
23
+ @color_for ||= {
24
+ 'DEBUG' => :cyan,
25
+ 'INFO' => :magenta,
26
+ 'WARN' => :yellow,
27
+ 'ERROR' => :red,
28
+ 'FATAL' => :red,
29
+ }
30
+ end
31
+ end
32
+ end