PT-1717 - resume pt-online-schema-change if it's interrupted

- Added options --history, --history-table, and --binary-index
- Added code that creates history table to store pt-osc progress
This commit is contained in:
Sveta Smirnova
2024-02-20 20:08:20 +03:00
parent ca368e6e5a
commit ab6e5aa1bb
4 changed files with 409 additions and 9 deletions

View File

@@ -8578,6 +8578,7 @@ sub main {
# Get configuration information.
# ########################################################################
my $q = new Quoter();
my $tp = TableParser->new(Quoter => $q);
my $o = new OptionParser();
$o->get_specs();
$o->get_opts();
@@ -8795,6 +8796,32 @@ sub main {
my $cxn = $make_cxn->(dsn => $dsn);
my $aux_cxn = $make_cxn->(dsn => $dsn, prev_dsn => $dsn);
if ( $o->get('history') ) {
create_history_table(
dbh => $cxn->dbh(),
hist_table => $o->get('history-table'),
OptionParser => $o,
TableParser => $tp,
Quoter => $q,
);
# parsing args here
use JSON;
my $json = JSON->new->allow_nonref;
my %opts = $o->opts();
_d("DB: $db, tbl: $tbl");
my $opt_hash = {};
while ( my ($opt, $value) = each (%opts) ) {
if ( $value->{got} ) {
$opt_hash->{$opt} = $value->{value} ;
}
}
my $json_str = $json->encode($opt_hash);
print "$json_str\n";
#_d(Dumper($o->opts()));
}
my $cluster = Percona::XtraDB::Cluster->new;
if ( $cluster->is_cluster_node($cxn) ) {
# Because of https://bugs.launchpad.net/codership-mysql/+bug/1040108
@@ -9208,7 +9235,6 @@ sub main {
# ########################################################################
# Setup and check the original table.
# ########################################################################
my $tp = TableParser->new(Quoter => $q);
# Common table data struct (that modules like NibbleIterator expect).
my $orig_tbl = {
@@ -10487,6 +10513,110 @@ sub main {
# Subroutines.
# ############################################################################
sub create_history_table {
my ( %args ) = @_;
my @required_args = qw(dbh hist_table OptionParser TableParser Quoter);
foreach my $arg ( @required_args ) {
die "I need a $arg argument" unless $args{$arg};
}
my ($dbh, $hist_table, $o, $tp, $q) = @args{@required_args};
PTDEBUG && _d('Creating --history table', $hist_table);
# ########################################################################
# Create the --history database.
# ########################################################################
# Create history database if it not exists.
my ($db, $tbl) = $q->split_unquote($hist_table);
my $show_db_sql = "SHOW DATABASES LIKE '$db'";
PTDEBUG && _d($show_db_sql);
my @db_exists = $dbh->selectrow_array($show_db_sql);
if ( !@db_exists ) {
my $create_db_sql
= "CREATE DATABASE IF NOT EXISTS "
. $q->quote($db)
. " /* pt-table-checksum */";
PTDEBUG && _d($create_db_sql);
eval {
$dbh->do($create_db_sql);
};
# CREATE DATABASE IF NOT EXISTS failed but the db could already
# exist and the error could be due, for example, to the user not
# having privs to create it, but they still have privs to use it.
if ( $EVAL_ERROR) {
warn $EVAL_ERROR;
die "--history database $db does not exist and it cannot be "
. "created automatically. You need to create the database.\n";
}
}
# ########################################################################
# Create the --history table.
# ########################################################################
# Prepare DDL
PTDEBUG && _d('Creating --history table', $hist_table);
my $sql = $o->read_para_after(__FILE__, qr/MAGIC_create_pt_osc_history/);
$sql =~ s/CREATE TABLE pt_osc_history/CREATE TABLE `$tbl`/;
if ( $o->get('binary-index') ) {
$sql =~ s/`?lower_boundary`?\s+TEXT/`lower_boundary` BLOB/is;
$sql =~ s/`?upper_boundary`?\s+TEXT/`upper_boundary` BLOB/is;
}
PTDEBUG && _d($dbh, $sql);
# Check if the history table exists; if not, create it, maybe.
my $tbl_exists = $tp->check_table(
dbh => $dbh,
db => $db,
tbl => $tbl,
);
PTDEBUG && _d('--history table exists:', $tbl_exists ? 'yes' : 'no');
if ( $tbl_exists ) {
# check if the table has expected structure
my $tbl_sql = $tp->get_create_table(
$dbh,
$db,
$tbl,
);
# we will compare SQL code
my $sql_minimized = $sql;
my $tbl_sql_minimized = $tbl_sql;
$sql_minimized =~ s/;$//;
$sql_minimized =~ s/\s+/ /g;
$sql_minimized =~ s/`//g;
$sql_minimized =~ s/^\s+//g;
$sql_minimized =~ s/engine\=[\w\d_= ]+$//i;
$tbl_sql_minimized =~ s/\s+/ /g;
$tbl_sql_minimized =~ s/`//g;
$tbl_sql_minimized =~ s/^\s+//g;
$tbl_sql_minimized =~ s/engine\=[\w\d_= ]+$//i;
if ( lc($sql_minimized) ne lc($tbl_sql_minimized) ) {
die "--history table $tbl exists but does not have expected structure.\n"
. "Explected structure:\n$sql \n"
. "User-defined table:\n$tbl_sql\n"
. "Exiting to avoid damage of the user-defined table.";
}
}
else {
$sql =~ s/CREATE TABLE `$tbl`/CREATE TABLE IF NOT EXISTS `$db`.`$tbl`/;
PTDEBUG && _d("Executing SQL for the --history table:\n $sql");
eval {
$dbh->do($sql);
};
if ( $EVAL_ERROR ) {
die "Create --history table $tbl failed: $EVAL_ERROR";
}
}
return;
}
sub validate_tries {
my ($o) = @_;
my @ops = qw(
@@ -11912,6 +12042,9 @@ sub exec_nibble {
PTDEBUG && _d($sth->{nibble}->{Statement},
'lower boundary:', @{$boundary->{lower}},
'upper boundary:', @{$boundary->{upper}});
# _d($sth->{nibble}->{Statement},
# 'lower boundary:', @{$boundary->{lower}},
# 'upper boundary:', @{$boundary->{upper}});
$sth->{nibble}->execute(
# WHERE
@{$boundary->{lower}}, # upper boundary values
@@ -12414,6 +12547,15 @@ the server very busy, this can cause an outage.
Prompt for a password when connecting to MySQL.
=item --binary-index
This option modifies the behavior of L<"--history"> such that the
history table's upper and lower boundary columns are created with the BLOB
data type.
This is useful in cases where you changing large tables with keys that
include a binary data type or that have non-standard character sets.
See L<"--history"> and L<"--resume">.
=item --channel
type: string
@@ -12615,6 +12757,17 @@ type: Array
Read this comma-separated list of config files; if specified, this must be the
first option on the command line.
=item --history-table
type: string; default: percona.pt_osc_history
Create the L<"--history"> database and table if they do not exist.
The structure of the history table is the same as the suggested table
mentioned in L<"--history">.
By default, L<"--history"> creates the database and the table, specified by this option,
automatically if they do not exist.
=item --critical-load
type: Array; default: Threads_running=50
@@ -12764,6 +12917,28 @@ This option also allows to use option --where without options --no-drop-new-tabl
Show help and exit.
=item --history
default: 0
Write job progress to a table. Unfinished jobs may be restarted by the option L<"--resume">.
The history table must have this structure (MAGIC_create_pt_osc_history):
CREATE TABLE pt_osc_history (
job_id INT NOT NULL AUTO_INCREMENT,
db CHAR(64) NOT NULL,
tbl CHAR(64) NOT NULL,
altr TEXT NOT NULL,
args TEXT NOT NULL,
lower_boundary TEXT,
upper_boundary TEXT,
done ENUM('yes','no') NOT NULL DEFAULT 'no',
ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (job_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Note: lower_boundary and upper_boundary data type can be BLOB. See L<"--binary-index">.
=item --host
short form: -h; type: string

View File

@@ -12019,7 +12019,7 @@ sub check_repl_table {
# Create the --replicate database.
# ########################################################################
# If the repl db doesn't exit, auto-create it, maybe.
# If the repl db doesn't exist, auto-create it, maybe.
my ($db, $tbl) = $q->split_unquote($repl_table);
my $show_db_sql = "SHOW DATABASES LIKE '$db'";
PTDEBUG && _d($show_db_sql);

View File

@@ -310,15 +310,11 @@ sub find_possible_keys {
# * dbh dbh: active dbh
# * db scalar: database name to check
# * tbl scalar: table name to check
# Optional args:
# * all_privs bool: check for all privs (select,insert,update,delete)
# Returns: bool
# Can die: no
# check_table() checks the given table for certain criteria and returns
# true if all criteria are found, else it returns false. The existence
# of the table is always checked; if no optional args are given, then this
# is the only check. Any error causes a false return value (e.g. if the
# table is crashed).
# check_table() checks the given table for the existence and returns
# true if the table is found, else it returns false.
# Any error causes a false return value (e.g. if the table is crashed).
sub check_table {
my ( $self, %args ) = @_;
my @required_args = qw(dbh db tbl);

View File

@@ -0,0 +1,229 @@
#!/usr/bin/env perl
BEGIN {
die "The PERCONA_TOOLKIT_BRANCH environment variable is not set.\n"
unless $ENV{PERCONA_TOOLKIT_BRANCH} && -d $ENV{PERCONA_TOOLKIT_BRANCH};
unshift @INC, "$ENV{PERCONA_TOOLKIT_BRANCH}/lib";
};
use strict;
use warnings FATAL => 'all';
use English qw(-no_match_vars);
use Test::More;
use PerconaTest;
use Sandbox;
require "$trunk/bin/pt-online-schema-change";
require VersionParser;
use Data::Dumper;
my $dp = new DSNParser(opts=>$dsn_opts);
my $sb = new Sandbox(basedir => '/tmp', DSNParser => $dp);
my $master_dbh = $sb->get_dbh_for('master');
my $slave_dbh = $sb->get_dbh_for('slave1');
if ( !$master_dbh ) {
plan skip_all => 'Cannot connect to sandbox master';
}
elsif ( !$slave_dbh ) {
plan skip_all => 'Cannot connect to sandbox slave';
}
my @args = qw(--set-vars innodb_lock_wait_timeout=3);
my $output = "";
my $dsn = "h=127.1,P=12345,u=msandbox,p=msandbox";
my $exit = 0;
my $sample = "t/pt-online-schema-change/samples";
$sb->load_file('master', "$sample/basic_no_fks_innodb.sql");
# First test option --history
# * - Test done for the development step
# ** - Test done for two development steps
# 1.** If table percona.pt_osc not created when option not specified
# 2. If table percona.pt_osc created when option present
# 2.1.** Default name
# 2.2.** Custom name
# 2.3.* Second run should not fail or modify this table (except inserting a row for new job)
# 2.4.* Case for binary index
# 2.5.** Second run for the binary index
# 2.6.** Case for invalid existing table
# 2.7.** Case for invalid existing table and binary index
# 3. Inserting db, tbl, alter, args
# 4. Updating lower and upper boundaries
# 4.1. In situation when pt-osc finishes correctly
# 4.1.1. `done` set to 'yes'
# 4.2. In failures
# 4.2.1. `done` set to 'no'
($output, $exit) = full_output(
sub { pt_online_schema_change::main(@args, "$dsn,D=pt_osc,t=t",
'--alter', 'engine=innodb', '--execute') }
);
is(
$exit,
0,
'basic test finished OK'
) or diag($output);
$output = `/tmp/12345/use -N -e "select count(*) from information_schema.tables where TABLE_SCHEMA='percona' and table_name='pt_osc_history'"`;
is(
$output + 0,
0,
'--history table not created when option --history not provided'
);
($output, $exit) = full_output(
sub { pt_online_schema_change::main(@args, "$dsn,D=pt_osc,t=t",
'--alter', 'engine=innodb', '--execute', '--history') }
);
is(
$exit,
0,
'basic test with option --history finished OK'
) or diag($output);
$output = `/tmp/12345/use -N -e "select count(*) from information_schema.tables where TABLE_SCHEMA='percona' and table_name='pt_osc_history'"`;
is(
$output + 0,
1,
'--history table created when option --history was provided'
);
($output, $exit) = full_output(
sub { pt_online_schema_change::main(@args, "$dsn,D=pt_osc,t=t",
'--alter', 'engine=innodb', '--execute', '--history') }
);
is(
$exit,
0,
'basic test with option --history finished OK when table already exists'
) or diag($output);
$output = `/tmp/12345/use -N -e "select count(*) from information_schema.tables where TABLE_SCHEMA='percona' and table_name='pt_osc_history'"`;
is(
$output + 0,
1,
'--history table was created when option --history was provided only once'
);
diag(`/tmp/12345/use -N -e "drop table percona.pt_osc_history"`);
($output, $exit) = full_output(
sub { pt_online_schema_change::main(@args, "$dsn,D=pt_osc,t=t",
'--alter', 'engine=innodb', '--execute', '--history', '--binary-index') }
);
is(
$exit,
0,
'basic test with option --binary-index finished OK'
) or diag($output);
$output = `/tmp/12345/use -N -e "select count(*) from information_schema.tables where TABLE_SCHEMA='percona' and table_name='pt_osc_history'"`;
is(
$output + 0,
1,
'--history table was created when option --history and --binary-index were provided'
);
$output = `/tmp/12345/use -e "show create table percona.pt_osc_history"`;
like(
$output,
qr/`lower_boundary` blob,\\n\s+`upper_boundary` blob/i,
'--history table created with BLOB data type for boundary columns with --binary-index'
);
($output, $exit) = full_output(
sub { pt_online_schema_change::main(@args, "$dsn,D=pt_osc,t=t",
'--alter', 'engine=innodb', '--execute', '--history', '--binary-index') }
);
is(
$exit,
0,
'second with option --binary-index finished OK'
) or diag($output);
$output = `/tmp/12345/use -N -e "select count(*) from information_schema.tables where TABLE_SCHEMA='percona' and table_name='pt_osc_history'"`;
is(
$output + 0,
1,
'--history table was created only once with --binary-index'
);
($output, $exit) = full_output(
sub { pt_online_schema_change::main(@args, "$dsn,D=pt_osc,t=t",
'--alter', 'engine=innodb', '--execute', '--history') }
);
isnt(
$exit,
0,
'pt_osc with --history failed if table with the same name and different structure exists'
) or diag($output);
diag(`/tmp/12345/use -e "alter table percona.pt_osc_history add column foo int"`);
($output, $exit) = full_output(
sub { pt_online_schema_change::main(@args, "$dsn,D=pt_osc,t=t",
'--alter', 'engine=innodb', '--execute', '--history', '--binary-index') }
);
isnt(
$exit,
0,
'pt_osc with --history and --binary-index failed if table with the same name and different structure exists'
) or diag($output);
# Custom table
($output, $exit) = full_output(
sub { pt_online_schema_change::main(@args, "$dsn,D=pt_osc,t=t",
'--alter', 'engine=innodb', '--execute', '--history',
'--history-table=pt_1717.pt_1717_history') }
);
is(
$exit,
0,
'basic test with option --history-table finished OK'
) or diag($output);
$output = `/tmp/12345/use -N -e "select count(*) from information_schema.tables where TABLE_SCHEMA='pt_1717' and table_name='pt_1717_history'"`;
is(
$output + 0,
1,
'Custom --history table created'
);
($output, $exit) = full_output(
sub { pt_online_schema_change::main(@args, "$dsn,D=pt_osc,t=t",
'--alter', 'engine=innodb', '--execute', '--history',
'--history-table=pt_1717.pt_1717_history') }
);
is(
$exit,
0,
'basic test with option --history-table finished OK when table already exists'
) or diag($output);
$output = `/tmp/12345/use -N -e "select count(*) from information_schema.tables where TABLE_SCHEMA='percona' and table_name='pt_osc_history'"`;
is(
$output + 0,
1,
'Custom --history table was created only once'
);
# #############################################################################
# Done.
# #############################################################################
$sb->wipe_clean($master_dbh);
ok($sb->ok(), "Sandbox servers") or BAIL_OUT(__FILE__ . " broke the sandbox");
#
done_testing;