Limit how many RabbitMQ messages I get from the server - c#

I have written a wrapper around RabbitMQ, and everything is working fine, too well actually, I am receiving messages quicker than I can process them. How do I limit how many messages I get from the queue or better, only consume and process one at a time?
public void Consume()
{
if (_channel != null)
{
// setup a listener for new messages
var consumer = new EventingBasicConsumer(_channel);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
var evt = new MessageEventArgs(body, message);
OnMessageReceived(evt);
};
_channel.BasicConsume(queue: _queue, autoAck: true, consumer: consumer);
}
}

To limit the messages consumed
//limit to 5 messages
channel.BasicQos(0, 5, false);
After this you can call the BasicConsume method with noAck parameter to false.
channel.BasicConsume(queue: _queue, autoAck: false, consumer: consumer);

Related

C# RabbitMQ why is the second worker not picking up work?

I am trying to understand all the RabbitMQ options what I think I want its just a worker queue so I have one queue and workers just take off one item and process it.
I create a new Direct Exchange (I think that is right!?)
Firstly I would like to know why in this example, I add 4 new messages to the exchange/queue. I don't start any workers. I then start the first worked and then the second, but the second one does not process any and the first worked processes them all!?
What am I doing wrong, why does this not work? please find the full example code below.
I also don't seem to have the publish confirms working right, as only sometimes on the right most output does it say "Message Acknowledged..."
I have read https://www.rabbitmq.com/tutorials/tutorial-two-dotnet.html and gone through the other pages that follow, but it's not hugely clear.
The emitter:
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Text;
using RabbitMQ.Client;
namespace PublishConfirms.Emit
{
class Program
{
public static ConcurrentDictionary<ulong, string> _outstandingConfirms = new ConcurrentDictionary<ulong, string>();
static void Main(string[] args)
{
var factory = new ConnectionFactory
{
HostName = "localhost"
};
using (var connection = factory.CreateConnection())
{
using (var channel = connection.CreateModel())
{
// We need to enable published confirms on the channel
channel.ConfirmSelect();
channel.ExchangeDeclare(exchange: "DirectExchange",
type: ExchangeType.Direct);
var queueName = "DirectExchangeQueue";
// Make sure to create the queue in case it doesn't exits
channel.QueueDeclare(queue: queueName,
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
channel.BasicAcks += (sender, ea) =>
{
Console.WriteLine("Message Acknowledged with Delivery Tag {0}", ea.DeliveryTag);
// message is confirmed
CleanOutstandingConfirms(ea.DeliveryTag, ea.Multiple);
};
channel.BasicNacks += (sender, ea) =>
{
// message is nack-ed (messages that have been lost
_outstandingConfirms.TryGetValue(ea.DeliveryTag, out string body);
Console.WriteLine($"Message with body {body} has been nack-ed. Sequence number: {ea.DeliveryTag}, multiple: {ea.Multiple}");
CleanOutstandingConfirms(ea.DeliveryTag, ea.Multiple);
};
var message = "A message here";
var body = Encoding.UTF8.GetBytes(message);
_outstandingConfirms.TryAdd(channel.NextPublishSeqNo, message);
var properties = channel.CreateBasicProperties();
properties.Persistent = true;
channel.BasicPublish(exchange: "",
routingKey: queueName,
basicProperties: properties,
body: body);
Console.WriteLine("Sent message '{0}'", message);
}
}
Console.WriteLine("Press any key to exit");
Console.ReadLine();
}
static void CleanOutstandingConfirms(ulong sequenceNumber, bool multiple)
{
if (multiple)
{
var confirmed = _outstandingConfirms.Where(k => k.Key <= sequenceNumber);
foreach (var entry in confirmed)
{
_outstandingConfirms.TryRemove(entry.Key, out _);
}
}
else
{
_outstandingConfirms.TryRemove(sequenceNumber, out _);
}
}
}
}
The worker/receiver
using System;
using System.Text;
using System.Threading;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
namespace PublishConfirms.Receive
{
class Program
{
// We can run multiples of these and only one will get a messages from the queue with no sharing
static void Main(string[] args)
{
var factory = new ConnectionFactory
{
HostName = "localhost"
};
using (var connection = factory.CreateConnection())
{
using (var channel = connection.CreateModel())
{
// This will create the exchange if needed
channel.ExchangeDeclare(exchange: "DirectExchange",
type: ExchangeType.Direct);
var queueName = "DirectExchangeQueue";
// Make sure to create the queue in case it doesn't exits
channel.QueueDeclare(queue: queueName,
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
channel.QueueBind(queue: queueName,
exchange: "DirectExchange",
routingKey: ""); // We keep the routing key the same as we dont want different handlers
// If we were to have different routes then we
// would most probably have to create a random queue e.g.
// var queueName = channel.QueueDeclare().QueueName;
Console.WriteLine("Waiting for messages...");
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine("Received message: {0}", message);
Console.WriteLine("Processing...");
Thread.Sleep(3000); // simulate some work
Console.WriteLine("Processing Complete");
// send an acknowledgement back
channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
};
channel.BasicConsume(queue: queueName,
autoAck: false,
consumer: consumer);
Console.WriteLine("Press any key to exit");
Console.ReadLine();
}
}
}
}
}
Your problem lays in understanding how the Direct exchange works, and probably the queue binding keys and routing keys as well.
A Direct exchange deliveries its messages to the queues whose binding key exactly matches the routing key of the message.
The binding key is the key the queue is binding to the exchange by.
The routing key is the key the message sent is routed by
Therefore the problem with your code is either:
With publishing your messages:
...
channel.BasicPublish(exchange: "",
routingKey: "", //
basicProperties: properties,
body: body);
...
Or you could change the binding key when creating the queue:
...
channel.QueueBind(queue: queueName,
exchange: "DirectExchange",
routingKey: queueName); // As you stated in your code that
// the queue name is your bindingKey
...
You can choose whichever one you prefer.
Other than that, when you publish your message you do it to an empty exchange name,
it should be:
channel.BasicPublish(exchange: "DirectExchange", // As you stated at the start of your code
routingKey: queueName,
basicProperties: properties,
body: body);
I'd suggest reading up more about RabbitMQ and understanding more of the concepts about their implementation.
You may want to see references about:
Fanout Exchange
Routing
I hope that helped.
After reading what itama said I had another look and firstly the BasicPublish exchange was an empty string so that would have not helped! I decided against using the routing key as the queue name and kept it as an empty string.
For me I wanted an exchange with a single queue and then multiple workers that would handle one item from the queue at a time.
The issue with it only being handled by one worked seemed to be resolved when I added in this piece of code on the worker.
channel.BasicQos(0, 1, false);
After adding this the other workers got a message each. I ended up adding 50-100 messages to the queue and then started 10 workers, they all got a queue item and processed it which was nice to see.
For the Publish Confirm BasicAcks and BasicNacks callbacks I found this link https://rianjs.net/2013/12/publisher-confirms-with-rabbitmq-and-c-sharp this states that we need to call the channel.WaitFormConfirmsOrDie() after making the BasicPublish call, previously my code was not waiting for an acknowledgement.
The full code is as follows:
The emitter/producer:
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Text;
using RabbitMQ.Client;
namespace PublishConfirms.Emit
{
class Program
{
public static ConcurrentDictionary<ulong, string> _outstandingConfirms = new ConcurrentDictionary<ulong, string>();
static void Main(string[] args)
{
var factory = new ConnectionFactory
{
HostName = "localhost"
};
using (var connection = factory.CreateConnection())
{
using (var channel = connection.CreateModel())
{
// -------------------------------------------------------------------
// Setup the Exchange and Queue then bind the queue to the exchange
// -------------------------------------------------------------------
// This will create the exchange if needed
channel.ExchangeDeclare(exchange: "DirectExchange",
type: ExchangeType.Direct);
var queueName = "DirectExchangeQueue";
// Make sure to create the queue in case it doesn't exits
channel.QueueDeclare(queue: queueName,
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
channel.QueueBind(queue: queueName,
exchange: "DirectExchange",
routingKey: "");
// -------------------------------------------------------------------
// Setup Publish Confirms
// -------------------------------------------------------------------
channel.BasicAcks += (sender, ea) =>
{
Console.WriteLine("Message Acknowledged with Delivery Tag {0}", ea.DeliveryTag);
// message is confirmed
CleanOutstandingConfirms(ea.DeliveryTag, ea.Multiple);
};
channel.BasicNacks += (sender, ea) =>
{
// message is nack-ed (messages that have been lost)
_outstandingConfirms.TryGetValue(ea.DeliveryTag, out string body);
Console.WriteLine($"Message with body {body} has been nack-ed. Sequence number: {ea.DeliveryTag}, multiple: {ea.Multiple}");
CleanOutstandingConfirms(ea.DeliveryTag, ea.Multiple);
};
// We need to enable published confirms on the channel
channel.ConfirmSelect();
// -------------------------------------------------------------------
// Setup The message and add it to the ConcurrentDictionary, so we can
// Remove it when the BasicAcks is called
// -------------------------------------------------------------------
var message = "YOUR MESSAGE HERE";
var body = Encoding.UTF8.GetBytes(message);
var nextPublishSequenceNo = channel.NextPublishSeqNo;
Console.WriteLine("Next Publish Sequenece Number: {0}", nextPublishSequenceNo);
_outstandingConfirms.TryAdd(nextPublishSequenceNo, message);
// Make sure the message is written to disk as soon as it reaches the queue
// Imagine this will be slower but safer, this is also stored in memory if there is no memory pressure!
var properties = channel.CreateBasicProperties();
properties.Persistent = true;
channel.BasicPublish(exchange: "DirectExchange",
routingKey: "",
basicProperties: properties,
body: body);
channel.WaitForConfirmsOrDie();
Console.WriteLine("Sent message '{0}'", message);
}
}
}
static void CleanOutstandingConfirms(ulong sequenceNumber, bool multiple)
{
if (multiple)
{
var confirmed = _outstandingConfirms.Where(k => k.Key <= sequenceNumber);
foreach (var entry in confirmed)
{
_outstandingConfirms.TryRemove(entry.Key, out _);
}
}
else
{
_outstandingConfirms.TryRemove(sequenceNumber, out _);
}
}
}
}
The receiver/worker:
using System;
using System.Text;
using System.Threading;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
namespace PublishConfirms.Receive
{
class Program
{
// We can run multiples of these and only one will get a messages from the queue with no sharing
static void Main(string[] args)
{
var factory = new ConnectionFactory
{
HostName = "localhost"
};
using (var connection = factory.CreateConnection())
{
using (var channel = connection.CreateModel())
{
// -------------------------------------------------------------------
// Setup the Exchange and Queue then bind the queue to the exchange
// -------------------------------------------------------------------
// This will create the exchange if needed
channel.ExchangeDeclare(exchange: "DirectExchange",
type: ExchangeType.Direct);
var queueName = "DirectExchangeQueue";
// Make sure to create the queue in case it doesn't exits
channel.QueueDeclare(queue: queueName,
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
// Only dispatch one message at a time to a worked and wait for its acknowledgement
// Adding this in seems to correct the issue where only one worker would pick
// up the queued items
channel.BasicQos(0, 1, false);
// We keep the routing key the same as we don't want different handlers
// If we were to have different routes then we
// would most probably have to create a random queue e.g.
// var queueName = channel.QueueDeclare().QueueName;
channel.QueueBind(queue: queueName,
exchange: "DirectExchange",
routingKey: "");
Console.WriteLine("Waiting for messages...");
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine("Received message: {0}", message);
Console.WriteLine("Processing...");
Thread.Sleep(3000); // simulate some work
Console.WriteLine("Processing Complete");
// send an acknowledgement back
channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
};
channel.BasicConsume(queue: queueName,
autoAck: false,
consumer: consumer);
Console.WriteLine("Press any key to exit");
Console.ReadLine();
}
}
}
}
}

Using RabbitMQ in ASP.NET Core MVC

I am writing an ASP.NET Core MVC app with RabbitMQ. I am able to implement pub/sub patterns successfully. However, I am facing an issue to show subscribe message on the App.
Connection method on HomeController:
[HttpPost]
public IActionResult Index(string serverName, string userName, string password, string port)
{
RabbitMq rabbitMq = new RabbitMq();
var pubConnection = rabbitMq.CreateConnection(serverName, userName, password, port);
if (!pubConnection.IsOpen)
{
ViewBag.NotConnected = "Not Connected. Try Again.";
return View();
}
var subConnection = rabbitMq.CreateConnection(serverName, userName, password, port);
MyClient myClient = new MyClient();
myClient.CreatePublisher(pubConnection);
myClient.CreateSubscriber(subConnection);
return RedirectToAction(actionName: "Index", controllerName: "MyClientXYZ", routeValues: null);
}
Subscriber method on MyClient class :
public void CreateSubscriber(IConnection pubSubConnection)
{
var channel = pubSubConnection.CreateModel();
channel.ExchangeDeclare("MyExchange", ExchangeType.Fanout, durable: true, true);
channel.ExchangeBind("MyExchange", "ClientExchange", "#", null);
var slotQueueName = channel.QueueDeclare("my queue", true, autoDelete: true).QueueName;
channel.QueueBind(slotQueueName, "MyExchange", routingKey: "");
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine(" [x] {0}", message);
};
channel.BasicConsume(queue: slotQueueName,
autoAck: true,
consumer: consumer);
}
I am able to print subscriber messages on the console. But I need to show this message on the UI (View, any JS popup, etc.) I tried many ways like set subscribe message into TempData["subMessage"], redirectToAction(), open JS popup to show subscriber message. But, I couldn't do.
I just want to show the subscriber message on the UI when the callback method executes.
This is not right. RabbitMQ connections, and consumers, are long lived entities; not something ephemeral that you initiate in a controller action.
The short answer is to use a framework on top of RabbitMQ. Here's two good ones to consider:
https://masstransit-project.com/
https://easynetq.com/
The long answer is to roll your own RabbitMQ infrastructure. You do this if you are an expert and have some special need. In that case, maybe start here:
https://www.rabbitmq.com/tutorials/tutorial-one-dotnet.html
The other answer is to completely re-evaluate what you are trying to achieve.
Good luck.

Cancel Session ServiceBus consumer actvity from publisher

I am dispatching a message from the service bus publisher, & essentially doing a db polling for 5 seconds.
var message = new Message(Encoding.UTF8.GetBytes(object));
message.SessionId = sessionId;
return client.SendAsync(message);
On the consumer side, I am receiving the message and doing some processing which may or may not take more than 5 seconds. We can call CompleteAsync(), AbadonAsync() to close that session.
Can we cancel from the publisher client?
Essentially, I want something like such
queueClient.publish(message); // publish message to queue
var tokenSource - new CancellationToken();
var task = Task.Run(()=>{
...
}, tokenSource.Token);
if(!task.Wait(5000, tokenSource.Token))
{
queueClient.Cancel(); // <-----Cancel/Abadon processing in service bus consumer.
}
You can try configuring the timeout for the queue client
MessagingFactorySettings factorySettings = new MessagingFactorySettings()
{
TokenProvider = TokenProvider.CreateSharedAccessSignatureTokenProvider("[keyName]", "[sharedAccessKey]"),
OperationTimeout = TimeSpan.FromSeconds(5),
};
MessagingFactory messagingFactory = MessagingFactory.Create(serviceBusUri, factorySettings);
QueueClient Client = messagingFactory.CreateQueueClient("[QueueName]");

ASP.NET MVC - View is not returned until asynchonous emails sending is completed

I have a Contact page where users are able to complete a form which is sent to a Controller.
This Controller generates an email based on form data and returns a success view to the user.
The problem is that the success view is not displayed until email sending is completed, but this takes too much time (around 30 seconds), which is not acceptable.
First, I tried synchronous sending:
public ActionResult Contact(string TestMessage)
{
// Prepare SMTP client
SmtpClient Client = new SmtpClient()
{
DeliveryMethod = SmtpDeliveryMethod.Network,
EnableSsl = true,
Host = "smtp.test.com",
Port = 587,
UseDefaultCredentials = false,
Credentials = new System.Net.NetworkCredential("smtpuser", "smtppass")
};
// Prepare message
MailMessage MailMessage = new MailMessage()
{
From = new MailAddress("sender#mydomain.com"),
Subject = "Test",
BodyEncoding = System.Text.Encoding.UTF8,
SubjectEncoding = System.Text.Encoding.UTF8
};
MailMessage.To.Add(new MailAddress("recipient#mydomain.com"));
MailMessage.Body = TestMessage;
// Send mail
Client.Send(MailMessage);
// Return success view
return View("ContactSuccess");
}
After that I tried asynchonous sending. I have spent hours reading StackOverflow questions to find the miracle solution. But the result is always the same, page is still waiting for email sending to be completed.
Asynchronous attempt 1
// Send mail
Client.SendCompleted += (s, e) =>
{
Client.Dispose();
MailMessage.Dispose();
};
System.Threading.ThreadPool.QueueUserWorkItem(o =>
Client.SendAsync(MailMessage, Tuple.Create(Client, MailMessage)));
Asynchronous attempt 2
System.Threading.Tasks.Task.Run(() => SendViaAsync(MailMessage));
Asynchronous attempt 3
System.Web.Hosting.HostingEnvironment.QueueBackgroundWorkItem(
cancellationToken => Client.Send(MailMessage)
);
Asynchronous attempt 4
// Send mail
System.Threading.ThreadPool.QueueUserWorkItem(SendViaAsync, MailMessage);
With added methods:
private void SendViaAsync(object MailMessageObject)
{
MailMessage MailMessage = (MailMessage)MailMessageObject;
SmtpClient Client = Utilities.CreateEmailClient();
Client.SendCompleted += new SendCompletedEventHandler(SmtpClient_SendCompleted);
Client.Send(MailMessage);
}
void SmtpClient_SendCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
{
MailMessage mail = e.UserState as MailMessage;
if (!e.Cancelled && e.Error != null)
{
// error handling
}
}
Asynchronous attempt 5
public async System.Threading.Tasks.Task<ActionResult> Contact(string TestMessage)
// ...
// Send mail
await SendEmail(MailMessage);
With added method:
public async System.Threading.Tasks.Task SendEmail(MailMessage MailMessage)
{
SmtpClient Client = new SmtpClient()
{
DeliveryMethod = SmtpDeliveryMethod.Network,
EnableSsl = true,
Host = "smtp.test.com",
Port = 587,
UseDefaultCredentials = false,
Credentials = new System.Net.NetworkCredential("smtpuser", "smtppass")
};
await Client.SendMailAsync(MailMessage);
}
Please note that when locally testing, email is sent immediately (in both synchronous or asynchronous modes). In production only, it takes around 30 seconds. So maybe there is something like an antivirus scan in my hosting provider, but anyway I can't figure out why the success view is not sent until mail sending is completed.
I am desperate. Any help would be much appreciated.
Any attempts with await will definitely fail to accomplish what you want by definition. But in general I don't think it's wise to have an asp.net page starting up processes which may keep running beyond the lifetime of the request. And perhaps that's exactly what's happening internally: ASP.Net not finalising the request until all its child processes have finished.
Personally, I would de-couple the process altogether. First because each part will be simpler to maintain, but also because it's more secure.
Have your application dump the email, or the data to build the email, into a queue of sorts. This could be a database, a file, a folder or whatever, but some temporary storage.
Then develop a background service or daemon, which reads this queue and processes the emails. It will end up being more flexible, more scalable and safer.
I think I have understood the problem. I can't believe it.
I didn't mention it in my bug reproduction, but I am using Google reCAPTCHA in the contact form.
It seems that reCAPTCHA is very slow in production environment!
After deactivating it, it appears that email is immediately sent.
Now, I just have to replace reCAPTCHA with a faster checking mecanism.
Sorry for disturbance, I hope this will help some of us later. At least some asynchronous programming examples are listed here.
There is a builtin functionality in asp net you can use it as easy as written below
public ActionResult SendEmail(User user)
{
HostingEnvironment.QueueBackgroundWorkItem(ct =>
{
// Prepare SMTP client
SmtpClient Client = new SmtpClient()
{
DeliveryMethod = SmtpDeliveryMethod.Network,
EnableSsl = true,
Host = "smtp.test.com",
Port = 587,
UseDefaultCredentials = false,
Credentials = new System.Net.NetworkCredential("smtpuser", "smtppass")
};
// Prepare message
MailMessage MailMessage = new MailMessage()
{
From = new MailAddress("sender#mydomain.com"),
Subject = "Test",
BodyEncoding = System.Text.Encoding.UTF8,
SubjectEncoding = System.Text.Encoding.UTF8
};
MailMessage.To.Add(new MailAddress("recipient#mydomain.com"));
MailMessage.Body = TestMessage;
// Send mail
Client.Send(MailMessage);
});
// Return success view
return View("ContactSuccess");
}
if you want to go over advance scenarios , like handling many background jobs you can use Hangfire library
this blog post will also help you solve your problem

Azure Service Bus "The lock supplied is invalid" after message work successfully completed

I've an Azure Web Job with the following initialization code
class Program
{
static void Main(string[] args)
{
IHostBuilder builder = new HostBuilder()
.ConfigureWebJobs(b =>
{
b.AddServiceBus((opt) =>
{
opt.ConnectionString = "Connection string";
opt.MessageHandlerOptions = new MessageHandlerOptions((ex) =>
{
return Task.Run(() =>
{
// logging the error message
});
})
{
MaxAutoRenewDuration = new TimeSpan(0, 0, 5, 0),
MaxConcurrentCalls = 1
};
});
})
.UseConsoleLifetime();
IHost host = builder.Build();
using (host)
{
host.Run();
}
}
}
The Service Bus queue is configured to have a Lock Duration of 5 minutes, that is the maximum time that Azure allows.
The message processing can take more than 30 minutes and the lock renew mechanism works correctly.
When the process ends correctly, an exception is thrown The lock supplied is invalid. Either the lock expired, or the message has already been removed from the queue, or was received by a different receiver instance and the message goes back to the queue again.
When you call messsage.Complete() (or CompleteAsync()) then you should instantiate an MessageHandlerOptions object, set AutoComplete to false, and pass it into your message handler registration.
new MessageHandlerOptions(OnException)
{
AutoComplete = false,
MaxConcurrentCalls = MaxConcurrentCalls, // 1
MaxAutoRenewDuration = MaxAutoRenewDuration // 2 hrs
}
For more details, you could refer to this article.

Categories

Resources