-
Notifications
You must be signed in to change notification settings - Fork 2
Hamza/feature/retry budget alerts #252
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
1fdac04
Alert when a retry group's shared budget runs out
hamzahalq 0b9295d
List the failures behind a retry group's spent budget
hamzahalq 651a69d
Fix budget alert lost after a failed send, and review findings
hamzahalq f78ea1d
Require TLS before SMTP auth, and clean up removed groups atomically
hamzahalq 1e1d3a9
Keep alert handler secrets out of responses, and reject a password wi…
hamzahalq File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using SW.Bitween.Model; | ||
|
|
||
| namespace SW.Bitween.Domain; | ||
|
|
||
| /// <summary> | ||
| /// The most specific level of the retry-alert hierarchy: where one subscription's failures in one | ||
| /// retry group should be alerted, overriding whatever the group or the policy says. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Deliberately its own table rather than columns on <see cref="RetryGroupUsage"/>. Usage rows are | ||
| /// deleted by <c>RetryPolicies/resetusage</c>, so config stored there would be silently discarded | ||
| /// every time someone cleared a spent budget. | ||
| /// </remarks> | ||
| public class RetryAlertOverride | ||
| { | ||
| /// <summary>The subscription this override applies to.</summary> | ||
| public int SubscriptionId { get; set; } | ||
|
|
||
| /// <summary><c>RetryGroup.Id</c>, which survives policy edits, so the override does too.</summary> | ||
| public Guid GroupId { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Whether this level sends, stays silent, or defers upward. A row whose mode is | ||
| /// <see cref="RetryAlertMode.Inherit"/> is equivalent to having no row at all. | ||
| /// </summary> | ||
| public RetryAlertMode AlertMode { get; set; } | ||
|
|
||
| /// <summary>Adapter that delivers the alert. Required when <see cref="AlertMode"/> is Send.</summary> | ||
| public string AlertHandlerId { get; set; } | ||
|
|
||
| /// <summary>That adapter's own settings — api key, recipients, subject.</summary> | ||
| public IReadOnlyDictionary<string, string> AlertHandlerProperties { get; set; } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| using System; | ||
| using SW.PrimitiveTypes; | ||
|
|
||
| namespace SW.Bitween.Domain; | ||
|
|
||
| /// <summary> | ||
| /// Raised the one time a retry group's <c>MaxAttemptsTotal</c> runs out for a subscription, so the | ||
| /// configured alert handler can be told that failures matching that group have stopped being retried. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// Deliberately not an <c>IHasWorkGroup</c> event: it publishes under its own type name and is picked | ||
| /// up by a dedicated <c>IConsume<RetryBudgetExhaustedEvent></c> consumer with its own queue. A | ||
| /// slow or broken alert handler therefore cannot delay or fail the ordinary notifier path, which | ||
| /// shares the work group's result queue. | ||
| /// </para> | ||
| /// <para> | ||
| /// Carried on <see cref="XchangeResult"/> rather than published directly, so it only reaches the bus | ||
| /// once the failure it describes has actually been committed. | ||
| /// </para> | ||
| /// </remarks> | ||
| public class RetryBudgetExhaustedEvent : BaseDomainEvent | ||
| { | ||
| /// <summary>The failure that found the budget empty.</summary> | ||
| public string XchangeId { get; set; } | ||
|
|
||
| public int SubscriptionId { get; set; } | ||
|
|
||
| public Guid GroupId { get; set; } | ||
|
|
||
| /// <summary>The group's name as it was when the budget ran out, in case it is later renamed.</summary> | ||
| public string GroupName { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// The ceiling that was reached. Not paired with an "used" count, because at exhaustion the two | ||
| /// are the same number — except when the ceiling was lowered below what had already been spent, | ||
| /// where the ceiling is still the meaningful figure. | ||
| /// </summary> | ||
| public int MaxAttemptsTotal { get; set; } | ||
|
|
||
| public DateTime OccurredOn { get; set; } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| using System.Linq; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.EntityFrameworkCore; | ||
| using SW.Bitween.Domain; | ||
| using SW.Bitween.Domain.Accounts; | ||
| using SW.Bitween.Model; | ||
| using SW.PrimitiveTypes; | ||
|
|
||
| namespace SW.Bitween.Resources.RetryPolicies; | ||
|
|
||
| /// <summary> | ||
| /// Lists the failures one group caught for one subscription — what a row of <see cref="Usage"/> | ||
| /// spent its budget on. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// Separate from <see cref="Usage"/> and asked for one pair at a time, because a policy with fifty | ||
| /// subscriptions would otherwise pay for fifty of these joins to answer a question about one row. | ||
| /// </para> | ||
| /// <para> | ||
| /// Only failures carrying a group id appear, so nothing recorded before the group was stamped onto | ||
| /// results is listed. A pair whose counter is well spent can therefore come back empty, which is | ||
| /// why <see cref="RetryGroupAttempts.Total"/> is the count of what is listable rather than the | ||
| /// counter's own value. | ||
| /// </para> | ||
| /// </remarks> | ||
| [HandlerName("attempts")] | ||
| public class Attempts : ICommandHandler<int, RetryGroupAttemptsRequest, object> | ||
| { | ||
| /// <summary> | ||
| /// Enough to show what keeps failing without turning one table row into a page. The caller is | ||
| /// told the total, so a short list never reads as the whole story. | ||
| /// </summary> | ||
| private const int Limit = 10; | ||
|
|
||
| private readonly BitweenDbContext _dbContext; | ||
| private readonly RequestContext _requestContext; | ||
|
|
||
| public Attempts(BitweenDbContext dbContext, RequestContext requestContext) | ||
| { | ||
| _dbContext = dbContext; | ||
| _requestContext = requestContext; | ||
| } | ||
|
|
||
| public async Task<object> Handle(int key, RetryGroupAttemptsRequest request) | ||
| { | ||
| _requestContext.EnsureAccess(AccountRole.Admin, AccountRole.Member); | ||
|
|
||
| // Both halves of the pair have to belong to the policy in the route. For the subscription | ||
| // that keeps this from becoming a way to read any subscription's failures through any | ||
| // policy id; for the group it is about the answer being readable — an unknown group would | ||
| // otherwise report zero failures, which is indistinguishable from a group that genuinely | ||
| // has none. | ||
| var policy = await _dbContext.Set<RetryPolicy>().AsNoTracking() | ||
| .FirstOrDefaultAsync(p => p.Id == key); | ||
| if (policy == null) throw new SWNotFoundException(key.ToString()); | ||
|
|
||
| if (policy.Groups.All(g => g.Id != request.GroupId)) | ||
| throw new SWNotFoundException($"{key}/{request.GroupId}"); | ||
|
|
||
| var belongs = await _dbContext.Set<Subscription>().AsNoTracking() | ||
| .AnyAsync(s => s.Id == request.SubscriptionId && s.RetryPolicyId == key); | ||
| if (!belongs) throw new SWNotFoundException($"{key}/{request.SubscriptionId}"); | ||
|
|
||
| var query = from result in _dbContext.Set<XchangeResult>() | ||
| join xchange in _dbContext.Set<Xchange>() on result.Id equals xchange.Id | ||
| join pending in _dbContext.Set<DelayedRetry>() on result.Id equals pending.Id into scheduled | ||
| from pending in scheduled.DefaultIfEmpty() | ||
| where xchange.SubscriptionId == request.SubscriptionId | ||
| && result.RetryGroupId == request.GroupId | ||
| select new RetryGroupAttemptRow | ||
| { | ||
| XchangeId = result.Id, | ||
| AttemptNumber = result.AttemptNumber, | ||
| FailedOn = result.FinishedOn, | ||
| Exception = result.Exception, | ||
| // A row survives here only until its retry runs, which is what separates a failure | ||
| // still being worked on from one that has been given up. | ||
| RetryPending = pending != null, | ||
| RetryBlockedReason = result.RetryBlockedReason | ||
| }; | ||
|
|
||
| query = query.AsNoTracking(); | ||
|
|
||
| return new RetryGroupAttempts | ||
| { | ||
| Total = await query.CountAsync(), | ||
| // Pending first, so the ones still moving cannot be pushed out of the list by a long | ||
| // history of failures that are already over. | ||
| Attempts = await query | ||
| .OrderByDescending(r => r.RetryPending) | ||
| .ThenByDescending(r => r.FailedOn) | ||
| .Take(Limit) | ||
| .ToListAsync() | ||
| }; | ||
| } | ||
| } | ||
|
hamzahalq marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Make retry-budget alert delivery retryable and atomic.
ForRetryBudgetAlertrecords both failed and successful sends.RetryAlertService.Processtreats every row with a nullNotifierIdas already sent. A transient handler failure therefore suppresses all later delivery attempts.The read-then-send-then-insert flow also allows concurrent redeliveries to send duplicates. Use an atomic, uniquely constrained delivery claim with an explicit delivery state. Mark it complete only after a successful send. Release or retry failed claims.
🤖 Prompt for AI Agents