Skip to content

Support Polling Clients behind a load balancer with a Redis backed Pending Request Queue - #665

Merged
LukeButters merged 153 commits into
mainfrom
luke/redis-queue-reimagined
Aug 22, 2025
Merged

Support Polling Clients behind a load balancer with a Redis backed Pending Request Queue#665
LukeButters merged 153 commits into
mainfrom
luke/redis-queue-reimagined

Conversation

@LukeButters

@LukeButters LukeButters commented Jul 28, 2025

Copy link
Copy Markdown
Contributor

Background

(In beta, don't use it in production!)

One of the issues with Polling Services in Halibut, is when we have a cluster of Clients (so many client nodes) and the polling service is configured to connect to one of those clients e.g. via a LB. The issue is the the client which has the work may not be the one that is connected to the service, and so the request is unable to be sent to the service.

image

The Redis Pending Request Queue solves this problem by having all Clients in the cluster, talk to a single Redis server. This allows any client to place a Request on to the Redis backed queue, and any client the service is connected to can retrieve that request from the queue.

image

This allows for multi node setups, where a polling service is connected to a single client yet any client can send work to the service.

See Redis Queue doco for more details.

How to review this PR

Quality ✔️

Pre-requisites

  • I have read How we use GitHub Issues for help deciding when and where it's appropriate to make an issue.
  • I have considered informing or consulting the right people, according to the ownership map.
  • I have considered appropriate testing for my change.

return true;
}

if (exception is HalibutClientException)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use model retryability without depending on specific exception messages?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the following comment:

// Sometimes the error occurs NOT on the node executing the RPC, e.g. the Node talking to tentacle.
                // In that case we need to look at error messages, since we won't have the original exception type.
                // We will also need to check error messages any time Error Responses are raised rather than a raw exception
                // bubbling out of the QueueAndWait method.

If we could have checked the Exception type itself, feel free to make that change.

Comment thread source/Halibut/HalibutRuntime.cs Outdated
var queue = queues.GetOrAdd(target, u => createdQueue = queueFactory.CreateQueue(target));
if (createdQueue != null && !ReferenceEquals(createdQueue, queue))
{
createdQueue.DisposeAsync();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not awaited?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that does seem strange but as it turns out that is fine, we don't care how that goes just long as it gets disposed. I will make that clearer in the code:

// We created a queue that won't be used, dispose of it in the background.
                Task.Run(() => Try.IgnoringError(() => createdQueue.DisposeAsync()));

// And it is impossible to deserialize the wrong type - any mismatched type will refuse to deserialize
class MessageEnvelope<T>
{
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can just provide a constructor here to avoid the pragmas.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file has a comment at the top describing why it is like it is.

I have also added:

// This class is copied from `MessageSerializer`, since this class tries to
        // use what it can from that battle tested class. That class had this envelope,
        // and so shall we, since we don't want to learn the hard way why it was like that :D.

I don't want to stray away from the MessageSerializer class, unless it gives us significant value.

Comment thread source/Halibut/Queue/Redis/RedisPendingRequestQueue.cs
Comment thread source/Halibut/Queue/Redis/RedisPendingRequestQueue.cs Outdated
Comment thread source/Halibut/Queue/Redis/RedisPendingRequestQueue.cs Outdated
Comment thread source/Halibut/Queue/Redis/RedisPendingRequestQueue.cs Outdated
}
}

public async ValueTask DisposeAsync()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're not relying in the async dispose pattern here, (i.e. we're calling DisopseAsync() directly), it would be more readable if we named this method with a more descriptive name for what it is doing.

Can it throw, given each task is already swallowing exceptions?
If not, we stop using Try.IgnoringError in client code that calls it.

@LukeButters LukeButters Aug 21, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I deleted this class.

Replaced the one usage with await Task.WhenAll(tasks.Select(t => Try.IgnoringError(() => t)));

Thanks revisiting this is useful.

@LukeButters
LukeButters marked this pull request as ready for review August 21, 2025 23:59
@LukeButters
LukeButters requested a review from a team as a code owner August 21, 2025 23:59

@rhysparry rhysparry left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ I've looked over this and have talked it through with Luke. Test coverage looks good and appropriate safety guards are in place.

Tests as they are should make refactoring safer in the future.

@LukeButters
LukeButters enabled auto-merge (squash) August 22, 2025 02:02
@LukeButters
LukeButters merged commit 73222bd into main Aug 22, 2025
17 checks passed
@LukeButters
LukeButters deleted the luke/redis-queue-reimagined branch August 22, 2025 02:17
@LukeButters LukeButters changed the title Redis queue reimagined Support Polling Clients behind a load balancer with a Redis backed Pending Request Queue Aug 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants