Skip to content

Commit 95294e7

Browse files
cquirosjbartekwasielak
authored andcommitted
Ensure RetryAcknowledgementBehavior gets triggered
1 parent 0ae2d64 commit 95294e7

File tree

18 files changed

+732
-39
lines changed

18 files changed

+732
-39
lines changed

src/ServiceControl.AcceptanceTesting/NServiceBusAcceptanceTest.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
namespace ServiceControl.AcceptanceTesting
22
{
3+
using System;
34
using System.Linq;
45
using System.Threading;
6+
using NServiceBus.AcceptanceTesting;
57
using NServiceBus.AcceptanceTesting.Customization;
68
using NServiceBus.Logging;
79
using NUnit.Framework;
10+
using NUnit.Framework.Interfaces;
11+
using NUnit.Framework.Internal;
812

913
/// <summary>
1014
/// Base class for all the NSB test that sets up our conventions
@@ -35,5 +39,37 @@ public void SetUp()
3539
return testName + "." + endpointBuilder;
3640
};
3741
}
42+
43+
[TearDown]
44+
public void TearDown()
45+
{
46+
if (!TestExecutionContext.CurrentContext.TryGetRunDescriptor(out var runDescriptor))
47+
{
48+
return;
49+
}
50+
51+
var scenarioContext = runDescriptor.ScenarioContext;
52+
53+
// if (Environment.GetEnvironmentVariable("CI") != "true" || Environment.GetEnvironmentVariable("VERBOSE_TEST_LOGGING")?.ToLower() == "true")
54+
// {
55+
TestContext.Out.WriteLine($@"Test settings:
56+
{string.Join(Environment.NewLine, runDescriptor.Settings.Select(setting => $" {setting.Key}: {setting.Value}"))}");
57+
58+
TestContext.Out.WriteLine($@"Context:
59+
{string.Join(Environment.NewLine, scenarioContext.GetType().GetProperties().Select(p => $"{p.Name} = {p.GetValue(scenarioContext, null)}"))}");
60+
// }
61+
62+
if (TestExecutionContext.CurrentContext.CurrentResult.ResultState == ResultState.Failure || TestExecutionContext.CurrentContext.CurrentResult.ResultState == ResultState.Error)
63+
{
64+
TestContext.Out.WriteLine(string.Empty);
65+
TestContext.Out.WriteLine($"Log entries (log level: {scenarioContext.LogLevel}):");
66+
TestContext.Out.WriteLine("--- Start log entries ---------------------------------------------------");
67+
foreach (var logEntry in scenarioContext.Logs)
68+
{
69+
TestContext.Out.WriteLine($"{logEntry.Timestamp:T} {logEntry.Level} {logEntry.Endpoint ?? TestContext.CurrentContext.Test.Name}: {logEntry.Message}");
70+
}
71+
TestContext.Out.WriteLine("--- End log entries ---------------------------------------------------");
72+
}
73+
}
3874
}
3975
}

src/ServiceControl.AcceptanceTests.RavenDB/ServiceControl.AcceptanceTests.RavenDB.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,8 @@
3030
<Compile Include="..\ServiceControl.Persistence.Tests.RavenDB\StopSharedDatabase.cs" />
3131
</ItemGroup>
3232

33+
<ItemGroup>
34+
<Folder Include="Shared\Recoverability\ExternalIntegration\" />
35+
</ItemGroup>
36+
3337
</Project>

