pt-osc 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 0.0.5
2
+
3
+ - remove dependence on ActiveSupport/Rails
4
+ - can specify log file in `percona` config
5
+
1
6
  ## 0.0.4
2
7
 
3
8
  - Make sure `active_record/migration` is `require`d
data/README.md CHANGED
@@ -39,6 +39,7 @@ environment:
39
39
  Additional/modified options for the `percona` hash include:
40
40
  - `defaults-file`: Can be specified as an absolute path (with leading `/`) or relative (without). Relative paths will be treated as relative to your project's working directory.
41
41
  - `run_mode`: Specify `'execute'` to actually run `pt-online-schema-change` when the migration runs. Specify `'print'` to output the commands to run to STDOUT instead. Default is `'print'`.
42
+ - `log`: Specify the file used for logging activity. Can be a relative or absolute path.
42
43
 
43
44
  ## Caveats
44
45
 
@@ -109,25 +109,23 @@ module ActiveRecord
109
109
  end
110
110
 
111
111
  def get_commands_string(table_name)
112
- @osc_commands ||= {}
113
- @osc_commands[table_name] ||= []
114
- @osc_commands[table_name].join(',')
112
+ get_commands[table_name] ||= []
113
+ get_commands[table_name].join(',')
115
114
  end
116
115
 
117
116
  def get_commanded_tables
118
- @osc_commands.keys
117
+ get_commands.keys
119
118
  end
120
119
 
121
120
  protected
122
121
  def add_command(table_name, command)
123
- @osc_commands ||= {}
124
- @osc_commands[table_name] ||= []
125
- @osc_commands[table_name] << command
122
+ get_commands[table_name] ||= []
123
+ get_commands[table_name] << command
126
124
  end
127
125
 
128
- def get_commands(table_name)
126
+ def get_commands(table_name = nil)
129
127
  @osc_commands ||= {}
130
- @osc_commands[table_name]
128
+ table_name.nil? ? @osc_commands : @osc_commands[table_name]
131
129
  end
132
130
  end
133
131
  end
@@ -71,33 +71,36 @@ module ActiveRecord
71
71
  protected
72
72
  def execute_pt_osc
73
73
  return unless @connection.is_a? ActiveRecord::ConnectionAdapters::PtOscAdapter
74
+ return if @connection.get_commanded_tables.empty?
74
75
 
75
- @connection.get_commanded_tables.each do |table_name|
76
- execute_sql = @connection.get_commands_string(table_name)
76
+ database_name = database_config[:database]
77
+ announce 'running pt-online-schema-change'
77
78
 
78
- Rails.logger.tagged('pt-osc') do |logger|
79
+ @connection.get_commanded_tables.each { |table| migrate_table(database_name, table) }
80
+ @connection.clear_commands
81
+ end
79
82
 
80
- database_name = database_config[:database]
83
+ def migrate_table(database_name, table_name)
84
+ execute_sql = @connection.get_commands_string(table_name)
81
85
 
82
- logger.info "Running on #{database_name}|#{table_name}: #{execute_sql}"
83
- announce 'running pt-online-schema-change'
86
+ logger.info "Running on #{database_name}|#{table_name}: #{execute_sql}"
84
87
 
85
- [true, false].each do |dry_run|
86
- command = percona_command(execute_sql, database_name, table_name, execute: !dry_run)
87
- logger.info "Command is #{command}"
88
- success = Kernel.system command
89
- if success
90
- logger.info "Successfully #{dry_run ? 'dry ran' : 'executed'} on #{database_name}|#{table_name}: #{execute_sql}"
91
- else
92
- failure_message = "Unable to #{dry_run ? 'dry run' : 'execute'} query on #{database_name}|#{table_name}: #{execute_sql}"
93
- logger.error failure_message
94
- raise RuntimeError.new(failure_message)
95
- end
96
- end
97
- end
98
- end
88
+ [true, false].each { |dry_run| execute_sql_for_table(execute_sql, database_name, table_name, dry_run) }
89
+ end
99
90
 
