jetpants 0.7.10 → 0.8.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.
@@ -38,7 +38,8 @@ module Jetpants
38
38
  end
39
39
 
40
40
  # Returns a Host object for the machine Jetpants is running on.
41
- def self.local(interface='bond0')
41
+ def self.local(interface=false)
42
+ interface ||= Jetpants.private_interface
42
43
  # This technique is adapted from Sergio Rubio Gracia's, described at
43
44
  # http://blog.frameos.org/2006/12/09/getting-network-interface-addresses-using-ioctl-pure-ruby-2/
44
45
  sock = Socket.new(Socket::AF_INET, Socket::SOCK_DGRAM,0)
@@ -156,38 +157,6 @@ module Jetpants
156
157
  @available
157
158
  end
158
159
 
159
- ###### ini file manipulation ###############################################
160
-
161
- # Comments-out lines of an ini file beginning with any of the supplied prefixes
162
- def comment_out_ini(file, *prefixes)
163
- toggle_ini(file, prefixes, false)
164
- end
165
-
166
- # Un-comments-out lines of an ini file beginning with any of the supplied prefixes
167
- # The prefixes should NOT include the # comment-out character -- ie, pass them
168
- # the same as you would to DB#comment_out_ini
169
- def uncomment_out_ini(file, *prefixes)
170
- toggle_ini(file, prefixes, true)
171
- end
172
-
173
- # Comments-out (if enable is true) or un-comments-out (if enable is false) lines of an ini file.
174
- def toggle_ini(file, prefixes, enable)
175
- prefixes.flatten!
176
- commands = []
177
- prefixes.each do |setting|
178
- if enable
179
- search = '^#(\s*%s\s*(?:=.*)?)$' % setting
180
- replace = '\1'
181
- else
182
- search = '^(\s*%s\s*(?:=.*)?)$' % setting
183
- replace = '#\1'
184
- end
185
- commands << "ruby -i -pe 'sub(%r[#{search}], %q[#{replace}])' #{file}"
186
- end
187
- cmd_line = commands.join '; '
188
- ssh_cmd cmd_line
189
- end
190
-
191
160
 
192
161
  ###### Directory Copying / Listing / Comparison methods ####################
193
162
 
@@ -346,6 +315,22 @@ module Jetpants
346
315
  end
347
316
  total_size
348
317
  end
318
+
319
+ def mount_stats(mount)
320
+ mount_stats = {}
321
+
322
+ output = ssh_cmd "df -k " + mount + "|tail -1| awk '{print $2\",\"$3\",\"$4}'"
323
+ if output
324
+ output = output.split(',').map{|s| s.to_i}
325
+
326
+ mount_stats['total'] = output[0] * 1024
327
+ mount_stats['used'] = output[1] * 1024
328
+ mount_stats['available'] = output[2] * 1024
329
+ return mount_stats
330
+ else
331
+ false
332
+ end
333
+ end
349
334
 
350
335
 
351
336
  ###### Misc methods ########################################################
@@ -358,8 +343,8 @@ module Jetpants
358
343
  # methods that call Host#service with :status operation (such as
359
344
  # DB#probe_running) in a custom plugin, to parse the output properly on
360
345
  # your chosen Linux distro.
361
- def service(operation, name)
362
- ssh_cmd "service #{name} #{operation.to_s}"
346
+ def service(operation, name, options='')
347
+ ssh_cmd "service #{name} #{operation.to_s} #{options}".rstrip
363
348
  end
364
349
 
365
350
  # Changes the I/O scheduler to name (such as 'deadline', 'noop', 'cfq')
@@ -376,6 +361,19 @@ module Jetpants
376
361
  true
377
362
  end
378
363
 
364
+ # Checks if there's a process with the given process ID running on this host.
365
+ # Optionally also checks if matching_string is contained in the process name.
366
+ # Returns true if so, false if not.
367
+ # Warning: this implementation assumes Linux-style "ps" command; will not work
368
+ # on BSD hosts.
369
+ def pid_running?(pid, matching_string=false)
370
+ if matching_string
371
+ ssh_cmd("ps --no-headers -o command #{pid} | grep '#{matching_string}' | wc -l").chomp.to_i > 0
372
+ else
373
+ ssh_cmd("ps --no-headers #{pid} | wc -l").chomp.to_i > 0
374
+ end
375
+ end
376
+
379
377
  # Returns number of cores on machine. (reflects virtual cores if hyperthreading
