cassandra-utils 0.2.1 → 0.3.1.pre.beta.1

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: 12d130d6d4d0301a5b1c7d20f5e74118f46c37f5
4
- data.tar.gz: 2c1f1aa3aaa0c753deeb77c27a91895e8bddc6cf
3
+ metadata.gz: 4e551a8b992a71ae2b24834faef52d77f61137eb
4
+ data.tar.gz: 002209ac50837ebd0e4a542b86bcdb442b2073be
5
5
  SHA512:
6
- metadata.gz: a5aa7e037428fc7410d28ae231f74a7e6aa2241b5f6777243383338f74478afbe7df3c6a1ecad435f869359cf8bdefbf458d4e0229170593d77cdfb2ce146ba2
7
- data.tar.gz: ebfe0dcd4fe290c800a79108a1452a0ac2beeaaa1996d6562e7454b24c0a0c6e541e8cba2d00d47c7dda0a8a6d892fc183e2e0ef629805f2ecefa4c8e10bdc4d
6
+ metadata.gz: b2f744c06e90c15931aba8bd6a9bfdabd4dfef6f5469e83ccc681928f6bcbde06067e0ada434c774b8df9482af046c795d74708eb337186e89296822cf98af67
7
+ data.tar.gz: 982bb9611cc120b08493f160b07dcda482cda4022c361126d1999c63ab066babd4f94cd409388ea2219b19cf6788f5386ed8feec2d0e421086db96f9a0114e3c
data/.gitignore CHANGED
@@ -6,3 +6,4 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ *.swp
data/Gemfile CHANGED
@@ -3,6 +3,9 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in cassandra-utils.gemspec
4
4
  gemspec
5
5
 
6
+ # TODO: Remove this when the next version of diplomat is released
7
+ gem 'diplomat', github: 'WeAreFarmGeek/diplomat', ref: 'e64b0dec3b3616ded40bb64139a388dfc1a68232'
8
+
6
9
  group :development do
7
10
  gem 'bundler', '~> 1.7'
8
11
  gem 'thor-scmversion', '= 1.7.0'
data/Gemfile.lock CHANGED
@@ -1,3 +1,12 @@
1
+ GIT
2
+ remote: git://github.com/WeAreFarmGeek/diplomat.git
3
+ revision: e64b0dec3b3616ded40bb64139a388dfc1a68232
4
+ ref: e64b0dec3b3616ded40bb64139a388dfc1a68232
5
+ specs:
6
+ diplomat (1.1.0)
7
+ faraday (~> 0.9)
8
+ json
9
+
1
10
  PATH
2
11
  remote: .
3
12
  specs:
@@ -10,10 +19,16 @@ PATH
10
19
  GEM
11
20
  remote: https://rubygems.org/
12
21
  specs:
13
- daemon_runner (0.2.2)
22
+ daemon_runner (0.4.1)
23
+ diplomat (~> 1.0)
14
24
  logging (~> 2.1)
15
25
  mixlib-shellout (~> 2.2)
26
+ retryable (~> 2.0)
27
+ rufus-scheduler (~> 3.2)
16
28
  dogstatsd-ruby (1.6.0)
29
+ faraday (0.10.0)
30
+ multipart-post (>= 1.2, < 3)
31
+ json (2.0.2)
17
32
  little-plugger (1.1.4)
18
33
  logging (2.1.0)
19
34
  little-plugger (~> 1.1)
@@ -21,7 +36,10 @@ GEM
21
36
  minitest (5.9.0)
22
37
  mixlib-shellout (2.2.7)
23
38
  multi_json (1.12.1)
39
+ multipart-post (2.0.0)
24
40
  rake (10.5.0)
41
+ retryable (2.0.4)
42
+ rufus-scheduler (3.2.2)
25
43
  thor (0.19.1)
26
44
  thor-scmversion (1.7.0)
27
45
  mixlib-shellout
@@ -33,6 +51,7 @@ PLATFORMS
33
51
  DEPENDENCIES
