diff --git a/go/logic/migrator.go b/go/logic/migrator.go index aa9a97c1c..612fe056c 100644 --- a/go/logic/migrator.go +++ b/go/logic/migrator.go @@ -111,7 +111,15 @@ func NewMigrator(context *base.MigrationContext, appVersion string) *Migrator { ghostTableMigrated: make(chan bool), firstThrottlingCollected: make(chan bool, 3), rowCopyComplete: make(chan error), - allEventsUpToLockProcessed: make(chan *lockProcessedStruct), + // Buffered to MaxRetries() to prevent a deadlock when waitForEventsUpToLock times + // out. The sentinel applyEventFunc may still be queued in applyEventsQueue when the + // timeout fires; when the worker eventually executes it, it sends on this channel + // with no active receiver. An unbuffered channel would block the worker permanently: + // the queue fills, the listener goroutine stalls, heartbeat lag grows unboundedly, + // and no further cutover attempts are made. With a buffer sized to the retry limit + // the send always completes immediately. Stale sentinels accumulate in the buffer + // and are discarded by the stale-skip loop in waitForEventsUpToLock. + allEventsUpToLockProcessed: make(chan *lockProcessedStruct, context.MaxRetries()), copyRowsQueue: make(chan tableWriteFunc), applyEventsQueue: make(chan *applyEventStruct, base.MaxEventsBatchSize),