From 60cf89ae260c0c3330ef378f7deda06922b9d78b Mon Sep 17 00:00:00 2001 From: jjaw Date: Fri, 12 Jun 2026 13:51:27 +0800 Subject: [PATCH] fix: recover scheduler outbox invalid dedup index --- .../internal/repository/migrations_runner.go | 16 ++++++-- .../repository/migrations_runner_notx_test.go | 38 +++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/backend/internal/repository/migrations_runner.go b/backend/internal/repository/migrations_runner.go index 6dbb9fbd7c..155dfaa11a 100644 --- a/backend/internal/repository/migrations_runner.go +++ b/backend/internal/repository/migrations_runner.go @@ -53,6 +53,8 @@ const migrationsLockRetryInterval = 500 * time.Millisecond const nonTransactionalMigrationSuffix = "_notx.sql" const paymentOrdersOutTradeNoUniqueMigration = "120_enforce_payment_orders_out_trade_no_unique_notx.sql" const paymentOrdersOutTradeNoUniqueIndex = "paymentorder_out_trade_no_unique" +const schedulerOutboxPendingDedupKeyMigration = "152_scheduler_outbox_pending_dedup_key_index_notx.sql" +const schedulerOutboxPendingDedupKeyIndex = "idx_scheduler_outbox_pending_dedup_key" type migrationChecksumCompatibilityRule struct { fileChecksum string @@ -258,6 +260,8 @@ func prepareNonTransactionalMigration(ctx context.Context, db *sql.DB, name stri switch name { case paymentOrdersOutTradeNoUniqueMigration: return preparePaymentOrdersOutTradeNoUniqueMigration(ctx, db) + case schedulerOutboxPendingDedupKeyMigration: + return dropInvalidIndexIfPresent(ctx, db, schedulerOutboxPendingDedupKeyIndex) default: return nil } @@ -276,16 +280,20 @@ func preparePaymentOrdersOutTradeNoUniqueMigration(ctx context.Context, db *sql. ) } - invalid, err := indexIsInvalid(ctx, db, paymentOrdersOutTradeNoUniqueIndex) + return dropInvalidIndexIfPresent(ctx, db, paymentOrdersOutTradeNoUniqueIndex) +} + +func dropInvalidIndexIfPresent(ctx context.Context, db *sql.DB, indexName string) error { + invalid, err := indexIsInvalid(ctx, db, indexName) if err != nil { - return fmt.Errorf("check invalid index %s: %w", paymentOrdersOutTradeNoUniqueIndex, err) + return fmt.Errorf("check invalid index %s: %w", indexName, err) } if !invalid { return nil } - if _, err := db.ExecContext(ctx, fmt.Sprintf("DROP INDEX CONCURRENTLY IF EXISTS %s", paymentOrdersOutTradeNoUniqueIndex)); err != nil { - return fmt.Errorf("drop invalid index %s: %w", paymentOrdersOutTradeNoUniqueIndex, err) + if _, err := db.ExecContext(ctx, fmt.Sprintf("DROP INDEX CONCURRENTLY IF EXISTS %s", indexName)); err != nil { + return fmt.Errorf("drop invalid index %s: %w", indexName, err) } return nil } diff --git a/backend/internal/repository/migrations_runner_notx_test.go b/backend/internal/repository/migrations_runner_notx_test.go index b7cb396c47..f5453c4bd0 100644 --- a/backend/internal/repository/migrations_runner_notx_test.go +++ b/backend/internal/repository/migrations_runner_notx_test.go @@ -194,6 +194,44 @@ DROP INDEX CONCURRENTLY IF EXISTS paymentorder_out_trade_no; require.NoError(t, mock.ExpectationsWereMet()) } +func TestApplyMigrationsFS_SchedulerOutboxPendingDedupKeyMigration_DropsInvalidIndexBeforeRetry(t *testing.T) { + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer func() { _ = db.Close() }() + + prepareMigrationsBootstrapExpectations(mock) + mock.ExpectQuery("SELECT checksum FROM schema_migrations WHERE filename = \\$1"). + WithArgs("152_scheduler_outbox_pending_dedup_key_index_notx.sql"). + WillReturnError(sql.ErrNoRows) + mock.ExpectQuery("SELECT EXISTS \\("). + WithArgs("idx_scheduler_outbox_pending_dedup_key"). + WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true)) + mock.ExpectExec("DROP INDEX CONCURRENTLY IF EXISTS idx_scheduler_outbox_pending_dedup_key"). + WillReturnResult(sqlmock.NewResult(0, 0)) + mock.ExpectExec("CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS idx_scheduler_outbox_pending_dedup_key"). + WillReturnResult(sqlmock.NewResult(0, 0)) + mock.ExpectExec("INSERT INTO schema_migrations \\(filename, checksum\\) VALUES \\(\\$1, \\$2\\)"). + WithArgs("152_scheduler_outbox_pending_dedup_key_index_notx.sql", sqlmock.AnyArg()). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectExec("SELECT pg_advisory_unlock\\(\\$1\\)"). + WithArgs(migrationsAdvisoryLockID). + WillReturnResult(sqlmock.NewResult(0, 1)) + + fsys := fstest.MapFS{ + "152_scheduler_outbox_pending_dedup_key_index_notx.sql": &fstest.MapFile{ + Data: []byte(` +CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS idx_scheduler_outbox_pending_dedup_key + ON scheduler_outbox (dedup_key) + WHERE dedup_key IS NOT NULL; +`), + }, + } + + err = applyMigrationsFS(context.Background(), db, fsys) + require.NoError(t, err) + require.NoError(t, mock.ExpectationsWereMet()) +} + func TestApplyMigrationsFS_TransactionalMigration(t *testing.T) { db, mock, err := sqlmock.New() require.NoError(t, err)