34
52
  bundler (~> 1.7)
35
53
  cassandra-utils!
54
+ diplomat!
36
55
  minitest (~> 5.0)
37
56
  rake (~> 10.0)
38
57
  thor-scmversion (= 1.7.0)
data/bin/cass-util CHANGED
@@ -1,17 +1,29 @@
1
1
  #!/usr/bin/env ruby
2
2
  require_relative '../lib/cassandra/utils'
3
+ require_relative '../lib/cassandra/tasks'
3
4
  require 'thor'
4
5
 
5
6
  class CassandraUtils < Thor
6
7
 
7
- method_option :loop_sleep_time, type: :numeric,
8
+ class_option :loop_sleep_time, type: :numeric,
8
9
  required: true, default: 120,
9
10
  desc: 'Frequency tasks are run'
10
- desc 'stats', 'Write metrics to Datadog'
11
- def stats
11
+ class_option :cleanup_service_name, type: :string,
12
+ required: true, default: 'cassandra',
13
+ desc: 'Unique string to be used in obtaining a Semaphore. Example: cassandra-#{cluster_name}'
14
+ class_option :cleanup_lock_count, type: :numeric,
15
+ required: true, default: 1,
16
+ desc: 'Number of nodes that can obtain a Semaphore lock'
17
+ desc 'util', 'Perform various utilities'
18
+ def util
12
19
  s = ::Cassandra::Utils::Daemon.new(options)
13
20
  s.start!
14
21
  end
22
+ # Backwards compatibility
23
+ desc 'stats', '[DEPRECATED - Use util] Write metrics to Datadog'
24
+ def stats
25
+ send(:util)
26
+ end
15
27
  end
16
28
 
17
29
  CassandraUtils.start
