cassandra-utils 0.3.2 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cbc1561d91999dc7f77a4e43dd4d747eae9207d9
4
- data.tar.gz: c24462312be9a71cf6e56df72658018e188e98ea
3
+ metadata.gz: 9e37957c030dfb9729a4d2b98df7db3acc002f68
4
+ data.tar.gz: b56baf5d6d2a88963a0803e1f0abfd1931ea7f77
5
5
  SHA512:
6
- metadata.gz: 8b3bde9c1bc2a93e32466e5ad5d016c5cfd1b2499dfc05d78144cc09331e015eb6f4595528cc24cb43597fbe346d9b9aa9414ae84fdc8d1e0fcbefabbe91c6db
7
- data.tar.gz: 6005912c05f287dcb04cc7815677873b40e14191032a9e2dd164dc22cb4606844fc057094d8387b681604ed98cf22f44073058c023ef03e50ecc7bc7c25665c0
6
+ metadata.gz: ce642b41f5a82d32cf9005a7fa263dd00be640c747b2dd70266d9876ce476845db347301dbe52a74707bafddd1b1d42186cdaa4301a1370578e2b9be8f403b65
7
+ data.tar.gz: ec1f02d0538917e2a6b64ffbeaeddce83c226b68e27215b21bfdd8602706208c71a1a1f573a7030372c65861e6854fa0ddac493007d4e2656166760e0c4106cd
data/bin/cass-util CHANGED
@@ -14,6 +14,9 @@ class CassandraUtils < Thor
14
14
  class_option :cleanup_lock_count, type: :numeric,
15
15
  required: true, default: 1,
16
16
  desc: 'Number of nodes that can obtain a Semaphore lock'
17
+ class_option :seed_service_name, type: :string,
18
+ required: true, default: 'cassandra',
19
+ desc: 'Unique string to be used in obtaining a Consul seed lock. Example: cassandra-seed-#{cluster_name}'
17
20
  desc 'util', 'Perform various utilities'
18
21
  def util
19
22
  s = ::Cassandra::Utils::Daemon.new(options)
@@ -1 +1,2 @@
1
1
  require_relative 'tasks/autoclean'
2
+ require_relative 'tasks/seedregistry'
@@ -74,12 +74,24 @@ module Cassandra
74
74
  # Run the Cassandra cleanup process if necessary
75
75
  #
76
76
  def run!
77
- return unless status == :up
78
- return unless state == :normal
77
+ node_status = status
78
+ unless node_status == :up
79
+ logger.debug "Cleanup skipped because of node status. Expected up got #{node_status}"
80
+ return
81
+ end
82
+
83
+ node_state = state
84
+ unless node_state == :normal
85
+ logger.debug "Cleanup skipped because of node state. Expected normal got #{node_state}"
86
+ return
87
+ end
79
88
 
80
89
  new_tokens = Set.new tokens
81
90
  old_tokens = Set.new cached_tokens
82
- return if new_tokens == old_tokens
91
+ if new_tokens == old_tokens
92
+ logger.debug "Cleanup skipped because tokens haven't changed"
93
+ return
94
+ end
83
95
 
84
96
  ::DaemonRunner::Semaphore.lock(@service_name, @lock_count) do
85
97
  result = nodetool_cleanup
@@ -92,18 +104,37 @@ module Cassandra
92
104
  # @return [Array<String>] Cached tokens
93
105
  #
94
106
  def cached_tokens
107
+ if token_cache.closed?
108
+ logger.debug "Failed to read cached tokens because file is closed."
109
+ return []
110
+ end
111
+
112
+ token_cache.seek 0
95
113
  data = token_cache.read
96
114
  data = JSON.parse data
97
- return [] unless data['version'] == ::Cassandra::Utils::VERSION
115
+
116
+ unless data['version'] == ::Cassandra::Utils::VERSION
117
+ logger.debug "Failed to read cached tokens because version didn't match. Expected #{::Cassandra::Utils::VERSION} got #{data['version']}"
118
+ return []
119
+ end
98
120
 
99
121
  tokens = data['tokens']
100
- return [] if tokens.nil?
101
- return [] unless tokens.respond_to? :each
122
+ if tokens.nil?
123
+ logger.debug "Failed to read cached tokens because they're nil"
124
+ return []
125
+ end
126
+
127
+ unless tokens.respond_to? :each
128
+ logger.debug "Failed to read cached tokens because they're invalid"
129
+ return []
130
+ end
102
131
 
103
132
  tokens.sort!
