jetpants 0.7.10 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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