@@ -0,0 +1,257 @@
1
+ require 'socket'
2
+ require 'json'
3
+ require 'time'
4
+ require 'set'
5
+ require 'tmpdir'
6
+ require_relative '../utils/version'
7
+
8
+ module Cassandra
9
+ module Tasks
10
+ class Autoclean
11
+ include ::DaemonRunner::Logger
12
+
13
+ # @return [String] the path on disk where tokens will be cached
14
+ attr_reader :token_cache_path
15
+
16
+ # Create a new Autoclean task
17
+ #
18
+ # @param options [Object] optional configuration settings
19
+ # (see #token_cache_path)
20
+ #
21
+ # @return [Autoclean]
22
+ #
23
+ def initialize(options = {})
24
+ @token_cache_path = options[:token_cache_path]
25
+ @token_cache_path ||= File.join(Dir.tmpdir, 'autoclean-tokens.json')
26
+ @service_name = options[:cleanup_service_name]
27
+ @lock_count = options[:cleanup_lock_count]
28
+ end
29
+
30
+ # Schedule the Cassandra cleanup process to run daily
31
+ #
32
+ def schedule
33
+ [:interval, '1d']
34
+ end
35
+
36
+ # Return the status of the Cassandra node
37
+ #
38
+ # A node is considered up if it has a status of "Up" as reported by
39
+ # "nodetool status". If multiple nodes with this node's IP address show
40
+ # up in "nodetool status", this node is considered down.
41
+ #
42
+ # @return [:up, :down]
43
+ #
44
+ def status
45
+ return(:down).tap { logger.warn 'Cassandra node is DOWN' } if address.nil?
46
+ results = (nodetool_status || '').split("\n")
47
+ results.map! { |line| line.strip }
48
+ results.select! { |line| line.include? address }
49
+ results.map! { |line| line.split(/\s+/)[0] }
50
+ results.compact!
51
+ return(:down).tap do
52
+ logger.warn "Cannot find the Cassandra node (#{address}) in `nodetool status`"
53
+ end if results.size != 1
54
+ (results.first[0] == 'U') ? :up : :down
55
+ end
56
+
57
+ # Return the state of the Cassandra node
58
+ #
59
+ # The returned state is reported by "nodetool netstats".
60
+ #
61
+ # @return [state, nil]
62
+ #
63
+ def state
64
+ results = (nodetool_netstats || '').split("\n")
65
+ results.map! { |line| line.strip }
66
+ results.select! { |line| line.include? 'Mode:' }
67
+ results.map! { |line| line.split(':')[1] }
68
+ results.compact!
69
+ return nil if results.size != 1
70
+ results.first.strip.downcase.to_sym
71
+ end
72
+
73
+ # Run the Cassandra cleanup process if necessary
74
+ #
75
+ def run!
76
+ return unless status == :up
77
+ return unless state == :normal
78
+
79
+ new_tokens = Set.new tokens
80
+ old_tokens = Set.new cached_tokens
81
+ return if new_tokens == old_tokens
82
+
83
+ ::DaemonRunner::Semaphore.lock(@service_name, @lock_count) do
84
+ result = nodetool_cleanup
85
+ save_tokens if !result.nil? && result.exitstatus == 0
86
+ end
87
+ end
88
+
89
+ # Get the cached tokens this node owns
90
+ #
91
+ # @return [Array<String>] Cached tokens
92
+ #
93
+ def cached_tokens
94
+ data = token_cache.read
95
+ data = JSON.parse data
96
+ return [] unless data['version'] == ::Cassandra::Utils::VERSION
97
+
98
+ tokens = data['tokens']
99
+ return [] if tokens.nil?
100
+ return [] unless tokens.respond_to? :each
101
+
102
+ tokens.sort!
103
+ tokens
104
+ # Token file could not be opend or parsed
105
+ rescue Errno::ENOENT, JSON::ParserError
106
+ []
107
+ end
108
+
109
+ # Save the list of tokens this node owns to disk
110
+ # These can be read by `cached_tokens`
111
+ #
112
+ def save_tokens
113
+ data = {
114
+ :timestamp => Time.now.iso8601,
115
+ :tokens => tokens,
116
+ :version => ::Cassandra::Utils::VERSION
117
+ }
118
+
119
+ token_cache.write data.to_json
120
+ token_cache.flush
121
+ end
122
+
123
+ # Get the tokens this node owns
124
+ #
125
+ # The "nodetool ring" command returns
126
+ #
127
+ # Address Rack Status State Load Size Owns Token
128
+ # 127.0.0.1 r1 Up Normal 10 GB 33% 123456789
129
+ #
130
+ # @return [Array<String>] Tokens owned by this node
131
+ #
132
+ def tokens
133
+ return [] if address.nil?
134
+ results = (nodetool_ring || '').split("\n")
135
+ results.map! { |line| line.strip }
136
+ results.select! { |line| line.start_with? address }
137
+ results.map! { |line| line.split(/\s+/)[7] }
138
+ results.compact!
139
+ results.sort
140
+ end
141
+
142
+ # Get the IP address of this node
143
+ #
144
+ # @return [String, nil] IP address of this node
145
+ #
146
+ def address
147
+ if @address.nil?
148
+ addr = Socket.ip_address_list.find { |addr| addr.ipv4_private? }
149
+ @address = addr.ip_address unless addr.nil?
150
+ end
151
+ @address
152
+ end
153
+
154
+ def task_id
155
+ ['autoclean', 'nodetool']
156
+ end
157
+
158
+ private
159
+
160
+ # Run the "nodetool ring" command and return the output
161
+ #
162
+ # @return [String, nil] Output from the "nodetool ring" command
163
+ #
164
+ def nodetool_ring
165
+ @nodetool_ring ||= DaemonRunner::ShellOut.new(command: 'nodetool ring', timeout: 300)
166
+ @nodetool_ring.run!
167
+ @nodetool_ring.stdout
168
+ end
169
+
170
+ # Run the "nodetool status' command and return the output
171
+ #
172
+ # @return [String, nil] Output from the "nodetool status" command
173
+ #
174
+ def nodetool_status
175
+ @nodetool_status ||= DaemonRunner::ShellOut.new(command: 'nodetool status', timeout: 300)
176
+ @nodetool_status.run!
177
+ @nodetool_status.stdout
178
+ end
179
+
180
+ # Run the "nodetool netstats' command and return the output
181
+ #
182
+ # @return [String, nil] Output from the "nodetool netstats" command
183
+ #
184
+ def nodetool_netstats
185
+ @nodetool_netstats ||= DaemonRunner::ShellOut.new(command: 'nodetool netstats', timeout: 300)
186
+ @nodetool_netstats.run!
187
+ @nodetool_netstats.stdout
188
+ end
189
+
190
+ # Get the status of a "nodetool cleanup" command
191
+ #
192
+ # This will atempt to track a running "nodetool cleanup" process if one's
193
+ # found. If a running process isn't found, a new process will be launched.
194
+ #
195
+ # @return [Process::Status, nil]
196
+ #
197
+ def nodetool_cleanup
198
+ pid = find_nodetool_cleanup
199
+ if pid
200
+ logger.debug "Found nodetool cleanup process #{pid} already running"
201
+ Utils::Statsd.new('cassandra.cleanup.running').push!(1)
202
+ end
203
+ pid = exec_nodetool_cleanup
204
+ if pid
205
+ logger.debug "Started nodetool cleanup process #{pid}"
206
+ Utils::Statsd.new('cassandra.cleanup.running').push!(1)
207
+ status = wait_nodetool_cleanup pid
208
+ logger.debug "Completed nodetool cleanup process #{pid}"
209
+ end
210
+ status
211
+ end
212
+
213
+ # Get the ID of the first running "nodetool cleanup" process found
214
+ #
215
+ # @return [Integer, nil]
216
+ #
217
+ def find_nodetool_cleanup
218
+ @pgrep_nodetool_cleanup ||= ::DaemonRunner::ShellOut.new(command: 'pgrep -f "NodeCmd.+cleanu[p]"', valid_exit_codes: [0,1])
219
+ @pgrep_nodetool_cleanup.run!
220
+ pids = @pgrep_nodetool_cleanup.stdout.strip.split "\n"
221
+ return nil if pids.empty?
222
+ pids.first.to_i
223
+ end
224
+
225
+ # Run "nodetool cleanup" command
226
+ #
227
+ # @return [Integer] ID of the "nodetool cleanup" command
228
+ #
229
+ def exec_nodetool_cleanup
230
+ # The `pgroup: true` option spawns cleanup in its own process group.
231
+ # So if this process dies, cleanup continues to run.
232
+ @nodetool_cleanup ||= ::DaemonRunner::ShellOut.new(command: 'nodetool cleanup', wait: false)
233
+ @nodetool_cleanup.run!
234
+ end
235
+
236
+ # Wait for a "nodetool cleanup" process to exit
237
+ #
238
+ # This handles the `SystemCallError` that's raised if no child process is
239
+ # found. In that case, the returned status will be `nil`.
240
+ #
241
+ # @return [Process::Status, nil] status
242
+ #
243
+ def wait_nodetool_cleanup pid
244
+ logger.debug "Waiting for nodetool cleanup process #{pid} to complete"
245
+ ::DaemonRunner::ShellOut.wait2(pid, Process::WUNTRACED)
246
+ end
247
+
248
+ # Get the cache tokens wil be saved in
249
+ #
250
+ # @return [File] File where tokens wil be saved
251
+ #
252
+ def token_cache
253
+ File.new(token_cache_path, 'w+')
254
+ end
255
+ end
256
+ end
257
+ end
@@ -0,0 +1 @@
1
+ require_relative 'tasks/autoclean'
@@ -1,5 +1,4 @@
1
1
  require 'mixlib/shellout'