104
133
  tokens
105
134
  # Token file could not be opend or parsed
106
- rescue Errno::ENOENT, JSON::ParserError
135
+ rescue Errno::ENOENT, JSON::ParserError => e
136
+ logger.debug "Caught exception while reading cached tokens"
137
+ logger.debug e
107
138
  []
108
139
  end
109
140
 
@@ -117,8 +148,14 @@ module Cassandra
117
148
  :version => ::Cassandra::Utils::VERSION
118
149
  }
119
150
 
151
+ if token_cache.closed?
152
+ logger.debug "Failed to save cached tokens because file is closed."
153
+ return []
154
+ end
155
+
156
+ token_cache.seek 0
157
+ token_cache.truncate 0
120
158
  token_cache.write data.to_json
121
- token_cache.flush
122
159
  end
123
160
 
124
161
  # Get the tokens this node owns
@@ -131,7 +168,11 @@ module Cassandra
131
168
  # @return [Array<String>] Tokens owned by this node
132
169
  #
133
170
  def tokens
134
- return [] if address.nil?
171
+ if address.nil?
172
+ logger.debug "Failed to read live tokens because address is nil"
173
+ return []
174
+ end
175
+
135
176
  results = (nodetool_ring || '').split("\n")
136
177
  results.map! { |line| line.strip }
137
178
  results.select! { |line| line.start_with? address }
@@ -251,7 +292,8 @@ module Cassandra
251
292
  # @return [File] File where tokens wil be saved
252
293
  #
253
294
  def token_cache
254
- File.new(token_cache_path, 'w+')
295
+ mode = File::CREAT | File::RDWR | File::SYNC
296
+ @token_cache ||= File.new(token_cache_path, mode)
255
297
  end
256
298
  end
257
299
  end
