Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/ServiceControl.AcceptanceTesting/HttpExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public static async Task<ManyResult<T>> TryGetMany<T>(this IAcceptanceTestInfras
return ManyResult<T>.Empty;
}

return ManyResult<T>.New(true, response);
return ManyResult<T>.New(true, response.Where(m => condition(m)).ToList());
}

public static async Task<HttpStatusCode> Patch<T>(this IAcceptanceTestInfrastructureProvider provider, string url, T payload = null) where T : class
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace ServiceControl.MultiInstance.AcceptanceTests.Recoverability;

using System.Threading.Tasks;
using AcceptanceTesting;
using MessageFailures;
using MessageFailures.Api;
using TestSupport;

abstract class WhenRetrying : AcceptanceTest
{
protected Task<SingleResult<FailedMessage>> GetFailedMessage(string uniqueMessageId, string instance, FailedMessageStatus expectedStatus)
{
if (uniqueMessageId == null)
{
return Task.FromResult(SingleResult<FailedMessage>.Empty);
}

return this.TryGet<FailedMessage>($"/api/errors/{uniqueMessageId}", f => f.Status == expectedStatus, instance);
}

protected Task<ManyResult<FailedMessageView>> GetAllFailedMessage(string instance, FailedMessageStatus expectedStatus) => this.TryGetMany<FailedMessageView>("/api/errors", f => f.Status == expectedStatus, instance);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
namespace ServiceControl.MultiInstance.AcceptanceTests.Recoverability
{
using System;
using System.Linq;
using System.Threading.Tasks;
using AcceptanceTesting;
using AcceptanceTesting.EndpointTemplates;
using MessageFailures;
using NServiceBus;
using NServiceBus.AcceptanceTesting;
using NServiceBus.Settings;
using NUnit.Framework;
using ServiceControl.Infrastructure;
using TestSupport;

class WhenRetryingSameMessageMultipleTimes : WhenRetrying
{
public enum RetryType
{
NoEdit,
Edit
}

[TestCase(new[] { RetryType.NoEdit, RetryType.NoEdit, RetryType.Edit })]
[TestCase(new[] { RetryType.Edit, RetryType.NoEdit, RetryType.Edit })]
[TestCase(new[] { RetryType.NoEdit, RetryType.Edit, RetryType.NoEdit })]
[TestCase(new[] { RetryType.Edit, RetryType.Edit, RetryType.NoEdit })]
public async Task WithMixOfRetryTypes(RetryType[] retryTypes)
{
CustomServiceControlPrimarySettings = s => { s.AllowMessageEditing = true; };

await Define<MyContext>()
.WithEndpoint<FailureEndpoint>(b =>
b.When(bus => bus.SendLocal(new MyMessage())).DoNotFailOnErrorMessages())
.Done(async c =>
{
if (c.RetryCount >= retryTypes.Length) // Are all retries done?
{
return !(await GetAllFailedMessage(ServiceControlInstanceName, FailedMessageStatus.Unresolved))
.HasResult; // Should return true if all failed messages are no longer unresolved
}

if (retryTypes[c.RetryCount] == RetryType.Edit)
{
var results = await GetAllFailedMessage(ServiceControlInstanceName,
FailedMessageStatus.Unresolved);
if (!results.HasResult)
{
return false; // No failed messages yet
}

var result = results.Items.Single();

c.MessageId = result.MessageId;
}

var failedMessage = await GetFailedMessage(c.UniqueMessageId, ServiceControlInstanceName, FailedMessageStatus.Unresolved);
if (!failedMessage.HasResult)
{
return false; // No failed message yet
}

if (retryTypes[c.RetryCount] == RetryType.Edit)
{
await this.Post<object>($"/api/edit/{failedMessage.Item.UniqueMessageId}",
new
{
MessageBody = $"{{ \"Name\": \"Hello {c.RetryCount}\" }}",
MessageHeaders = failedMessage.Item.ProcessingAttempts[^1].Headers
}, null,
ServiceControlInstanceName);
}
else
{
await this.Post<object>($"/api/errors/{failedMessage.Item.UniqueMessageId}/retry", null, null,
ServiceControlInstanceName);
}

c.RetryCount++;

return false;

})
.Run(TimeSpan.FromMinutes(2));
}

class FailureEndpoint : EndpointConfigurationBuilder
{
public FailureEndpoint() => EndpointSetup<DefaultServerWithoutAudit>(c => { c.NoRetries(); });

public class MyMessageHandler(MyContext testContext, IReadOnlySettings settings)
: IHandleMessages<MyMessage>
{
public Task Handle(MyMessage message, IMessageHandlerContext context)
{
testContext.MessageId = context.MessageId.Replace(@"\", "-");
testContext.EndpointNameOfReceivingEndpoint = settings.EndpointName();

if (testContext.RetryCount < 3)
{
Console.Out.WriteLine("Throwing exception");
throw new Exception("Simulated exception");
}

Console.Out.WriteLine("Handling message");

return Task.CompletedTask;
}
}
}

class MyMessage : ICommand
{
public string Name { get; set; }
}

class MyContext : ScenarioContext
{
public string MessageId { get; set; }

public string EndpointNameOfReceivingEndpoint { get; set; }

public string UniqueMessageId => DeterministicGuid.MakeId(MessageId, EndpointNameOfReceivingEndpoint).ToString();
public int RetryCount { get; set; }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
namespace ServiceControl.MultiInstance.AcceptanceTests.Recoverability;

using System;
using System.Threading.Tasks;
using AcceptanceTesting;
using AcceptanceTesting.EndpointTemplates;
using MessageFailures;
using NServiceBus;
using NServiceBus.AcceptanceTesting;
using NServiceBus.Settings;
using NUnit.Framework;
using TestSupport;
using ServiceControl.Infrastructure;

class WhenRetryingWithEdit : WhenRetrying
{
[Test]
public async Task ShouldCreateNewMessageAndResolveEditedMessage()
{
CustomServiceControlPrimarySettings = s => { s.AllowMessageEditing = true; };

await Define<MyContext>()
.WithEndpoint<FailureEndpoint>(b =>
b.When(bus => bus.SendLocal(new MyMessage { Password = "Bad password!" })).DoNotFailOnErrorMessages())
.Done(async c =>
{
if (!c.ErrorRetried)
{
var failedMessage = await GetFailedMessage(c.UniqueMessageId, ServiceControlInstanceName,
FailedMessageStatus.Unresolved);
if (!failedMessage.HasResult)
{
return false; // No failed message yet
}

await this.Post<object>($"/api/edit/{failedMessage.Item.UniqueMessageId}",
new
{
MessageBody = "{ \"Password\": \"VerySecretPassword\" }",
MessageHeaders = failedMessage.Item.ProcessingAttempts[^1].Headers
}, null,
ServiceControlInstanceName);
c.ErrorRetried = true;

return false;
}

var failedResolvedMessage = await GetFailedMessage(c.UniqueMessageId, ServiceControlInstanceName, FailedMessageStatus.Resolved);

return failedResolvedMessage.HasResult; // If there is a result it means the message was resolved
})
.Run(TimeSpan.FromMinutes(2));
}

class FailureEndpoint : EndpointConfigurationBuilder
{
public FailureEndpoint() => EndpointSetup<DefaultServerWithoutAudit>(c => { c.NoRetries(); });

public class MyMessageHandler(MyContext testContext, IReadOnlySettings settings) : IHandleMessages<MyMessage>
{
public Task Handle(MyMessage message, IMessageHandlerContext context)
{
if (message.Password == "VerySecretPassword")
{
Console.Out.WriteLine("Handling message");
return Task.CompletedTask;
}

testContext.MessageId = context.MessageId.Replace(@"\", "-");
testContext.EndpointNameOfReceivingEndpoint = settings.EndpointName();

Console.Out.WriteLine("Throwing exception");
throw new Exception("Simulated exception");
}
}
}

class MyMessage : ICommand
{
public string Password { get; set; }
}

class MyContext : ScenarioContext
{
public string MessageId { get; set; }

public string EndpointNameOfReceivingEndpoint { get; set; }

public string UniqueMessageId => DeterministicGuid.MakeId(MessageId, EndpointNameOfReceivingEndpoint).ToString();
public bool ErrorRetried { get; set; }
}
}
2 changes: 1 addition & 1 deletion src/ServiceControl/Recoverability/Editing/EditHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public async Task Handle(EditAndSend message, IMessageHandlerContext context)
var outgoingMessage = BuildMessage(message);
// mark the new message with a link to the original message id
outgoingMessage.Headers.Add("ServiceControl.EditOf", message.FailedMessageId);
outgoingMessage.Headers["ServiceControl.Retry.AcknowledgementQueue"] = "";
outgoingMessage.Headers.Remove("ServiceControl.Retry.AcknowledgementQueue");
var address = ApplyRedirect(attempt.FailureDetails.AddressOfFailingEndpoint, redirects);

if (outgoingMessage.Headers.TryGetValue("ServiceControl.RetryTo", out var retryTo))
Expand Down