I've got a problem with the Retries on MassTransit, with rabbitmq.
I have got the code configuration:
private static void ConfigureMassTransitWithRabbitMq(this IServiceCollection services, string host, string username, string pwd, string cluster, Assembly assembly)
{
services.AddMassTransit(config =>
{
config.AddConsumers(assembly);
config.SetKebabCaseEndpointNameFormatter();
config.UsingRabbitMq((context, rabbitMqConfig) =>
{
rabbitMqConfig.UseRetry(e => Retry.Immediate(3));
rabbitMqConfig.ConfigureEndpoints(context);
rabbitMqConfig.ConfigurePublish(pipe =>
{
pipe.UseExecute(context1 =>
{
if (context1.CorrelationId == null)
{
context1.CorrelationId = Guid.NewGuid();
}
});
});
rabbitMqConfig.Host(new Uri(host), hst =>
{
hst.Username(username);
hst.Password(pwd);
hst.PublisherConfirmation = true;
hst.Heartbeat(5);
hst.UseCluster(c =>
{
var clusters = cluster?.Split(';');
if (cluster == null || clusters.Length == 0)
return;
foreach (var item in clusters)
c.Node(item);
});
});
rabbitMqConfig.ManagementEndpoint((conf) =>
{
conf.ConfigurePublish((p) =>
{
p.UseRetry((r) =>
{
r.Exponential(3, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15), TimeSpan.FromSeconds(5));
});
});
});
});
});
services.AddMassTransitHostedService();
}
I add my consumers by assembly.
I just try to use:
rabbitMqConfig.UseRetry(e => Retry.Immediate(3)); When I configure the MassTransit
But in this case, de configuration stay an infinite loop
And I try to use this:
try
{
var order = _mapper.Map<object>(context.Message);
await Execute(order);
await context.ConsumeCompleted;
}
catch (Exception)
{
var retry = context.GetRetryCount();
if (maxAttempts > 3)
{
throw;
}
await context.Redeliver(TimeSpan.FromSeconds(5));
}
And I put this code in MassTransitConfiguration:
rabbitMqConfig.UseDelayedExchangeMessageScheduler();
In the last case, I got the retry, but I never got the amount of retry, so I don't get to control it. I try to get this amount by header and other properties too, but I always got 0.
Can anyone help me?!
The order of your configuration is wrong, the corrected configuration based upon your example above is shown below:
private static void ConfigureMassTransitWithRabbitMq(this IServiceCollection services, string host, string username, string pwd, string cluster, Assembly assembly)
{
services.AddMassTransit(config =>
{
config.AddConsumers(assembly);
config.SetKebabCaseEndpointNameFormatter();
config.UsingRabbitMq((context, rabbitMqConfig) =>
{
rabbitMqConfig.Host(new Uri(host), hst =>
{
hst.Username(username);
hst.Password(pwd);
hst.Heartbeat(5);
hst.UseCluster(c =>
{
var clusters = cluster?.Split(';');
if (cluster == null || clusters.Length == 0)
return;
foreach (var item in clusters)
c.Node(item);
});
});
rabbitMqConfig.ConfigurePublish(pipe =>
{
pipe.UseExecute(context1 =>
{
context1.CorrelationId ??= Guid.NewGuid();
});
});
rabbitMqConfig.UseRetry(e => Retry.Immediate(3));
rabbitMqConfig.ConfigureEndpoints(context);
});
});
services.AddMassTransitHostedService();
}
I also removed anything that wasn't required, either because it was default behavior, or unused.
Related
Below is InvokeAsync method which need to tested.
public async Task<bool> InvokeAsync(Batch batch)
{
var refundRequests = await this.RefundRepository.GetsAsync(batch.RefundRequests.Select(x => x.Id));
refundRequests.RemoveAll(x => x.Status != RefundRequestStatus.Approved);
var distributions = await DistributionRepository.GetsAsync(refundRequests.Select(x => x.Distribution.Id));
var bundles = await this.BundleRepository.GetsAsync(distributions.Select(x => x.BundleId));
foreach (var getRefundRequest in refundRequests)
{
var distribution = distributions.FirstOrDefault(x => x.Id == getRefundRequest.Distribution.Id);
if (distribution?.BundleId != null)
{
var bundle = bundles.First(x => x.Id == distribution?.BundleId);
Bundle result = await Reverse.InvokeAsync(getRefundRequest, distribution, bundle); //MOCK
}
getRefundRequest.Status = RefundRequestStatus.Closed;
getRefundRequest.LastUpdatedBy = "Test User";
bool isUpdated = await UpdateRefund.InvokeAsync(getRefundRequest); //MOCK
}
batch.Status = BatchStatus.Posted;
batch.LastUpdatedBy = "Test User";
var isSuccess = await UpdateBatch.InvokeAsync(batch); //MOCK
return isSuccess;
}
Unit test method
[Fact]
public async Task Post_Batch()
{
foreach (var refundBatch in Factory.Batch.CreateRefundBatchesData())
{
var refundRequests = await this.RefundRequestRepository.GetsAsync(batch.RefundRequests.Select(x => x.Id));
var distributions = await this.DistributionRepository.GetsAsync(refundRequests.Select(x => x.Distribution.Id));
var bundles = await this.BundleRepository.GetsAsync(distributions.Select(x => x.BundleId));
for (int i = 0; i < refundRequests.Count; i++)
{
var refundRequest = refundRequests[i];
var bundle = bundles[i];
var distribution = distributions[i];
MockSetupReverse(refundRequest, distribution, bundle);
MockSetupUpdateRefund(refundRequest);
}
MockSetupUpdateBatch(batch);
//Act
var postRefund = await UseCase.InvokeAsync(batch);
//Assert
postRefund.ShouldNotBeNull();
postRefund.IsPosted.ShouldBeTrue();
}
}
MockSetup methods
private void MockSetupReverse(RefundRequest refundRequest, Distribution distribution, Bundle bundle)
{
this.MockReverse
.Setup(x => x.InvokeAsync(refundRequest, distribution, bundle))
.Returns(async () =>
{
bundle.Status = BundleStatus.Closed;
return await Task.Run(() => bundle);
});
}
private void MockSetupUpdateRefund(RefundRequest refundRequest)
{
this.MockUpdateRefund
.Setup(x => x.InvokeAsync(refundRequest))
.Returns(async () =>
{
refundRequest.Status = RefundRequestStatus.Closed;
refundRequest.LastUpdatedBy = Factory.RefundRequest.TestUserName;
return await Task.Run(() => true);
});
}
private void MockSetupUpdateBatch(Batch batch)
{
this.MockUpdateBatch
.Setup(x => x.InvokeAsync(batch))
.Returns(async () =>
{
refundBatch.Status = BatchStatus.Posted;
refundBatch.LastUpdatedBy = Factory.RefundRequest.TestUserName;
return await Task.Run(() => true);
});
}
The mocking of the UpdateBatch is working and returns true when the method is invoked. But, the mocking of the Reverse and UpdateRefund returns false when the respective method is invoked. Any thoughts?
Please let me know if more info is required to support the question.
When you Setup your mocks with a specific parameter, the Returns only applies when this specific parameter is used.
The reason UpdateBatch works is because you're using the same reference to the same Batch in both the mock and the class under test:
MockSetupUpdateBatch(batch);
//Act
var postRefund = await UseCase.InvokeAsync(batch); // <---- Same "batch"
When your test code is calling RefundRequestRepository.GetsAsync you probably get different results than when the tested class calls GetsAsync, so the setup is not relevant for the calls of the tested class and probably returns the default bool value (false).
For more information on how to mock correctly refer to this GitHub page
I'm trying to use Polly as retry policy handler for grpc in my .net core 6 project. I noticed that the retryFunc is never invoked. I started from this project gRPC & ASP.NET Core 3.1: Resiliency with Polly
class Program
{
static async Task Main(string[] args)
{
// DI
var services = new ServiceCollection();
var loggerFactory = LoggerFactory.Create(logging =>
{
logging.AddConsole();
logging.SetMinimumLevel(LogLevel.Debug);
});
var serverErrors = new HttpStatusCode[] {
HttpStatusCode.BadGateway,
HttpStatusCode.GatewayTimeout,
HttpStatusCode.ServiceUnavailable,
HttpStatusCode.InternalServerError,
HttpStatusCode.TooManyRequests,
HttpStatusCode.RequestTimeout
};
var gRpcErrors = new StatusCode[] {
StatusCode.DeadlineExceeded,
StatusCode.Internal,
StatusCode.NotFound,
StatusCode.ResourceExhausted,
StatusCode.Unavailable,
StatusCode.Unknown
};
Func<HttpRequestMessage, IAsyncPolicy<HttpResponseMessage>> retryFunc = (request) =>
{
return Policy.HandleResult<HttpResponseMessage>(r => {
var grpcStatus = StatusManager.GetStatusCode(r);
var httpStatusCode = r.StatusCode;
return (grpcStatus == null && serverErrors.Contains(httpStatusCode)) || // if the server send an error before gRPC pipeline
(httpStatusCode == HttpStatusCode.OK && gRpcErrors.Contains(grpcStatus.Value)); // if gRPC pipeline handled the request (gRPC always answers OK)
})
.WaitAndRetryAsync(3, (input) => TimeSpan.FromSeconds(3 + input), (result, timeSpan, retryCount, context) =>
{
var grpcStatus = StatusManager.GetStatusCode(result.Result);
Console.WriteLine($"Request failed with {grpcStatus}. Retry");
});
};
services.AddGrpcClient<CountryServiceClient>(o =>
{
o.Address = new Uri("https://localhost:5001");
}).AddPolicyHandler(retryFunc);
var provider = services.BuildServiceProvider();
var client = provider.GetRequiredService<CountryServiceClient>();
try
{
var countries = (await client.GetAllAsync(new EmptyRequest())).Countries.Select(x => new Country
{
CountryId = x.Id,
Description = x.Description,
CountryName = x.Name
}).ToList();
Console.WriteLine("Found countries");
countries.ForEach(x => Console.WriteLine($"Found country {x.CountryName} ({x.CountryId}) {x.Description}"));
}
catch (RpcException e)
{
Console.WriteLine(e.Message);
}
}
}
but at the end WaitAndRetryAsync is never called.
I created a small project available on github in order to reproduce it.
My test is fairly simple. I start the client without a listening back-end, expecting to read 3 times the output from Console.WriteLine($"Request failed with {grpcStatus}. Retry"); on the console. But the policy handler in never fired. I have the following exception instead
Status(StatusCode="Unavailable", Detail="Error connecting to
subchannel.", DebugException="System.Net.Sockets.SocketException
(10061): No connection could be made because the target machine
actively refused it.
without any retry.
This is not working for you because Retry is now built into Grpc. In order to make this work, register your service as follows:
var defaultMethodConfig = new MethodConfig
{
Names = { MethodName.Default },
RetryPolicy = new RetryPolicy
{
MaxAttempts = 3,
InitialBackoff = TimeSpan.FromSeconds(3),
MaxBackoff = TimeSpan.FromSeconds(3),
BackoffMultiplier = 1,
RetryableStatusCodes =
{
// Whatever status codes you want to look for
StatusCode.Unauthenticated, StatusCode.NotFound, StatusCode.Unavailable,
}
}
};
var services = new ServiceCollection();
services.AddGrpcClient<TestServiceClient>(o => {
o.Address = new Uri("https://localhost:5001");
o.ChannelOptionsActions.Add(options =>
{
options.ServiceConfig = new ServiceConfig {MethodConfigs = {defaultMethodConfig}};
});
});
That will add the retry policy to your client. One other thing that you might run into. I didn't realize this at the time, but in my service implementation, I was setting up errors something like this:
var response = new MyServiceResponse()
// something bad happens
context.Status = new Status(StatusCode.Internal, "Something went wrong");
return response;
The retry logic will not kick in if you implement your service like that, you actually have to do something more like this:
// something bad happens
throw new RpcException(new Status(StatusCode.Internal, "Something went wrong"));
The retry logic you configured when registering your client will then work. Hope that helps.
With the help of #PeterCsala I tried some fix.
As a first attempt I tried without DependencyInjection, registering the policy as follows
var policy = Policy
.Handle<Exception>()
.RetryAsync(3, (exception, count) =>
{
Console.WriteLine($"Request {count}, {exception.Message}. Retry");
});
var channel = GrpcChannel.ForAddress("https://localhost:5001");
TestServiceClient client = new TestServiceClient(channel);
await policy.ExecuteAsync(async () => await client.TestAsync(new Empty()));
This way it's working.
Then I came back to DI and used to register the policy as follows
IAsyncPolicy<HttpResponseMessage> policy =
Policy<HttpResponseMessage>.Handle<Exception>().RetryAsync(3, (exception, count) =>
{
Console.WriteLine($"Request {count}, {exception.Exception.Message}. Retry");
});
var services = new ServiceCollection();
services.AddGrpcClient<TestServiceClient>(o => {
o.Address = new Uri("https://localhost:5001");
}).AddPolicyHandler(policy);
var provider = services.BuildServiceProvider();
var client = provider.GetRequiredService<TestServiceClient>();
var testClient = (await client.TestAsync(new Empty()));
And still not working.
At the end it seems AddPolicyHandler is not suitable for grpc clients?
Say, I have the following minimal API:
var builder = WebApplication.CreateBuilder(args);
// Routing options
builder.Services
.Configure<RouteOptions>(options =>
{
options.LowercaseUrls = true;
options.LowercaseQueryStrings = true;
});
await using var app = builder.Build();
// API
app.MapGet("/api/customers/{id:int}", async (VRContext db, int id) =>
await db.Customers.FindAsync(id) switch
{
{ } customer => Results.Ok(customer),
null => Results.NotFound(new { Requested_Id = id, Message = $"Customer not found." })
});
//app.MapControllers();
await app.RunAsync();
When I pass non-existing id, I get the following JSON:
{
"requested_Id": 15,
"message": "Customer not found."
}
The problem is that letter I in requested_Id is not lowercase, although I configured it in Configure<RouteOptions>. But when I start using fully-fledged controller, then I correctly get requested_id. How do I achieve the same with MapGet?
Configure the default Json options (Note the using for the correct namespace)
using Microsoft.AspNetCore.Http.Json;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<JsonOptions>(
options =>
{
options.SerializerOptions.PropertyNamingPolicy = new LowerCaseNamingPolicy();
});
await using var app = builder.Build();
// API
app.MapGet("/api/customers/{id:int}", (int id) =>
Results.NotFound(new { Requested_Id = id, Message = $"Customer not found." })
);
await app.RunAsync();
internal class LowerCaseNamingPolicy : JsonNamingPolicy
{
public override string ConvertName(string name) => name.ToLowerInvariant();
}
You can use below line fo code to convert all words in URL as lowercase
services.AddRouting(options => options.LowercaseUrls = true);
so I'm working on a mute and unmute command and what I want it to do is find if there is a role called "Muted" in the server if there is then give the user that role if there isn't then create the role with the necessary permissions. I've tried messing with bot permissions, role permissions, and hierarchy and it just doesn't do anything. There is no error given to me via Console nor is there an error generated in the text, it just simply seems to do nothing no matter what I try, can anyone see what I'm doing wrong? I created a pre-existing role called "Muted" and even with the role pre-applied it didn't add it. It also doesn't work while trying to remove the role if I manually added it to the user. This is what I've got:
[Command("mute")]
[Remarks("Mutes A User")]
[RequireUserPermission(GuildPermission.MuteMembers)]
public async Task Mute(SocketGuildUser user)
{
var UserCheck = Context.Guild.GetUser(Context.User.Id);
if (!UserCheck.GuildPermissions.MuteMembers)
{
await Context.Message.Channel.SendMessageAsync("", false, new EmbedBuilder()
{
Color = Color.LightOrange,
Title = "You dont have Permission!",
Description = $"Sorry, {Context.Message.Author.Mention} but you do not have permission to use this command",
Author = new EmbedAuthorBuilder()
{
Name = Context.Message.Author.ToString(),
IconUrl = Context.Message.Author.GetAvatarUrl(),
Url = Context.Message.GetJumpUrl()
}
}.Build());
}
else
{
await Context.Guild.GetUser(user.Id).ModifyAsync(x => x.Mute = true);
var muteRole = await GetMuteRole(user.Guild);
if (!user.Roles.Any(r => r.Id == muteRole.Id))
await user.AddRoleAsync(muteRole);//.ConfigureAwait(false);
}
}
[Command("unmute")]
[Remarks("Unmutes A User")]
[RequireUserPermission(GuildPermission.MuteMembers)]
public async Task Unmute(SocketGuildUser user)
{
var UserCheck = Context.Guild.GetUser(Context.User.Id);
if (!UserCheck.GuildPermissions.MuteMembers)
{
await Context.Message.Channel.SendMessageAsync("", false, new EmbedBuilder()
{
Color = Color.LightOrange,
Title = "You dont have Permission!",
Description = $"Sorry, {Context.Message.Author.Mention} but you do not have permission to use this command",
Author = new EmbedAuthorBuilder()
{
Name = Context.Message.Author.ToString(),
IconUrl = Context.Message.Author.GetAvatarUrl(),
Url = Context.Message.GetJumpUrl()
}
}.Build());
}
else
{
await Context.Guild.GetUser(user.Id).ModifyAsync(x => x.Mute = false).ConfigureAwait(false);
try { await user.ModifyAsync(x => x.Mute = false);/*.ConfigureAwait(false); */} catch { ReplyAsync("no"); }
try { await user.RemoveRoleAsync(await GetMuteRole(user.Guild));/*.ConfigureAwait(false); */} catch { ReplyAsync("No lmao"); }
}
}
public async Task<IRole> GetMuteRole(IGuild guild)
{
const string defaultMuteRoleName = "Muted";
var muteRoleName = "Muted";
var muteRole = guild.Roles.FirstOrDefault(r => r.Name == muteRoleName);
if (muteRole == null)
{
try
{
muteRole = await guild.CreateRoleAsync(muteRoleName, GuildPermissions.None, Color.Default, false, false);//.ConfigureAwait(false);
}
catch
{
muteRole = guild.Roles.FirstOrDefault(r => r.Name == muteRoleName) ?? await guild.CreateRoleAsync(defaultMuteRoleName, GuildPermissions.None, Color.Default, false, false);//.ConfigureAwait(false);
}
}
foreach (var toOverwrite in (await guild.GetTextChannelsAsync()))
{
try
{
if (!toOverwrite.PermissionOverwrites.Any(x => x.TargetId == muteRole.Id && x.TargetType == PermissionTarget.Role))
{
await toOverwrite.AddPermissionOverwriteAsync(muteRole, denyOverwrite);//.ConfigureAwait(false);
await Task.Delay(200);//.ConfigureAwait(false);
}
}
catch
{
}
}
return muteRole;
}
If anyone can help me that would be great, cheers!
What is the best way to handle dependent events such as;
There is an object for which I need to test if connection is succeeded or failed.
But the object first needs to pass the initialization step which I test for success or failure and then continue to connection step.
If initialization fails return is connection failed.
If initialization succeeds return is result of the connection step.
My code is below. Is there a better way to handle those dependent events because I'm subscribing for connection inside initialization subscription?
If I have more dependent events like this will I keep nesting the subscriptions?
public static void Test()
{
const int maxValue = 501;
var random = new Random(BitConverter.ToInt32(Guid.NewGuid().ToByteArray(), 0));
var initOk = Observable.Interval(TimeSpan.FromMilliseconds(random.Next(maxValue))).Select(i => true);
var initKo = Observable.Interval(TimeSpan.FromMilliseconds(random.Next(maxValue))).Select(i => false);
var connectOk = Observable.Interval(TimeSpan.FromMilliseconds(random.Next(maxValue))).Select(i => true);
var connectKo = Observable.Interval(TimeSpan.FromMilliseconds(random.Next(maxValue))).Select(i => false);
var initResult = initOk.Amb(initKo).Take(1);
var connectResult = connectOk.Amb(connectKo).Take(1);
var id =
initResult.Subscribe(ir =>
{
if (ir)
{
var cd =
connectResult.Subscribe(cr =>
{
Console.WriteLine(cr
? "Connection succeeded."
: "Connection failed.");
});
}
else
{
Console.WriteLine("Initialization failed thus connection failed.");
}
});
}
You can normally avoid nesting by utilising a variety of the rx operators to chain calls up.
Your example could be tidied up in using:
initResult.SelectMany(ir =>
{
if (ir != null)
{
return connectResult;
}
Console.WriteLine("Initialization failed thus connection failed.");
return Observable.Throw(new Exception("Some Exception"));
})
.Subscribe(cr =>
{
Console.WriteLine(cr != null
? "Connection succeeded."
: "Connection failed.");
})
You could use this:
var finalResult =
initResult
.Select(ir =>
Observable.If(() => ir, connectResult, Observable.Return(false)))
.Merge();
To get your messages out, you could change it like this:
var initResultText =
initResult
.Select(ir =>
ir ? (string)null : "Initialization failed thus connection failed.");
var connectResultText =
connectResult
.Select(cr =>
String.Format("Connection {0}.", cr ? "succeeded" : "failed"));
var finalResult =
initResultText
.Select(irt =>
Observable.If(() =>
irt == null, connectResultText, Observable.Return(irt)))
.Merge();
If you need to nest further than this you should consider making an extension method that wraps up the complexity and thus composition would be much easier.