Skip to content

Commit e94bb05

Browse files
Fixed duplicate key exception for Hangfire jobs with AutomaticRetry (#3631)
1 parent 2d728ae commit e94bb05

File tree

3 files changed

+61
-1
lines changed

3 files changed

+61
-1
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
Sentry will reject all metrics sent after October 7, 2024.
99
Learn more: https://sentry.zendesk.com/hc/en-us/articles/26369339769883-Upcoming-API-Changes-to-Metrics ([#3619](https://github.com/getsentry/sentry-dotnet/pull/3619))
1010

11+
## Fixes
12+
13+
- Fixed duplicate key exception for Hangfire jobs with AutomaticRetry ([#3631](https://github.com/getsentry/sentry-dotnet/pull/3631))
14+
1115
### Features
1216

1317
- Added a flag to options `DisableFileWrite` to allow users to opt-out of all file writing operations. Note that toggling this will affect features such as offline caching and auto-session tracking and release health as these rely on some file persistency ([#3614](https://github.com/getsentry/sentry-dotnet/pull/3614))

src/Sentry.Hangfire/SentryServerFilter.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ public void OnPerforming(PerformingContext context)
3434
}
3535

3636
var checkInId = _hub.CaptureCheckIn(monitorSlug, CheckInStatus.InProgress);
37-
context.Items.Add(SentryCheckInIdKey, checkInId);
37+
38+
// Note that we may be overwriting context.Items[SentryCheckInIdKey] here, which is intentional. If that happens
39+
// then implicitly OnPerforming was called previously with the same context, but we never made it to OnPerformed
40+
// This might happen if a Hangfire job failed at least once, with automatic retries configured.
41+
context.Items[SentryCheckInIdKey] = checkInId;
3842
}
3943

4044
public void OnPerformed(PerformedContext context)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using Hangfire;
2+
using Hangfire.Common;
3+
using Hangfire.Server;
4+
using Hangfire.Storage;
5+
6+
namespace Sentry.Hangfire.Tests;
7+
8+
public class ServerFilterTests
9+
{
10+
[Fact]
11+
public void OnPerforming_IsReentrant()
12+
{
13+
// Arrange
14+
const string jobId = "test-id";
15+
const string monitorSlug = "test-monitor-slug";
16+
17+
var storageConnection = Substitute.For<IStorageConnection>();
18+
storageConnection.GetJobParameter(jobId, SentryServerFilter.SentryMonitorSlugKey).Returns(
19+
SerializationHelper.Serialize(monitorSlug)
20+
);
21+
22+
var backgroundJob = new BackgroundJob(jobId, null, DateTime.Now);
23+
var cancellationToken = Substitute.For<IJobCancellationToken>();
24+
var performContext = new PerformContext(
25+
null,
26+
storageConnection,
27+
backgroundJob,
28+
cancellationToken
29+
);
30+
var performingContext = new PerformingContext(performContext);
31+
32+
var hub = Substitute.For<IHub>();
33+
hub.CaptureCheckIn(monitorSlug, CheckInStatus.InProgress).Returns(SentryId.Create());
34+
35+
var logger = Substitute.For<IDiagnosticLogger>();
36+
var filter = new SentryServerFilter(hub, logger);
37+
38+
// Act
39+
filter.OnPerforming(performingContext);
40+
41+
// Assert
42+
performContext.Items.ContainsKey(SentryServerFilter.SentryCheckInIdKey).Should().BeTrue();
43+
var firstKey = performingContext.Items[SentryServerFilter.SentryCheckInIdKey];
44+
45+
// Act
46+
filter.OnPerforming(performingContext);
47+
48+
// Assert
49+
performContext.Items.ContainsKey(SentryServerFilter.SentryCheckInIdKey).Should().BeTrue();
50+
performingContext.Items[SentryServerFilter.SentryCheckInIdKey].Should().NotBeSameAs(firstKey);
51+
}
52+
}

0 commit comments

Comments
 (0)