From 0d7af14e6341f0f08fd34848b6d49c09a5cc2b38 Mon Sep 17 00:00:00 2001 From: Dirkjan Bussink Date: Tue, 7 May 2024 15:33:46 +0200 Subject: [PATCH] mysqlctl: Improve backup restore compatibility check (#15856) Signed-off-by: Dirkjan Bussink --- go/vt/mysqlctl/backup_test.go | 12 -- go/vt/mysqlctl/backupengine.go | 51 +++++++ go/vt/mysqlctl/backupengine_test.go | 215 ++++++++++++++++++++++++++++ go/vt/mysqlctl/version.go | 4 + 4 files changed, 270 insertions(+), 12 deletions(-) create mode 100644 go/vt/mysqlctl/backupengine_test.go diff --git a/go/vt/mysqlctl/backup_test.go b/go/vt/mysqlctl/backup_test.go index 1fa3749ea8d..d1d6a73b7bd 100644 --- a/go/vt/mysqlctl/backup_test.go +++ b/go/vt/mysqlctl/backup_test.go @@ -423,18 +423,6 @@ func TestRestoreManifestMySQLVersionValidation(t *testing.T) { upgradeSafe bool wantErr bool }{ - { - fromVersion: "mysqld Ver 5.6.42", - toVersion: "mysqld Ver 5.7.40", - upgradeSafe: false, - wantErr: true, - }, - { - fromVersion: "mysqld Ver 5.6.42", - toVersion: "mysqld Ver 5.7.40", - upgradeSafe: true, - wantErr: false, - }, { fromVersion: "mysqld Ver 5.7.42", toVersion: "mysqld Ver 8.0.32", diff --git a/go/vt/mysqlctl/backupengine.go b/go/vt/mysqlctl/backupengine.go index 915b5e6894f..c483aff3d78 100644 --- a/go/vt/mysqlctl/backupengine.go +++ b/go/vt/mysqlctl/backupengine.go @@ -592,6 +592,15 @@ func FindBackupToRestore(ctx context.Context, params RestoreParams, bhs []backup return restorePath, nil } +// See https://github.com/mysql/mysql-server/commit/9a940abe085fc75e1ffe7b72286927fdc9f11207 for the +// importance of this specific version and why downgrades within patches are allowed since that version. +var mysql8035 = ServerVersion{Major: 8, Minor: 0, Patch: 35} +var ltsVersions = []ServerVersion{ + {Major: 5, Minor: 7, Patch: 0}, + {Major: 8, Minor: 0, Patch: 0}, + {Major: 8, Minor: 4, Patch: 0}, +} + func validateMySQLVersionUpgradeCompatible(to string, from string, upgradeSafe bool) error { // It's always safe to use the same version. if to == from { @@ -616,6 +625,48 @@ func validateMySQLVersionUpgradeCompatible(to string, from string, upgradeSafe b return nil } + // If we're not on the same LTS stream, we have to do additional checks to see if it's safe to + // to upgrade. It can only be one newer LTS version for the destination and the backup + // has to be marked as upgrade safe. + + // If something is across different LTS streams and not upgrade safe, we can't use it. + if !parsedFrom.isSameRelease(parsedTo) { + if !upgradeSafe { + if parsedTo.atLeast(parsedFrom) { + return fmt.Errorf("running MySQL version %q is newer than backup MySQL version %q which is not safe to upgrade", to, from) + } + return fmt.Errorf("running MySQL version %q is older than backup MySQL version %q", to, from) + } + + // Alright, we're across different LTS streams and the backup is upgrade safe. + // We can only upgrade to the next LTS version. + for i, ltsVersion := range ltsVersions { + if parsedFrom.isSameRelease(ltsVersion) { + if i < len(ltsVersions)-1 && parsedTo.isSameRelease(ltsVersions[i+1]) { + return nil + } + if parsedTo.atLeast(parsedFrom) { + return fmt.Errorf("running MySQL version %q is too new for backup MySQL version %q", to, from) + } + return fmt.Errorf("running MySQL version %q is older than backup MySQL version %q", to, from) + } + } + if parsedTo.atLeast(parsedFrom) { + return fmt.Errorf("running MySQL version %q is newer than backup MySQL version %q which is not safe to upgrade", to, from) + } + return fmt.Errorf("running MySQL version %q is older than backup MySQL version %q", to, from) + } + + // At this point we know the versions are not the same, but we're withing the same version stream + // and only the patch version number mismatches. + + // Starting with MySQL 8.0.35, the data dictionary format is stable for 8.0.x, so we can upgrade + // from 8.0.35 or later here, also if the backup was taken with innodb_fast_shutdown=0. + // This also applies for any version newer like 8.4.x. + if parsedFrom.atLeast(mysql8035) && parsedTo.atLeast(mysql8035) { + return nil + } + if !parsedTo.atLeast(parsedFrom) { return fmt.Errorf("running MySQL version %q is older than backup MySQL version %q", to, from) } diff --git a/go/vt/mysqlctl/backupengine_test.go b/go/vt/mysqlctl/backupengine_test.go new file mode 100644 index 00000000000..77a02662460 --- /dev/null +++ b/go/vt/mysqlctl/backupengine_test.go @@ -0,0 +1,215 @@ +package mysqlctl + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidateMySQLVersionUpgradeCompatible(t *testing.T) { + // Test that the MySQL version is compatible with the upgrade. + testCases := []struct { + name string + fromVersion string + toVersion string + upgradeSafe bool + error string + }{ + { + name: "upgrade from 5.7 to 8.0", + fromVersion: "mysqld Ver 5.7.35", + toVersion: "mysqld Ver 8.0.23", + upgradeSafe: true, + }, + { + name: "downgrade from 8.0 to 5.7", + fromVersion: "mysqld Ver 8.0.23", + toVersion: "mysqld Ver 5.7.35", + upgradeSafe: true, + error: `running MySQL version "mysqld Ver 5.7.35" is older than backup MySQL version "mysqld Ver 8.0.23"`, + }, + { + name: "upgrade from 5.7 to 8.0", + fromVersion: "mysqld Ver 5.7.35", + toVersion: "mysqld Ver 8.0.23", + upgradeSafe: false, + error: `running MySQL version "mysqld Ver 8.0.23" is newer than backup MySQL version "mysqld Ver 5.7.35" which is not safe to upgrade`, + }, + { + name: "downgrade from 8.0 to 5.7", + fromVersion: "mysqld Ver 8.0.23", + toVersion: "mysqld Ver 5.7.35", + upgradeSafe: false, + error: `running MySQL version "mysqld Ver 5.7.35" is older than backup MySQL version "mysqld Ver 8.0.23"`, + }, + { + name: "upgrade from 8.0.23 to 8.0.34", + fromVersion: "mysqld Ver 8.0.23", + toVersion: "mysqld Ver 8.0.34", + upgradeSafe: true, + }, + { + name: "downgrade from 8.0.34 to 8.0.23", + fromVersion: "mysqld Ver 8.0.34", + toVersion: "mysqld Ver 8.0.23", + upgradeSafe: true, + error: `running MySQL version "mysqld Ver 8.0.23" is older than backup MySQL version "mysqld Ver 8.0.34"`, + }, + { + name: "upgrade from 8.0.23 to 8.0.34", + fromVersion: "mysqld Ver 8.0.23", + toVersion: "mysqld Ver 8.0.34", + upgradeSafe: false, + error: `running MySQL version "mysqld Ver 8.0.34" is newer than backup MySQL version "mysqld Ver 8.0.23" which is not safe to upgrade`, + }, + { + name: "downgrade from 8.0.34 to 8.0.23", + fromVersion: "mysqld Ver 8.0.34", + toVersion: "mysqld Ver 8.0.23", + upgradeSafe: false, + error: `running MySQL version "mysqld Ver 8.0.23" is older than backup MySQL version "mysqld Ver 8.0.34"`, + }, + { + name: "upgrade from 8.0.32 to 8.0.36", + fromVersion: "mysqld Ver 8.0.32", + toVersion: "mysqld Ver 8.0.36", + upgradeSafe: true, + }, + { + name: "downgrade from 8.0.36 to 8.0.32", + fromVersion: "mysqld Ver 8.0.36", + toVersion: "mysqld Ver 8.0.32", + upgradeSafe: true, + error: `running MySQL version "mysqld Ver 8.0.32" is older than backup MySQL version "mysqld Ver 8.0.36"`, + }, + { + name: "upgrade from 8.0.32 to 8.0.36", + fromVersion: "mysqld Ver 8.0.32", + toVersion: "mysqld Ver 8.0.36", + upgradeSafe: false, + error: `running MySQL version "mysqld Ver 8.0.36" is newer than backup MySQL version "mysqld Ver 8.0.32" which is not safe to upgrade`, + }, + { + name: "downgrade from 8.0.36 to 8.0.32", + fromVersion: "mysqld Ver 8.0.36", + toVersion: "mysqld Ver 8.0.32", + upgradeSafe: false, + error: `running MySQL version "mysqld Ver 8.0.32" is older than backup MySQL version "mysqld Ver 8.0.36"`, + }, + { + name: "upgrade from 8.0.35 to 8.0.36", + fromVersion: "mysqld Ver 8.0.35", + toVersion: "mysqld Ver 8.0.36", + upgradeSafe: true, + }, + { + name: "downgrade from 8.0.36 to 8.0.35", + fromVersion: "mysqld Ver 8.0.36", + toVersion: "mysqld Ver 8.0.35", + upgradeSafe: true, + }, + { + name: "upgrade from 8.0.35 to 8.0.36", + fromVersion: "mysqld Ver 8.0.35", + toVersion: "mysqld Ver 8.0.36", + upgradeSafe: false, + }, + { + name: "downgrade from 8.0.36 to 8.0.35", + fromVersion: "mysqld Ver 8.0.36", + toVersion: "mysqld Ver 8.0.35", + upgradeSafe: false, + }, + { + name: "upgrade from 8.4.0 to 8.4.1", + fromVersion: "mysqld Ver 8.4.0", + toVersion: "mysqld Ver 8.4.1", + upgradeSafe: true, + }, + { + name: "downgrade from 8.4.1 to 8.4.0", + fromVersion: "mysqld Ver 8.4.1", + toVersion: "mysqld Ver 8.4.0", + upgradeSafe: true, + }, + { + name: "upgrade from 8.4.0 to 8.4.1", + fromVersion: "mysqld Ver 8.4.0", + toVersion: "mysqld Ver 8.4.1", + upgradeSafe: false, + }, + { + name: "downgrade from 8.4.1 to 8.4.0", + fromVersion: "mysqld Ver 8.4.1", + toVersion: "mysqld Ver 8.4.0", + upgradeSafe: false, + }, + { + name: "upgrade from 8.0.35 to 8.4.0", + fromVersion: "mysqld Ver 8.0.32", + toVersion: "mysqld Ver 8.4.0", + upgradeSafe: true, + }, + { + name: "downgrade from 8.4.0 to 8.0.32", + fromVersion: "mysqld Ver 8.4.0", + toVersion: "mysqld Ver 8.0.32", + upgradeSafe: true, + error: `running MySQL version "mysqld Ver 8.0.32" is older than backup MySQL version "mysqld Ver 8.4.0"`, + }, + { + name: "upgrade from 8.0.32 to 8.4.0", + fromVersion: "mysqld Ver 8.0.32", + toVersion: "mysqld Ver 8.4.0", + upgradeSafe: false, + error: `running MySQL version "mysqld Ver 8.4.0" is newer than backup MySQL version "mysqld Ver 8.0.32" which is not safe to upgrade`, + }, + { + name: "downgrade from 8.4.0 to 8.0.32", + fromVersion: "mysqld Ver 8.4.0", + toVersion: "mysqld Ver 8.0.32", + upgradeSafe: false, + error: `running MySQL version "mysqld Ver 8.0.32" is older than backup MySQL version "mysqld Ver 8.4.0"`, + }, + { + name: "upgrade from 5.7.35 to 8.4.0", + fromVersion: "mysqld Ver 5.7.32", + toVersion: "mysqld Ver 8.4.0", + upgradeSafe: true, + error: `running MySQL version "mysqld Ver 8.4.0" is too new for backup MySQL version "mysqld Ver 5.7.32"`, + }, + { + name: "downgrade from 8.4.0 to 5.7.32", + fromVersion: "mysqld Ver 8.4.0", + toVersion: "mysqld Ver 5.7.32", + upgradeSafe: true, + error: `running MySQL version "mysqld Ver 5.7.32" is older than backup MySQL version "mysqld Ver 8.4.0"`, + }, + { + name: "upgrade from 5.7.32 to 8.4.0", + fromVersion: "mysqld Ver 5.7.32", + toVersion: "mysqld Ver 8.4.0", + upgradeSafe: false, + error: `running MySQL version "mysqld Ver 8.4.0" is newer than backup MySQL version "mysqld Ver 5.7.32" which is not safe to upgrade`, + }, + { + name: "downgrade from 8.4.0 to 5.7.32", + fromVersion: "mysqld Ver 8.4.0", + toVersion: "mysqld Ver 5.7.32", + upgradeSafe: false, + error: `running MySQL version "mysqld Ver 5.7.32" is older than backup MySQL version "mysqld Ver 8.4.0"`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := validateMySQLVersionUpgradeCompatible(tc.toVersion, tc.fromVersion, tc.upgradeSafe) + if tc.error == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tc.error) + } + }) + } + +} diff --git a/go/vt/mysqlctl/version.go b/go/vt/mysqlctl/version.go index aa454319573..3bdb76be3a6 100644 --- a/go/vt/mysqlctl/version.go +++ b/go/vt/mysqlctl/version.go @@ -36,3 +36,7 @@ func (v *ServerVersion) atLeast(compare ServerVersion) bool { } return false } + +func (v *ServerVersion) isSameRelease(compare ServerVersion) bool { + return v.Major == compare.Major && v.Minor == compare.Minor +}