elasticsearch-drain 0.0.6 → 0.1.4

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: 5636da7177de03b2cc46e1f5f9c3382c542c25b1
4
- data.tar.gz: ae9b51d1c2ace5e83497a6a92ba956523d713e01
3
+ metadata.gz: 59b1a35ac25472ad0caf43d4f30b5370bf59250f
4
+ data.tar.gz: 43dfb6c5d57c216ad4a947d86402c13ecd8148b4
5
5
  SHA512:
6
- metadata.gz: 7bbe3a3434ff69a13826251cb29561a6d95431294a7bc8590883480edbba598ff206c4d17d048a17a053e95fae4fb53f19ddf624599bf334215fa0b79ab34c1b
7
- data.tar.gz: dfec04ba5e72334f4d75a597b512230f93670374d22afcbfc92f4b6e3a0683a2cf191394dba4072b84c68fefdf25f1def1417984d5a44f1f4d6a2616142ec346
6
+ metadata.gz: 9844b71974cd6f7b838e7eb5a40de1f0d0440e0108c4dbe26dfa827329aeb95e7306624e22d2a2d5946bd9d3f8cd193bbb276e8a3bb9a5e4cffe60857a77223e
7
+ data.tar.gz: 3445323f27e4b549e27de00af09285cfec0ab9db3fa9a648c261a7b80d657a957d306008e754344432183359ca815f4938122e9ef2180eb7c5dc3b90367092e9
@@ -14,25 +14,25 @@ GEM
14
14
  ast (2.1.0)
15
15
  astrolabe (1.3.1)
16
16
  parser (~> 2.2)
17
- aws-sdk (2.4.4)
18
- aws-sdk-resources (= 2.4.4)
19
- aws-sdk-core (2.4.4)
17
+ aws-sdk (2.6.14)
18
+ aws-sdk-resources (= 2.6.14)
19
+ aws-sdk-core (2.6.14)
20
20
  jmespath (~> 1.0)
21
- aws-sdk-resources (2.4.4)
22
- aws-sdk-core (= 2.4.4)
21
+ aws-sdk-resources (2.6.14)
22
+ aws-sdk-core (= 2.6.14)
23
23
  coderay (1.1.0)
24
24
  crack (0.4.2)
25
25
  safe_yaml (~> 1.0.0)
26
26
  docile (1.1.5)
27
- elasticsearch (1.0.14)
28
- elasticsearch-api (= 1.0.14)
29
- elasticsearch-transport (= 1.0.14)
30
- elasticsearch-api (1.0.14)
27
+ elasticsearch (1.1.0)
28
+ elasticsearch-api (= 1.1.0)
29
+ elasticsearch-transport (= 1.1.0)
30
+ elasticsearch-api (1.1.0)
31
31
  multi_json
32
32
  elasticsearch-extensions (0.0.18)
33
33
  ansi
34
34
  ruby-prof
35
- elasticsearch-transport (1.0.14)
35
+ elasticsearch-transport (1.1.0)
36
36
  faraday
37
37
  multi_json
38
38
  faraday (0.9.2)
@@ -127,3 +127,6 @@ DEPENDENCIES
127
127
  vcr (~> 2.9)
128
128
  webmock (~> 1.21)
129
129
  yard (~> 0.8)
130
+
131
+ BUNDLED WITH
132
+ 1.13.2
@@ -38,12 +38,12 @@ module Elasticsearch
38
38
  @asg_client ||= AutoScaling.new(@asg_name, @region)
39
39
  end
40
40
 
41
- # Convience method to access {Elasticsearch::Drain::Nodes}
41
+ # Convenience method to access {Elasticsearch::Drain::Nodes}
42
42
  def nodes
43
43
  @nodes ||= Nodes.new(client, asg)
44
44
  end
45
45
 
46
- # Convience method to access {Elasticsearch::Drain::Cluster#cluster}
46
+ # Convenience method to access {Elasticsearch::Drain::Cluster#cluster}
47
47
  #
48
48
  # @return [Elasticsearch::API::Cluster] Elasticsearch cluster client
49
49
  def cluster
@@ -8,20 +8,27 @@ module Elasticsearch
8
8
  attr_reader :asg
9
9
 
10
10
  # @attribute [r]
