Skip to content

Commit 6b9a5b1

Browse files
cquirosjSzymonPobiegabartekwasielak
authored
Introduce an integration event for the Edit and Retry feature (#5149)
Co-authored-by: SzymonPobiega <[email protected]> Co-authored-by: Bartek Wasielak <[email protected]>
1 parent 5f33fba commit 6b9a5b1

File tree

16 files changed

+742
-31
lines changed

16 files changed

+742
-31
lines changed

src/Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
<PackageVersion Include="RavenDB.Embedded" Version="6.2.6" />
6969
<PackageVersion Include="ReactiveUI.WPF" Version="20.1.63" />
7070
<PackageVersion Include="Seq.Extensions.Logging" Version="8.0.0" />
71-
<PackageVersion Include="ServiceControl.Contracts" Version="5.0.0" />
71+
<PackageVersion Include="ServiceControl.Contracts" Version="5.1.0" />
7272
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="8.0.1" />
7373
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="8.0.1" />
7474
<PackageVersion Include="System.DirectoryServices.AccountManagement" Version="8.0.1" />

src/ServiceControl.AcceptanceTesting/NServiceBusAcceptanceTest.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
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.Internal;
811

912
/// <summary>
1013
/// Base class for all the NSB test that sets up our conventions
@@ -35,5 +38,22 @@ public void SetUp()
3538
return testName + "." + endpointBuilder;
3639
};
3740
}
41+
42+
[TearDown]
43+
public void TearDown()
44+
{
45+
if (!TestExecutionContext.CurrentContext.TryGetRunDescriptor(out var runDescriptor))
46+
{
47+
return;
48+
}
49+
50+
var scenarioContext = runDescriptor.ScenarioContext;
51+
52+
TestContext.Out.WriteLine($@"Test settings:
53+
{string.Join(Environment.NewLine, runDescriptor.Settings.Select(setting => $" {setting.Key}: {setting.Value}"))}");
54+
55+
TestContext.Out.WriteLine($@"Context:
56+
{string.Join(Environment.NewLine, scenarioContext.GetType().GetProperties().Select(p => $"{p.Name} = {p.GetValue(scenarioContext, null)}"))}");
57+
}
3858
}
3959
}

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

0 commit comments

Comments
 (0)