2
- require 'statsd'
3
2
 
4
3
  module Cassandra
5
4
  module Utils
@@ -12,7 +11,7 @@ module Cassandra
12
11
  end
13
12
 
14
13
  def timeout
15
- 15
14
+ 300
16
15
  end
17
16
 
18
17
  def runner
@@ -29,23 +28,9 @@ module Cassandra
29
28
  @command.error!
30
29
  @stdout = @command.stdout
31
30
  out = output
32
- push_metric(out)
31
+ Utils::Statsd.new(metric_name).to_dd(out).push!
33
32
  out
34
33
  end
35
-
36
- protected
37
-
38
- def statsd
39
- @statsd ||= ::Statsd.new('localhost', 8125)
40
- end
41
-
42
- def push_metric(value)
43
- statsd.gauge(metric_name, value)
44
- end
45
-
46
- def to_dd(out)
47
- out == true ? 1 : 0
48
- end
49
34
  end
50
35
  end
51
36
  end
@@ -6,10 +6,30 @@ module Cassandra
6
6
 
7
7
  def tasks
8
8
  [
9
- [::Cassandra::Utils::Stats::Compaction.new, 'run!'],
10
- [::Cassandra::Utils::Stats::Cleanup.new, 'run!']
9
+ [auto_clean_task, 'run!'],
10
+ [health_stat, 'run!'],
11
+ [compaction_stat, 'run!'],
12
+ [cleanup_stat, 'run!']
11
13
  ]