src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/ExternalIntegrationAcceptanceTest.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public ErrorSender() =>
2020
EndpointSetup<DefaultServerWithoutAudit>(c =>
2121
{
2222
c.NoDelayedRetries();
23+
//TODO: Get back to this to determine whether or not this duplication for simulating the production code is really needed
2324
c.ReportSuccessfulRetriesToServiceControl();
2425
});
2526

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Threading.Tasks;
4+
using ServiceControl.AcceptanceTesting;
5+
using ServiceControl.AcceptanceTests;
6+
using ServiceControl.MessageFailures.Api;
7+
8+
public static class FailedMessageExtensions
9+
{
10+
internal static async Task<string> GetOnlyFailedUnresolvedMessageId(this AcceptanceTest test)
11+
{
12+
var allFailedMessages =
13+
await test.TryGet<IList<FailedMessageView>>($"/api/errors/?status=unresolved");
14+
if (!allFailedMessages.HasResult)
15+
{
16+
return null;
17+
}
18+
19+
if (allFailedMessages.Item.Count != 1)
20+
{
21+
return null;
22+
}
23+
24+
return allFailedMessages.Item.First().Id;
25+
}
26+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
namespace ServiceControl.AcceptanceTests.Recoverability.ExternalIntegration
2+
{
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using AcceptanceTesting;
6+
using AcceptanceTesting.EndpointTemplates;
7+
using Contracts;
8+
using NServiceBus;
9+
using NServiceBus.AcceptanceTesting;
10+
using NUnit.Framework;
11+
using ServiceControl.MessageFailures;
12+
using ServiceControl.MessageFailures.Api;
13+
using JsonSerializer = System.Text.Json.JsonSerializer;
14+
15+
class When_a_failed_edit_is_resolved_by_retry : ExternalIntegrationAcceptanceTest
16+
{
17+
[Test]
18+
public async Task Should_publish_notification()
19+
{
20+
CustomConfiguration = config => config.OnEndpointSubscribed<EditMessageResolutionContext>((s, ctx) =>
21+
{
22+
ctx.ExternalProcessorSubscribed = s.SubscriberReturnAddress.Contains(nameof(MessageReceiver));
23+
});
24+
25+
var context = await Define<EditMessageResolutionContext>()
26+
.WithEndpoint<MessageReceiver>(b => b.When(async (bus, c) =>
27+
{
28+
await bus.Subscribe<MessageFailureResolvedByRetry>();
29+
30+
if (c.HasNativePubSubSupport)
31+
{
32+
c.ExternalProcessorSubscribed = true;
33+
}
34+
}).When(c => c.SendLocal(new EditResolutionMessage())).DoNotFailOnErrorMessages())
35+
.Done(async ctx =>
36+
{
37+
if (!ctx.ExternalProcessorSubscribed)
38+
{
39+
return false;
40+
}
41+
42+
// second message - edit & retry
43+
if (ctx.MessageSentCount == 0 && ctx.MessageHandledCount == 1)
44+
{
45+
var failedMessagedId = await this.GetOnlyFailedUnresolvedMessageId();
46+
if (failedMessagedId == null)
47+
{
48+
return false;
49+
}
50+
51+
ctx.OriginalMessageFailureId = failedMessagedId;
52+
ctx.MessageSentCount = 1;
53+
54+
string editedMessage = JsonSerializer.Serialize(new EditResolutionMessage
55+
{ });
56+
57+
SingleResult<FailedMessage> failedMessage =
58+
await this.TryGet<FailedMessage>($"/api/errors/{ctx.OriginalMessageFailureId}");
59+
60+
var editModel = new EditMessageModel
61+
{
62+
MessageBody = editedMessage,
63+
MessageHeaders = failedMessage.Item.ProcessingAttempts.Last().Headers
64+
};
65+
await this.Post($"/api/edit/{ctx.OriginalMessageFailureId}", editModel);
66+
return false;
67+
}
68+
69+
// third message - retry
70+
if (ctx.MessageSentCount == 1 && ctx.MessageHandledCount == 2)
71+
{
72+
var failedMessageIdAfterId = await this.GetOnlyFailedUnresolvedMessageId();
73+
if (failedMessageIdAfterId == null)
74+
{
75+
return false;
76+
}
77+
78+
ctx.EditedMessageFailureId = failedMessageIdAfterId;
79+
ctx.MessageSentCount = 2;
80+
81+
await this.Post<object>($"/api/errors/{ctx.EditedMessageFailureId}/retry");
82+
return false;
83+
}
84+
85+
if (ctx.MessageHandledCount != 3)
86+
{
87+
return false;
88+
}
89+
90+
// if (ctx.EditedMessageEditOf == null)
91+
// {
92+
// return false;
93+
// }
94+
95+
if (!ctx.MessageResolved)
96+
{
97+
return false;
98+
}
99+
100+
return true;
101+
}).Run();
102+
103+
Assert.Multiple(() =>
104+
{
105+
Assert.That(context.ResolvedMessageId, Is.EqualTo(context.OriginalMessageFailureId));
106+
Assert.That(context.EditedMessageEditOf, Is.EqualTo(context.OriginalMessageFailureId));
107+
});
108+
}
109+
110+
111+
public class EditMessageResolutionContext : ScenarioContext
112+
{
113+
public string OriginalMessageFailureId { get; set; }
114+
public int MessageSentCount { get; set; }
115+
public int MessageHandledCount { get; set; }
116+
117+
public string ResolvedMessageId { get; set; }
118+
119+
public string EditedMessageFailureId { get; set; }
120+
121+
public string EditedMessageEditOf { get; set; }
122+
public bool ExternalProcessorSubscribed { get; set; }
123+
public bool MessageResolved { get; set; }
124+
}
125+
126+
public class MessageReceiver : EndpointConfigurationBuilder
127+
{
128+
public MessageReceiver() => EndpointSetup<DefaultServerWithoutAudit>(c => c.NoRetries());
129+
130+
131+
public class EditMessageResolutionHandler(EditMessageResolutionContext testContext)
132+
: IHandleMessages<EditResolutionMessage>, IHandleMessages<MessageFailureResolvedByRetry>
133+
{
134+
public Task Handle(EditResolutionMessage message, IMessageHandlerContext context)
135+
{
136+
// First run - supposed to fail
137+
if (testContext.MessageSentCount == 0)
138+
{
139+
testContext.MessageHandledCount = 1;
140+
throw new SimulatedException();
141+
}
142+
143+
// Second run - edit retry - supposed to fail
144+
if (testContext.MessageSentCount == 1)
145+
{
146+
testContext.EditedMessageEditOf = context.MessageHeaders["ServiceControl.EditOf"];
147+
testContext.MessageHandledCount = 2;
148+
throw new SimulatedException();
149+
}
150+
151+
// Last run - normal retry - supposed to succeed
152+
testContext.MessageHandledCount = 3;
153+
return Task.CompletedTask;
154+
}
155+
156+
public Task Handle(MessageFailureResolvedByRetry message, IMessageHandlerContext context)
157+
{
158+
testContext.ResolvedMessageId = message.FailedMessageId;
159+
testContext.MessageResolved = true;
160+
return Task.CompletedTask;
161+
}
162+
}
163+
}
164+
165+
public class EditResolutionMessage : IMessage
166+
{
167+
}
168+
}
169+
}
170+

0 commit comments

Comments
 (0)