380
378
  # enabled, so might be 2x real value in that case.)
381
379
  # Not currently used by anything in Jetpants base, but might be useful for plugins
@@ -1,5 +1,14 @@
1
1
  # This file contains any methods we're adding to core Ruby modules
2
2
 
3
+ # Add a deep_merge method to Hash in order to more effectively join configs
4
+ class Hash
5
+ def deep_merge!(other_hash)
6
+ merge!(other_hash) do |key, oldval, newval|
7
+ (oldval.class == self.class && newval.class == oldval.class) ? oldval.deep_merge!(newval) : newval
8
+ end
9
+ end
10
+ end
11
+
3
12
  # Reopen Enumerable to add some concurrent iterators
4
13
  module Enumerable
5
14
  # Works like each but runs the block in a separate thread per item.
@@ -125,16 +125,21 @@ module Jetpants
125
125
 
126
126
  # Remove a slave from a pool entirely. This is destructive, ie, it does a
127
127
  # RESET SLAVE on the db.
128
+ #
128
129
  # Note that a plugin may want to override this (or implement after_remove_slave!)
129
130
  # to actually sync the change to an asset tracker, depending on how the plugin
130
131
  # implements Pool#sync_configuration. (If the implementation makes sync_configuration
131
132
  # work by iterating over the pool's current slaves to update their status/role/pool, it
132
133
  # won't see any slaves that have been removed, and therefore won't update them.)
134
+ #
135
+ # This method has no effect on slaves that are unavailable via SSH or have MySQL
136
+ # stopped, because these are only considered to be in the pool if your asset tracker
137
+ # plugin intentionally adds them. Such plugins could also handle this in the
138
+ # after_remove_slave! callback.
133
139
  def remove_slave!(slave_db)
134
140
  raise "Slave is not in this pool" unless slave_db.pool == self
141
+ return false unless (slave_db.running? && slave_db.available?)
135
142
  slave_db.disable_monitoring
136
- slave_db.stop_replication
137
- slave_db.repl_binlog_coordinates # displays how far we replicated, in case you need to roll back this change manually
138
143
  slave_db.disable_replication!
139
144
  sync_configuration # may or may not be sufficient -- see note above.
140
145
  end
@@ -177,21 +182,23 @@ module Jetpants
177
182
  end
178
183
 
179
184
  binlog_pos = extended_info ? details[@master][:coordinates].join(':') : ''
180
- print "\tmaster = %-15s %-30s %s\n" % [@master.ip, @master.hostname, binlog_pos]
185
+ print "\tmaster = %-15s %-32s %s\n" % [@master.ip, @master.hostname, binlog_pos]
181
186
 
182
187
  [:active, :standby, :backup].each do |type|
183
188
  slave_list = slaves(type)
184
189
  slave_list.sort.each_with_index do |s, i|
185
190
  binlog_pos = extended_info ? details[s][:coordinates].join(':') : ''
186
191
  slave_lag = extended_info ? "lag=#{details[s][:lag]}" : ''
187
- print "\t%-7s slave #{i + 1} = %-15s %-30s %-26s %s\n" % [type, s.ip, s.hostname, binlog_pos, slave_lag]
192
+ print "\t%-7s slave #{i + 1} = %-15s %-32s %-26s %s\n" % [type, s.ip, s.hostname, binlog_pos, slave_lag]
188
193
  end
189
194
  end
190
195
  true
191
196
  end
192
197
 
193
198
  # Demotes the pool's existing master, promoting a slave in its place.
194
- def master_promotion!(promoted)
199
+ # The old master will become a slave of the new master if enslave_old_master is true,
200
+ # unless the old master is unavailable/crashed.
201
+ def master_promotion!(promoted, enslave_old_master=true)
195
202
  demoted = @master
196
203
  raise "Demoted node is already the master of this pool!" if demoted == promoted
197
204
  raise "Promoted host is not in the right pool!" unless demoted.slaves.include?(promoted)
@@ -201,7 +208,7 @@ module Jetpants
201
208
  # If demoted machine is available, confirm it is read-only and binlog isn't moving,
202
209
  # and then wait for slaves to catch up to this position
203
210
  if demoted.running?
204
- demoted.enable_read_only! unless demoted.read_only?
211
+ demoted.enable_read_only!
205
212
  raise "Unable to enable global read-only mode on demoted machine" unless demoted.read_only?
206
213
  coordinates = demoted.binlog_coordinates
207
214
  raise "Demoted machine still taking writes (from superuser or replication?) despite being read-only" unless coordinates == demoted.binlog_coordinates
@@ -241,7 +248,7 @@ module Jetpants
241
248
 
242
249
  # gather our new replicas
243
250
  replicas.delete promoted
244
- replicas << demoted if demoted.running?
251
+ replicas << demoted if demoted.running? && enslave_old_master
245
252
 
246
253
  # perform promotion
247
254
  replicas.each do |r|
@@ -34,8 +34,8 @@ module Jetpants
34
34
  # :replicating -- Child shard that is being cloned to new replicas. Shard not in production yet.
35
35
  # :child -- Child shard that is in production for reads, but still slaving from its parent for writes.
36
36
  # :needs_cleanup -- Child shard that is fully in production, but parent replication not torn down yet, and redundant data (from wrong range) not removed yet
37
- # :deprecated -- Parent shard that has been split but children are still in :child or :needs_cleanup state. Shard may still be in production for writes.
38
- # :recycle -- Parent shard that has been split and children are now in the :ready state. Shard no longer in production.
37
+ # :deprecated -- Parent shard that has been split but children are still in :child or :needs_cleanup state. Shard may still be in production for writes / replication not torn down yet.
38
+ # :recycle -- Parent shard that has been split and children are now in the :ready state. Shard no longer in production, replication to children has been torn down.
39
39
  attr_accessor :state
40
40
 
41
41
  # Constructor for Shard --
@@ -126,9 +126,9 @@ module Jetpants
126
126
  # If you omit id_ranges, the parent's ID range will be divided evenly amongst the
127
127
  # children automatically.
128
128
  def init_children(count, id_ranges=false)
129
- # Make sure we have enough machines in spare pool
130
- raise "Not enough master role machines in spare pool!" if count > Jetpants.topology.count_spares(role: 'master')
131
- raise "Not enough standby_slave role machines in spare pool!" if count * Jetpants.standby_slaves_per_pool > Jetpants.topology.count_spares(role: 'standby_slave')
129
+ # Make sure we have enough machines (of correct hardware spec and role) in spare pool
130
+ raise "Not enough master role machines in spare pool!" if count > Jetpants.topology.count_spares(role: :master, like: master)
131
+ raise "Not enough standby_slave role machines in spare pool!" if count * Jetpants.standby_slaves_per_pool > Jetpants.topology.count_spares(role: :standby_slave, like: slaves.first)
132
132
 
133
133
  # Make sure enough slaves of shard being split
134
134
  raise "Must have at least #{Jetpants.standby_slaves_per_pool} slaves of shard being split" if master.slaves.count < Jetpants.standby_slaves_per_pool
@@ -149,7 +149,7 @@ module Jetpants
149
149
  end
150
150
 
151
151
  count.times do |i|
152
- spare = Jetpants.topology.claim_spare(role: 'master')
152
+ spare = Jetpants.topology.claim_spare(role: :master, like: master)
153
153
  spare.disable_read_only! if (spare.running? && spare.read_only?)
154
154
  spare.output "Using ID range of #{id_ranges[i][0]} to #{id_ranges[i][1]} (inclusive)"
155
155
  s = Shard.new(id_ranges[i][0], id_ranges[i][1], spare, :initializing)
@@ -170,7 +170,6 @@ module Jetpants
170
170
 
171
171
  init_children(pieces) unless @children.count > 0
172
172
 
173
- @children.concurrent_each {|c| c.disable_binary_logging}
174
173
  clone_to_children!
175
174
  @children.concurrent_each {|c| c.rebuild!}
176
175
  @children.each {|c| c.sync_configuration}
@@ -208,7 +207,6 @@ module Jetpants
208
207
  raise "Child shard master #{child_shard.master} is already a slave of another pool"
209
208
  elsif child_shard.master.is_slave?
210
209
  child_shard.output "Already slaving from parent shard master"
211
- child_shard.restart_mysql # to make previous disable of binary logging take effect
212
210
  else
213
211
  targets << child_shard.master
214
212
  end
@@ -233,12 +231,12 @@ module Jetpants
233
231
  raise "Cannot rebuild a shard that isn't still slaving from another shard" unless @master.is_slave?
234
232
  raise "Cannot rebuild an active shard" if in_config?
235
233
 
236
- stop_query_killer
237
234
  tables = Table.from_config 'sharded_tables'
238
235
 
239
236
  if [:initializing, :exporting].include? @state
240
237
  @state = :exporting
241
238
  sync_configuration
239
+ stop_query_killer
242
240
  export_schemata tables
243
241
  export_data tables, @min_id, @max_id
244
242
  end
@@ -248,17 +246,17 @@ module Jetpants
248
246
  sync_configuration
249
247
  import_schemata!
250
248
  alter_schemata if respond_to? :alter_schemata
249
+ restart_mysql '--skip-log-bin', '--skip-log-slave-updates', '--innodb-autoinc-lock-mode=2', '--skip-slave-start'
251
250
  import_data tables, @min_id, @max_id
251
+ restart_mysql # to clear out previous options '--skip-log-bin', '--skip-log-slave-updates', '--innodb-autoinc-lock-mode=2'
252
252
  start_query_killer
253
253
  end
254
254
 
255
255
  if [:importing, :replicating].include? @state
256
- enable_binary_logging
257
- restart_mysql
258
256
  @state = :replicating
259
257
  sync_configuration
260
258
  if Jetpants.standby_slaves_per_pool > 0
261
- my_slaves = Jetpants.topology.claim_spares(Jetpants.standby_slaves_per_pool, role: 'standby_slave')
259
+ my_slaves = Jetpants.topology.claim_spares(Jetpants.standby_slaves_per_pool, role: :standby_slave, like: parent.slaves.first)
262
260
  enslave!(my_slaves)
263
261
  my_slaves.each {|slv| slv.resume_replication}
264
262
  [self, my_slaves].flatten.each {|db| db.catch_up_to_master}
@@ -47,6 +47,10 @@ module Jetpants
47
47
  # 'sharding_key' (or equivalently 'primary_key'), 'chunks', and 'order_by'.
48
48
  def initialize(name, params={})
49
49
  @name = name
50
+ parse_params(params)
51
+ end
52
+
53
+ def parse_params(params = {})
50
54
  params['sharding_key'] ||= params['primary_keys'] || params['primary_key'] || 'user_id'
51
55
  @sharding_keys = (params['sharding_key'].is_a?(Array) ? params['sharding_key'] : [params['sharding_key']])
52
56
  @chunks = params['chunks'] || 1
@@ -71,18 +71,23 @@ module Jetpants
71
71
  synchronized
72
72
  # Plugin should override so that this returns an array of [count] Jetpants::DB
73
73
  # objects, or throws an exception if not enough left.
74
- # Options hash is plugin-specific. The only assumed option used by the rest of
75
- # Jetpants is :role of 'MASTER' or 'STANDBY_SLAVE', for grabbing hardware
76
- # suited for a particular purpose. This can be ignored if your hardware is
77
- # entirely uniform and/or a burn-in process is already performed on all new
78
- # hardware intakes.
74
+ #
75
+ # Options hash is plugin-specific. Jetpants core will provide these two options,
76
+ # but it's up to a plugin to handle (or ignore) them:
77
+ #
78
+ # :role => :master or :standby_slave, indicating what purpose the new node(s)
79
+ # will be used for. Useful if your hardware spec varies by node role
80
+ # (not recommended!) or if you vet your master candidates more carefully.
81
+ # :like => a Jetpants::DB object, indicating that the spare node hardware spec
82
+ # should be like the specified DB's spec.
79
83
  def claim_spares(count, options={})
80
84
  raise "Plugin must override Topology#claim_spares"
81
85
  end
82
86
 
83
87
  synchronized
84
88
  # Plugin should override so that this returns a count of spare machines
85
- # matching the selected options.
89
+ # matching the selected options. options hash follows same format as for
90
+ # Topology#claim_spares.
86
91
  def count_spares(options={})
87
92
  raise "Plugin must override Topology#count_spares"
88
93
  end
@@ -109,12 +114,13 @@ module Jetpants
109
114
  @pools.reject {|p| p.is_a? Shard}
110
115
  end
111
116
 
112
- # Finds and returns a single Jetpants::Pool. Target may be a name (string) or master (DB object).
117
+ # Finds and returns a single Jetpants::Pool. Target may be a name (string, case insensitive)
118
+ # or master (DB object).
113
119
  def pool(target)
114
120
  if target.is_a?(DB)
115
121
  @pools.select {|p| p.master == target}.first
116
122
  else
117
- @pools.select {|p| p.name == target}.first
123
+ @pools.select {|p| p.name.downcase == target.downcase}.first
118
124
  end
119
125
  end
120
126
 
@@ -137,8 +143,21 @@ module Jetpants
137
143
  end
138
144
 
139
145
  # Returns the Jetpants::Shard that handles the given ID.
146
+ # During a shard split, if the child isn't "in production" yet (ie, it's
147
+ # still being built), this will always return the parent shard. Once the
148
+ # child is fully built / in production, this method will always return
149
+ # the child shard. However, Shard#db(:write) will correctly delegate writes
150
+ # to the parent shard when appropriate in this case. (see also: Topology#shard_db_for_id)
140
151
  def shard_for_id(id)
141
- @shards.select {|s| s.min_id <= id && (s.max_id == 'INFINITY' || s.max_id >= id)}[0]
152
+ choices = shards.select {|s| s.min_id <= id && (s.max_id == 'INFINITY' || s.max_id >= id)}
153
+ choices.reject! {|s| s.parent && ! s.in_config?} # filter out child shards that are still being built
154
+
155
+ # Preferentially return child shards at this point
156
+ if choices.any? {|s| s.parent}
157
+ choices.select {|s| s.parent}.first
158
+ else
159
+ choices.first
160
+ end
142
161
  end
143
162
 
144
163
  # Returns the Jetpants::DB that handles the given ID with the specified
@@ -69,9 +69,21 @@ module Jetpants
69
69
 
70
70
  ##### NEW METHODS ##########################################################
71
71
 
72
+ # Returns true if this database is located in the same datacenter as jetpants_collins
73
+ # has been figured for, false otherwise.
72
74
  def in_remote_datacenter?
73
75
  @host.collins_location != Plugin::JetCollins.datacenter
74
76
  end
75
77
 
78
+ # Returns true if this database is a spare node and looks ready for use, false otherwise.
79
+ # The default implementation just ensures a collins status of Provisioned.
80
+ # Downstream plugins may override this to do additional checks to ensure the node is
81
+ # in a sane state. (The caller of this method already checks that the node is SSHable,
82
+ # and that MySQL is running, and the node isn't already in a pool -- so no need to
83
+ # check any of those here.)
84
+ def usable_spare?
85
+ collins_status.downcase == 'provisioned'
86
+ end
87
+
76
88
  end
77
89
  end
@@ -62,7 +62,7 @@ module Jetpants
62
62
  end
63
63
 
64
64
  # We make these 4 accessors available to ANY class including this mixin
65
- collins_attr_accessor :primary_role, :secondary_role, :pool, :status
65
+ collins_attr_accessor :primary_role, :secondary_role, :pool, :status, :state
66
66
  end
67
67
  end
68
68
 
@@ -137,12 +137,18 @@ module Jetpants
137
137
  asset = collins_asset
138
138
  if field_names.count > 1 || field_names[0].is_a?(Array)
139
139
  field_names.flatten!
140
+ want_state = !! field_names.delete(:state)
140
141
  results = Hash[field_names.map {|field| [field, (asset ? asset.send(field) : '')]}]
142
+ results[:state] = asset.state.name if want_state
141
143
  results[:asset] = asset
142
144
  results
143
145
  elsif field_names.count == 1
144
146
  return '' unless asset
145
- asset.send field_names[0]
147
+ if field_names[0] == :state
148
+ asset.state.name
149
+ else
150
+ asset.send field_names[0]
151
+ end
146
152
  else
147
153
  nil
148
154
  end
@@ -174,11 +180,32 @@ module Jetpants
174
180
  output "WARNING: unable to set Collins status to #{val}"
175
181
  next
176
182
  end
177
- previous_value = asset.status
178
- if previous_value != val.to_s
179
- success = Jetpants::Plugin::JetCollins.set_status!(asset, val)
180
- raise "#{self}: Unable to set Collins status to #{val}" unless success
181
- output "Collins status changed from #{previous_value} to #{val}"
183
+ if attrs[:state]
184
+ previous_state = asset.state.name
185
+ previous_status = asset.status
186
+ if previous_state != attrs[:state].to_s || previous_status != attrs[:status].to_s
187
+ success = Jetpants::Plugin::JetCollins.set_status!(asset, attrs[:status], 'changed through jetpants', attrs[:state])
188
+ unless success
189
+ Jetpants::Plugin::JetCollins.state_create!(attrs[:state], attrs[:state], attrs[:state], attrs[:status])
190
+ success = Jetpants::Plugin::JetCollins.set_status!(asset, attrs[:status], 'changed through jetpants', attrs[:state])
191
+ end
192
+ raise "#{self}: Unable to set Collins state to #{attrs[:state]} and Unable to set Collins status to #{attrs[:status]}" unless success
193
+ output "Collins state changed from #{previous_state} to #{attrs[:state]}"
194
+ output "Collins status changed from #{previous_status} to #{attrs[:status]}"
195
+ end
196
+ else
197
+ previous_value = asset.status
198
+ if previous_value != val.to_s
199
+ success = Jetpants::Plugin::JetCollins.set_status!(asset, val)
200
+ raise "#{self}: Unable to set Collins status to #{val}" unless success
201
+ output "Collins status changed from #{previous_value} to #{val}"
202
+ end
203
+ end
204
+ when :state
205
+ unless asset && asset.status && attrs[:status]
206
+ raise "#{self}: Unable to set state without settings a status" unless attrs[:status]
207
+ output "WARNING: unable to set Collins state to #{val}"
208
+ next
182
209
  end
183
210
  else
184
211
  unless asset
@@ -111,7 +111,7 @@ module Jetpants
111
111
 
112
112
  # If the demoted master was offline, record some info in Collins, otherwise
113
113
  # there will be 2 masters listed
114
- def after_master_promotion!(promoted)
114
+ def after_master_promotion!(promoted, enslave_old_master=true)
115
115
  Jetpants.topology.clear_asset_cache
116
116
 
117
117
  # Find the master asset(s) for this pool, filtering down to only current datacenter
@@ -119,9 +119,13 @@ module Jetpants
119
119
  assets.reject! {|a| a.location && a.location.upcase != Plugin::JetCollins.datacenter}
120
120
  assets.map(&:to_db).each do |db|
121
121
  if db != @master || !db.running?
122
- db.collins_status = 'Maintenance'
123
122
  db.collins_pool = ''
124
123
  db.collins_secondary_role = ''
124
+ if enslave_old_master
125
+ db.output 'REMINDER: you must manually put this host into Maintenance status in Collins' unless db.collins_status.downcase == 'maintenance'
126
+ else
127
+ db.collins_status = 'Unallocated'
128
+ end
125
129
  end
126
130
  end
127
131
  end