Skip to content

Commit 999b5da

Browse files
authored
Merge pull request #5652 from Particular/john/failedmessages_ingestion_failures
Derive failed error import id from the message, not a random Guid
2 parents 6f33fbf + f249ee3 commit 999b5da

4 files changed

Lines changed: 147 additions & 1 deletion

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
namespace ServiceControl.Persistence.Tests.RavenDB.Operations
2+
{
3+
using System.Collections.Generic;
4+
using System.Threading.Tasks;
5+
using NServiceBus;
6+
using NUnit.Framework;
7+
using Raven.Client.Documents;
8+
using ServiceControl.Operations;
9+
10+
[TestFixture]
11+
class FailedErrorImportDedupeTests : RavenPersistenceTestBase
12+
{
13+
[Test]
14+
public async Task Repeated_failure_of_the_same_message_stores_one_document()
15+
{
16+
var headers = new Dictionary<string, string>
17+
{
18+
{ Headers.MessageId, "message-1" },
19+
{ Headers.ProcessingEndpoint, "Sales" }
20+
};
21+
22+
await StoreFailure(headers, "the first failure");
23+
await StoreFailure(headers, "the second failure");
24+
25+
DocumentStore.WaitForIndexing();
26+
27+
using var session = DocumentStore.OpenAsyncSession();
28+
var documents = await session.Query<FailedErrorImport>().ToListAsync();
29+
30+
using (Assert.EnterMultipleScope())
31+
{
32+
Assert.That(documents, Has.Count.EqualTo(1));
33+
Assert.That(documents[0].ExceptionInfo, Is.EqualTo("the second failure"));
34+
}
35+
}
36+
37+
Task StoreFailure(IReadOnlyDictionary<string, string> headers, string exceptionInfo) =>
38+
ErrorStore.StoreFailedErrorImport(new FailedErrorImport
39+
{
40+
Id = FailedErrorImport.MakeDocumentId(FailedErrorImport.DeriveKey(headers, "native-1")),
41+
Message = new FailedTransportMessage
42+
{
43+
Id = "native-1",
44+
Headers = new Dictionary<string, string>(headers),
45+
Body = []
46+
},
47+
ExceptionInfo = exceptionInfo
48+
});
49+
}
50+
}

src/ServiceControl.Persistence/FailedErrorImport.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
namespace ServiceControl.Operations
22
{
33
using System;
4+
using System.Collections.Generic;
5+
using ServiceControl.Persistence.Infrastructure;
46

57
public class FailedErrorImport
68
{
@@ -9,5 +11,24 @@ public class FailedErrorImport
911
public string ExceptionInfo { get; set; }
1012

1113
public static string MakeDocumentId(Guid id) => $"FailedErrorImports/{id}";
14+
15+
public static Guid DeriveKey(IReadOnlyDictionary<string, string> headers, string nativeMessageId)
16+
{
17+
try
18+
{
19+
if (Guid.TryParse(headers.UniqueId(), out var uniqueMessageId))
20+
{
21+
return uniqueMessageId;
22+
}
23+
}
24+
catch (Exception)
25+
{
26+
// UniqueId() derives the processing endpoint, which throws when the failed message
27+
// carries no endpoint header. Malformed messages are a leading cause of import
28+
// failure, so fall back to a key derived from the id the transport always supplies.
29+
}
30+
31+
return DeterministicGuid.MakeId(nativeMessageId);
32+
}
1233
}
1334
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
namespace ServiceControl.UnitTests.Operations;
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using NServiceBus;
6+
using NUnit.Framework;
7+
using ServiceControl.Operations;
8+
using ServiceControl.Persistence.Infrastructure;
9+
10+
[TestFixture]
11+
public class When_deriving_a_failed_error_import_key
12+
{
13+
[Test]
14+
public void Uses_the_unique_message_id_when_headers_are_well_formed()
15+
{
16+
var headers = new Dictionary<string, string>
17+
{
18+
{ Headers.MessageId, "message-1" },
19+
{ Headers.ProcessingEndpoint, "Sales" }
20+
};
21+
22+
var key = FailedErrorImport.DeriveKey(headers, "native-1");
23+
24+
Assert.That(key, Is.EqualTo(DeterministicGuid.MakeId("message-1", "Sales")));
25+
}
26+
27+
[Test]
28+
public void Prefers_an_existing_retry_unique_message_id()
29+
{
30+
var uniqueMessageId = Guid.NewGuid();
31+
var headers = new Dictionary<string, string>
32+
{
33+
{ "ServiceControl.Retry.UniqueMessageId", uniqueMessageId.ToString() }
34+
};
35+
36+
var key = FailedErrorImport.DeriveKey(headers, "native-1");
37+
38+
Assert.That(key, Is.EqualTo(uniqueMessageId));
39+
}
40+
41+
[Test]
42+
public void Falls_back_to_the_native_id_when_no_processing_endpoint_can_be_derived()
43+
{
44+
var headers = new Dictionary<string, string>
45+
{
46+
{ Headers.MessageId, "message-1" }
47+
};
48+
49+
var key = FailedErrorImport.DeriveKey(headers, "native-1");
50+
51+
Assert.That(key, Is.EqualTo(DeterministicGuid.MakeId("native-1")));
52+
}
53+
54+
[Test]
55+
public void Falls_back_when_the_retry_unique_message_id_is_not_a_guid()
56+
{
57+
var headers = new Dictionary<string, string>
58+
{
59+
{ "ServiceControl.Retry.UniqueMessageId", "not-a-guid" }
60+
};
61+
62+
var key = FailedErrorImport.DeriveKey(headers, "native-1");
63+
64+
Assert.That(key, Is.EqualTo(DeterministicGuid.MakeId("native-1")));
65+
}
66+
67+
[Test]
68+
public void Is_stable_across_repeated_failures_of_the_same_malformed_message()
69+
{
70+
var first = FailedErrorImport.DeriveKey(new Dictionary<string, string>(), "native-1");
71+
var second = FailedErrorImport.DeriveKey(new Dictionary<string, string>(), "native-1");
72+
73+
Assert.That(second, Is.EqualTo(first));
74+
}
75+
}

src/ServiceControl/Operations/ErrorIngestionFaultPolicy.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ async Task Handle(ErrorContext errorContext, CancellationToken cancellationToken
5757
Body = errorContext.Body.ToArray()
5858
},
5959
ExceptionInfo = errorContext.Exception.ToFriendlyString(),
60-
Id = FailedErrorImport.MakeDocumentId(Guid.NewGuid())
60+
Id = FailedErrorImport.MakeDocumentId(FailedErrorImport.DeriveKey(errorContext.Headers, errorContext.MessageId))
6161
};
6262

6363
try

0 commit comments

Comments
 (0)