Skip to content

Conversation

@rylev
Copy link
Collaborator

@rylev rylev commented Oct 2, 2025

Fixes #3281

Wraps outbound requests in a semaphore with configurable permit size. This allows runtime config to decide how many outbound requests may be made at a give time.

@rylev rylev requested review from dicej and lann October 2, 2025 10:19
@lann
Copy link
Collaborator

lann commented Oct 2, 2025

It just occured to me how we could limit actual connections instead of requests:

struct PermittedTcpStream {
  inner: TcpStream,
  permit: OwnedSemaphorePermit,
}

// simple wrappers just like our existing `RustlsStream`
impl AsyncRead for PermittedTcpStream { ... }
impl AsyncWrite for PermittedTcpStream { ... }

The permit would be acquired out in RequestSender::send_request and passed in to ConnectOptions and used to construct the PermittedTcpStream in HttpConnector. The permit would automatically be dropped when the stream is dropped.

@rylev rylev force-pushed the max-number-requests branch from f20022c to d9fab0e Compare October 6, 2025 12:35
@rylev rylev requested a review from dicej October 6, 2025 12:40
rylev added 2 commits October 6, 2025 14:40
Signed-off-by: Ryan Levick <ryan.levick@fermyon.com>
Signed-off-by: Ryan Levick <ryan.levick@fermyon.com>
@rylev rylev force-pushed the max-number-requests branch from d9fab0e to 68e5471 Compare October 6, 2025 12:43
Copy link
Contributor

@dicej dicej left a comment

Choose a reason for hiding this comment

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

LGTM, thanks!

Comment on lines +63 to +65
// Permit count is the max concurrent connections + 1.
// i.e., 0 concurrent connections means 1 total connection.
.map(|n| Arc::new(Semaphore::new(n + 1))),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not a huge deal since we're talking about rather large limits but I don't understand this interpretation. I would expect "0 concurrent connections" to mean "you can't ever connect" (which should perhaps be invalid).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I guess I have a different interpretation. A concurrent request is by definition one that is happening during another request. 0 concurrent requests would therefore mean "no requests happening at the same time" not "no requests at all". I think we should just pick which ever interpretation is most convenient and use that.

Copy link
Collaborator

Choose a reason for hiding this comment

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

How many requests are involved in "2 concurrent requests"?

Copy link
Collaborator

Choose a reason for hiding this comment

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

A concurrent request is by definition one that is happening during another request

That is strictly true, but I think that doing the +1 makes everything else more confusing. The first request becomes concurrent once there is two total requests.

I'd vote for removing the + 1

Comment on lines 460 to 464
// If we're limiting concurrent outbound requests, acquire a permit
let permit = match self.concurrent_outbound_connections_semaphore {
Some(s) => s.acquire_owned().await.ok().map(Arc::new),
None => None,
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this needs to be moved down into ConnectOptions::connect_tcp or it will still be limiting requests instead of connections.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah yea of course!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Should be good now: a472018

Signed-off-by: Ryan Levick <ryan.levick@fermyon.com>
@rylev rylev requested a review from lann October 7, 2025 09:26
@rylev
Copy link
Collaborator Author

rylev commented Oct 15, 2025

@lann ping on a review.

Copy link
Collaborator

@lann lann left a comment

Choose a reason for hiding this comment

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

I think this can land without it but we probably ought to add some metrics around this like a gauge of available leases and a counter for when it can't immediately acquire a lease.


// If we're limiting concurrent outbound requests, acquire a permit
// Note: since we don't have access to the underlying connection, we can only
// limit the number of concurrent requests, not connections.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Teeechnically we probably could via https://docs.rs/reqwest/0.12.24/reqwest/struct.ClientBuilder.html#method.connector_layer but given that this interface is effectively deprecated this seems fine.

@lann
Copy link
Collaborator

lann commented Oct 15, 2025

metrics

Tracking: #3307

@rylev rylev merged commit 463a3ff into main Oct 20, 2025
17 checks passed
@rylev rylev deleted the max-number-requests branch October 20, 2025 09:21
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.

Limit the number of concurrent outbound HTTP requests

5 participants