Skip to content
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

fix: pass pool_timer to hyper_util to enable the idle cleanup task #2434

Merged
merged 2 commits into from
Sep 30, 2024

Conversation

RobMor
Copy link
Contributor

@RobMor RobMor commented Sep 26, 2024

While testing an application I'm working on that makes requests to many different endpoints I found that reqwest wasn't cleaning up pooled connections even after the pool_idle_timeout.

After investigating it seems like the hyper_util connection pool is bailing on this condition: https://github.com/hyperium/hyper-util/blob/master/src/client/legacy/pool.rs#L429-L433. This seems to be because reqwest doesn't pass a pool_timer.

Reproduction:

server.rs:

use warp::Filter;

#[tokio::main]
async fn main() {
    simple_logger::init_with_level(log::Level::Info).unwrap();

    let basic = warp::filters::path::end()
        .map(|| "hello");

    let server = warp::serve(basic)
        .run(([127, 0, 0, 1], 8000));

    server.await;
}

client.rs:

#[tokio::main]
async fn main() {
    simple_logger::init_with_level(log::Level::Trace).unwrap();

    let client = reqwest::Client::builder()
        .pool_idle_timeout(std::time::Duration::from_secs(5))
        .build()
        .unwrap();

    client.get("http://localhost:8000").send().await.unwrap();

    tokio::time::sleep(std::time::Duration::from_secs(10)).await;

    client.get("http://localhost:8000").send().await.unwrap();
}

Output, using master:

2024-09-26T08:27:19.715Z DEBUG [reqwest::connect] starting new connection: http://localhost:8000/
...
2024-09-26T08:27:29.719Z TRACE [hyper_util::client::legacy::pool] removing expired connection for ("http", localhost:8000)
...
2024-09-26T08:27:29.719Z DEBUG [reqwest::connect] starting new connection: http://localhost:8000/

(connection is only cleaned up when creating a new connection)

Output, after this commit:

2024-09-26T08:26:10.285Z DEBUG [reqwest::connect] starting new connection: http://localhost:8000/
...
2024-09-26T08:26:15.289Z TRACE [hyper_util::client::legacy::pool] idle interval evicting expired for ("http", localhost:8000)
...
2024-09-26T08:26:20.289Z DEBUG [reqwest::connect] starting new connection: http://localhost:8000/

(connection is actively cleaned up after the idle timeout by the IdleTask)

This is important for applications that make requests to many different endpoints. If these old connections don't get cleaned up we run into memory and file descriptor issues.

@RobMor
Copy link
Contributor Author

RobMor commented Sep 26, 2024

I'm not an expert in the history here but it seems like this was just missed during the hyper v1 upgrade. It looks like at that time the separate pool_timer builder option was already in place.

Any ideas on how one could add some kind of automated test case around this behavior? Maybe I could add a test that spins up a server, makes a request using a client with a low idle timeout, and asserts that after that idle timeout the connection was closed from the servers perspective.

@RobMor
Copy link
Contributor Author

RobMor commented Sep 26, 2024

Also, was wrapping both in the wasm32 check the right thing to do? Not really sure what that was there for.

@seanmonstar
Copy link
Owner

Oh wow, thanks for fixing this!

Any ideas on how one could add some kind of automated test case around this behavior?

Coming up with an integration test in reqwest is likely hard, since there's no way for you access the details outside the library. However, I'm thinking the mistake is in hyper-util: it should notice when the timeouts are set but not timer is set (hyper does the same for its timeouts).

Also, was wrapping both in the wasm32 check the right thing to do?

Um, that seems like it previously wasn't doing anything. None of that code is ever configured on for WASM, so that extra cfg doesn't matter. We can probably just remove it!

@RobMor
Copy link
Contributor Author

RobMor commented Sep 27, 2024

there's no way for you access the details outside the library

I was thinking maybe we could spin up a dummy server for the test. That would give us access to the server side connection state where we should be able to see that the connection gets closed after the idle timeout.

I'm thinking the mistake is in hyper-util: it should notice when the timeouts are set but not timer is set