12
14
  end
15
+
16
+ private
17
+
18
+ def auto_clean_task
19
+ @auto_clean_task ||= ::Cassandra::Tasks::Autoclean.new(options)
20
+ end
21
+
22
+ def health_stat
23
+ @health_stat ||= ::Cassandra::Utils::Stats::Health.new
24
+ end
25
+
26
+ def compaction_stat
27
+ @compaction_stat ||= ::Cassandra::Utils::Stats::Compaction.new
28
+ end
29
+
30
+ def cleanup_stat
31
+ @cleanup_stat ||= ::Cassandra::Utils::Stats::Cleanup.new
32
+ end
13
33
  end
14
34
  end
15
35
  end
@@ -9,12 +9,15 @@ module Cassandra
9
9
 
10
10
  def output
11
11
  cleanup = stdout.lines.any? { |l| l.include?('Cleanup') }
12
- to_dd(cleanup)
13
12
  end
14
13
 
15
14
  def metric_name
16
15
  'cassandra.cleanup.running'
17
16
  end
17
+
18
+ def task_id
19
+ ['cleanup', 'nodetool']
20
+ end
18
21
  end
19
22
  end
20
23
  end
@@ -9,12 +9,15 @@ module Cassandra
9
9
 
10
10
  def output
11
11
  compaction = stdout.lines.any? { |l| l.include?('Compaction') }
12
- to_dd(compaction)
13
12
  end
14
13
 
15
14
  def metric_name
16
15
  'cassandra.compaction.running'
17
16
  end
17
+
18
+ def task_id
19
+ ['compaction', 'nodetool']
20
+ end
18
21
  end
19
22
  end
20
23
  end