100
- @connection.clear_commands
91
+ def execute_sql_for_table(execute_sql, database_name, table_name, dry_run = true)
92
+ command = percona_command(execute_sql, database_name, table_name, execute: !dry_run)
93
+ logger.info "Command is #{command}"
94
+
95
+ success = Kernel.system command
96
+
97
+ if success
98
+ logger.info "Successfully #{dry_run ? 'dry ran' : 'executed'} on #{database_name}|#{table_name}: #{execute_sql}"
99
+ else
100
+ failure_message = "Unable to #{dry_run ? 'dry run' : 'execute'} query on #{database_name}|#{table_name}: #{execute_sql}"
101
+ logger.error failure_message
102
+ raise RuntimeError.new(failure_message)
103
+ end
101
104
  end
102
105
 
103
106
  def print_pt_osc
@@ -159,6 +162,18 @@ module ActiveRecord
159
162
  database_config[:percona] || {}
160
163
  end
161
164
 
165
+ def logfile
166
+ File.open(make_path_absolute(percona_config[:log] || 'log/pt_osc.log'), 'a')
167
+ end
168
+
169
+ def logger
170
+ return @logger if @logger
171
+ @logger = Logger.new(logfile)
172
+ @logger.formatter = Logger::Formatter.new # Don't let ActiveSupport override with SimpleFormatter
173
+ @logger.progname = 'pt-osc'
174
+ @logger
175
+ end
176
+
162
177
  private
163
178
  # Flag mutators
164
179
  def make_path_absolute(path)
@@ -1,5 +1,5 @@
1
1
  module Pt
2
2
  module Osc
3
- VERSION = '0.0.4'
3
+ VERSION = '0.0.5'
4
4
  end
5
5
  end
@@ -191,6 +191,25 @@ class PtOscAdapterTest < Test::Unit::TestCase
191
191
  assert_nil @adapter.send(:get_commands, table_name)
192
192
  end
193
193
  end
194
+
195
+ context 'with existing commands' do
196
+ setup do
197
+ @commands_hash = 3.times.inject({}) do |hash|
198
+ hash[Faker::Lorem.word] = [Faker::Lorem.sentence]
199
+ hash
200
+ end
201
+ @adapter.instance_variable_set(:@osc_commands, @commands_hash)
202
+ end
203
+
204
+ should 'return the entire commands hash when no table is given' do
205
+ assert_equal @commands_hash, @adapter.send(:get_commands)
206
+ end
207
+
208
+ should "return only the given table's command array" do
209
+ table = @commands_hash.keys.first
210
+ assert_equal @commands_hash[table], @adapter.send(:get_commands, table)
211
+ end
212
+ end
194
213
  end
195
214
 
196
215
  context '#get_commands_string' do
@@ -99,5 +99,237 @@ class PtOscMigrationUnitTest < Test::Unit::TestCase
99
99
  end
100
100
  end
101
101
  end
