Repository navigation
v0.18.0
Highlights
-
The lockless checksum verifies
move, and is available formigratebehind a flag. It verifies a table without a setup lock or long-livedREPEATABLE READsnapshots. A row that is written continuously is compared against its next binlog after-image instead of aSELECT.movealways uses it, across N sources and M targets, andDistributedCheckeris removed (#1281).migrate --enable-experimental-lockless-checksum(default off) uses it as the pre-cutover gate, with the same behaviour as the default checker (#1250 #1270). See pkg/checksum/README.md for how it works and how the two checkers compare. Related changes: hot-row settling (#1273 #1274 #1358), hot-range splitting (#1237 #1249 #1243), retry pacing (#1279 #1403), one checker factory and repair policy (#1251 #1271 #1253 #1405), progress reporting (#1230 #1247), cancellation (by @aparajon in #1258) and docs (#1287 #1288). By @morgo -
Type and charset correctness. A sweep through type handling, much of it from comparing Spirit with gh-ost on the same types. Most of these were caught by the checksum, which repaired the affected rows or refused the cutover; the ones it could not see are listed first:
FLOAT→FLOATcopies lost precision past 6 significant digits. MySQL's text protocol renders aFLOATwith 6 digits (0.12345679→0.123457), and the checksum'scharcast rendered the same 6, so it compared equal. Conversions out ofFLOAT(toDECIMAL,DOUBLE,VARCHAR) were caught by the checksum. Copy and checksum now read the exactDOUBLEvalue, andFLOATprimary keys are refused — #1311- A
STOREDgenerated column made regular by theALTERwas not copied. The column was outside the column mapping, so the copy, replay and checksum all skipped it and it came outNULL(the same class of bug is open in gh-ost as github/gh-ost#808) — #1312 - The
AUTO_INCREMENTcounter could go backwards at cutover, in bothmigrateandmove, when rows above the copy position were inserted and then deleted during the run (e.g. queue-style tables). The counter is not row data, so the checksum does not cover it. It is now read live and carried over at cutover — #1317 - A trigger, or an inbound foreign key, created during a migration was lost at cutover. Both were only checked at preflight. They are now detected from the binlog and re-checked under the cutover lock — #1342
- Binlog replay of strings in non-utf8 charsets (
latin1,gbk,utf16,ucs2, …) wrote the wrong bytes. The copy path was unaffected and the checksum repaired replayed rows it saw; the exposure was changes applied between the checksum and the cutover. Strings are now emitted in their column's charset (also github/gh-ost#1571) — #1320 - DDL text restored from the parser. An explicit
CHARACTER SETon anENUM/SETcolumn was dropped from the executedALTER(#1352), and a backslash in aDEFAULTorCOMMENTliteral was restored unescaped, storing a different default (by @Kiran01bm in #1269). - Caught by the checksum, now correct the first time:
YEAR0000copied as2000(the XOR checksum only saw an odd count of them per chunk) (#1311);ENUM/SETmembers containing a 4-byte character, which MySQL reports as?(#1368), and backslash escapes inCOLUMN_TYPE(#1370); a key written to the target ahead of the copier and then discarded above the high watermark (#1395); and row changes dropped past a 4GiBLogPoswrap withgtid_modeoff, or with an unknown rows-event subtype such asPARTIAL_UPDATE_ROWS— both now fatal (#1039). - Refused instead of failing late. A primary key collation change makes the checksum's range predicate select different rows on each side, so the run copied the table and then aborted before cutover (#1284; predicted at statement scope by @aparajon in #1286). XA transactions write row events at
PREPARE, before the outcome is known, so any XA activity on the source now stops the run (#1079). - By @morgo unless noted.
-
Force-kill only kills what is actually blocking. Force-kill has been mandatory since v0.17.0, so its mistakes cost more:
- It killed application transactions holding a granted metadata lock even when the
ALTERwas not waiting on them, e.g. during anINPLACErebuild. It now kills only while the statement is waiting for its lock — by @aparajon in #1343 - A cancel could return a
LOCK TABLESsession to the pool with the lock still held, so later borrowers failed with error 1100. Lock sessions are now kept until cleanup — #1277 #1278 - Each retry re-arms the kill timer (by @Kiran01bm in #1280), and retries stop when a kill cannot help (#1331 #1361). #1361 also checks
PROCESSandCONNECTION_ADMINat preflight, and handles MySQL 9.7 error 3854 — by @aparajon - A
KILLdenied with error 1095 is retried asCALL mysql.rds_kill(pid), AWS's supported way to kill another user's session. Before, a user whoserds_superuser_rolelackedCONNECTION_ADMINpassed preflight, and every kill at cutover was denied — by @Kiran01bm in #1434 - The new
--force-kill-afterflag sets the kill delay separately from--lock-wait-timeout— by @deanbaker in #1263 - A cutover
RENAMEthat has started now finishes when the run is cancelled (#1367). Related cancel and connect-timeout fixes: #1363 #1365 #1369.
- It killed application transactions holding a granted metadata lock even when the
-
spirit diffconverges on what MySQL stores, and emits DDL MySQL accepts. A schema file now diffs clean against its own live table in many more cases. Before, some emittedALTERs would have changed the table if applied: for example,NVARCHARcolumns were converted to utf8mb4 (#1301). The emitted DDL was also tested against MySQL on 100 source/target pairs. Statements that failed, changed the table unasked, or never converged are fixed for columns, indexes and foreign keys (#1420) and for partitions (#1417). AMODIFYthat changes a column's collation now names it (by @aparajon in #1418). See docs/diff.md. The 21 normalization rules: #1224 #1225 #1268 #1304 #1315 #1316 #1337 #1339 #1341 #1345 #1346 #1347 #1349 #1350 #1353 #1354 #1355 #1356 #1357 #1375, plus #1301;spirit fmtand the normalization pipeline are documented in #1297. By @morgo and @aparajon. -
moveandsyncrefuse the schema objects they do not copy. Both copy base tables only. A moved table with a trigger was cut over with the trigger left on_oldand none on the target, exit 0; a view failed discovery withno primary key found.movenow refuses source triggers, views, routines and events (#1364) and target triggers and events (#1393);syncskips source views and refuses target triggers and events (#1371). By @morgo -
Aurora throttling and autoscaling, across all three commands.
migratehas always throttled on Aurora;moveandsynconly did so with autoscaling engaged, so a move or sync onto Aurora ran with no load throttling at all. They now build the same throttlers for every Aurora target and gain--max-commit-latency(#1289).--enable-experimental-autoscalingis added tomove, scaling all shards together on the busiest target host (#1216), and tosync, through the copy, continuous verification and checksum repairs (#1235). The three commands now share one set of flags and one engage rule (#1404). Small Aurora instances (≤2 vCPUs and ≤1.5 GiB buffer pool, e.g.db.t4g.medium) ran out of memory on the default 4 read threads, 4 write threads, 16 MiB chunks and 8 concurrent flushes; with--enable-experimental-autoscalingset they now run in a low-memory mode: 1 read thread, 1 write thread, 1 MiB chunks and 1 concurrent flush (#1426). By @morgo
Upgrade Notes
-
Checkpoint compatibility: complete any in-progress schema changes before upgrading, as the checkpoint format is not guaranteed to be stable between versions.
-
MySQL versions. CI now tests 8.0.28 (Aurora 3.04), 8.0.42 (Aurora 3.10 LTS), 8.0.46, 8.4, 9.7, and MySQL 26.7 for the first time. Aurora 3.04 standard support ends 2026-10-31. After v0.18.0, the 8.0.28 job moves to the next Aurora LTS, so this is the last release tested against MySQL 8.0.28 (#1425).
-
The sentinel only holds a run that set
--defer-cutover(#1429). Before, any_spirit_sentinelin the schema held everymigrateandmovecutover — including one left behind by an earlier deferred run that was cancelled or failed, which could hold an unrelated run for up to 48 hours. Now:- Creating
_spirit_sentinelby hand during a run started without--defer-cutoverno longer holds it. To hold such a run: cancel it, create the table, and resume with--defer-cutover. - A deferred run resumed without
--defer-cutovercuts over even if the sentinel still exists. - A run that finds a sentinel it is not waiting on logs a warning.
- The hidden
--respect-sentinelflag is removed and is now a parse error.
- Creating
-
Flags are shared across
migrate,moveandsync(#1404).move --threadsdefaults to 4 (was 2).movegains--force-kill-after,--lock-wait-timeout,--tls-modeand--tls-ca;syncgains--tls-mode,--tls-caand--checkpoint-max-age.syncenforces--checkpoint-max-age(default 168h) on resume: an older checkpoint fails withstatus.ErrCheckpointTooOldrather than re-copying into a non-empty target;--forcere-copies.migrate --enable-experimental-autoscalingon a non-Aurora target no longer engages, matchingmoveandsync.- Negative
--lock-wait-timeoutand--max-commit-latencyare rejected. Before, they silently became the default and "disabled", respectively.
-
New privileges are required.
moveon source schemas:EVENT, plusSHOW_ROUTINEon*.*(orSELECTon*.*, orEXECUTE/ALTER ROUTINE/CREATE ROUTINEon the schema).information_schemahides events and routines from a user without them, so the new object check would otherwise miss them (#1364).moveon target schemas:TRIGGERandEVENT(#1393).migrateandmove:PROCESS, and eitherCONNECTION_ADMIN/SUPERorEXECUTEonmysql.rds_kill, are now enforced at preflight. They were already needed by force-kill; a user without them now fails before the copy instead of at cutover.rds_superuser_roleis no longer accepted by its name; an active role still passes through the privileges it actually grants (#1361 #1434).CREATE TEMPORARY TABLESfor tables whoseENUM/SETmembers contain a 4-byte character (#1368).
-
New refusals. Each of these previously ran and either failed late or produced wrong data:
partial_revokes=ON— preflight passed and the run failed at cutover onLOCK TABLES(#1376). Turning it off is a server-wide security change.- Changing a primary key column's collation, including
utf8mb3_bin→utf8mb4_bin(#1284 #1286). FLOATandBITprimary keys, inmigrate,moveandsync(#1311 #1321 #1366)..or a backtick in a schema or table name (#1340).- Any XA activity on the source server, including on unrelated tables (#1079); a binlog
LogPoswrap; unknown rows-event subtypes (#1039). - Inline column
REFERENCESin a migration (#1366). move: triggers, views, routines and events (see Highlights).- A temporal precision narrowing (e.g.
TIMESTAMP(6)→TIMESTAMP) that would truncate a stored fraction now fails the checksum. MySQL accepts the change, but Spirit documents that it does not support lossy changes, and the checksum previously rounded both sides the same way and let it through; this makes it consistent with that rule. If no row holds a fraction, the migration still succeeds (#1294).
-
Behaviour changes.
moveandsyncthrottle on Aurora by default, with--max-commit-latencydefaulting to 100ms (0disables it). The flag--enable-experimental-autoscalingnow only adds thread-count scaling (#1289).wait_timeout=600is set on every connection, so a frozen Spirit process can no longer holdLOCK TABLES ... WRITEfor the server's default 8 hours (#1321).- Periodic flush errors stop the run instead of being logged (#1321), and a fatal abort — including a failed checkpoint write — returns its cause instead of
context.Canceled(#1321 #1366). - The checksum during the sentinel wait reports, and does not repair (#1253 #1405). A divergence found while
--defer-cutoveris waiting abortsmigrateandmovewithErrPermanentDivergence; the checksum watermark is cleared before the wait, so the resumed run's initial checksum is a full pass and repairs it. Inmovethis checksum now runs at--threads(autoscaled when enabled) instead of 1 thread, and a move restarted during the wait re-runs the full initial checksum. syncstops on a divergence found after its first clean pass, instead of repairing it. Its initial verification can returnErrVerificationUnresolvedon a range that never converges (#1405).- A cancel during cutover can take up to
lock_wait_timeout+ 30s to return, because a startedRENAMEis allowed to finish (#1367). spirit diffemits different statements forNCHAR/NVARCHAR, utf8mb3 and other fixed-default charsets, width-lessZEROFILL, and utf8mb4 tables or columns without aCOLLATE(#1301 #1304 #1345 #1349 #1357), for the column, index, foreign-key and partition cases in Highlights (#1417 #1420), and a collation-changingMODIFYnow names the collation (#1418). These are the cases where the old output did not converge or was not accepted. Some changes are now emitted as more than one statement, in order (e.g. a secondFULLTEXTindex, a spatialSRIDchange,VIRTUAL→ regular).- Lint can report new errors. A new linter,
spirit_compatible, reports an error for a new table that Spirit could never alter later: no primary key, aFLOATorBITprimary key, a foreign key (in either direction), an inlineREFERENCES, or a./backtick in the name. Existing tables are not reported; it can be disabled throughEnabled(#1423). The reserved-words list is now the union of what 8.0.46, 8.4, 9.7 and 26.7 reserve, addingINTERSECT,QUALIFY,TABLESAMPLE,EXTERNALandLIBRARY(#1425). - Other lint changes:
rename_columnreports a case-only change as its own warning, not a rename error (#1232);allow_charsettreatsutf8andutf8mb3as one charset (#1308);type_pedanticrecommends the collation that replaces the others (e.g.utf8mb4_0900_ai_cioverutf8mb4_general_ci) even when it is in the minority, instead of the most common one (#1421), and its messages quote and count the example tables (#1387) — by @aparajon. - Copy progress in
Summarycounts rows, not auto-increment keyspace distance (#1220), and the copy ETA averages the last 2 minutes of rate samples instead of the last 10s, so it no longer alternates between its true value and double it under throttling (by @aparajon in #1400).
-
Library API changes (for programs embedding Spirit):
- The MySQL driver is
github.com/block/mysql(#1219).applier.Target.Config,check.SourceResource.Configanddbconn.UnsafeWarningError.Warningcarry its types, so a caller passinggo-sql-driver/mysqltypes no longer compiles. Linking both drivers also breaks silently: atls=rdsDSN name only resolves in the driver that registered it, and error classification stops recognising the other driver's*mysql.MySQLError.dbconn.DriverNameis the registered name;depguardenforces one driver in-tree (#1222). The driver maps a signedtinyint(1)to a Goboolby default (tinyInt1IsBool), which collapses every non-zero value totrue; Spirit disables it in the DSNs it builds, so every Spirit connection readstinyint(1)as an integer (by @aparajon in #1226). - RDS TLS comes from the driver (#1221): the embedded bundle is removed,
--tls-mode=DISABLEDnow sendstls=falseexplicitly, GovCloud and China endpoints are no longer auto-detected as RDS, the minimum is TLS 1.2, andRejectReadOnlyis removed. applier.New(targets []Target, cfg)returns a*MySQLApplier;NewSingleTargetApplierandNewShardedApplierare removed, and under-lock flushes match locks to targets by connection identity in every topology (#1292 #1306).- Checksum:
checksum.NewCheckeris the only constructor,CheckerConfig.Locklessreplaces theAlgorithmenum, the lockless types are renamedLocklessChecker*, andDistributedCheckeris removed (#1250 #1251 #1271 #1281).CheckerConfig.FixDifferences,CheckerConfig.MinPassInterval,ErrRepairUnverifiedandLocklessChecker.ConfirmedDifferences()are removed, andCheckerConfig.Applieris required (#1405). - CLI structs embed
flags.Commonandflags.Cutover(newpkg/flags), so composite literals must name them:migration.Migration{Common: flags.Common{Threads: 8}, Cutover: flags.Cutover{DeferCutOver: true}}. Field access (m.Threads) is unchanged.Migration.RespectSentinelis removed (#1404 #1429). charset.Collationreports how a collation compares strings (case, accent and kana sensitivity,Binary,UCAVersion,DeprecatedByCollationID);GetCollationByNameis renamedFindCollationByNameandFindCollationByIDis added. 25utf8mb3_*collations in the registry were wrongly markedNO PADand are corrected toPAD SPACE— by @aparajon in #1419change.SourcegainsVerifyRowAtNextChange;RowParker,ParkedRowandFeedStats.MergeSubscriptionsare exported for out-of-tree implementations (#1273 #1274).status.Progress.Copy(structured row-copy progress) (#1220);Column.DefaultKind(#1225); a schema load reports the tables its filters excluded (#1241);dbconn.NewContext(#1369);dbconn.ErrLockHeld, so a caller can tell "another run holds this table's advisory lock" from a failed run witherrors.Is(by @aparajon in #1430);DBConfig.ForceKillAfter(#1263).dbconn.AnalyzeTable(#1410).- Removed:
dbconn.BeginStandardTrx(#1278);LazyFindP90,QuoteColumnsand other duplicated helpers (#1300); and exported functions nothing in Spirit calls, includinglint.Enable/Disable/List/Get/Reset,lint.FilterByLinter,sqlescape.FormatSQL,dbconn.GetLockingTransactions,AdvisoryLock.CloseDBConnectionandmove.ReverseFeed.Run;KillLockingTransactionsdrops its unusedconfigparameter (#1412).
- The MySQL driver is
Additional Changes
-
move's reverse window checkpoints its feed positions. Before, a restart during--reverse-windowre-read the target binlog from cutover, and failed if those binlogs had been purged. By @morgo in #1394 -
One applier.
SingleTargetApplierandShardedApplierare merged intoMySQLApplier, with one target as the N=1 case. By @morgo in #1292 #1306 -
Fixes found by comparing the three runners.
migrateignoredANALYZE TABLEerror rows (#1410);Cancelbefore or duringRuncould panic or be lost (#1408);moveandsyncnow report applier metrics (#1409); andmove's status shows its sentinel-wait checksum (#1411). By @morgo -
Progress accounting. Rows copied and the copy aggregate stay correct across a resume (by @aparajon in #1254 #1255); copier cancellation and phase timing are fixed (#1324 #1325 #1336). An
AUTO_INCREMENTkey whose key space is more than 5× wider than its rows (e.g. after one very large explicit id) reports progress and ETA in rows instead of key space (by @aparajon in #1433). By @morgo unless noted. -
Small fixes. Checksum casts for
DECIMAL UNSIGNEDandBIT(N)(patches by @aparajon in #1294); the index thatAUTO_INCREMENTneeds is kept when secondary indexes are deferred (#1245); negative replica lag (#1332); a binlogALTER USERform (#1229);StripAutoIncrementformatting (by @aparajon in #1240); identifier escaping (#1321); and stale tables after native DDL (#1366). By @morgo unless noted. -
Refactors. Code the three runners duplicated is shared (#1404 #1411 #1422 #1424), as are the binlog and GTID client methods (#1414), along with helper consolidation (#1300 #1307 #1309 #1318) and dead-code removal (#1412). By @morgo
-
Tests, docs and CI. CI stability fixes (#1261 #1265 #1295 #1296 #1305 #1323 #1329 #1348 #1351 #1374 #1382 #1383 #1386 #1389 #1390 #1391 #1392 #1399 #1415 #1428) and docs (#1379 #1406 #1407). By @morgo. Dependency bumps by @dependabot[bot] in #1228 #1259
Full Changelog: v0.17.0...v0.18.0