From 2450c328fba7715e7fb3436bf10357f4c0a98680 Mon Sep 17 00:00:00 2001 From: Fernando Nogueira Date: Thu, 21 May 2026 00:53:31 -0300 Subject: [PATCH 1/2] feat(drift): add idempotency key support for drift analysis runs --- ...add_drift_analysis_run_idempotency_key.sql | 6 +++ pkg/repository/drift_analysis_repository.go | 17 +++++++ pkg/repository/queries/db.go | 2 +- pkg/repository/queries/drift_analysis.sql | 9 +++- pkg/repository/queries/drift_analysis.sql.go | 49 +++++++++++++++--- .../queries/git_organization.sql.go | 2 +- pkg/repository/queries/git_repository.sql.go | 2 +- pkg/repository/queries/models.go | 3 +- pkg/repository/queries/sync_org.sql.go | 2 +- .../queries/sync_status_user.sql.go | 2 +- pkg/repository/queries/users.sql.go | 2 +- pkg/usecase/drift_stream/api.go | 51 ++++++++++++++++--- 12 files changed, 125 insertions(+), 22 deletions(-) create mode 100644 migrations/V20260521000000__add_drift_analysis_run_idempotency_key.sql diff --git a/migrations/V20260521000000__add_drift_analysis_run_idempotency_key.sql b/migrations/V20260521000000__add_drift_analysis_run_idempotency_key.sql new file mode 100644 index 0000000..486aad1 --- /dev/null +++ b/migrations/V20260521000000__add_drift_analysis_run_idempotency_key.sql @@ -0,0 +1,6 @@ +ALTER TABLE drift_analysis_run + ADD COLUMN idempotency_key TEXT; + +CREATE UNIQUE INDEX drift_analysis_run_repo_idem_key_uidx + ON drift_analysis_run (repository_id, idempotency_key) + WHERE idempotency_key IS NOT NULL; diff --git a/pkg/repository/drift_analysis_repository.go b/pkg/repository/drift_analysis_repository.go index 714e53f..31e06ed 100644 --- a/pkg/repository/drift_analysis_repository.go +++ b/pkg/repository/drift_analysis_repository.go @@ -2,10 +2,12 @@ package repository import ( "context" + "errors" "driftive.cloud/api/pkg/db" "driftive.cloud/api/pkg/repository/queries" "github.com/google/uuid" + "github.com/jackc/pgx/v5" ) type DriftAnalysisRepository interface { @@ -13,6 +15,7 @@ type DriftAnalysisRepository interface { CreateDriftAnalysisProject(ctx context.Context, params queries.CreateDriftAnalysisProjectParams) (queries.DriftAnalysisProject, error) FindDriftAnalysisRunsByRepositoryID(ctx context.Context, repoId int64, page int) ([]queries.DriftAnalysisRun, error) FindDriftAnalysisRunByUUID(ctx context.Context, uuid uuid.UUID) (queries.DriftAnalysisRun, error) + FindRunByRepoAndIdempotencyKey(ctx context.Context, repoId int64, idempotencyKey string) (*queries.DriftAnalysisRun, error) FindDriftAnalysisProjectsByRunId(ctx context.Context, runId uuid.UUID) ([]queries.DriftAnalysisProject, error) GetRepositoryRunStats(ctx context.Context, repoId int64) (queries.GetRepositoryRunStatsRow, error) GetLatestRunForRepository(ctx context.Context, repoId int64) (queries.DriftAnalysisRun, error) @@ -54,6 +57,20 @@ func (r *DriftAnalysisRepo) FindDriftAnalysisRunByUUID(ctx context.Context, uuid return r.db.Queries(ctx).FindDriftAnalysisRunByUUID(ctx, uuid) } +func (r *DriftAnalysisRepo) FindRunByRepoAndIdempotencyKey(ctx context.Context, repoId int64, idempotencyKey string) (*queries.DriftAnalysisRun, error) { + run, err := r.db.Queries(ctx).FindDriftAnalysisRunByRepoAndIdempotencyKey(ctx, queries.FindDriftAnalysisRunByRepoAndIdempotencyKeyParams{ + RepositoryID: repoId, + IdempotencyKey: &idempotencyKey, + }) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, nil + } + return nil, err + } + return &run, nil +} + func (r *DriftAnalysisRepo) FindDriftAnalysisProjectsByRunId(ctx context.Context, runId uuid.UUID) ([]queries.DriftAnalysisProject, error) { return r.db.Queries(ctx).FindDriftAnalysisProjectsByRunId(ctx, runId) } diff --git a/pkg/repository/queries/db.go b/pkg/repository/queries/db.go index 2b5c1c7..c69f0c5 100644 --- a/pkg/repository/queries/db.go +++ b/pkg/repository/queries/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.1 package queries diff --git a/pkg/repository/queries/drift_analysis.sql b/pkg/repository/queries/drift_analysis.sql index 5b6aee6..8964186 100644 --- a/pkg/repository/queries/drift_analysis.sql +++ b/pkg/repository/queries/drift_analysis.sql @@ -1,8 +1,13 @@ -- name: CreateDriftAnalysisRun :one -INSERT INTO drift_analysis_run (uuid, repository_id, total_projects, total_projects_drifted, total_projects_errored, total_projects_skipped, analysis_duration_millis) -VALUES (@uuid, @repository_id, @total_projects, @total_projects_drifted, @total_projects_errored, @total_projects_skipped, @analysis_duration_millis) +INSERT INTO drift_analysis_run (uuid, repository_id, total_projects, total_projects_drifted, total_projects_errored, total_projects_skipped, analysis_duration_millis, idempotency_key) +VALUES (@uuid, @repository_id, @total_projects, @total_projects_drifted, @total_projects_errored, @total_projects_skipped, @analysis_duration_millis, @idempotency_key) RETURNING *; +-- name: FindDriftAnalysisRunByRepoAndIdempotencyKey :one +SELECT * +FROM drift_analysis_run +WHERE repository_id = @repository_id AND idempotency_key = @idempotency_key; + -- name: CreateDriftAnalysisProject :one INSERT INTO drift_analysis_project (drift_analysis_run_id, dir, type, drifted, succeeded, init_output, plan_output, skipped_due_to_pr) VALUES (@drift_analysis_run_id, @dir, @type, @drifted, @succeeded, @init_output, @plan_output, @skipped_due_to_pr) diff --git a/pkg/repository/queries/drift_analysis.sql.go b/pkg/repository/queries/drift_analysis.sql.go index dfb982f..247df8c 100644 --- a/pkg/repository/queries/drift_analysis.sql.go +++ b/pkg/repository/queries/drift_analysis.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.1 // source: drift_analysis.sql package queries @@ -57,9 +57,9 @@ func (q *Queries) CreateDriftAnalysisProject(ctx context.Context, arg CreateDrif } const createDriftAnalysisRun = `-- name: CreateDriftAnalysisRun :one -INSERT INTO drift_analysis_run (uuid, repository_id, total_projects, total_projects_drifted, total_projects_errored, total_projects_skipped, analysis_duration_millis) -VALUES ($1, $2, $3, $4, $5, $6, $7) -RETURNING uuid, repository_id, total_projects, total_projects_drifted, analysis_duration_millis, created_at, updated_at, total_projects_errored, total_projects_skipped +INSERT INTO drift_analysis_run (uuid, repository_id, total_projects, total_projects_drifted, total_projects_errored, total_projects_skipped, analysis_duration_millis, idempotency_key) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) +RETURNING uuid, repository_id, total_projects, total_projects_drifted, analysis_duration_millis, created_at, updated_at, total_projects_errored, total_projects_skipped, idempotency_key ` type CreateDriftAnalysisRunParams struct { @@ -70,6 +70,7 @@ type CreateDriftAnalysisRunParams struct { TotalProjectsErrored int32 TotalProjectsSkipped int32 AnalysisDurationMillis int64 + IdempotencyKey *string } func (q *Queries) CreateDriftAnalysisRun(ctx context.Context, arg CreateDriftAnalysisRunParams) (DriftAnalysisRun, error) { @@ -81,6 +82,7 @@ func (q *Queries) CreateDriftAnalysisRun(ctx context.Context, arg CreateDriftAna arg.TotalProjectsErrored, arg.TotalProjectsSkipped, arg.AnalysisDurationMillis, + arg.IdempotencyKey, ) var i DriftAnalysisRun err := row.Scan( @@ -93,6 +95,7 @@ func (q *Queries) CreateDriftAnalysisRun(ctx context.Context, arg CreateDriftAna &i.UpdatedAt, &i.TotalProjectsErrored, &i.TotalProjectsSkipped, + &i.IdempotencyKey, ) return i, err } @@ -162,8 +165,37 @@ func (q *Queries) FindDriftAnalysisProjectsByRunId(ctx context.Context, driftAna return items, nil } +const findDriftAnalysisRunByRepoAndIdempotencyKey = `-- name: FindDriftAnalysisRunByRepoAndIdempotencyKey :one +SELECT uuid, repository_id, total_projects, total_projects_drifted, analysis_duration_millis, created_at, updated_at, total_projects_errored, total_projects_skipped, idempotency_key +FROM drift_analysis_run +WHERE repository_id = $1 AND idempotency_key = $2 +` + +type FindDriftAnalysisRunByRepoAndIdempotencyKeyParams struct { + RepositoryID int64 + IdempotencyKey *string +} + +func (q *Queries) FindDriftAnalysisRunByRepoAndIdempotencyKey(ctx context.Context, arg FindDriftAnalysisRunByRepoAndIdempotencyKeyParams) (DriftAnalysisRun, error) { + row := q.db.QueryRow(ctx, findDriftAnalysisRunByRepoAndIdempotencyKey, arg.RepositoryID, arg.IdempotencyKey) + var i DriftAnalysisRun + err := row.Scan( + &i.Uuid, + &i.RepositoryID, + &i.TotalProjects, + &i.TotalProjectsDrifted, + &i.AnalysisDurationMillis, + &i.CreatedAt, + &i.UpdatedAt, + &i.TotalProjectsErrored, + &i.TotalProjectsSkipped, + &i.IdempotencyKey, + ) + return i, err +} + const findDriftAnalysisRunByUUID = `-- name: FindDriftAnalysisRunByUUID :one -SELECT uuid, repository_id, total_projects, total_projects_drifted, analysis_duration_millis, created_at, updated_at, total_projects_errored, total_projects_skipped +SELECT uuid, repository_id, total_projects, total_projects_drifted, analysis_duration_millis, created_at, updated_at, total_projects_errored, total_projects_skipped, idempotency_key FROM drift_analysis_run WHERE uuid = $1 ` @@ -181,12 +213,13 @@ func (q *Queries) FindDriftAnalysisRunByUUID(ctx context.Context, argUuid uuid.U &i.UpdatedAt, &i.TotalProjectsErrored, &i.TotalProjectsSkipped, + &i.IdempotencyKey, ) return i, err } const findDriftAnalysisRunsByRepositoryId = `-- name: FindDriftAnalysisRunsByRepositoryId :many -SELECT uuid, repository_id, total_projects, total_projects_drifted, analysis_duration_millis, created_at, updated_at, total_projects_errored, total_projects_skipped +SELECT uuid, repository_id, total_projects, total_projects_drifted, analysis_duration_millis, created_at, updated_at, total_projects_errored, total_projects_skipped, idempotency_key FROM drift_analysis_run WHERE repository_id = $1 ORDER BY created_at DESC @@ -218,6 +251,7 @@ func (q *Queries) FindDriftAnalysisRunsByRepositoryId(ctx context.Context, arg F &i.UpdatedAt, &i.TotalProjectsErrored, &i.TotalProjectsSkipped, + &i.IdempotencyKey, ); err != nil { return nil, err } @@ -310,7 +344,7 @@ func (q *Queries) GetDriftRateOverTime(ctx context.Context, arg GetDriftRateOver } const getLatestRunForRepository = `-- name: GetLatestRunForRepository :one -SELECT uuid, repository_id, total_projects, total_projects_drifted, analysis_duration_millis, created_at, updated_at, total_projects_errored, total_projects_skipped +SELECT uuid, repository_id, total_projects, total_projects_drifted, analysis_duration_millis, created_at, updated_at, total_projects_errored, total_projects_skipped, idempotency_key FROM drift_analysis_run WHERE repository_id = $1 ORDER BY created_at DESC @@ -330,6 +364,7 @@ func (q *Queries) GetLatestRunForRepository(ctx context.Context, repositoryID in &i.UpdatedAt, &i.TotalProjectsErrored, &i.TotalProjectsSkipped, + &i.IdempotencyKey, ) return i, err } diff --git a/pkg/repository/queries/git_organization.sql.go b/pkg/repository/queries/git_organization.sql.go index c0c7c9f..b68b449 100644 --- a/pkg/repository/queries/git_organization.sql.go +++ b/pkg/repository/queries/git_organization.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.1 // source: git_organization.sql package queries diff --git a/pkg/repository/queries/git_repository.sql.go b/pkg/repository/queries/git_repository.sql.go index 65ebfd8..016964e 100644 --- a/pkg/repository/queries/git_repository.sql.go +++ b/pkg/repository/queries/git_repository.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.1 // source: git_repository.sql package queries diff --git a/pkg/repository/queries/models.go b/pkg/repository/queries/models.go index eeb9234..86269ea 100644 --- a/pkg/repository/queries/models.go +++ b/pkg/repository/queries/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.1 package queries @@ -32,6 +32,7 @@ type DriftAnalysisRun struct { UpdatedAt time.Time TotalProjectsErrored int32 TotalProjectsSkipped int32 + IdempotencyKey *string } type GitOrganization struct { diff --git a/pkg/repository/queries/sync_org.sql.go b/pkg/repository/queries/sync_org.sql.go index 9b93493..19289b3 100644 --- a/pkg/repository/queries/sync_org.sql.go +++ b/pkg/repository/queries/sync_org.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.1 // source: sync_org.sql package queries diff --git a/pkg/repository/queries/sync_status_user.sql.go b/pkg/repository/queries/sync_status_user.sql.go index 75226be..ee5a759 100644 --- a/pkg/repository/queries/sync_status_user.sql.go +++ b/pkg/repository/queries/sync_status_user.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.1 // source: sync_status_user.sql package queries diff --git a/pkg/repository/queries/users.sql.go b/pkg/repository/queries/users.sql.go index 6c7d5ef..dd60db5 100644 --- a/pkg/repository/queries/users.sql.go +++ b/pkg/repository/queries/users.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.1 // source: users.sql package queries diff --git a/pkg/usecase/drift_stream/api.go b/pkg/usecase/drift_stream/api.go index 0c43a6c..c830c8d 100644 --- a/pkg/usecase/drift_stream/api.go +++ b/pkg/usecase/drift_stream/api.go @@ -17,9 +17,13 @@ import ( "github.com/gofiber/fiber/v3/log" "github.com/google/uuid" "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" "time" ) +// pgUniqueViolation is the SQLSTATE code for unique_violation. +const pgUniqueViolation = "23505" + type DriftStateHandler struct { cfg *config.Config orgRepository repository.GitOrgRepository @@ -97,6 +101,22 @@ func (d *DriftStateHandler) HandleUpdate(c fiber.Ctx) error { return c.SendStatus(fiber.StatusInternalServerError) } + idemKey := strings.TrimSpace(c.Get("Idempotency-Key")) + + // If the client sent an Idempotency-Key and we already have a run for it, return that run + // without re-inserting. This lets the CLI safely retry transient failures. + if idemKey != "" { + existing, err := d.driftAnalysisRepository.FindRunByRepoAndIdempotencyKey(c.Context(), repo.ID, idemKey) + if err != nil { + log.Errorf("Error looking up run by idempotency key: %v", err) + return c.SendStatus(fiber.StatusInternalServerError) + } + if existing != nil { + log.Infof("Idempotent replay for repository %d, key %s -> run %s", repo.ID, idemKey, existing.Uuid) + return c.JSON(buildAnalysisResponse(d.cfg.Frontend.FrontendURL, org, repo, existing.Uuid)) + } + } + var state DriftDetectionResult if err := c.Bind().Body(&state); err != nil { return c.SendStatus(fiber.StatusBadRequest) @@ -116,6 +136,11 @@ func (d *DriftStateHandler) HandleUpdate(c fiber.Ctx) error { } } + var idemKeyPtr *string + if idemKey != "" { + idemKeyPtr = &idemKey + } + var runUUID uuid.UUID err = d.driftAnalysisRepository.WithTx(c.Context(), func(ctx context.Context) error { params := queries.CreateDriftAnalysisRunParams{ @@ -126,6 +151,7 @@ func (d *DriftStateHandler) HandleUpdate(c fiber.Ctx) error { TotalProjectsErrored: totalErrored, TotalProjectsSkipped: state.TotalSkipped, AnalysisDurationMillis: state.Duration.Milliseconds(), + IdempotencyKey: idemKeyPtr, } run, err := d.driftAnalysisRepository.CreateDriftAnalysisRun(ctx, params) @@ -165,6 +191,19 @@ func (d *DriftStateHandler) HandleUpdate(c fiber.Ctx) error { }) if err != nil { + // Race: a concurrent retry with the same idempotency key may have inserted first. + // The partial unique index on (repository_id, idempotency_key) trips with SQLSTATE 23505; + // re-fetch and return the winning row. + if idemKey != "" { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) && pgErr.Code == pgUniqueViolation { + existing, lookupErr := d.driftAnalysisRepository.FindRunByRepoAndIdempotencyKey(c.Context(), repo.ID, idemKey) + if lookupErr == nil && existing != nil { + log.Infof("Idempotent race resolved for repository %d, key %s -> run %s", repo.ID, idemKey, existing.Uuid) + return c.JSON(buildAnalysisResponse(d.cfg.Frontend.FrontendURL, org, repo, existing.Uuid)) + } + } + } log.Errorf("Error handling drift state update: %v", err) return c.SendStatus(fiber.StatusInternalServerError) } @@ -176,21 +215,21 @@ func (d *DriftStateHandler) HandleUpdate(c fiber.Ctx) error { } } - // Build dashboard URL: /:provider/:org/:repo/run/:run_uuid + return c.JSON(buildAnalysisResponse(d.cfg.Frontend.FrontendURL, org, repo, runUUID)) +} + +func buildAnalysisResponse(frontendURL string, org queries.GitOrganization, repo queries.GitRepository, runUUID uuid.UUID) DriftAnalysisResponse { dashboardURL := fmt.Sprintf("%s/%s/%s/%s/run/%s", - d.cfg.Frontend.FrontendURL, + frontendURL, providerToSlug(org.Provider), org.Name, repo.Name, runUUID.String(), ) - - response := DriftAnalysisResponse{ + return DriftAnalysisResponse{ RunID: runUUID.String(), DashboardURL: dashboardURL, } - - return c.JSON(response) } func (d *DriftStateHandler) ListRunsByRepoId(c fiber.Ctx) error { From 5aac68172fd416e8f6c454c7e5f8db5185d04263 Mon Sep 17 00:00:00 2001 From: Fernando Nogueira Date: Thu, 21 May 2026 00:58:39 -0300 Subject: [PATCH 2/2] feat(drift): add idempotency key support for drift analysis runs --- pkg/repository/drift_analysis_repository.go | 17 +++++------------ pkg/usecase/drift_stream/api.go | 12 ++++++------ 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/pkg/repository/drift_analysis_repository.go b/pkg/repository/drift_analysis_repository.go index 31e06ed..71252b5 100644 --- a/pkg/repository/drift_analysis_repository.go +++ b/pkg/repository/drift_analysis_repository.go @@ -2,12 +2,10 @@ package repository import ( "context" - "errors" "driftive.cloud/api/pkg/db" "driftive.cloud/api/pkg/repository/queries" "github.com/google/uuid" - "github.com/jackc/pgx/v5" ) type DriftAnalysisRepository interface { @@ -15,7 +13,7 @@ type DriftAnalysisRepository interface { CreateDriftAnalysisProject(ctx context.Context, params queries.CreateDriftAnalysisProjectParams) (queries.DriftAnalysisProject, error) FindDriftAnalysisRunsByRepositoryID(ctx context.Context, repoId int64, page int) ([]queries.DriftAnalysisRun, error) FindDriftAnalysisRunByUUID(ctx context.Context, uuid uuid.UUID) (queries.DriftAnalysisRun, error) - FindRunByRepoAndIdempotencyKey(ctx context.Context, repoId int64, idempotencyKey string) (*queries.DriftAnalysisRun, error) + FindRunByRepoAndIdempotencyKey(ctx context.Context, repoId int64, idempotencyKey string) (queries.DriftAnalysisRun, error) FindDriftAnalysisProjectsByRunId(ctx context.Context, runId uuid.UUID) ([]queries.DriftAnalysisProject, error) GetRepositoryRunStats(ctx context.Context, repoId int64) (queries.GetRepositoryRunStatsRow, error) GetLatestRunForRepository(ctx context.Context, repoId int64) (queries.DriftAnalysisRun, error) @@ -57,18 +55,13 @@ func (r *DriftAnalysisRepo) FindDriftAnalysisRunByUUID(ctx context.Context, uuid return r.db.Queries(ctx).FindDriftAnalysisRunByUUID(ctx, uuid) } -func (r *DriftAnalysisRepo) FindRunByRepoAndIdempotencyKey(ctx context.Context, repoId int64, idempotencyKey string) (*queries.DriftAnalysisRun, error) { - run, err := r.db.Queries(ctx).FindDriftAnalysisRunByRepoAndIdempotencyKey(ctx, queries.FindDriftAnalysisRunByRepoAndIdempotencyKeyParams{ +// FindRunByRepoAndIdempotencyKey returns pgx.ErrNoRows when no matching run exists; callers +// should check with errors.Is(err, pgx.ErrNoRows). +func (r *DriftAnalysisRepo) FindRunByRepoAndIdempotencyKey(ctx context.Context, repoId int64, idempotencyKey string) (queries.DriftAnalysisRun, error) { + return r.db.Queries(ctx).FindDriftAnalysisRunByRepoAndIdempotencyKey(ctx, queries.FindDriftAnalysisRunByRepoAndIdempotencyKeyParams{ RepositoryID: repoId, IdempotencyKey: &idempotencyKey, }) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return nil, nil - } - return nil, err - } - return &run, nil } func (r *DriftAnalysisRepo) FindDriftAnalysisProjectsByRunId(ctx context.Context, runId uuid.UUID) ([]queries.DriftAnalysisProject, error) { diff --git a/pkg/usecase/drift_stream/api.go b/pkg/usecase/drift_stream/api.go index c830c8d..f728496 100644 --- a/pkg/usecase/drift_stream/api.go +++ b/pkg/usecase/drift_stream/api.go @@ -107,14 +107,14 @@ func (d *DriftStateHandler) HandleUpdate(c fiber.Ctx) error { // without re-inserting. This lets the CLI safely retry transient failures. if idemKey != "" { existing, err := d.driftAnalysisRepository.FindRunByRepoAndIdempotencyKey(c.Context(), repo.ID, idemKey) - if err != nil { - log.Errorf("Error looking up run by idempotency key: %v", err) - return c.SendStatus(fiber.StatusInternalServerError) - } - if existing != nil { + if err == nil { log.Infof("Idempotent replay for repository %d, key %s -> run %s", repo.ID, idemKey, existing.Uuid) return c.JSON(buildAnalysisResponse(d.cfg.Frontend.FrontendURL, org, repo, existing.Uuid)) } + if !errors.Is(err, pgx.ErrNoRows) { + log.Errorf("Error looking up run by idempotency key: %v", err) + return c.SendStatus(fiber.StatusInternalServerError) + } } var state DriftDetectionResult @@ -198,7 +198,7 @@ func (d *DriftStateHandler) HandleUpdate(c fiber.Ctx) error { var pgErr *pgconn.PgError if errors.As(err, &pgErr) && pgErr.Code == pgUniqueViolation { existing, lookupErr := d.driftAnalysisRepository.FindRunByRepoAndIdempotencyKey(c.Context(), repo.ID, idemKey) - if lookupErr == nil && existing != nil { + if lookupErr == nil { log.Infof("Idempotent race resolved for repository %d, key %s -> run %s", repo.ID, idemKey, existing.Uuid) return c.JSON(buildAnalysisResponse(d.cfg.Frontend.FrontendURL, org, repo, existing.Uuid)) }