Curious on how one would implement a check like this? It seems like it would be a breaking change to start panicking or erroring on previously "working" code.

We can probably just remove it!

Will do

@seanmonstar
Copy link
Owner

Curious on how one would implement a check like this? It seems like it would be a breaking change to start panicking or erroring on previously "working" code.

Yea... hyper starting at 1.0 would cause a panic if a timer is missing but timeouts were configured. It'd be unfortunate to suddenly panic in a patch version of hyper-util. Sigh, I'm bummed that I messed up the timer design, but oh well...

@RobMor
Copy link
Contributor Author

RobMor commented Sep 27, 2024

I've added a test! I confirmed that this test fails without this change, and passes with it...

Let me know what you think of it.

@RobMor RobMor force-pushed the master branch 2 times, most recently from 9b1ac81 to 1a7df7c Compare September 27, 2024 18:46
@RobMor
Copy link
Contributor Author

RobMor commented Sep 27, 2024

Realized I didn't need to use tokio's unbounded_channel, and also had to fix some formatting. Should be good now...

@RobMor
Copy link
Contributor Author

RobMor commented Sep 27, 2024

Agh, tests should be passing with the http3 feature now too.

@seanmonstar seanmonstar merged commit baf9712 into seanmonstar:master Sep 30, 2024
36 checks passed
@RobMor
Copy link
Contributor Author

RobMor commented Sep 30, 2024

Thanks for merging! When do you expect the next release will happen?

@seanmonstar
Copy link
Owner

Published v0.12.8 just now :)

kodiakhq bot pushed a commit to pdylanross/fatigue that referenced this pull request Oct 1, 2024
Bumps reqwest from 0.12.7 to 0.12.8.

Release notes
Sourced from reqwest's releases.

v0.12.8
What's Changed

Add support for SOCKS4 proxies.
Add multipart::Form::file() method for adding files easily.
Add Body::wrap() to wrap any http_body::Body type.
Fix the pool configuration to use a timer to remove expired connections.

New Contributors

@​workingjubilee made their first contribution in seanmonstar/reqwest#2402
@​NaokiM03 made their first contribution in seanmonstar/reqwest#2106
@​Xuanwo made their first contribution in seanmonstar/reqwest#2255
@​Jaltaire made their first contribution in seanmonstar/reqwest#2400
@​Hyask made their first contribution in seanmonstar/reqwest#2418
@​Jake-Shadle made their first contribution in seanmonstar/reqwest#2427
@​RobMor made their first contribution in seanmonstar/reqwest#2434

Full Changelog: seanmonstar/reqwest@v0.12.7...v0.12.8



Changelog
Sourced from reqwest's changelog.

v0.12.8

Add support for SOCKS4 proxies.
Add multipart::Form::file() method for adding files easily.
Add Body::wrap() to wrap any http_body::Body type.
Fix the pool configuration to use a timer to remove expired connections.




Commits

95fec09 v0.12.8
baf9712 fix: pass pool_timer to hyper_util to enable the idle cleanup task (#2434)
d85f44b Bump rustls-native-certs (#2427)
c8665be tests: use a documented test network for testing
964b1c6 tests: bypass the proxy when testing timeouts
09884ed feat: Add support for SOCKS4 (#610) (#2400)
a13a6bc ci: pin tokio-util for msrv job (#2412)
4cc8ec8 feat: Expose streaming as public API wrap (#2255)
cc3dd51 Add file function to async::multipart (#2106)
193ed1f chore: Depend on wasm-bindgen 0.2.89 or higher
See full diff in compare view




Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

@dependabot rebase will rebase this PR
@dependabot recreate will recreate this PR, overwriting any edits that have been made to it
@dependabot merge will merge this PR after your CI passes on it
@dependabot squash and merge will squash and merge this PR after your CI passes on it
@dependabot cancel merge will cancel a previously requested merge and block automerging
@dependabot reopen will reopen this PR if it is closed
@dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
@dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
@dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
@dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
@dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
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.

2 participants