apache / apache/iggy

Support per-request timeout to TCP/QUIC/WebSocket/Http clients

Open
#3,419 4 comments 0 reactions 0 assignees View on GitHub
enhancement sdk
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.