Repository navigation
Conversation
Previously every data iteration worker stalled while throttled and all of them resumed with full-sized batches as soon as the throttle cleared. Copy batches are now admitted by a controller shared by the cursors of one DataIterator.Run, before the source transaction is opened: - While throttled, no new batch starts; running batches finish normally. - Once clear, one batch at a time copies 1/8 of the configured batch size. After each healthy DataIterationThrottleRecovery.RecoveryInterval (default 5s) the batch size doubles, then concurrency grows by one up to DataIterationConcurrency. A stage is healthy after a successful batch admitted in that stage and, with the LagThrottler, a fresh lag sample. - A new throttle restarts recovery; a failed batch restarts the stage. - Admission is FIFO, so every table keeps making progress. - Throttler.Disabled restores the configured limits immediately, so cutover and RunStandaloneDataCopy are not delayed. Binlog replay is unchanged and resumes as soon as the throttle clears. DataIterationThrottleRecovery.Disabled restores the previous behavior. Refs #471
This was referenced Oct 30, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #471
Stack: 2/3. Base: #472 (
throttle-recovery/01-safe-inputs). Next: #474. Merge order: #472 → #473 (this PR) → #474. This diff is relative to #472.Behavior
Today all copy workers stall while throttled and all resume with full-sized batches as soon as the throttle clears. After this change, copy load recovers gradually:
max(1, configured / 8)rows.DataIterationThrottleRecovery.RecoveryInterval(default5s), the batch-size divisor steps 8 → 4 → 2 → 1. Then concurrency grows by one per interval up toDataIterationConcurrency. With the defaults that is 25 → 50 → 100 → 200 rows at concurrency 1, then concurrency 2 → 3 → 4. Only one control moves per interval.LagThrottler, the stage also needs a lag sample newer than the one at stage start and no older than 2 ×UpdateInterval. NULL or missing rows do not count as samples.RunStandaloneDataCopydo) restores the configured limits at the next admission and clears recovery history. Re-enabling it does not replay an old timer.DataIterationThrottleRecovery.Disabled: truekeeps today's immediate resume. The hard throttle still applies.Unchanged: the
Throttlerinterface,WaitForThrottle, the binlog writer's hard stop, the pause API, lag semantics, binlog coordinates, serialized state, and status fields.Implementation
copy_throttle.go(new) addscopyThrottleController. It uses a mutex, a condition variable, FIFO tickets, in-flight/limit/divisor state, and a stage generation. Permits are value types. A single 500 ms ticker re-reads the throttler while batches wait, and acquire and release also re-read it. Cancelling closes the controller, and queued acquisitions returncontext.Canceled.DataIterator.Runbuilds one controller per run, outsideFerry.Run, soRunStandaloneDataCopyworks too. It starts and joins the controller's ticker and stops it before the done listeners run.Ferry.NewDataIteratorcopiesConfig.DataIterationThrottleRecovery. Verifier and binlog-writer cursors are unaffected.Cursor.Eachacquires a permit inside each read retry, beforeDB.Begin. It reads the configured base after admission, andcopyThrottlePermit.batchSizescales it. The permit is held through fetch, listeners, target write and source rollback, and is released exactly once on every path. ExportedFetchkeeps the full configured size and delegates to the new privatefetch(db, batchSize).LagThrottlerrecords a sample sequence and observation time for successful non-NULL samples only, and exposes them through the privaterecoverySample().NewLagThrottlerrejects a non-positiveUpdateInterval.ValidateConfigrejects a negativeDataIterationConcurrencyand an invalidRecoveryInterval.CHANGELOG.mdanddocs/technicaloverview.md.Tests
copy_throttle_test.go: controller tests using a fake clock, FIFO contenders and no sleeps. Cases:(1,25) (1,50) (1,100) (1,200) (2,200) (3,200) (4,200), asserting admitted and waiting counts at every stage.test/go/race_conditions_integration_test.go, real MySQL with a full Ferry:TestCopyThrottleResumesOneSmallBatchWithoutSourceLockspauses four running table copies and drains them. The first resumed batch has 25 rows (or 10 with an 80-row override). While it is held:Lock wait timeout(positive control).innodb_lock_wait_timeout=1.TestCopyThrottleLagThrottlerRecoversCopyGraduallyuses a realLagThrottleronmeta.lag_table. Lag above the threshold stops both copy and replay. Fresh low lag resumes with 25 rows and replays the sentinel. Batch sizes step 25 → 50 → 100 → 200 without skipping a stage. With all lag rows deleted, sizes stay at 50.test/go/data_iterator_test.go: with recovery disabled, the hard pause still holds and copying resumes at full size. A paused cold start resumes with 1-row batches (7 / 8) and finishes the table with exact rows.test/go/config_test.goandtest/go/lag_throttler_test.go: validation bounds.Checks run (MySQL 8.0.32 from
docker-compose_8.0.yml)go test -race . -run '^TestCopyThrottle' -count=1: pass. Also passed with-count=30, and with-count=20 -cpu 1,2,8.MYSQL_VERSION=8.0 go test ./test/go -run 'TestCopyThrottle|TestDataIterator|TestThrottler|TestNewThrottler|TestSelectUpdateBinlogCopy|TestUpdateBinlogSelectCopy|TestSelectCopyUpdateBinlog|TestOnlyDeleteRowWithMaxPaginationKey' -count=1 -p=1: pass.MYSQL_VERSION=8.0 go test -race ./test/go -run 'TestCopyThrottle|TestThrottlerThrottlesAndUnthrottles|TestThrottlerMultipleServerIDs' -count=1 -p=1: pass.MYSQL_VERSION=8.0 go test . ./test/go ./copydb/test ./sharding/test -count=1 -p=1: pass.GHOSTFERRY_BINLOG_COORDINATE_MODE=gtid: pass on rerun. The first run hitTestThrottlerMultipleServerIDs, an existing timing flake: in-count=100loops, unmodifiedorigin/mainfailed 3 of 200 runs, and in a-count=40loop this branch failed 5 of 80.MYSQL_VERSION=8.0 bundle exec rake test TEST=test/integration/interrupt_resume_test.rb: 26/26 passed in 4 consecutive runs on this branch. One earlier run hung. A hang captured later on the stack tip traced to an existing test flake intest_interrupt_resume_inline_verifier_will_verify_entries_in_reverify_store, where the deleted seed id can be a hole; details in Gradual copy throttle recovery #471.Begin+ fetch makesTestCopyThrottleResumesOneSmallBatchWithoutSourceLocksfail withError 1205on the waiting-table UPDATE.