Skip to content

Enforce MaxAttemptsTotal across messages instead of per message - #242

Merged
hamzahalq merged 2 commits into
releases/r8.0from
hamza/fix/retry-total-attempts-cap
Aug 11, 2026
Merged

Enforce MaxAttemptsTotal across messages instead of per message#242
hamzahalq merged 2 commits into
releases/r8.0from
hamza/fix/retry-total-attempts-cap

Conversation

@hamzahalq

Copy link
Copy Markdown
Contributor

The group total was tracked in a dictionary carried on each xchange, so every failing message started from zero and got its own full budget: 4 messages under a total of 10 produced 12 retries. It also always equalled the per-message attempt count, so the cap could never fire above MaxAttemptsPerError.

The total now lives in a RetryGroupUsage table keyed by integration + group, and the evaluator claims from it via IRetryGroupBudget. Dry-runs use an in-memory implementation so simulating never spends a real budget. GroupAttemptCounts is dropped from Xchange and DelayedRetry.

The total never resets on its own, so /usage reports what each integration has spent and /resetusage clears it. XchangeResult now records why a retry was refused, since a group with an exhausted budget was previously indistinguishable from one that never matched.

The group total was tracked in a dictionary carried on each xchange, so
every failing message started from zero and got its own full budget:
4 messages under a total of 10 produced 12 retries. It also always equalled
the per-message attempt count, so the cap could never fire above
MaxAttemptsPerError.
The total now lives in a RetryGroupUsage table keyed by integration + group,
and the evaluator claims from it via IRetryGroupBudget. Dry-runs use an
in-memory implementation so simulating never spends a real budget.
GroupAttemptCounts is dropped from Xchange and DelayedRetry.
The total never resets on its own, so /usage reports what each integration
has spent and /resetusage clears it. XchangeResult now records why a retry
was refused, since a group with an exhausted budget was previously
indistinguishable from one that never matched.
@coderabbitai

coderabbitaiBot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

What changed

  • Enforced MaxAttemptsTotal across all messages in an integration and retry group.
  • Added persistent RetryGroupUsage tracking keyed by SubscriptionId and GroupId.
  • Added database-backed and in-memory IRetryGroupBudget implementations.
  • Removed GroupAttemptCounts from retry entities and evaluator state.
  • Added /usage reporting and /resetusage reset operations.
  • Added RetryBlockedReason to retry results and search responses.
  • Preserved the original processing exception and XchangeResult when retry evaluation fails.
  • Added SQL Server, MySQL, and PostgreSQL migrations.

Risk

risk:medium

Retry scheduling now depends on persistent shared state. Concurrent claims use conditional updates, but database-provider behavior and transaction isolation require validation. Existing callers must update for changed public APIs.

Security-sensitive areas

  • /usage and /resetusage require Admin or Member permissions.
  • Reset operations must remain scoped to the selected policy, subscription, and group.
  • Usage reporting exposes retry consumption and integration identifiers.

Test coverage impact

  • Added integration tests for shared limits, subscription isolation, concurrent claims, usage reporting, reset behavior, policy isolation, and group removal cleanup.
  • Updated evaluator tests for asynchronous budget evaluation and per-message cap behavior.
  • Removed tests for the deleted GroupAttemptCounts state model.

Deployment and operational concerns

  • Apply the provider-specific migrations before enabling the new retry behavior.
  • The migration removes stored GroupAttemptCounts data and creates persistent usage records.
  • Rollback drops RetryGroupUsages, which loses accumulated usage.
  • Usage persists until an authorized reset.
  • Verify migration behavior, concurrent claims, permission enforcement, and usage cleanup in each supported database provider.

Walkthrough

The change replaces per-record retry-count dictionaries with persistent, subscription-scoped group usage. Retry evaluation is asynchronous and budget-backed. New usage and reset handlers expose counters. Retry-block reasons are stored and returned in exchange searches. Database migrations support SQL Server, MySQL, and PostgreSQL.

Changes

Shared retry budget

Layer / File(s)Summary
Budget contracts and evaluation
SW.Bitween.Sdk/Model/AutoRetry/*, SW.Bitween.Sdk/Model/RetryPolicyModel.cs, SW.Bitween.UnitTests/RetryPolicyEvaluatorTests.cs
Retry evaluation uses asynchronous IRetryGroupBudget claims. Tests cover shared budgets, independent budgets, per-message caps, delay behavior, and block actions.
Persistent usage model and schema
SW.Bitween.Api/Domain/*, SW.Bitween.Api/Data/BitweenDbContext.cs, SW.Bitween.*Sql/Migrations/*
RetryGroupUsage replaces serialized attempt counts. Provider mappings and migrations add composite-key usage tables and nullable 500-character retry-block columns.
Retry budget orchestration
SW.Bitween.Api/Services/*, SW.Bitween.IntegrationTests/Tests/RetryJobTests.cs
RetryGroupBudget persists claims. XchangeService creates results before retry evaluation, prevents duplicate scheduling, removes count propagation, and records blocked reasons.
Usage reporting and reset controls
SW.Bitween.Api/Resources/RetryPolicies/*, SW.Bitween.Api/Resources/Xchanges/Search.cs, SW.Bitween.Sdk/Model/Xchange.cs
Handlers report policy-scoped usage and reset selected counters. Policy updates and deletion clean obsolete usage rows. Search projections include RetryBlockedReason.
End-to-end budget validation
SW.Bitween.IntegrationTests/Tests/RetryPolicyTests.cs
Integration tests verify shared totals across messages, subscription isolation, concurrency limits, usage reporting, reset behavior, policy isolation, and group cleanup.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested labels:security, database, testing, risk:high

Suggested reviewers:mmalkhatib

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 50.75% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly states that MaxAttemptsTotal is enforced across messages, matching the primary change.
Description check✅ PassedThe description accurately explains persistent group budgets, in-memory dry runs, usage controls, and retry-block reasons.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

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.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@SW.Bitween.Api/Resources/RetryPolicies/ResetUsage.cs`:
- Around line 42-45: Persist the owning policy identifier on each
RetryGroupUsage row and update both handlers to query by that durable policy
scope instead of the current policy.Groups collection. In
SW.Bitween.Api/Resources/RetryPolicies/ResetUsage.cs lines 42-45, use the
persisted policy ownership so reset removes usage for deleted groups; in
SW.Bitween.Api/Resources/RetryPolicies/Usage.cs lines 42-50, report those
historical rows and return a null GroupName when the group no longer exists.
In `@SW.Bitween.Api/Resources/RetryPolicies/Usage.cs`:
- Around line 18-25: Update Usage.Handle to inject RequestContext and call
EnsureAccess with the policy key and permitted AccountRole.Admin and
AccountRole.Member roles before loading or returning retry-policy usage.
Preserve the existing usage response for authorized callers.
In `@SW.Bitween.Api/Services/RetryGroupBudget.cs`:
- Around line 23-45: Make RetryGroupBudget.TryConsume atomic by replacing the
read-modify-write logic with a conditional database increment that succeeds only
when AttemptsUsed is below maxAttemptsTotal, and handle absent rows through a
first-row upsert or duplicate-key retry. Keep the budget mutation and related
DelayedRetry insertion within the same explicit transaction, preserving the
false result when the shared budget is exhausted.
In `@SW.Bitween.Api/Services/XchangeService.cs`:
- Around line 428-435: Update the exception path around TryScheduleAutoRetry in
the XchangeService catch block so retry-scheduling or retry-related persistence
failures cannot replace the original failure or prevent XchangeResult from being
saved. Isolate scheduling and ensure the failure record is persisted through a
separate context or fallback save, while preserving the original exception
details in XchangeResult.
In `@SW.Bitween.IntegrationTests/Tests/RetryPolicyTests.cs`:
- Around line 369-377: The retry budget test currently serializes claims through
one DbContext and cannot expose races in RetryGroupBudget.TryConsume. Implement
atomic claim/concurrency control in RetryGroupBudget, then add a concurrent test
using separate service scopes and DbContext instances that performs 10 claims
and asserts exactly 10 succeed, preserving MaxAttemptsTotal enforcement.
In `@SW.Bitween.MsSql/Migrations/20260811081235_SharedRetryGroupTotals.cs`:
- Around line 14-34: Update the migration around RetryGroupUsages creation to
preserve existing GroupAttemptCounts: create the new table first, then backfill
one row per (SubscriptionId, GroupId) by aggregating active retry-chain counts
from Xchanges and DelayedRetries, including attempts already consumed by each
shared group. Only drop the source GroupAttemptCounts columns after the backfill
completes, and retain the composite primary key and required non-null fields.
In `@SW.Bitween.MySql/Migrations/20260811081221_SharedRetryGroupTotals.cs`:
- Around line 14-20: Update
SW.Bitween.MySql/Migrations/20260811081221_SharedRetryGroupTotals.cs lines 14-20
to create and backfill RetryGroupUsages from the authoritative
GroupAttemptCounts data for each subscription and group before dropping either
column, with explicit deduplication when Xchanges and DelayedRetries represent
the same retry state. Apply the equivalent create-and-backfill sequence in
SW.Bitween.PgSql/Migrations/20260811081200_SharedRetryGroupTotals.cs lines 15-23
for retry_group_usage before dropping group_attempt_counts.
In `@SW.Bitween.PgSql/Migrations/20260811092318_RetryBlockedReason.Designer.cs`:
- Around line 1925-1932: Remove the seeded default API credential object from
the HasData call in the generated model, including the deterministic Key value
and related default record. Provision the bootstrap credential through a secure
mechanism outside source control and rotate the exposed credential.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: simplify9/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7f2320c9-d1ed-4b0b-b65e-196f5acf5d6e

📥 Commits

Reviewing files that changed from the base of the PR and between ae5bf12 and e04a65e.

📒 Files selected for processing (34)
  • SW.Bitween.Api/Data/BitweenDbContext.cs
  • SW.Bitween.Api/Domain/DelayedRetry.cs
  • SW.Bitween.Api/Domain/RetryGroupUsage.cs
  • SW.Bitween.Api/Domain/Xchange/Xchange.cs
  • SW.Bitween.Api/Domain/XchangeResult/XchangeResult.cs
  • SW.Bitween.Api/Resources/RetryPolicies/ResetUsage.cs
  • SW.Bitween.Api/Resources/RetryPolicies/Test.cs
  • SW.Bitween.Api/Resources/RetryPolicies/Usage.cs
  • SW.Bitween.Api/Resources/Xchanges/Search.cs
  • SW.Bitween.Api/Services/RetryGroupBudget.cs
  • SW.Bitween.Api/Services/XchangeService.cs
  • SW.Bitween.IntegrationTests/Tests/RetryJobTests.cs
  • SW.Bitween.IntegrationTests/Tests/RetryPolicyTests.cs
  • SW.Bitween.MsSql/Migrations/20260811081235_SharedRetryGroupTotals.Designer.cs
  • SW.Bitween.MsSql/Migrations/20260811081235_SharedRetryGroupTotals.cs
  • SW.Bitween.MsSql/Migrations/20260811092327_RetryBlockedReason.Designer.cs
  • SW.Bitween.MsSql/Migrations/20260811092327_RetryBlockedReason.cs
  • SW.Bitween.MsSql/Migrations/BitweenDbContextModelSnapshot.cs
  • SW.Bitween.MySql/Migrations/20260811081221_SharedRetryGroupTotals.Designer.cs
  • SW.Bitween.MySql/Migrations/20260811081221_SharedRetryGroupTotals.cs
  • SW.Bitween.MySql/Migrations/20260811092323_RetryBlockedReason.Designer.cs
  • SW.Bitween.MySql/Migrations/20260811092323_RetryBlockedReason.cs
  • SW.Bitween.MySql/Migrations/BitweenDbContextModelSnapshot.cs
  • SW.Bitween.PgSql/BitweenDbContext.cs
  • SW.Bitween.PgSql/Migrations/20260811081200_SharedRetryGroupTotals.Designer.cs
  • SW.Bitween.PgSql/Migrations/20260811081200_SharedRetryGroupTotals.cs
  • SW.Bitween.PgSql/Migrations/20260811092318_RetryBlockedReason.Designer.cs
  • SW.Bitween.PgSql/Migrations/20260811092318_RetryBlockedReason.cs
  • SW.Bitween.PgSql/Migrations/BitweenDbContextModelSnapshot.cs
  • SW.Bitween.Sdk/Model/AutoRetry/IRetryGroupBudget.cs
  • SW.Bitween.Sdk/Model/AutoRetry/RetryPolicyEvaluator.cs
  • SW.Bitween.Sdk/Model/RetryPolicyModel.cs
  • SW.Bitween.Sdk/Model/Xchange.cs
  • SW.Bitween.UnitTests/RetryPolicyEvaluatorTests.cs
💤 Files with no reviewable changes (1)
  • SW.Bitween.Api/Domain/DelayedRetry.cs
📜 Review details
🧰 Additional context used
🪛 Betterleaks (1.7.3)
SW.Bitween.MsSql/Migrations/20260811092327_RetryBlockedReason.Designer.cs

[high] 1679-1679: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

SW.Bitween.MySql/Migrations/20260811081221_SharedRetryGroupTotals.Designer.cs

[high] 1672-1672: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

SW.Bitween.PgSql/Migrations/20260811081200_SharedRetryGroupTotals.Designer.cs

[high] 1925-1925: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

SW.Bitween.MsSql/Migrations/20260811081235_SharedRetryGroupTotals.Designer.cs

[high] 1675-1675: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

SW.Bitween.MySql/Migrations/20260811092323_RetryBlockedReason.Designer.cs

[high] 1676-1676: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

SW.Bitween.PgSql/Migrations/20260811092318_RetryBlockedReason.Designer.cs

[high] 1930-1930: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🔇 Additional comments (14)
SW.Bitween.IntegrationTests/Tests/RetryPolicyTests.cs (2)

2-2: LGTM!


386-429: LGTM!

Also applies to: 431-477, 479-508

SW.Bitween.MySql/Migrations/20260811092323_RetryBlockedReason.cs (1)

11-28: LGTM!

SW.Bitween.PgSql/Migrations/20260811092318_RetryBlockedReason.cs (1)

11-29: LGTM!

SW.Bitween.Sdk/Model/RetryPolicyModel.cs (1)

1-1: LGTM!

Also applies to: 21-56

SW.Bitween.Api/Resources/Xchanges/Search.cs (1)

76-77: LGTM!

SW.Bitween.Sdk/Model/Xchange.cs (1)

100-102: LGTM!

SW.Bitween.Api/Resources/RetryPolicies/Test.cs (1)

24-24: LGTM!

Also applies to: 33-40, 57-57

SW.Bitween.Sdk/Model/AutoRetry/IRetryGroupBudget.cs (1)

17-44: LGTM!

SW.Bitween.Sdk/Model/AutoRetry/RetryPolicyEvaluator.cs (1)

13-23: LGTM!

Also applies to: 43-43, 65-69

SW.Bitween.UnitTests/RetryPolicyEvaluatorTests.cs (1)

61-64: LGTM!

Also applies to: 227-344, 378-440, 445-459

SW.Bitween.Api/Services/XchangeService.cs (2)

88-101: LGTM!

Also applies to: 150-150


438-472: LGTM!

SW.Bitween.IntegrationTests/Tests/RetryJobTests.cs (1)

125-128: LGTM!

Comment threadSW.Bitween.Api/Resources/RetryPolicies/ResetUsage.cs
Comment threadSW.Bitween.Api/Resources/RetryPolicies/Usage.cs
Comment threadSW.Bitween.Api/Services/RetryGroupBudget.cs
Comment threadSW.Bitween.Api/Services/XchangeService.cs
Comment threadSW.Bitween.IntegrationTests/Tests/RetryPolicyTests.cs
Claim a group slot with one conditional UPDATE instead of read-then-write, so
concurrent failures across instances cannot both take the last slot. The first
row is inserted on its own context, falling back to the increment if that race
is lost.
Guard retry evaluation so a throw there can no longer replace the original
exception and discard the XchangeResult.
Clear a group's usage rows when it is removed from a policy or the policy is
deleted, and require Admin or Member to read usage.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
SW.Bitween.Api/Services/XchangeService.cs (1)

482-492: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make retry evaluation idempotent before claiming the budget.

Two workers can both observe no DelayedRetry at Line 471. Both workers then claim a shared budget slot at Line 482 before one DelayedRetry insert fails on its primary key. This spends multiple slots for one failed Xchange and can make the group stop retrying early.

Use an atomic retry-evaluation reservation keyed by Xchange.Id, or otherwise serialize the existence check, budget claim, and delayed-retry creation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@SW.Bitween.Api/Services/XchangeService.cs` around lines 482 - 492, The retry
flow around CountRetryChainDepth, evaluator.Evaluate, and DelayedRetry creation
must atomically reserve evaluation by xchange.Id before claiming
RetryGroupBudget. Serialize or otherwise make the existence check, budget claim,
and DelayedRetry insertion one idempotent operation, ensuring concurrent workers
cannot consume multiple budget slots for the same Xchange.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@SW.Bitween.Api/Resources/RetryPolicies/Delete.cs`:
- Around line 33-34: Update the delete handler’s flow after
FindAsync<RetryPolicy> to detect a null policy and return the handler’s existing
controlled not-found error before accessing policy.Groups. Preserve the current
group ID collection behavior for policies that are found.
In `@SW.Bitween.Api/Resources/RetryPolicies/Update.cs`:
- Around line 40-43: Coordinate removed-group cleanup in Update.cs (lines 40-43)
with the existing budget-claim invalidation mechanism so stale evaluators cannot
recreate usage after cleanup. Apply the same claim invalidation before deleting
policy usage in Delete.cs (lines 38-41); update the relevant retry-policy
cleanup methods while preserving their existing deletion behavior.
In `@SW.Bitween.Api/Services/RetryGroupBudget.cs`:
- Around line 68-70: Update the DbUpdateException handling in TryIncrement so it
retries only when a concurrent insert has created the expected (subscriptionId,
groupId) row; if no such row exists, rethrow the original DbUpdateException
instead of recording “Group total cap reached.”
---
Outside diff comments:
In `@SW.Bitween.Api/Services/XchangeService.cs`:
- Around line 482-492: The retry flow around CountRetryChainDepth,
evaluator.Evaluate, and DelayedRetry creation must atomically reserve evaluation
by xchange.Id before claiming RetryGroupBudget. Serialize or otherwise make the
existence check, budget claim, and DelayedRetry insertion one idempotent
operation, ensuring concurrent workers cannot consume multiple budget slots for
the same Xchange.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: simplify9/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d2070cad-c581-427e-bdda-1a5c7a80a088

📥 Commits

Reviewing files that changed from the base of the PR and between e04a65e and ca08886.

📒 Files selected for processing (6)
  • SW.Bitween.Api/Resources/RetryPolicies/Delete.cs
  • SW.Bitween.Api/Resources/RetryPolicies/Update.cs
  • SW.Bitween.Api/Resources/RetryPolicies/Usage.cs
  • SW.Bitween.Api/Services/RetryGroupBudget.cs
  • SW.Bitween.Api/Services/XchangeService.cs
  • SW.Bitween.IntegrationTests/Tests/RetryPolicyTests.cs

Comment on lines +33 to +34
var policy = await _dbContext.FindAsync<RetryPolicy>(key);
var groupIds = policy.Groups.Select(g => g.Id).ToList();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle a missing policy before reading Groups.

If key does not identify a policy, FindAsync returns null and Line 34 throws NullReferenceException. Return the handler's controlled not-found error before accessing policy.Groups.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@SW.Bitween.Api/Resources/RetryPolicies/Delete.cs` around lines 33 - 34,
Update the delete handler’s flow after FindAsync<RetryPolicy> to detect a null
policy and return the handler’s existing controlled not-found error before
accessing policy.Groups. Preserve the current group ID collection behavior for
policies that are found.

Comment on lines +40 to +43
if (removedGroupIds.Count > 0)
await _dbContext.Set<RetryGroupUsage>()
.Where(u => removedGroupIds.Contains(u.GroupId))
.ExecuteDeleteAsync();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Prevent claims after policy cleanup.

An evaluator that loaded the old policy can commit a usage claim after either cleanup query completes. The new row is then stranded after a group removal or policy deletion.

  • SW.Bitween.Api/Resources/RetryPolicies/Update.cs#L40-L43: coordinate removed-group cleanup with budget claims so no stale evaluator can recreate usage.
  • SW.Bitween.Api/Resources/RetryPolicies/Delete.cs#L38-L41: use the same claim-invalidation mechanism before deleting policy usage.
📍 Affects 2 files
  • SW.Bitween.Api/Resources/RetryPolicies/Update.cs#L40-L43 (this comment)
  • SW.Bitween.Api/Resources/RetryPolicies/Delete.cs#L38-L41
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@SW.Bitween.Api/Resources/RetryPolicies/Update.cs` around lines 40 - 43,
Coordinate removed-group cleanup in Update.cs (lines 40-43) with the existing
budget-claim invalidation mechanism so stale evaluators cannot recreate usage
after cleanup. Apply the same claim invalidation before deleting policy usage in
Delete.cs (lines 38-41); update the relevant retry-policy cleanup methods while
preserving their existing deletion behavior.

Comment on lines +68 to +70
catch (DbUpdateException)
{
return await TryIncrement(dbContext, groupId, maxAttemptsTotal);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -e
file=$(fd -t f 'RetryGroupBudget\.cs$'.| head -n 1)printf'%s\n'"FILE: $file"
wc -l "$file"
sed -n '1,150p'"$file"printf'\nCALL SITES AND RELATED TYPES:\n'
rg -n -C 4 'TryIncrement|Group total cap reached|RetryGroupBudget|DbUpdateException' --glob '*.cs'.

Repository: simplify9/Bitween-api

Length of output: 25933


🏁 Script executed:

#!/bin/bashset -e
printf'%s\n''RetryGroupUsage declarations and mappings:'
rg -n -C 8 'class RetryGroupUsage|RetryGroupUsage|HasKey|HasForeignKey|RetryGroupUsage' \
--glob '*.cs' SW.Bitween.Api SW.Bitween.Domain SW.Bitween.Model SW.Bitween.Infrastructure SW.Bitween.Migrations 2>/dev/null ||trueprintf'\nRelevant database migrations/schema references:\n'
rg -n -C 5 'RetryGroupUsage|AttemptsUsed|LastAttemptOn' --glob '*.cs' --glob '*.sql'.printf'\nException handling and scheduling path:\n'
sed -n '450,525p' SW.Bitween.Api/Services/XchangeService.cs
sed -n '55,75p' SW.Bitween.Sdk/Model/AutoRetry/RetryPolicyEvaluator.cs

Repository: simplify9/Bitween-api

Length of output: 50378


🏁 Script executed:

#!/bin/bashset -e
printf'%s\n''RetryGroupUsage migration definition:'
sed -n '20,75p' SW.Bitween.MsSql/Migrations/20260811081235_SharedRetryGroupTotals.cs
printf'\nRetry scheduling caller:\n'
sed -n '460,515p' SW.Bitween.Api/Services/XchangeService.cs
printf'\nRetry decision mapping:\n'
sed -n '55,75p' SW.Bitween.Sdk/Model/AutoRetry/RetryPolicyEvaluator.cs
printf'\nDatabase context construction and registration:\n'
rg -n -C 4 'AddDbContext|BitweenDbContext\(' --glob '*.cs' SW.Bitween.Api SW.Bitween.IntegrationTests | head -n 160
printf'\nFocused static behavior probe:\n'
python3 - <<'PY'from pathlib import Pathp = Path("SW.Bitween.Api/Services/RetryGroupBudget.cs")s = p.read_text()catch = s[s.index("catch (DbUpdateException)"):s.index("\n }\n }", s.index("catch (DbUpdateException)")) + len("\n }")]print("catch body:")print(catch)print("fallback returns bool:", "return await TryIncrement(dbContext, groupId, maxAttemptsTotal);" in catch)print("fallback can return false:", " > 0;" in s[s.index("private async Task<bool> TryIncrement"):])e = Path("SW.Bitween.Sdk/Model/AutoRetry/RetryPolicyEvaluator.cs").read_text()print("false maps to cap message:", 'Group total cap reached' in e and "if (!await groupBudget.TryConsume" in e)m = Path("SW.Bitween.MsSql/Migrations/20260811081235_SharedRetryGroupTotals.cs").read_text()section = m[m.index("migrationBuilder.CreateTable"):m.index(");", m.index("migrationBuilder.CreateTable"))+2]print("usage table has composite primary key:", "RetryGroupUsages" in section and "primaryKey:" in section)print("usage table has explicit foreign key:", "foreignKey:" in section)PY

Repository: simplify9/Bitween-api

Length of output: 6894


Propagate non-duplicate insert failures

If the isolated insert fails and no (subscriptionId, groupId) row exists, rethrow the original DbUpdateException. Only retry TryIncrement when a concurrent insert created that row; otherwise the current code records "Group total cap reached" for a database failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@SW.Bitween.Api/Services/RetryGroupBudget.cs` around lines 68 - 70, Update the
DbUpdateException handling in TryIncrement so it retries only when a concurrent
insert has created the expected (subscriptionId, groupId) row; if no such row
exists, rethrow the original DbUpdateException instead of recording “Group total
cap reached.”

@hamzahalq
hamzahalq merged commit 102c35e into releases/r8.0Aug 11, 2026
5 checks passed
Sign up for freeto 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.

2 participants

@hamzahalq@mmalkhatib