11
- # EC2 Client
12
- attr_reader :ec2_client
11
+ # AWS region
12
+ attr_reader :region
13
13
 
14
14
  def initialize(asg, region)
15
15
  @asg = asg
16
- @asg_client = Aws::AutoScaling::Client.new(region: region)
17
- @ec2_client = Aws::EC2::Client.new(region: region)
16
+ @region = region
18
17
  @instances = nil
19
18
  @instance_ids = nil
20
19
  end
21
20
 
21
+ def asg_client
22
+ Aws::AutoScaling::Client.new(region: region)
23
+ end
24
+
25
+ def ec2_client
26
+ Aws::EC2::Client.new(region: region)
27
+ end
28
+
22
29
  def find_instances_in_asg
23
30
  instances = []
24
- @asg_client.describe_auto_scaling_instances.each do |page|
31
+ asg_client.describe_auto_scaling_instances.each do |page|
25
32
  instances << page.auto_scaling_instances.map do |i|
26
33
  i.instance_id if i.auto_scaling_group_name == asg
27
34
  end
@@ -38,7 +45,7 @@ module Elasticsearch
38
45
  instances = []
39
46
  find_instances_in_asg if @instance_ids.nil?
40
47
  return [] if @instance_ids.empty?
41
- @ec2_client.describe_instances(instance_ids: @instance_ids).each do |page|
48
+ ec2_client.describe_instances(instance_ids: @instance_ids).each do |page|
42
49
  instances << page.reservations.map(&:instances)
43
50
  end
44
51
  instances.flatten!
@@ -49,7 +56,7 @@ module Elasticsearch
49
56
  #
50
57
  # @return [Struct] AutoScaling Group
51
58
  def describe_autoscaling_group
