-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Threading review of SqsNotificationListener and Throttled Message Processing Strategy #422
Comments
Another concern is that if you have scaled out your application to run on multiple machines, each instance runs in it's own "world" and doesn't know about the other instances, so you could have one instance being too greedy and fetching all the messages from a queue, leaving the other instances "starved". |
Yeah, we pretty much have to trust that SQS distributes the messages fairly. A very similar issue is being able to throttle across multiple machines to prevent some downstream service from getting too much load. Consul has the concept of a distributed semaphore that could be used for that, and you could build your own |
IMHO, it is not worth trying to achieve accurate distributed consensus on which machine has the most resources to process the next message - in the time taken, you could probably have processed the messages instead. What we have to keep happening is that statistically, over time, they distribute the load fairly evenly. This is especially important under high load. |
I'd like to address the issue where we block under throttling and contention, this will only happen when you share an instance of We basically want line 24 to be awaited, this could be done by making this method Option 1public async Task StartWorker(Func<Task> action) // breaking change
{
var messageProcessingTask = new Task<Task>(() => ReleaseOnCompleted(action));
await _semaphore.WaitAsync();
messageProcessingTask.Start();
}
private async Task ReleaseOnCompleted(Func<Task> action)
{
try
{
await action();
}
finally
{
_semaphore.Release();
}
} One easy non-breaking alternative would be to move this wait into into the started task, like this: Options 2public void StartWorker(Func<Task> action)
{
var messageProcessingTask = new Task<Task>(() => ReleaseOnCompleted(action));
messageProcessingTask.Start(); // Could now be Task.Run
}
private async Task ReleaseOnCompleted(Func<Task> action)
{
await _semaphore.WaitAsync();
try
{
await action();
}
finally
{
_semaphore.Release();
}
} However this would now progress the listen loop to the Going back to option 1, we could add a new interface to not break the old one, but what would we call it? Maintaining 2 versions get more messy. I also feel like we would be building on top of a shaky foundation, because the interface is fundamentally flawed. It also seems wrong when you look at the signature, why should I'd welcome some thoughts on this, and other suggestions. Update I've just noticed that the |
@stuart-lang We've already decided that the next version of JustSaying will have some breaking changes, so this should be fine. |
The blocking in |
This has come up in recent discussions, so here is a braindump so that we don't lose this.
Looking at
IMessageProcessingStrategy
, it has the following members:Imagine we have the default
Throttled
implementation, with 10 max workers, and 6 workers in-flight (invoking handlers).When
SqsNotificationListener
wants to get messages it goes through the following process:AvailableWorkers
> 0? (if notawait
WaitForAvailableWorkers
)Here, yes, we have 4 available. We can continue.
StartWorker
with the handler (wrapped in a few bits and bobs).AvailableWorkers
> 0? No, nowawait
WaitForAvailableWorkers
In isolation, this process is alright, and seems sensible. Where it gets tricky is when you share the
IMessageProcessingStrategy
and have multipleSqsNotificationListener
going through this same workflow.However, it is quite likely you will want to share an instance of
IMessageProcessingStrategy
, as you might want a concurrency level across multiple queues, or for your whole application.There's a few problems with the
IMessageProcessingStrategy
interface as it stands when you have multiple listeners interacting with it:WaitForAvailableWorkers
step.They will all be released simultaneously and race.
They will all read the
AvailableWorkers
value, and likely see the same number (lets say 1).Once they receive the message, they will all try to call
StartWorker
, the first succeeding.In an old version of JustSaying (early 4), this race condition would cause
Throttled
to lose count, which wouldn't end well.In recent version, subsequent attempts to call
StartWorker
will block until there are available workers. (Blocking threadpool threads is bad)WaitForAvailableWorkers
, then they check theAvailableWorkers
count to know how many messages to request from SQS.When reading
AvailableWorkers
, this value could have changed and now might be 0, in which case an exception is thrown, logged and the loop continues.The
Throttled
implementation usesSemaphoreSlim
internally, this lets us guarantee the count is maintained in a thread safe way, and we can wait on it both synchronously and asynchronously.StartWorker
uses aTask
constructor to wrap the async work, then started. This will the task on the threadpool. This behaves the same asTask.Run
, but just less familiar.My view is that the
StartWorker
implementation behaves as we desire (with exception of the blocking scenario), it may just allocate more than it needs to.The throttling logic is a bit broken in it's design and could do with some drastic rethinking, and simplifying.
The text was updated successfully, but these errors were encountered: