-
-
Notifications
You must be signed in to change notification settings - Fork 512
Fixes stack stats with a work item job #2129
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
+773
−49
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
8da03c6
Fixes stack stats with a work item job
niemyjski 503d7c3
PR feedback
niemyjski b45c291
increased batch size
niemyjski b99fe38
Improves stack event stats calculation
niemyjski 037629c
Fixes stack stats job logic
niemyjski 12f6e64
Refactors stack stats fix job
niemyjski 70384a5
Renames `Organization` to `OrganizationId`
niemyjski cc4bae7
Ensures stack change notifications are sent
niemyjski eb60668
Replaces DocumentNotFoundException and handles missing stacks.
niemyjski 0ef83eb
Handles missing stacks during event counting
niemyjski 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
Some comments aren't visible on the classic Files Changed page.
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
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
155 changes: 155 additions & 0 deletions
155
src/Exceptionless.Core/Jobs/WorkItemHandlers/FixStackStatsWorkItemHandler.cs
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,155 @@ | ||
| using Exceptionless.Core.Models; | ||
| using Exceptionless.Core.Models.WorkItems; | ||
| using Exceptionless.Core.Repositories; | ||
| using Exceptionless.DateTimeExtensions; | ||
| using Foundatio.Jobs; | ||
| using Foundatio.Lock; | ||
| using Foundatio.Repositories; | ||
| using Foundatio.Repositories.Models; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace Exceptionless.Core.Jobs.WorkItemHandlers; | ||
|
|
||
| public class FixStackStatsWorkItemHandler : WorkItemHandlerBase | ||
| { | ||
| private readonly IStackRepository _stackRepository; | ||
| private readonly IEventRepository _eventRepository; | ||
| private readonly ILockProvider _lockProvider; | ||
| private readonly TimeProvider _timeProvider; | ||
|
|
||
| public FixStackStatsWorkItemHandler(IStackRepository stackRepository, IEventRepository eventRepository, ILockProvider lockProvider, TimeProvider timeProvider, ILoggerFactory loggerFactory) | ||
| : base(loggerFactory) | ||
| { | ||
| _stackRepository = stackRepository; | ||
| _eventRepository = eventRepository; | ||
| _lockProvider = lockProvider; | ||
| _timeProvider = timeProvider; | ||
| } | ||
|
|
||
| public override Task<ILock> GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) | ||
| { | ||
| return _lockProvider.AcquireAsync(nameof(FixStackStatsWorkItemHandler), TimeSpan.FromHours(1), cancellationToken); | ||
| } | ||
|
|
||
| public override async Task HandleItemAsync(WorkItemContext context) | ||
| { | ||
| var wi = context.GetData<FixStackStatsWorkItem>(); | ||
| var utcEnd = wi.UtcEnd ?? _timeProvider.GetUtcNow().UtcDateTime; | ||
|
|
||
| Log.LogInformation("Starting stack stats repair for {UtcStart:O} to {UtcEnd:O}. OrganizationId={Organization}", wi.UtcStart, utcEnd, wi.OrganizationId); | ||
| await context.ReportProgressAsync(0, $"Starting stack stats repair for window {wi.UtcStart:O} – {utcEnd:O}"); | ||
|
|
||
| var organizationIds = await GetOrganizationIdsAsync(wi, utcEnd); | ||
| Log.LogInformation("Found {OrganizationCount} organizations to process", organizationIds.Count); | ||
|
|
||
| int repaired = 0; | ||
| int skipped = 0; | ||
|
|
||
| for (int index = 0; index < organizationIds.Count; index++) | ||
| { | ||
| if (context.CancellationToken.IsCancellationRequested) | ||
| break; | ||
|
|
||
| var (organizationRepaired, organizationSkipped) = await ProcessOrganizationAsync(context, organizationIds[index], wi.UtcStart, utcEnd); | ||
| repaired += organizationRepaired; | ||
| skipped += organizationSkipped; | ||
|
|
||
| int percentage = (int)Math.Min(99, (index + 1) * 100.0 / organizationIds.Count); | ||
| await context.ReportProgressAsync(percentage, $"Organization {index + 1}/{organizationIds.Count} ({percentage}%): repaired {repaired}, skipped {skipped}"); | ||
| } | ||
|
|
||
| Log.LogInformation("Stack stats repair complete: Repaired={Repaired} Skipped={Skipped}", repaired, skipped); | ||
| await context.ReportProgressAsync(100, $"Done. Repaired {repaired} stacks, skipped={skipped}."); | ||
| } | ||
|
|
||
| private async Task<IReadOnlyList<string>> GetOrganizationIdsAsync(FixStackStatsWorkItem wi, DateTime utcEnd) | ||
| { | ||
| if (wi.OrganizationId is not null) | ||
| return [wi.OrganizationId]; | ||
|
|
||
| var countResult = await _eventRepository.CountAsync(q => q | ||
| .DateRange(wi.UtcStart, utcEnd, (PersistentEvent e) => e.Date) | ||
| .Index(wi.UtcStart, utcEnd) | ||
| .AggregationsExpression("terms:(organization_id~65536)")); | ||
|
|
||
| return countResult.Aggregations.Terms<string>("terms_organization_id")?.Buckets | ||
| .Select(b => b.Key) | ||
| .ToList() ?? []; | ||
| } | ||
|
|
||
| private async Task<(int Repaired, int Skipped)> ProcessOrganizationAsync(WorkItemContext context, string organizationId, DateTime utcStart, DateTime utcEnd) | ||
| { | ||
| using var _ = Log.BeginScope(new ExceptionlessState().Organization(organizationId)); | ||
| await context.RenewLockAsync(); | ||
|
|
||
| var countResult = await _eventRepository.CountAsync(q => q | ||
| .Organization(organizationId) | ||
| .DateRange(utcStart, utcEnd, (PersistentEvent e) => e.Date) | ||
| .Index(utcStart, utcEnd) | ||
| .AggregationsExpression("terms:(stack_id~65536 min:date max:date)")); | ||
|
|
||
| var stackBuckets = countResult.Aggregations.Terms<string>("terms_stack_id")?.Buckets ?? []; | ||
| if (stackBuckets.Count is 0) | ||
| return (0, 0); | ||
|
|
||
| var statsByStackId = new Dictionary<string, StackEventStats>(stackBuckets.Count); | ||
| foreach (var bucket in stackBuckets) | ||
| { | ||
| var firstOccurrence = bucket.Aggregations.Min<DateTime>("min_date")?.Value; | ||
| var lastOccurrence = bucket.Aggregations.Max<DateTime>("max_date")?.Value; | ||
| if (firstOccurrence is null || lastOccurrence is null || bucket.Total is null) | ||
| continue; | ||
|
|
||
| statsByStackId[bucket.Key] = new StackEventStats(firstOccurrence.Value, lastOccurrence.Value, bucket.Total.Value); | ||
| } | ||
|
|
||
| int repaired = 0; | ||
| int skipped = 0; | ||
|
|
||
| foreach (string[] batch in statsByStackId.Keys.Chunk(100)) | ||
| { | ||
| if (context.CancellationToken.IsCancellationRequested) | ||
| break; | ||
|
|
||
| await context.RenewLockAsync(); | ||
|
|
||
| var stacks = await _stackRepository.GetByIdsAsync(batch); | ||
| foreach (var stack in stacks) | ||
| { | ||
| if (!statsByStackId.TryGetValue(stack.Id, out var stats)) | ||
| { | ||
| skipped++; | ||
| continue; | ||
| } | ||
|
|
||
| bool shouldUpdateFirst = stack.FirstOccurrence.IsAfter(stats.FirstOccurrence); | ||
| bool shouldUpdateLast = stack.LastOccurrence.IsBefore(stats.LastOccurrence); | ||
| bool shouldUpdateTotal = stats.TotalOccurrences > stack.TotalOccurrences; | ||
| if (!shouldUpdateFirst && !shouldUpdateLast && !shouldUpdateTotal) | ||
| { | ||
| skipped++; | ||
| continue; | ||
| } | ||
|
|
||
| var newFirst = shouldUpdateFirst ? stats.FirstOccurrence : stack.FirstOccurrence; | ||
| var newLast = shouldUpdateLast ? stats.LastOccurrence : stack.LastOccurrence; | ||
| long newTotal = shouldUpdateTotal ? stats.TotalOccurrences : stack.TotalOccurrences; | ||
|
|
||
| Log.LogInformation( | ||
| "Repairing stack {StackId}: first={OldFirst:O}->{NewFirst:O} last={OldLast:O}->{NewLast:O} total={OldTotal}->{NewTotal}", | ||
| stack.Id, | ||
| stack.FirstOccurrence, newFirst, | ||
| stack.LastOccurrence, newLast, | ||
| stack.TotalOccurrences, newTotal); | ||
|
|
||
| await _stackRepository.SetEventCounterAsync(stack.Id, newFirst, newLast, newTotal, sendNotifications: false); | ||
| repaired++; | ||
| } | ||
| } | ||
|
|
||
| Log.LogDebug("Processed organization: Repaired={Repaired} Skipped={Skipped}", repaired, skipped); | ||
| return (repaired, skipped); | ||
| } | ||
| } | ||
|
|
||
| internal record StackEventStats(DateTime FirstOccurrence, DateTime LastOccurrence, long TotalOccurrences); |
14 changes: 14 additions & 0 deletions
14
src/Exceptionless.Core/Models/WorkItems/FixStackStatsWorkItem.cs
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,14 @@ | ||
| namespace Exceptionless.Core.Models.WorkItems; | ||
|
|
||
| public record FixStackStatsWorkItem | ||
| { | ||
| public DateTime UtcStart { get; init; } | ||
|
|
||
| public DateTime? UtcEnd { get; init; } | ||
|
|
||
| /// <summary> | ||
| /// When set, only stacks belonging to this organization are repaired. | ||
| /// When null, all organizations with events in the time window are processed. | ||
| /// </summary> | ||
| public string? OrganizationId { get; init; } | ||
| } | ||
22 changes: 0 additions & 22 deletions
22
src/Exceptionless.Core/Repositories/Exceptions/DocumentNotFoundException.cs
This file was deleted.
Oops, something went wrong.
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.
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.
Uh oh!
There was an error while loading. Please reload this page.