@@ -0,0 +1,173 @@
1
+ require 'daemon_runner/shell_out'
2
+ require 'daemon_runner/semaphore'
3
+
4
+ module Cassandra
5
+ module Tasks
6
+ class SeedRegistry
7
+ # Create a new SeedRegistry task
8
+ #
9
+ # @param cluster_name [String] unique name (in Consul) for the Cassandra cluster
10
+ #
11
+ # @return [SeedRegistry]
12
+ #
13
+ def initialize cluster_name
14
+ @cluster_name = cluster_name.to_s
15
+ raise ArgumentError.new('cluster_name must not be empty') if @cluster_name.empty?
16
+ @semaphore = nil
17
+ @renew_thread = nil
18
+ @nodetool_info_cache = nil
19
+ end
20
+
21
+ # Schedule the seed registration process to run periodically
22
+ #
23
+ def schedule
24
+ [:interval, '10s']
25
+ end
26
+
27
+ # Get a lock in Consul registering the Cassandra node as a seed
28
+ #
29
+ def run!
30
+ @nodetool_info_cache = nil
31
+ if can_seed?
32
+ try_get_seed_lock
33
+ else
34
+ release_seed_lock
35
+ end
36
+ end
37
+
38
+ # Return true if the Cassandra node is a valid seed, false otherwise
39
+ #
40
+ # @return [Boolean]
41
+ #
42
+ def can_seed?
43
+ return false unless state == :normal
44
+
45
+ results = (nodetool_info_cached || '').split("\n")
46
+ results.map! { |line| line.strip }
47
+
48
+ filter_results = lambda do |key|
49
+ potential = results.select { |line| line.include? key }
50
+ potential.map! { |line| line.split(':')[1] }
51
+ potential.compact!
52
+ potential.size == 1 && potential.first.strip == 'true'
53
+ end
54
+
55
+ return false unless filter_results.call('Gossip active')
56
+ return false unless filter_results.call('Thrift active')
57
+ return false unless filter_results.call('Native Transport active')
58
+
59
+ true
60
+ end
61
+
62
+ # Return the state of the Cassandra node
63
+ #
64
+ # The returned state is reported by "nodetool netstats".
65
+ #
66
+ # @return [state, nil]
67
+ #
68
+ def state
69
+ results = (nodetool_netstats || '').split("\n")
70
+ results.map! { |line| line.strip }
71
+ results.select! { |line| line.include? 'Mode:' }
72
+ results.map! { |line| line.split(':')[1] }
73
+ results.compact!
74
+ return nil if results.size != 1
75
+ results.first.strip.downcase.to_sym
76
+ end
77
+
78
+ # Return the data center the Cassandra node is in
79
+ #
80
+ # The returned data center is reported by "nodetool info".
81
+ #
82
+ # @return [String, nil]
83
+ #
84
+ def data_center
85
+ results = (nodetool_info_cached || '').split("\n")
86
+ results.map! { |line| line.strip }
87
+ results.select! { |line| line.include?('Data Center') }
88
+ results.map! { |line| line.split(':')[1] }
89
+ results.compact!
90
+ return nil if results.size != 1
91
+ results.first.strip
92
+ end
93
+
94
+ # Return the rack the Cassandra node is in
95
+ #
96
+ # The returned rack is reported by "nodetool info".
97
+ #
98
+ # @return [String, nil]
99
+ #
100
+ def rack
101
+ results = (nodetool_info_cached || '').split("\n")
102
+ results.map! { |line| line.strip }
103
+ results.select! { |line| line.include?('Rack') }
104
+ results.map! { |line| line.split(':')[1] }
105
+ results.compact!
106
+ return nil if results.size != 1
107
+ results.first.strip
108
+ end
109
+
110
+ private
111
+
112
+ # Run the "nodetool info" command and return the output
113
+ #
114
+ # @return [String, nil] Output from the "nodetool info" command
115
+ #
116
+ def nodetool_info
117
+ @nodetool_info ||= DaemonRunner::ShellOut.new(command: 'nodetool info', timeout: 300)
118
+ @nodetool_info.run!
119
+ @nodetool_info.stdout
120
+ end
121
+
122
+ # Return cached output from "nodetool info" command
123
+ #
124
+ # This will attempt to populate an empty cache by calling "nodetool info".
125
+ #
126
+ # @return [String, nil] Cached output from the "nodetool info" command
127
+ #
128
+ def nodetool_info_cached
129
+ @nodetool_info_cache ||= nodetool_info
130
+ end
131
+
132
+ # Run the "nodetool netstats" command and return the output
133
+ #
134
+ # @return [String, nil] Output from the "nodetool netstats" command
135
+ #
136
+ def nodetool_netstats
137
+ @nodetool_netstats ||= DaemonRunner::ShellOut.new(command: 'nodetool netstats', timeout: 300)
138
+ @nodetool_netstats.run!
139
+ @nodetool_netstats.stdout
140
+ end
141
+
142
+ # Try to get the lock in Consul for this node as a seed
143
+ #
144
+ def try_get_seed_lock
145
+ if @semaphore.nil?
146
+ name = "#{@cluster_name}/#{data_center}-#{rack}"
147
+ @semaphore = DaemonRunner::Semaphore.lock(name, 1)
148
+ end
149
+
150
+ if @renew_thread.nil?
151
+ @renew_thread = @semaphore.renew
152
+ end
153
+ end
154
+
155
+ # Release the lock in Consul for this node as a seed
156
+ #
157
+ def release_seed_lock
158
+ unless @renew_thread.nil?
159
+ @renew_thread.kill
160
+ @renew_thread = nil
161
+ end
162
+
163
+ unless @semaphore.nil?
164
+ while @semaphore.locked?
165
+ @semaphore.try_release
166
+ sleep 0.1
167
+ end
168
+ @semaphore = nil
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -6,6 +6,7 @@ module Cassandra
6
6
 
7
7
  def tasks
8
8
  [
9
+ [seed_registry_task, 'run!'],
9
10
  [auto_clean_task, 'run!'],
10
11
  [health_stat, 'run!'],
11
12
  [compaction_stat, 'run!'],
@@ -15,6 +16,11 @@ module Cassandra
15
16
 
16
17
  private
17
18
 
19
+ def seed_registry_task
20
+ name = options[:seed_service_name]
21
+ @seed_registry_task ||= ::Cassandra::Tasks::SeedRegistry.new(name)
22
+ end
23
+
18
24
  def auto_clean_task
19
25
  @auto_clean_task ||= ::Cassandra::Tasks::Autoclean.new(options)
20
26
  end
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.3.2
4
+ version: 0.4.0
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-12-02 00:00:00.000000000 Z
11
+ date: 2017-06-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mixlib-shellout
@@ -131,6 +131,7 @@ files:
131
131
  - cassandra-utils.gemspec
132
132
  - lib/cassandra/tasks.rb
133
133
  - lib/cassandra/tasks/autoclean.rb
134
+ - lib/cassandra/tasks/seedregistry.rb
134
135
  - lib/cassandra/utils.rb
135
136
  - lib/cassandra/utils/cli/base.rb
136
137
  - lib/cassandra/utils/daemon.rb