Skip to content

[AAP-66801] Fix race condition in OAuth2AccessToken.is_valid() causing HTTP 500#907

Open
DenisKoleda wants to merge 3 commits intoansible:develfrom
DenisKoleda:fix/oauth2-token-race-condition
Open

[AAP-66801] Fix race condition in OAuth2AccessToken.is_valid() causing HTTP 500#907
DenisKoleda wants to merge 3 commits intoansible:develfrom
DenisKoleda:fix/oauth2-token-race-condition

Conversation

@DenisKoleda
Copy link
Copy Markdown

@DenisKoleda DenisKoleda commented Dec 16, 2025

Summary

  • Fix race condition when concurrent requests use the same OAuth2 token
  • Replace save(update_fields=['last_used']) with QuerySet.update() to prevent DatabaseError

Problem

When multiple parallel requests authenticate with the same OAuth2 token, the Gateway crashes with:

django.db.utils.DatabaseError: Save with update_fields did not affect any rows.

Root cause: In is_valid() method, there's a race condition between exists() check and save() call. When concurrent requests try to update last_used timestamp on the same token:

  1. Request A checks exists() → True
  2. Request B checks exists() → True
  3. Request B calls save() → Success, row version changes
  4. Request A calls save()Fails because Django's save(update_fields=...) expects to affect exactly 1 row, but the row was already modified

Solution

Replace:

if OAuth2AccessToken.objects.filter(pk=self.pk).exists():
    self.save(update_fields=['last_used'])

With:

OAuth2AccessToken.objects.filter(pk=self.pk).update(last_used=self.last_used)

QuerySet.update() is atomic and simply returns 0 if no rows match, without raising an exception. This is the expected behavior for updating last_used — if the token was deleted between validation and update, we don't need to error out.

Test plan

  • Verify existing OAuth2 authentication tests pass
  • Manual test with concurrent requests using the same token
  • Confirm no HTTP 500 errors under parallel load

Summary by CodeRabbit

  • Bug Fixes
    • Improved token tracking reliability by refactoring how access token usage timestamps are updated, reducing potential race conditions during concurrent requests.

@AlanCoding
Copy link
Copy Markdown
Member

Soft approval. This is the type of solution I would expect for this type of problem.

Still want someone to look at it who was more involved with the oauth2 app development to weigh in. The existing save appears to me to be avoid any other model logic, and if assuming that's the case, this should be good.

@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Member

@john-westcott-iv john-westcott-iv left a comment

Choose a reason for hiding this comment

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

Clean fix for a real race condition — QuerySet.update() is the right tool here. Left one suggestion to add a comment explaining why update() is used instead of save(), so future developers don't inadvertently revert this.

Comment thread ansible_base/oauth2_provider/models/access_token.py
Copy link
Copy Markdown
Contributor

@BrennanPaciorek BrennanPaciorek left a comment

Choose a reason for hiding this comment

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

This should not cause any issues I do not think.

@BrennanPaciorek BrennanPaciorek self-requested a review February 26, 2026 16:33
Copy link
Copy Markdown
Contributor

@BrennanPaciorek BrennanPaciorek left a comment

Choose a reason for hiding this comment

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

We'll want to check how activitystream handles updates to last used in this case. I think it'll resolve cleanly, but I'm uncertain.

@john-westcott-iv john-westcott-iv changed the title Fix race condition in OAuth2AccessToken.is_valid() causing HTTP 500 [AAP-66801] Fix race condition in OAuth2AccessToken.is_valid() causing HTTP 500 Feb 26, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 26, 2026

Warning

Rate limit exceeded

@DenisKoleda has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 34 minutes and 8 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 34 minutes and 8 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ee60a51b-3c8f-42a4-8c89-32b267781e8c

📥 Commits

Reviewing files that changed from the base of the PR and between ce2a95d and f4ed60d.

📒 Files selected for processing (1)
  • ansible_base/oauth2_provider/models/access_token.py
📝 Walkthrough

Walkthrough

Modified the OAuth2AccessToken.is_valid method to replace a direct save() call with a deferred queryset update() executed via connection.on_commit(). Added explanatory comments on race-condition handling and the safety of using update() for the last_used field.

Changes

Cohort / File(s) Summary
OAuth2AccessToken Model Update
ansible_base/oauth2_provider/models/access_token.py
Refactored is_valid() method to use queryset update() instead of save(update_fields=...), with deferred execution via connection.on_commit() to address race conditions. Added inline comments explaining transaction safety.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: fixing a race condition in OAuth2AccessToken.is_valid() that causes HTTP 500 errors. It is specific, concise, and directly related to the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@john-westcott-iv
Copy link
Copy Markdown
Member

Thanks for this contribution and sorry for this PR getting lost in the shuffle. This should be scheduled to be worked on starting next Thursday and should merge shortly after that.

DenisKoleda and others added 2 commits February 26, 2026 13:39
Replace save(update_fields=['last_used']) with QuerySet.update() to avoid
DatabaseError when concurrent requests try to update the same token's
last_used timestamp.

The previous implementation could fail with:
django.db.utils.DatabaseError: Save with update_fields did not affect any rows.

This happened because between exists() check and save() call, another
process could modify the row, causing save() to affect 0 rows and raise
an exception. QuerySet.update() handles this gracefully by simply
returning 0 affected rows without raising an error.
@john-westcott-iv john-westcott-iv force-pushed the fix/oauth2-token-race-condition branch from 3e62180 to ce2a95d Compare February 26, 2026 18:39
@sonarqubecloud
Copy link
Copy Markdown

@DenisKoleda DenisKoleda force-pushed the fix/oauth2-token-race-condition branch from 5ad6734 to f4ed60d Compare April 17, 2026 06:23
@github-actions
Copy link
Copy Markdown

DVCS PR Check Results:

PR appears valid (JIRA key(s) found)

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.

4 participants