Support per-request timeout to TCP/QUIC/WebSocket/Http clients
- Dominant language
- Rust
- Stars
- 4.9k
- Forks
- 432
- Avg merge
- 2d 10h
- Merged PRs (30d)
- 173
Description
## Summary
I found this problem when I try to implement a new scenario in /bdd for rust sdk, which define the behavior of sdk when the network/server is down, and realized that the client would stuck forever which doesn't seem appropriate?
The Rust SDK (and other SDKs inheriting the same pattern) has no per-request timeout on TCP/QUIC/WebSocket transports. When the server becomes unreachable after a connection is established, in-flight requests hang indefinitely inside `send_raw_with_response` because `stream.read()` has no deadline.
## Reproduce
```rust
use iggy::prelude::*;
use std::process::Command;
use std::time::Duration;
#[tokio::main]
async fn main() {
// Start server
Command::new("docker")
.args(["run", "-d", "--name", "iggy-server",
"--security-opt", "seccomp=unconfined",
"-p", "8090:8090", "apache/iggy:latest"])
.status().unwrap();
loop {
if tokio::net::TcpStream::connect("127.0.0.1:8090").await.is_ok() { break; }
tokio::time::sleep(Duration::from_secs(1)).await;
}
let client = IggyClientBuilder::new()
.with_tcp()
.with_server_address("127.0.0.1:8090".parse().unwrap())
.build().unwrap();
client.connect().await.unwrap();
// Kill server after connected
Command::new("docker").args(["stop", "iggy-server"]).status().unwrap();
// This hangs forever -- need Ctrl+C to exit
let _ = client.login_user("iggy", "iggy").await;
}
```
## Root cause
`send_raw` itself has no read/write deadline on the TCP stream -- if the connection is alive but the server is unresponsive (e.g. paused process), `stream.read()` blocks until the OS TCP keepalive timeout.
## Comparison with Kafka (AI generated, didn't verify manually)
| Capability | Kafka (librdkafka) | Iggy |
|---|---|---|
| Per-request timeout | `request.timeout.ms` = 30s | None |
| Total delivery timeout | `message.timeout.ms` = 300s | None |
| Reconnect backoff | Exponential 100ms → 10s | Fixed 1s |
| Reconnect bound | Bounded by delivery timeout | Unlimited by default |
## Proposal
This could be exposed via the builder API:
Use client level timeout (default: 30 seconds per-request)
```rust
let client = IggyClientBuilder::new()
.with_tcp()
.with_server_address("127.0.0.1:8090".to_string())
.with_request_timeout(IggyDuration::from_str("5s")?)
.build()?;
```
Use the per-request level is
```rust
client.ping().with_timeout(IggyDuration::from_str("10ms").unwrap())
```
The per-request level should override the client level timeout.
Contributor guide
Research direction
Start with the Rust SDK's send_raw and send_raw_with_response paths, then trace how TCP, QUIC, WebSocket, and HTTP clients share request handling. Determine how client and per-request timeouts interact, including the default, and use the /bdd scenario and reproduction to verify that an unresponsive server stops within the configured timeout.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- backend-api-design, networking
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100