52
- groups = @asg_client.describe_auto_scaling_groups(
59
+ groups = asg_client.describe_auto_scaling_groups(
53
60
  auto_scaling_group_names: [asg]
54
61
  )
55
62
  groups.auto_scaling_groups.first
@@ -81,11 +88,11 @@ module Elasticsearch
81
88
  # @option [FixNum] count (0) The new MinSize of the AutoScalingGroup
82
89
  # @return [Struct] Empty response from the sdk
83
90
  def min_size=(count = 0)
84
- @asg_client.update_auto_scaling_group(
91
+ asg_client.update_auto_scaling_group(
85
92
  auto_scaling_group_name: asg,
86
93
  min_size: count
87
94
  )
88
- wait_until(0) do
95
+ wait_until(count) do
89
96
  min_size
90
97
  end
91
98
  end
@@ -108,7 +115,7 @@ module Elasticsearch
108
115
 
109
116
  def detach_instance(instance_id)
110
117
  current_desired_capacity = desired_capacity
111
- @asg_client.detach_instances(
118
+ asg_client.detach_instances(
112
119
  instance_ids: [instance_id],
113
120
  auto_scaling_group_name: asg,
114
121
  should_decrement_desired_capacity: true
@@ -2,35 +2,93 @@ require 'thor'
2
2
 
3
3
  module Elasticsearch
4
4
  class Drain
5
- class CLI < ::Thor
5
+ class CLI < ::Thor # rubocop:disable Metrics/ClassLength
6
6
  package_name :elasticsearch
7
7
 
8
8
  attr_reader :drainer
9
9
  attr_accessor :active_nodes
10
10
 
11
+ # rubocop:disable Metrics/LineLength
11
12
  desc 'asg', 'Drain all documents from all nodes in an EC2 AutoScaling Group'
12
13
  option :host, default: 'localhost:9200'
13
14
  option :asg, required: true
14
15
  option :region, required: true
15
- option :nodes
16
+ option :nodes, type: :array, desc: 'A comma separated list of node IDs to drain. If specified, the --number option has no effect'
17
+ option :number, type: :numeric, desc: 'The number of nodes to drain'
18
+ option :continue, type: :boolean, default: true, desc: 'Whether to continue draining nodes once the first iteration of --number is complete'
19
+ # rubocop:enable Metrics/LineLength
16
20
  def asg # rubocop:disable Metrics/MethodLength
17
21
  @drainer = Elasticsearch::Drain.new(options[:host],
18
22
  options[:asg],
19
23
  options[:region])
24
+
20
25
  ensure_cluster_healthy
21
26
  @active_nodes = drainer.active_nodes_in_asg
22
27
 
28
+ # If :nodes are specified, :number has no effect
29
+ if options[:nodes]
30
+ say "Nodes #{options[:nodes].join(', ')} have been specified, the --number option has no effect"
31
+ number_to_drain = nil
32
+ currently_draining_nodes = nil
33
+ else
34
+ number_to_drain = options[:number]
35
+ currently_draining_nodes = drainer.cluster.currently_draining('_id')
36
+ end
37
+
23
38
  # If a node or nodes are specified, only drain the requested node(s)
24
39
  @active_nodes = active_nodes.find_all do |n|
25
40
  instance_id = drainer.asg.instance(n.ipaddress).instance_id
26
- options[:nodes].split(',').include?(instance_id)
41
+ options[:nodes].include?(instance_id)
27
42
  end if options[:nodes]
28
43
 
29
44
  do_exit { say_status 'Complete', 'Nothing to do', :green } if active_nodes.empty?
30
- say_status 'Found Nodes', "AutoScalingGroup: #{instances}", :magenta
31
- ensure_cluster_healthy
32
- drain_nodes
33
- remove_nodes
45
+ say_status 'Found Nodes', "AutoScalingGroup: #{instances(active_nodes)}", :magenta
46
+
47
+ until active_nodes.empty?
48
+ ensure_cluster_healthy
49
+
50
+ nodes = active_nodes
51
+
52
+ # If there are nodes in cluster settings "transient.cluster.routing.allocation.exclude"
53
+ # test if those nodes are still in the ASG. If so, work on them first unless nodes are
54
+ # specified.
55
+ if currently_draining_nodes
56
+ nodes_to_drain = active_nodes.find_all { |n| currently_draining_nodes.split(',').include?(n.id) }
57
+
58
+ # If the list of nodes_to_drain isn't empty, we want to set nodes to the list of nodes
59
+ # we've already been working on.
60
+ unless nodes_to_drain.empty?
61
+ nodes = nodes_to_drain
62
+
63
+ say_status 'Active Nodes', "Resuming drain process on #{instances(nodes)}", :magenta
64
+ end
65
+
66
+ # We should only process currently_draining_nodes once
67
+ currently_draining_nodes = nil
68
+ end
69
+
70
+ # If we specify a number but DON'T specify nodes, sample the active_nodes.
71
+ if number_to_drain
72
+ nodes = nodes.sample(number_to_drain.to_i)
73
+ say_status 'Active Nodes', "Sampled #{number_to_drain} nodes and got #{instances(nodes)}", :magenta
74
+ end
75
+
76
+ @active_nodes = nodes unless options[:continue]
77
+
78
+ drain_nodes(nodes)
79
+ remove_nodes(nodes)
80
+
81
+ # Remove the drained nodes from the list of active_nodes
82
+ @active_nodes -= nodes
83
+
84
+ unless active_nodes.empty?
85
+ say_status 'Drain Nodes', "#{active_nodes.length} nodes remaining", :green
86
+
87
+ sleep_time = wait_sleep_time
88
+ say_status 'Waiting', "Sleeping for #{sleep_time} seconds before the next iteration", :green
89
+ sleep sleep_time
90
+ end
91
+ end
34
92
  say_status 'Complete', 'Draining nodes complete!', :green
35
93
  end
36
94
 
@@ -48,42 +106,53 @@ module Elasticsearch
48
106
  exit code
49
107
  end
50
108
 
51
- def instances
52
- instances = active_nodes.map(&:ipaddress)
109
+ def instances(nodes)
110
+ instances = nodes.map(&:ipaddress)
53
111
  instances.join(' ')
54
112
  end
55
113
 
56
- def adjusted_min_size
114
+ def adjusted_min_size(nodes)
57
115
  min_size = drainer.asg.min_size
58
116
  desired_capacity = drainer.asg.desired_capacity
59
- if (desired_capacity - active_nodes.length) >= min_size # Removing the active_nodes won't violate the min_size
60
- # Reduce the asg min_size proportionally
61
- desired_min_size = (min_size - active_nodes.length) <= 0 ? 0 : (min_size - active_nodes.length)
62
- else
63
- # Removing the active_nodes will result in the min_size being violated
64
- desired_min_size = desired_capacity - active_nodes.length
65
- end
117
+ desired_min_size = if (desired_capacity - nodes.length) >= min_size # Removing the nodes won't violate the min_size
118
+ # Reduce the asg min_size proportionally
119
+ (min_size - nodes.length) <= 0 ? 0 : (min_size - nodes.length)
120
+ else
121
+ # Removing the nodes will result in the min_size being violated
122
+ (desired_capacity - nodes.length) <= 0 ? 0 : (desired_capacity - nodes.length)
123
+ end
66
124
  desired_min_size
67
125
  end
68
126
 
69
- def drain_nodes
70
- drainer.asg.min_size = adjusted_min_size
71
- nodes_to_drain = active_nodes.map(&:id).join(',')
127
+ def drain_nodes(nodes)
128
+ drainer.asg.min_size = adjusted_min_size(nodes)
129
+ nodes_to_drain = nodes.map(&:id).join(',')
72
130
  say_status 'Drain Nodes', "Draining nodes: #{nodes_to_drain}", :magenta
73
131
  drainer.cluster.drain_nodes(nodes_to_drain, '_id')
74
132
  end
75
133
 
76
- def remove_nodes # rubocop:disable Metrics/MethodLength
77
- while active_nodes.length > 0
78
- active_nodes.each do |instance|
79
- instance = drainer.nodes.filter_nodes([instance], true).first
134
+ def wait_sleep_time
135
+ ips = active_nodes.map(&:ipaddress)
136
+ bytes = drainer.nodes.filter_nodes(ips).map(&:bytes_stored)
137
+ sleep_time = 10
138
+ sleep_time = 30 if bytes.any? { |b| b >= 100_000 }
139
+ sleep_time = 60 if bytes.any? { |b| b >= 1_000_000 }
140
+ sleep_time = 120 if bytes.any? { |b| b >= 10_000_000_000 }
141
+ sleep_time
142
+ end
143
+
144
+ def remove_nodes(nodes) # rubocop:disable Metrics/MethodLength
145
+ while nodes.length > 0
146
+ sleep_time = wait_sleep_time
147
+ nodes.each do |instance|
148
+ instance = drainer.nodes.filter_nodes([instance.ipaddress], true).first
80
149
  if instance.bytes_stored > 0
81
150
  say_status 'Drain Status', "Node #{instance.ipaddress} has #{instance.bytes_stored} bytes to move", :blue
82
- sleep 2
151
+ sleep sleep_time
83
152
  else
84
153
  next unless remove_node(instance)
85
- active_nodes.delete_if { |n| n.ipaddress == instance.ipaddress }
86
- break if active_nodes.length < 1
154
+ nodes.delete_if { |n| n.ipaddress == instance.ipaddress }
155
+ break if nodes.length < 1
87
156
  say_status 'Waiting', 'Sleeping for 1 minute before removing the next node', :green
88
157
  sleep 60
89
158
  end
@@ -97,7 +166,8 @@ module Elasticsearch
97
166
  say_status(
98
167
  'Removing Node',
99
168
  "Removing #{instance.ipaddress} from Elasticsearch cluster and #{drainer.asg.asg} AutoScalingGroup",
100
- :magenta)
169
+ :magenta
170
+ )
101
171
  sleep 5 unless instance.in_recovery?
102
172
  node = "#{instance.instance_id}(#{instance.ipaddress})"
103
173
  ensure_cluster_healthy
@@ -34,6 +34,11 @@ module Elasticsearch
34
34
  }
35
35
  )
36
36
  end
37
+
38
+ def currently_draining(exclude_by = '_ip')
39
+ settings = cluster.get_settings(:flat_settings => true)
40
+ settings.fetch('transient', {}).fetch("cluster.routing.allocation.exclude.#{exclude_by}", nil)
41
+ end
37
42
  end
38
43
  end
39
44
  end
@@ -99,7 +99,7 @@ module Elasticsearch
99
99
  @asg.ec2_client.wait_until(:instance_terminated,
100
100
  instance_ids: [instance_id]) do |w|
101
101
  w.max_attempts = 10
102
- w.delay = 30
102
+ w.delay = 60
103
103
  end
104
104
  end
105
105
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elasticsearch-drain
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Thompson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-08-09 00:00:00.000000000 Z
11
+ date: 2020-08-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler