Skip to content

fix: deleting identity shouldn't 500#4103

Merged
Flo4604 merged 12 commits intomainfrom
eng-2120-fix-timing-issue-when-deleting-same-identity-in-parallel
Oct 23, 2025
Merged

fix: deleting identity shouldn't 500#4103
Flo4604 merged 12 commits intomainfrom
eng-2120-fix-timing-issue-when-deleting-same-identity-in-parallel

Conversation

@Flo4604
Copy link
Member

@Flo4604 Flo4604 commented Oct 16, 2025

What does this PR do?

Fixes #4102 aka when you'd delete a identity we would sometimes 500 because we'd confuse which identity to hard delete and what to soft delete.

Improves the performance of finding an identity using a union query to either find id or external id as the OR was leading to a full table scan.

Splits up FindIdentity to FindIdentityByID and FindIdentityByExternalID for tests.

Selects ratelimits with the identity so we can skip the second query.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Chore (refactoring code, technical debt, workflow improvements)
  • Enhancement (small improvements)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How should this be tested?

Create identities via identities Api's, update them delete them in parallel and test around.

What didn't work was

Create Identity via API
Delete identity
Create Identity via API
Delete again
Create Identity via API
Try to delete -> we would 500 here.

Checklist

Required

  • Filled out the "How to test" section in this PR
  • Read Contributing Guide
  • Self-reviewed my own code
  • Commented on my code in hard-to-understand areas
  • Ran pnpm build
  • Ran pnpm fmt
  • Checked for warnings, there are none
  • Removed all console.logs
  • Merged the latest changes from main onto my branch with git pull origin main
  • My changes don't cause any responsiveness issues

Appreciated

  • If a UI change was made: Added a screen recording or screenshots to this PR
  • Updated the Unkey Docs if changes were necessary

@linear
Copy link

linear bot commented Oct 16, 2025

@changeset-bot
Copy link

changeset-bot bot commented Oct 16, 2025

⚠️ No Changeset found

Latest commit: a0d7e96

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link

vercel bot commented Oct 16, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
dashboard Ignored Ignored Preview Oct 22, 2025 6:09pm
engineering Ignored Ignored Preview Oct 22, 2025 6:09pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 16, 2025

📝 Walkthrough

Walkthrough

The PR refactors identity database query patterns by splitting a generic FindIdentity lookup into specialized FindIdentityByID and FindIdentityByExternalID methods. It introduces FindIdentityWithRatelimits for combined identity and ratelimit lookups and DeleteOldIdentityByExternalID for cleanup. Handlers are updated with improved error handling and soft-deletion semantics.

Changes

Cohort / File(s) Change Summary
New DB Query Methods (Generated)
go/pkg/db/identity_find_by_id.sql_generated.go, go/pkg/db/identity_find_by_external_id.sql_generated.go, go/pkg/db/identity_find_with_ratelimits.sql_generated.go, go/pkg/db/identity_delete_old_by_external_id.sql_generated.go
Added four new generated query methods: FindIdentityByID, FindIdentityByExternalID, FindIdentityWithRatelimits, and DeleteOldIdentityByExternalID with corresponding parameter and row structs. Removed old FindIdentity method.
SQL Query Definitions
go/pkg/db/queries/identity_find_by_id.sql, go/pkg/db/queries/identity_find_by_external_id.sql, go/pkg/db/queries/identity_find_with_ratelimits.sql, go/pkg/db/queries/identity_delete_old_by_external_id.sql
Replaced generic FindIdentity query with two specific lookup queries by ID and external ID. Added FindIdentityWithRatelimits for combined lookups with ratelimit aggregation and DeleteOldIdentityByExternalID for soft-deleted identity cleanup.
Query Interface
go/pkg/db/querier_generated.go
Updated Querier interface to remove FindIdentity, add FindIdentityByID, FindIdentityByExternalID, FindIdentityWithRatelimits, and DeleteOldIdentityByExternalID method signatures.
Identity Handler Updates
go/apps/api/routes/v2_identities_get_identity/handler.go
Replaced per-identity lookup with FindIdentityWithRatelimits call, added zero-result check for not-found, introduced ratelimits parsing from JSON payload, and removed separate transactional retrieval logic.
Identity Delete Handler
go/apps/api/routes/v2_identities_delete_identity/handler.go
Switched to FindIdentityWithRatelimits, added duplicate-key error handling with soft-deletion idempotency checks via FindIdentityByID and DeleteOldIdentityByExternalID, and added logic to clean up old soft-deleted identities on concurrent deletion attempts.
Identity Update Handler
go/apps/api/routes/v2_identities_update_identity/handler.go
Replaced per-identity lookup with FindIdentityWithRatelimits, introduced transaction-scoped result type carrying identity and final ratelimits, replaced separate ratelimit retrieval with in-memory construction and JSON parsing, eliminated follow-up SELECT for ratelimits.
Key Handler Updates
go/apps/api/routes/v2_keys_create_key/handler.go, go/apps/api/routes/v2_keys_update_key/handler.go
Replaced FindIdentity calls with FindIdentityByExternalID for identity lookups, updated parameter structs from FindIdentityParams to FindIdentityByExternalIDParams.
Get/List/Create/Update Identity Tests
go/apps/api/routes/v2_identities_create_identity/200_test.go, go/apps/api/routes/v2_identities_delete_identity/200_test.go, go/apps/api/routes/v2_identities_list_identities/200_test.go
Updated test queries to use FindIdentityByID and FindIdentityByExternalID methods, added test scenarios for duplicate-key deletion, soft-delete idempotency, and ratelimit-aware deletion.
Key Handler Tests
go/apps/api/routes/v2_keys_create_key/200_test.go, go/apps/api/routes/v2_keys_update_key/200_test.go, go/apps/api/routes/v2_keys_update_key/three_state_test.go
Updated identity lookup calls from FindIdentity to FindIdentityByExternalID or FindIdentityByID with corresponding parameter struct changes.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Handler as Delete Handler
    participant DB as Database
    participant Tx as Transaction

    Client->>Handler: DELETE /identities/{id}
    activate Handler
    
    Handler->>DB: FindIdentityWithRatelimits
    activate DB
    DB-->>Handler: identity + ratelimits
    deactivate DB
    
    Handler->>Tx: BEGIN
    activate Tx
    
    Tx->>Tx: Soft DELETE identity
    alt Duplicate Key Error
        Tx->>DB: FindIdentityByID (current)
        DB-->>Tx: check if already soft-deleted
        alt Already Deleted
            Tx-->>Handler: ✓ Idempotent (no audit)
        else Not Yet Deleted
            Tx->>DB: DeleteOldIdentityByExternalID
            DB-->>Tx: cleanup old soft-deleted
            Tx->>Tx: Retry soft DELETE
            alt Duplicate Key Again
                Tx-->>Handler: ✓ Idempotent (no audit)
            else Success
                Tx->>DB: INSERT audit logs
            end
        end
    else Success
        Tx->>DB: INSERT audit logs
    end
    
    Tx->>Tx: COMMIT
    deactivate Tx
    Handler-->>Client: 200 OK
    deactivate Handler
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

This PR involves significant refactoring across handler logic, generated database code, and SQL definitions. The complexity stems from: (1) new duplicate-key error handling with idempotency checks and concurrent cleanup logic in the delete handler, (2) transaction-scoped result management in the update handler, (3) JSON parsing of ratelimits across multiple handlers, (4) changes to the core database query layer affecting multiple call sites, and (5) heterogeneous logic patterns requiring separate reasoning for each affected handler (get, delete, update, create/update key). While the generated code changes are repetitive, the handler modifications introduce non-trivial control flow and state management logic.

Possibly related PRs

Suggested labels

wakes you up

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title "fix: deleting identity shouldn't 500" clearly and concisely describes the primary objective of the changeset - fixing a bug where identity deletion causes HTTP 500 errors. The title directly relates to the main issue being resolved (#4102) and follows good naming conventions with a clear, specific description of the fix. It effectively communicates the purpose of the PR to someone scanning the commit history.
Out of Scope Changes Check ✅ Passed All code changes appear to be within scope of the PR's stated objectives. The query refactoring (splitting FindIdentity into FindIdentityByID and FindIdentityByExternalID) and consolidation of ratelimits fetching are explicitly mentioned in the PR description as performance improvements and supporting infrastructure. The handler updates across v2_identities_delete_identity, v2_identities_get_identity, and v2_identities_update_identity are necessary to implement the core concurrency fix and use the new query patterns. Test updates across multiple files support the new query methods. No unrelated or extraneous changes are detected.
Description Check ✅ Passed The pull request description comprehensively follows the provided template. It includes a clear "What does this PR do?" section referencing issue #4102 with specific objectives, properly marks both "Bug fix" and "Enhancement" as types of change, provides detailed testing instructions with a clear reproduction sequence, and completes all required checklist items. The description demonstrates thorough understanding of the changes and provides sufficient context for reviewers to understand the scope and motivation.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch eng-2120-fix-timing-issue-when-deleting-same-identity-in-parallel

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6648d2b and a0d7e96.

📒 Files selected for processing (1)
  • go/apps/api/routes/v2_identities_delete_identity/200_test.go (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
go/apps/api/routes/v2_identities_delete_identity/200_test.go (5)
go/pkg/testutil/seed/seed.go (3)
  • CreateIdentityRequest (364-369)
  • CreateRatelimitRequest (307-315)
  • New (39-46)
go/pkg/db/identity_find_by_external_id.sql_generated.go (1)
  • FindIdentityByExternalIDParams (20-24)
go/pkg/testutil/http.go (1)
  • CallRoute (271-305)
go/pkg/db/identity_find_by_id.sql_generated.go (1)
  • FindIdentityByIDParams (20-24)
go/pkg/array/fill.go (1)
  • Fill (23-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Test Go API Local / Test
  • GitHub Check: Build / Build
🔇 Additional comments (7)
go/apps/api/routes/v2_identities_delete_identity/200_test.go (7)

13-13: LGTM! Imports support refactored test utilities.

The additions of array and seed packages are appropriate and directly support the test refactoring to use harness-provided identity creation and array utilities for ratelimit generation.

Also applies to: 16-16


39-80: LGTM! Soft deletion semantics properly validated.

This test correctly validates the soft deletion behavior:

  • Identity creation via seed-based request
  • Verification of existence before deletion
  • Confirmation of soft deletion (not found with deleted=false)
  • Confirmation identity persists with deleted=true

The refactored approach using external IDs aligns well with the API's intended workflow.


82-129: LGTM! Ratelimit persistence correctly validated.

This test appropriately validates that:

  • Ratelimits can be created alongside identities using array.Fill for test data generation
  • Soft deletion of identities preserves associated ratelimits for audit purposes
  • The assertion on line 128 correctly confirms ratelimits remain intact post-deletion

The use of array.Fill with a generator function is a clean pattern for creating test fixtures.


131-159: LGTM! Wildcard permission authorization validated.

This test properly validates that the wildcard permission pattern (identity.*.delete_identity) authorizes the deletion operation. The test structure is clear and assertions are appropriate.


161-204: LGTM! Audit log verification is thorough.

This test properly validates the observability requirement by:

  • Confirming at least one audit log exists for the identity
  • Searching for the specific identity.delete event
  • Verifying the workspace ID matches

The audit trail validation is important for compliance and debugging.


206-248: LGTM! Critical duplicate key handling validated.

This test validates a key aspect of the fix for issue #4102:

  • When a new identity reuses an external ID after deletion, the old identity is hard-deleted (cleanup)
  • The new identity undergoes soft deletion on subsequent delete operations
  • Lines 242-247 correctly confirm the old identity is permanently removed

This scenario is essential for preventing the race condition that caused HTTP 500 errors.


250-322: Excellent test! Directly validates the fix for issue #4102.

This test case is the crown jewel of this PR—it directly reproduces the delete→create→delete sequence that previously caused HTTP 500 errors:

  1. Lines 258-270: Creates initial identity with "Advanced" tier ratelimits
  2. Lines 273-275: First deletion (soft delete)
  3. Lines 278-290: Creates new identity with same externalID but "Starter" tier
  4. Lines 293-295: Second deletion—this is where the bug manifested
  5. Lines 298-305: Correctly asserts the new identity is soft deleted (not hard deleted)
  6. Lines 316-321: Verifies the old identity was properly hard deleted as cleanup

The test comprehensively validates that the handler now correctly distinguishes between which identity should be hard-deleted (the old one) versus soft-deleted (the current one), eliminating the race condition. The Stripe tier-change workflow provides excellent real-world context.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
go/apps/api/routes/v2_identities_delete_identity/handler.go (1)

182-210: Delete associated ratelimits or update audit logs
The handler logs each ratelimit as “Deleted” but only calls SoftDeleteIdentity (which updates identities) and never deletes ratelimit rows (no ON DELETE CASCADE). This leaves orphaned ratelimits and misleading logs. Either invoke DeleteManyRatelimitsByIDs/DeleteManyRatelimitsByIdentityID in the same transaction before inserting audit logs, or change the audit event to avoid implying ratelimit deletion.

🧹 Nitpick comments (8)
go/apps/api/routes/v2_keys_update_key/three_state_test.go (1)

274-277: Consider explicitly setting the Deleted field for clarity.

The Deleted field in FindIdentityByIDParams is currently omitted, relying on the zero value (false). For consistency with other test files (e.g., v2_keys_create_key/200_test.go line 286) and improved readability, consider explicitly setting Deleted: false.

Apply this diff to be explicit:

 identity, err := db.Query.FindIdentityByID(ctx, h.DB.RO(), db.FindIdentityByIDParams{
 	IdentityID:  key.IdentityID.String,
 	WorkspaceID: h.Resources().UserWorkspace.ID,
+	Deleted:     false,
 })

Also applies to: 321-324

go/apps/api/routes/v2_keys_update_key/200_test.go (1)

197-200: Consider explicitly setting the Deleted field for clarity.

Similar to other test files, the Deleted field is omitted, relying on the zero value. For consistency with v2_keys_create_key/200_test.go and improved readability, consider explicitly setting Deleted: false.

Apply this diff:

 identity, err := db.Query.FindIdentityByID(ctx, h.DB.RO(), db.FindIdentityByIDParams{
 	IdentityID:  key.IdentityID.String,
 	WorkspaceID: h.Resources().UserWorkspace.ID,
+	Deleted:     false,
 })
go/pkg/db/queries/identity_find_with_ratelimits.sql (1)

23-45: UNION ALL + LIMIT 1 is nondeterministic; prefer ID match deterministically.

If a user-chosen external_id happens to equal some identity ID, both branches can match; without ORDER BY the returned row is undefined. Prefer the ID match deterministically (e.g., wrap UNION ALL in a subquery with a priority column and ORDER BY priority DESC LIMIT 1), or enforce that external IDs cannot collide with ID format.

go/apps/api/routes/v2_identities_delete_identity/200_test.go (1)

69-69: Prefer db.IsNotFound(err) over comparing sql.ErrNoRows.

More resilient across drivers and future DB changes; improves readability.

Example change:

- require.Equal(t, sql.ErrNoRows, err)
+ require.True(t, db.IsNotFound(err))

Also applies to: 123-123, 158-158, 239-239, 247-247, 321-321

go/apps/api/routes/v2_identities_update_identity/handler.go (1)

138-143: Don’t silently ignore malformed ratelimits JSON

At least log unmarshal errors at debug to aid diagnostics; otherwise responses may miss existing rate limits without any trace.

- if ratelimitBytes, ok := identityRow.Ratelimits.([]byte); ok && ratelimitBytes != nil {
-   _ = json.Unmarshal(ratelimitBytes, &existingRatelimits) // Ignore error, default to empty array
- }
+ if ratelimitBytes, ok := identityRow.Ratelimits.([]byte); ok && ratelimitBytes != nil {
+   if err := json.Unmarshal(ratelimitBytes, &existingRatelimits); err != nil {
+     h.Logger.Debug(ctx, "failed to parse identity ratelimits JSON", "identity_id", identityRow.ID, "err", err.Error())
+   }
+ }
go/apps/api/routes/v2_identities_delete_identity/handler.go (1)

95-100: Log ratelimits JSON parse failures

Record at debug to avoid silently dropping ratelimit delete logs.

- if ratelimitBytes, ok := identity.Ratelimits.([]byte); ok && ratelimitBytes != nil {
-   _ = json.Unmarshal(ratelimitBytes, &ratelimits) // Ignore error, default to empty array
- }
+ if ratelimitBytes, ok := identity.Ratelimits.([]byte); ok && ratelimitBytes != nil {
+   if err := json.Unmarshal(ratelimitBytes, &ratelimits); err != nil {
+     h.Logger.Debug(ctx, "failed to parse ratelimits JSON for delete", "identity_id", identity.ID, "err", err.Error())
+   }
+ }
go/pkg/db/identity_find_with_ratelimits.sql_generated.go (1)

71-76: Prefer concrete type for Ratelimits to avoid type assertions

Map the JSON column to []byte or json.RawMessage via sqlc type overrides to drop interface{} and the unsafe type assertion in handlers.

go/pkg/db/identity_find.sql_generated.go (1)

12-18: Split readers by key are clear and efficient

Good move removing the OR path. Ensure suitable indexes back these queries for consistent performance.

Consider composite indexes:

  • (workspace_id, id, deleted)
  • (workspace_id, external_id, deleted)

Also applies to: 20-27, 49-56, 57-63, 70-73

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c764837 and 89711e4.

📒 Files selected for processing (18)
  • go/apps/api/routes/v2_identities_create_identity/200_test.go (10 hunks)
  • go/apps/api/routes/v2_identities_delete_identity/200_test.go (4 hunks)
  • go/apps/api/routes/v2_identities_delete_identity/handler.go (4 hunks)
  • go/apps/api/routes/v2_identities_get_identity/handler.go (1 hunks)
  • go/apps/api/routes/v2_identities_list_identities/200_test.go (1 hunks)
  • go/apps/api/routes/v2_identities_update_identity/handler.go (8 hunks)
  • go/apps/api/routes/v2_keys_create_key/200_test.go (1 hunks)
  • go/apps/api/routes/v2_keys_create_key/handler.go (1 hunks)
  • go/apps/api/routes/v2_keys_update_key/200_test.go (1 hunks)
  • go/apps/api/routes/v2_keys_update_key/handler.go (1 hunks)
  • go/apps/api/routes/v2_keys_update_key/three_state_test.go (2 hunks)
  • go/pkg/db/identity_delete_old_by_external_id.sql_generated.go (1 hunks)
  • go/pkg/db/identity_find.sql_generated.go (1 hunks)
  • go/pkg/db/identity_find_with_ratelimits.sql_generated.go (1 hunks)
  • go/pkg/db/querier_generated.go (2 hunks)
  • go/pkg/db/queries/identity_delete_old_by_external_id.sql (1 hunks)
  • go/pkg/db/queries/identity_find.sql (1 hunks)
  • go/pkg/db/queries/identity_find_with_ratelimits.sql (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (13)
go/apps/api/routes/v2_identities_list_identities/200_test.go (1)
go/pkg/db/identity_find.sql_generated.go (1)
  • FindIdentityByExternalIDParams (20-24)
go/apps/api/routes/v2_identities_get_identity/handler.go (3)
go/pkg/db/identity_find_with_ratelimits.sql_generated.go (1)
  • FindIdentityWithRatelimitsParams (60-64)
go/apps/api/openapi/gen.go (1)
  • Identity (161-173)
go/pkg/db/custom_types.go (1)
  • RatelimitInfo (23-31)
go/apps/api/routes/v2_keys_update_key/three_state_test.go (1)
go/pkg/db/identity_find.sql_generated.go (1)
  • FindIdentityByIDParams (57-61)
go/apps/api/routes/v2_keys_update_key/handler.go (1)
go/pkg/db/identity_find.sql_generated.go (1)
  • FindIdentityByExternalIDParams (20-24)
go/apps/api/routes/v2_keys_create_key/200_test.go (2)
go/pkg/db/identity_find.sql_generated.go (1)
  • FindIdentityByExternalIDParams (20-24)
go/pkg/testutil/seed/seed.go (1)
  • Resources (23-28)
go/apps/api/routes/v2_keys_update_key/200_test.go (1)
go/pkg/db/identity_find.sql_generated.go (1)
  • FindIdentityByIDParams (57-61)
go/apps/api/routes/v2_identities_create_identity/200_test.go (2)
go/pkg/db/identity_find.sql_generated.go (2)
  • FindIdentityByIDParams (57-61)
  • FindIdentityByExternalIDParams (20-24)
go/pkg/testutil/seed/seed.go (1)
  • Resources (23-28)
go/apps/api/routes/v2_identities_update_identity/handler.go (7)
go/pkg/db/queries.go (2)
  • Query (29-29)
  • BulkQuery (31-31)
go/pkg/db/identity_find_with_ratelimits.sql_generated.go (2)
  • FindIdentityWithRatelimitsParams (60-64)
  • FindIdentityWithRatelimitsRow (66-76)
go/apps/api/openapi/gen.go (4)
  • Identity (161-173)
  • RatelimitResponse (420-435)
  • Meta (279-282)
  • RatelimitRequest (381-417)
go/pkg/db/custom_types.go (1)
  • RatelimitInfo (23-31)
go/pkg/db/tx.go (1)
  • TxWithResult (113-147)
go/pkg/db/identity_insert_ratelimit.sql_generated.go (1)
  • InsertIdentityRatelimitParams (40-49)
go/pkg/uid/uid.go (1)
  • RatelimitPrefix (29-29)
go/apps/api/routes/v2_keys_create_key/handler.go (2)
go/pkg/db/queries.go (1)
  • Query (29-29)
go/pkg/db/identity_find.sql_generated.go (1)
  • FindIdentityByExternalIDParams (20-24)
go/apps/api/routes/v2_identities_delete_identity/handler.go (6)
go/pkg/db/identity_find_with_ratelimits.sql_generated.go (1)
  • FindIdentityWithRatelimitsParams (60-64)
go/pkg/db/models_generated.go (1)
  • Identity (629-638)
go/pkg/db/custom_types.go (1)
  • RatelimitInfo (23-31)
go/pkg/db/identity_find.sql_generated.go (1)
  • FindIdentityByIDParams (57-61)
go/pkg/db/identity_delete_old_by_external_id.sql_generated.go (1)
  • DeleteOldIdentityByExternalIDParams (22-26)
go/pkg/db/handle_err_duplicate_key.go (1)
  • IsDuplicateKeyError (7-13)
go/pkg/db/querier_generated.go (5)
go/pkg/partition/db/database.go (1)
  • DBTX (10-10)
go/pkg/db/identity_delete_old_by_external_id.sql_generated.go (1)
  • DeleteOldIdentityByExternalIDParams (22-26)
go/pkg/db/identity_find.sql_generated.go (2)
  • FindIdentityByExternalIDParams (20-24)
  • FindIdentityByIDParams (57-61)
go/pkg/db/models_generated.go (1)
  • Identity (629-638)
go/pkg/db/identity_find_with_ratelimits.sql_generated.go (2)
  • FindIdentityWithRatelimitsParams (60-64)
  • FindIdentityWithRatelimitsRow (66-76)
go/pkg/db/identity_find.sql_generated.go (1)
go/apps/api/openapi/gen.go (2)
  • Identity (161-173)
  • Meta (279-282)
go/apps/api/routes/v2_identities_delete_identity/200_test.go (4)
go/pkg/testutil/seed/seed.go (3)
  • CreateIdentityRequest (351-356)
  • CreateRatelimitRequest (307-315)
  • New (39-46)
go/pkg/db/identity_find.sql_generated.go (2)
  • FindIdentityByExternalIDParams (20-24)
  • FindIdentityByIDParams (57-61)
go/pkg/testutil/http.go (1)
  • CallRoute (271-305)
go/pkg/array/fill.go (1)
  • Fill (23-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Test Go API Local / Test
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Build / Build
🔇 Additional comments (15)
go/pkg/db/queries/identity_find.sql (1)

1-13: LGTM! Improved query performance.

The split from a single OR-based query to two separate queries (by ID and by external ID) should improve performance by avoiding full table scans and enabling better index usage.

go/pkg/db/identity_delete_old_by_external_id.sql_generated.go (1)

1-40: LGTM! Addresses the race condition.

This generated method correctly implements the cleanup of old soft-deleted identities by external ID while excluding the current identity. The LEFT JOIN ensures associated ratelimits are also removed, preventing orphaned data.

go/apps/api/routes/v2_keys_update_key/handler.go (1)

154-158: LGTM! Correct use of the new identity lookup.

The change from FindIdentity to FindIdentityByExternalID is appropriate here since we're looking up by the external ID from the request. The error handling and fallback logic remain intact.

go/apps/api/routes/v2_keys_create_key/200_test.go (1)

283-287: LGTM! Correctly verifies identity deduplication.

The change to FindIdentityByExternalID correctly verifies that only one identity was created despite concurrent key creation requests with the same external ID. The explicit Deleted: false parameter enhances readability.

go/pkg/db/querier_generated.go (1)

93-102: LGTM! Generated interface correctly reflects the new queries.

The generated interface methods properly expose:

  • DeleteOldIdentityByExternalID for cleanup of old soft-deleted identities
  • FindIdentityByExternalID and FindIdentityByID for explicit lookup paths
  • FindIdentityWithRatelimits using UNION ALL for flexible lookups with rate limit data

Also applies to: 268-330

go/pkg/db/queries/identity_delete_old_by_external_id.sql (1)

1-8: LGTM! Correctly handles cleanup of old identities.

This query properly cleans up old soft-deleted identities by external ID while:

  1. Excluding the current identity (i.id != current_identity_id)
  2. Only targeting soft-deleted records (i.deleted = true)
  3. Removing associated ratelimits via LEFT JOIN

This addresses the parallel deletion race condition mentioned in the PR objectives.

go/apps/api/routes/v2_identities_list_identities/200_test.go (1)

358-358: LGTM — test aligned with FindIdentityByExternalID params.

Correct workspace scoping and deleted=false filter.

go/apps/api/routes/v2_keys_create_key/handler.go (1)

213-216: LGTM — externalId-based lookup is correct and retry-suitable.

Params and placement inside the transaction look good. Retry handling for duplicate/deadlock is appropriate.

go/apps/api/routes/v2_identities_update_identity/handler.go (3)

115-121: Good switch to a single fast read path + clean not-found handling

UNION-based reader avoids the OR full-scan and simplifies the flow. Looks good.

Also applies to: 128-136


396-406: Response construction via in-tx assembled ratelimits

Avoids extra SELECT. Clean and efficient.


262-275: Upsert semantics and created_at handling are correctInsertIdentityRatelimit uses ON DUPLICATE KEY UPDATE and does not overwrite created_at on updates.

go/apps/api/routes/v2_identities_delete_identity/handler.go (2)

74-79: Lookup + not-found handling LGTM

Fast path query and explicit not-found handling are clear.

Also applies to: 86-94


101-121: Robust idempotency for concurrent deletes

Duplicate-key handling + old-deleted cleanup + retry covers the race. Good work.

Please confirm DeleteOldIdentityByExternalID only targets already-deleted rows and cascades related rows (if any).

Also applies to: 122-149

go/apps/api/routes/v2_identities_create_identity/200_test.go (1)

52-56: Tests correctly migrated to FindIdentityByID/ByExternalID

Lookup helpers and param shapes updated cleanly; assertions remain equivalent.

Also applies to: 75-79, 111-115, 133-137, 174-178, 233-237, 309-313, 350-354, 380-384, 420-424

go/pkg/db/identity_find_with_ratelimits.sql_generated.go (1)

13-31: JSON_OBJECT already emits real JSON booleans for rl.auto_apply = 1, so Go’s bool unmarshal will succeed; no SQL change needed

Likely an incorrect or invalid review comment.

Copy link
Contributor

@ogzhanolguncu ogzhanolguncu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than the file name and organization, everything works well. I tested it manually and ran the unit tests.

@github-actions
Copy link
Contributor

github-actions bot commented Oct 17, 2025

Thank you for following the naming conventions for pull request titles! 🙏

@Flo4604 Flo4604 requested a review from ogzhanolguncu October 17, 2025 13:47
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
go/pkg/db/queries/identity_find_by_id.sql (1)

2-6: *Use explicit column list instead of SELECT .

Reduces fragility when schema changes and keeps generated code stable.

-SELECT *
+SELECT id, external_id, workspace_id, environment, meta, deleted, created_at, updated_at
 FROM identities
 WHERE workspace_id = sqlc.arg(workspace_id)
   AND id = sqlc.arg(identity_id)
   AND deleted = sqlc.arg(deleted);

Indexing note: The query efficiently uses the PRIMARY KEY on id. The existing UNIQUE constraint (workspace_id, external_id, deleted) covers the external_id lookup pattern used elsewhere in the codebase. The current index strategy is adequate for this find-by-id query.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 89711e4 and fb3f042.

📒 Files selected for processing (4)
  • go/pkg/db/identity_find_by_external_id.sql_generated.go (1 hunks)
  • go/pkg/db/identity_find_by_id.sql_generated.go (1 hunks)
  • go/pkg/db/queries/identity_find_by_external_id.sql (1 hunks)
  • go/pkg/db/queries/identity_find_by_id.sql (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
go/pkg/db/identity_find_by_external_id.sql_generated.go (1)
go/pkg/db/models_generated.go (2)
  • Identity (629-638)
  • Environment (618-627)
go/pkg/db/identity_find_by_id.sql_generated.go (1)
go/pkg/db/models_generated.go (1)
  • Identity (629-638)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test Go API Local / Test
  • GitHub Check: Build / Build
🔇 Additional comments (3)
go/pkg/db/identity_find_by_external_id.sql_generated.go (1)

12-47: LGTM (generated).

Column order matches scan targets; method signature fits existing DBTX patterns. No manual changes.

Ensure upstream callers pass contexts with timeouts/deadlines for DB calls on request paths.

go/pkg/db/identity_find_by_id.sql_generated.go (1)

12-46: LGTM (generated).

Param mapping and scan order are correct; aligns with identity_find_by_id.sql.

go/pkg/db/queries/identity_find_by_external_id.sql (1)

2-6: Correct constraint design; keep SELECT * recommendation; collation relies on database default.

The unique constraint (workspace_id, external_id, deleted) is correctly designed for soft-delete patterns—it allows multiple rows per (workspace_id, external_id) pair with different deleted states. Since the query filters deleted = arg(deleted), single-row semantics are guaranteed. No changes needed to the constraint.

The external_id column uses default MySQL collation (case-insensitive typically); no explicit collation is set. If case-sensitive matching is required by the API, this should be verified with stakeholders, but no current misconfiguration is evident.

The SELECT * recommendation remains valid to avoid schema surprises:

-SELECT *
+SELECT id, external_id, workspace_id, environment, meta, deleted, created_at, updated_at
 FROM identities
 WHERE workspace_id = sqlc.arg(workspace_id)
   AND external_id = sqlc.arg(external_id)
   AND deleted = sqlc.arg(deleted);

@graphite-app
Copy link

graphite-app bot commented Oct 22, 2025

Graphite Automations

"Notify author when CI fails" took an action on this PR • (10/22/25)

1 teammate was notified to this PR based on Andreas Thomas's automation.

"Post a GIF when PR approved" took an action on this PR • (10/23/25)

1 gif was posted to this PR based on Andreas Thomas's automation.

Copy link
Contributor

@imeyer imeyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a nit and a random q, but LGTM otherwise!

@graphite-app
Copy link

graphite-app bot commented Oct 23, 2025

Celebrity gif. A young Keanu Reeves stands in the rain smiling. He raises up his arm and gives an enthusiastic thumbs up. (Added via Giphy)

@Flo4604 Flo4604 added this pull request to the merge queue Oct 23, 2025
Merged via the queue into main with commit 6280cc3 Oct 23, 2025
18 checks passed
@Flo4604 Flo4604 deleted the eng-2120-fix-timing-issue-when-deleting-same-identity-in-parallel branch October 23, 2025 15:06
@ogzhanolguncu ogzhanolguncu mentioned this pull request Oct 23, 2025
18 tasks
@coderabbitai coderabbitai bot mentioned this pull request Nov 10, 2025
19 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix timing issue when deleting same identity in parallel

3 participants