@@ -0,0 +1,73 @@
1
+ module Cassandra
2
+ module Utils
3
+ module Stats
4
+ class Health < Utils::CLI::Base
5
+ def run!
6
+ running = true
7
+ if state == :normal
8
+ running &&= nodetool_statusgossip.strip == 'running'
9
+ running &&= nodetool_statusthrift.strip == 'running'
10
+ end
11
+ Utils::Statsd.new(metric_name).to_dd(running).push!
12
+ running
13
+ end
14
+
15
+ def metric_name
16
+ 'cassandra.service.running'
17
+ end
18
+
19
+ # Return the state of the Cassandra node
20
+ #
21
+ # The returned state is reported by "nodetool netstats".
22
+ #
23
+ # @return [state, nil]
24
+ #
25
+ def state
26
+ results = (nodetool_netstats || '').split("\n")
27
+ results.map! { |line| line.strip }
28
+ results.select! { |line| line.include? 'Mode:' }
29
+ results.map! { |line| line.split(':')[1] }
30
+ results.compact!
31
+ return nil if results.size != 1
32
+ results.first.strip.downcase.to_sym
33
+ end
34
+
35
+ def task_id
36
+ ['health', 'nodetool']
37
+ end
38
+
39
+ private
40
+
41
+ # Run the "nodetool statusgossip' command and return the output
42
+ #
43
+ # @return [String, nil] Output from the "nodetool statusgossip" command
44
+ #
45
+ def nodetool_statusgossip
46
+ @nodetool_statusgossip ||= DaemonRunner::ShellOut.new(command: 'nodetool statusgossip')
47
+ @nodetool_statusgossip.run!
48
+ @nodetool_statusgossip.stdout
49
+ end
50
+
51
+ # Run the "nodetool statusthrift' command and return the output
52
+ #
53
+ # @return [String, nil] Output from the "nodetool statusthrift" command
54
+ #
55
+ def nodetool_statusthrift
56
+ @nodetool_statusthrift||= DaemonRunner::ShellOut.new(command: 'nodetool statusthrift')
57
+ @nodetool_statusthrift.run!
58
+ @nodetool_statusthrift.stdout
59
+ end
60
+
61
+ # Run the "nodetool netstats' command and return the output
62
+ #
63
+ # @return [String, nil] Output from the "nodetool netstats" command
64
+ #
65
+ def nodetool_netstats
66
+ @nodetool_netstats ||= DaemonRunner::ShellOut.new(command: 'nodetool netstats', timeout: 300)
67
+ @nodetool_netstats.run!
68
+ @nodetool_netstats.stdout
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -1,2 +1,3 @@
1
1
  require_relative 'stats/compaction'
2
2
  require_relative 'stats/cleanup'
3
+ require_relative 'stats/health'
@@ -0,0 +1,24 @@
1
+ require 'statsd'
2
+
3
+ module Cassandra
4
+ module Utils
5
+ class Statsd
6
+ attr_reader :statsd, :metric_name, :value
7
+
8
+ def initialize(metric_name)
9
+ @statsd ||= ::Statsd.new('localhost', 8125)
10
+ @metric_name = metric_name
11
+ self
12
+ end
13
+
14
+ def to_dd(value)
15
+ @value = (value == true ? 1 : 0)
16
+ self
17
+ end
18
+
19
+ def push!(value = @value)
20
+ statsd.gauge(metric_name, value)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,3 +1,4 @@
1
1
  require_relative 'utils/cli/base'
2
2
  require_relative 'utils/daemon'
3
+ require_relative 'utils/statsd'
3
4
  require_relative 'utils/stats'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cassandra-utils
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.1.pre.beta.1
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-10-12 00:00:00.000000000 Z
11
+ date: 2016-11-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mixlib-shellout
@@ -129,12 +129,16 @@ files:
129
129
  - bin/console
130
130
  - bin/setup
131
131
  - cassandra-utils.gemspec
132
+ - lib/cassandra/tasks.rb
133
+ - lib/cassandra/tasks/autoclean.rb
132
134
  - lib/cassandra/utils.rb
133
135
  - lib/cassandra/utils/cli/base.rb
134
136
  - lib/cassandra/utils/daemon.rb
135
137
  - lib/cassandra/utils/stats.rb
136
138
  - lib/cassandra/utils/stats/cleanup.rb
137
139
  - lib/cassandra/utils/stats/compaction.rb
140
+ - lib/cassandra/utils/stats/health.rb
141
+ - lib/cassandra/utils/statsd.rb
138
142
  - lib/cassandra/utils/version.rb
139
143
  homepage: https://github.com/rapid7/cassandra-utils
140
144
  licenses:
@@ -151,9 +155,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
151
155
  version: '0'
152
156
  required_rubygems_version: !ruby/object:Gem::Requirement
153
157
  requirements:
154
- - - ">="
158
+ - - ">"
155
159
  - !ruby/object:Gem::Version
156
- version: '0'
160
+ version: 1.3.1
157
161
  requirements: []
158
162
  rubyforge_project:
159
163
  rubygems_version: 2.4.3