Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions pkg/engine/planetscale/progress.go
Original file line number Diff line number Diff line change
Expand Up @@ -770,11 +770,7 @@ func aggregateShardProgress(rows []vitessMigrationRow) ([]engine.TableProgress,
latestCompletedAt = sh.completedAt
}

// Resolve effective shard state: running + ready_to_complete = ready_to_complete
shardState := sh.status
if sh.status == state.Vitess.Running && sh.readyToComplete {
shardState = state.Vitess.ReadyToComplete
}
shardState := effectiveShardState(sh.status, sh.readyToComplete)

shardPct := min(sh.progress, 100)
shardCopied := sh.rowsCopied
Expand Down Expand Up @@ -853,6 +849,26 @@ func aggregateShardProgress(rows []vitessMigrationRow) ([]engine.TableProgress,
return tables, overallProgress
}

// effectiveShardState resolves the shard state to display from the raw Vitess
// migration status and its ready_to_complete flag. ready_to_complete is the
// authoritative cutover-readiness signal: Vitess sets it while migration_status
// is still "running" (brief race before the status catches up) or still in an
// early status like "queued" (immediate operations such as CREATE/DROP TABLE
// are ready before they ever start copying). Folding those cases into
// ready_to_complete keeps the per-shard display consistent with the deploy
// request state, which counts the same shards as waiting for cutover. Terminal
// statuses (complete, failed, cancelled) take precedence — ready_to_complete
// can remain set after a migration finishes or is cancelled.
Comment thread
Kiran01bm marked this conversation as resolved.
Outdated
func effectiveShardState(status string, readyToComplete bool) string {
switch status {
case state.Vitess.Running, state.Vitess.Queued, state.Vitess.Requested, state.Vitess.Ready:
if readyToComplete {
return state.Vitess.ReadyToComplete
}
}
return status
}

// resolveTableState merges a shard's state into the current table state.
// A table has one Vitess migration per shard, each in a different state.
// This picks the "worst" state so the table reflects the least-progressed shard:
Expand Down
63 changes: 63 additions & 0 deletions pkg/engine/planetscale/progress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,40 @@ func TestAggregateShardProgress(t *testing.T) {
assert.Equal(t, int64(5000), tables[0].Shards[1].RowsCopied)
})

t.Run("queued shard with ready_to_complete shows ready", func(t *testing.T) {
// Immediate operations (CREATE/DROP TABLE) are ready for cutover while
// their Vitess migration status is still queued. The per-shard display
// and the table state must both show ready_to_complete so they agree
// with the deploy request state, which treats these shards as waiting
// for cutover.
Comment thread
Kiran01bm marked this conversation as resolved.
Outdated
rows := []vitessMigrationRow{
{MigrationUUID: "uuid-1", Keyspace: "commerce", Shard: "-80", Table: "orders", Status: "queued", ReadyToComplete: true},
{MigrationUUID: "uuid-1", Keyspace: "commerce", Shard: "80-", Table: "orders", Status: "queued", ReadyToComplete: true},
}

tables, _ := aggregateShardProgress(rows)
require.Len(t, tables, 1)
assert.Equal(t, state.Vitess.ReadyToComplete, tables[0].State)
assert.Equal(t, 100, tables[0].Progress)
for _, sh := range tables[0].Shards {
assert.Equal(t, state.Vitess.ReadyToComplete, sh.State, "shard %s", sh.Shard)
assert.Equal(t, 100, sh.Progress, "shard %s", sh.Shard)
}
})

t.Run("queued shard without ready_to_complete stays queued", func(t *testing.T) {
rows := []vitessMigrationRow{
{MigrationUUID: "uuid-1", Keyspace: "commerce", Shard: "-80", Table: "orders", Status: "queued"},
{MigrationUUID: "uuid-1", Keyspace: "commerce", Shard: "80-", Table: "orders", Status: "ready_to_complete"},
}

tables, _ := aggregateShardProgress(rows)
require.Len(t, tables, 1)
assert.Equal(t, state.Vitess.Queued, tables[0].State)
assert.Equal(t, state.Vitess.Queued, tables[0].Shards[0].State)
assert.Equal(t, state.Vitess.ReadyToComplete, tables[0].Shards[1].State)
})

t.Run("rows_copied exceeding table_rows clamps to 100%", func(t *testing.T) {
// While a shard is still copying, rows_copied can momentarily exceed the
// estimated table_rows because of concurrent inserts. Table and overall
Expand All @@ -160,6 +194,35 @@ func TestAggregateShardProgress(t *testing.T) {
})
}

func TestEffectiveShardState(t *testing.T) {
tests := []struct {
status string
readyToComplete bool
want string
}{
{state.Vitess.Running, true, state.Vitess.ReadyToComplete},
{state.Vitess.Running, false, state.Vitess.Running},
{state.Vitess.Queued, true, state.Vitess.ReadyToComplete},
{state.Vitess.Queued, false, state.Vitess.Queued},
{state.Vitess.Requested, true, state.Vitess.ReadyToComplete},
{state.Vitess.Ready, true, state.Vitess.ReadyToComplete},
{state.Vitess.ReadyToComplete, false, state.Vitess.ReadyToComplete},
// Terminal statuses win even when ready_to_complete is still set.
{state.Vitess.Complete, true, state.Vitess.Complete},
{state.Vitess.Failed, true, state.Vitess.Failed},
{state.Vitess.Cancelled, true, state.Vitess.Cancelled},
}
for _, tc := range tests {
name := tc.status
if tc.readyToComplete {
name += "+ready_to_complete"
}
t.Run(name, func(t *testing.T) {
assert.Equal(t, tc.want, effectiveShardState(tc.status, tc.readyToComplete))
})
}
}

func TestParseProgressPercent(t *testing.T) {
t.Run("fractional value rounds to nearest int", func(t *testing.T) {
pct, err := parseProgressPercent("54.35")
Expand Down
Loading