102
+
103
+ context '#execute_pt_osc' do
104
+ context 'with a pt-osc connection' do
105
+ setup do
106
+ @mock_connection = mock
107
+ @mock_connection.stubs(:is_a?).with(ActiveRecord::ConnectionAdapters::PtOscAdapter).returns(true)
108
+
109
+ @old_connection = @migration.instance_variable_get(:@connection)
110
+ @migration.instance_variable_set(:@connection, @mock_connection)
111
+ end
112
+
113
+ teardown do
114
+ @migration.instance_variable_set(:@connection, @old_connection)
115
+ end
116
+
117
+ context 'connected to a database' do
118
+ setup do
119
+ @database_name = Faker::Lorem.word
120
+ @migration.stubs(:database_config).returns(database: @database_name)
121
+ end
122
+
123
+ teardown do
124
+ @migration.unstub(:database_config)
125
+ end
126
+
127
+ context 'with no tables or commands' do
128
+ setup do
129
+ fake_not_empty_array = []
130
+ fake_not_empty_array.stubs(:empty?).returns(false)
131
+ @mock_connection.stubs(:get_commanded_tables).returns(fake_not_empty_array)
132
+ @mock_connection.stubs(:clear_commands)
133
+ end
134
+
135
+ teardown do
136
+ @mock_connection.unstub(:get_commanded_tables)
137
+ @mock_connection.unstub(:clear_commands)
138
+ end
139
+
140
+ should 'announce its intention' do
141
+ @migration.expects(:announce).with('running pt-online-schema-change')
142
+ @migration.send(:execute_pt_osc)
143
+ end
144
+
145
+ should 'call migrate_table for each table' do
146
+ num_tables = (0..5).to_a.sample
147
+ @mock_connection.unstub(:get_commanded_tables)
148
+ @mock_connection.expects(:get_commanded_tables).twice.returns(num_tables.times.map { Faker::Lorem.word })
149
+
150
+ @migration.expects(:migrate_table).times(num_tables)
151
+ quietly { @migration.send(:execute_pt_osc) }
152
+ end
153
+
154
+ should 'clear commands when finished' do
155
+ @mock_connection.expects(:clear_commands)
156
+ quietly { @migration.send(:execute_pt_osc) }
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
162
+
163
+ context '#migrate_table' do
164
+ context 'with a pt-osc connection' do
165
+ setup do
166
+ @mock_connection = mock
167
+
168
+ @old_connection = @migration.instance_variable_get(:@connection)
169
+ @migration.instance_variable_set(:@connection, @mock_connection)
170
+ end
171
+
172
+ teardown do
173
+ @migration.instance_variable_set(:@connection, @old_connection)
174
+ end
175
+
176
+ context 'with a command string' do
177
+ setup do
178
+ @mock_connection.stubs(:get_commands_string).returns('<<command string>>')
179
+ end
180
+
181
+ teardown do
182
+ @mock_connection.unstub(:get_commands_string)
183
+ end
184
+
185
+ context 'with stubbed log' do
186
+ setup do
187
+ @dummy_log = StringIO.new
188
+ @migration.stubs(:logfile).returns(@dummy_log)
189
+ end
190
+
191
+ teardown do
192
+ @migration.unstub(:logfile)
193
+ end
194
+
195
+ should 'log the database, table, and SQL command' do
196
+ database_name = Faker::Lorem.word
197
+ table_name = Faker::Lorem.word
198
+
199
+ @migration.stubs(:execute_sql_for_table)
200
+ @migration.send(:migrate_table, database_name, table_name)
201
+
202
+ assert database_name.in?(@dummy_log.string), 'Log entry did not contain database name'
203
+ assert table_name.in?(@dummy_log.string), 'Log entry did not contain table name'
204
+ assert '<<command string>>'.in?(@dummy_log.string), 'Log entry did not contain command string'
205
+ end
206
+
207
+ should 'call execute twice (dry run and execute)' do
208
+ @migration.expects(:execute_sql_for_table).with(anything, anything, anything, true).once
209
+ @migration.expects(:execute_sql_for_table).with(anything, anything, anything, false).once
210
+ @migration.send(:migrate_table, nil, nil)
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
216
+
217
+ context '#execute_sql_for_table' do
218
+ context 'with stubbed command' do
219
+ setup do
220
+ Kernel.stubs(:system).returns(true)
221
+ end
222
+
223
+ teardown do
224
+ Kernel.unstub(:system)
225
+ end
226
+
227
+ context 'with stubbed log' do
228
+ setup do
229
+ @dummy_log = StringIO.new
230
+ @migration.stubs(:logfile).returns(@dummy_log)
231
+ end
232
+
233
+ teardown do
234
+ @migration.unstub(:logfile)
235
+ end
236
+
237
+ should 'log the command' do
238
+ @migration.expects(:percona_command).returns('<<percona command>>')
239
+ @migration.send(:execute_sql_for_table, nil, nil, nil)
240
+ assert '<<percona command>>'.in?(@dummy_log.string), 'Log entry did not contain percona command'
241
+ end
242
+
243
+ context 'with successful execution' do
244
+ setup do
245
+ Kernel.expects(:system).returns(true)
246
+ end
247
+
248
+ teardown do
249
+ Kernel.unstub(:system)
250
+ end
251
+
252
+ should 'log success' do
253
+ @migration.send(:execute_sql_for_table, nil, nil, nil)
254
+ assert 'Success'.in?(@dummy_log.string), 'Success not mentioned in log'
255
+ end
256
+ end
257
+
258
+ context 'with failed execution' do
259
+ setup do
260
+ Kernel.expects(:system).returns(false)
261
+ end
262
+
263
+ teardown do
264
+ Kernel.unstub(:system)
265
+ end
266
+
267
+ should 'log failure' do
268
+ @migration.send(:execute_sql_for_table, nil, nil, nil) rescue nil
269
+ assert 'Unable to'.in?(@dummy_log.string), 'Failure not mentioned in log'
270
+ end
271
+
272
+ should 'raise a RuntimeError' do
273
+ assert_raises(RuntimeError) { @migration.send(:execute_sql_for_table, nil, nil, nil) }
274
+ end
275
+ end
276
+ end
277
+ end
278
+ end
279
+
280
+ context '#logger' do
281
+ context 'with stubbed log' do
282
+ setup do
283
+ @dummy_log = StringIO.new
284
+ @migration.stubs(:logfile).returns(@dummy_log)
285
+ end
286
+
287
+ teardown do
288
+ @migration.unstub(:logfile)
289
+ end
290
+
291
+ should 'log entries with "pt-osc"' do
292
+ logger = @migration.send(:logger)
293
+ logger.info 'test'
294
+ assert 'pt-osc'.in?(@dummy_log.string), "Log entry did not contain 'pt-osc': #{@dummy_log.string}"
295
+ end
296
+ end
297
+ end
298
+
299
+ context '#logfile' do
300
+ context 'with nothing in config' do
301
+ setup do
302
+ @migration.stubs(:percona_config).returns({})
303
+ end
304
+
305
+ teardown do
306
+ @migration.unstub(:percona_config)
307
+ end
308
+
309
+ should 'use log/pt_osc.log' do
310
+ @migration.stubs(:make_path_absolute).with('log/pt_osc.log')
311
+ .returns(File.expand_path('../dummy/log/pt_osc.log', File.dirname(__FILE__)))
312
+ logfile = @migration.send(:logfile)
313
+ assert 'log/pt_osc.log'.in?(logfile.path), 'Default log file not found in path'
314
+ end
315
+ end
316
+
317
+ context 'with a logfile specified in config' do
318
+ setup do
319
+ @migration.stubs(:percona_config).returns(log: 'log/fakelog.file')
320
+ end
321
+
322
+ teardown do
323
+ @migration.unstub(:percona_config)
324
+ end
325
+
326
+ should 'use log/pt_osc.log' do
327
+ @migration.stubs(:make_path_absolute).with('log/fakelog.file')
328
+ .returns(File.expand_path('../dummy/log/fakelog.file', File.dirname(__FILE__)))
329
+ logfile = @migration.send(:logfile)
330
+ assert 'log/fakelog.file'.in?(logfile.path), 'Configured log file not found in path'
331
+ end
332
+ end
333
+ end
102
334
  end
103
335
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pt-osc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -229,7 +229,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
229
229
  version: '0'
230
230
  segments:
231
231
  - 0
232
- hash: -2272605143598001323
232
+ hash: -1631463669037670360
233
233
  required_rubygems_version: !ruby/object:Gem::Requirement
234
234
  none: false
235
235
  requirements:
@@ -238,7 +238,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
238
238
  version: '0'
239
239
  segments:
240
240
  - 0
241
- hash: -2272605143598001323
241
+ hash: -1631463669037670360
242
242
  requirements: []
243
243
  rubyforge_project:
244
244
  rubygems_version: 1.8.23