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

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