Skip to content

Commit f235d67

Browse files
committed
feat(gateway): expose yamux config and add reconnecting pool
1 parent 3d1aab1 commit f235d67

7 files changed

Lines changed: 190 additions & 60 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gateway/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ http-client = { workspace = true, features = ["prpc"] }
4848
sha2.workspace = true
4949
dstack-types.workspace = true
5050
serde-duration.workspace = true
51+
size-parser = { workspace = true, features = ["serde"] }
5152
reqwest = { workspace = true, features = ["json"] }
5253
hyper = { workspace = true, features = ["server", "http1"] }
5354
hyper-util = { version = "0.1", features = ["tokio"] }

gateway/dstack-app/builder/entrypoint.sh

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,16 @@ idle = "10m"
149149
write = "5s"
150150
shutdown = "5s"
151151
total = "5h"
152-
yamux_ping = "${YAMUX_PING_TIMEOUT:-15s}"
152+
153+
[core.proxy.yamux]
154+
listen_addr = "${YAMUX_LISTEN_ADDR:-0.0.0.0}"
155+
listen_port = ${YAMUX_LISTEN_PORT:-0}
156+
## Set to 0 to disable max connection receive window limit.
157+
max_connection_receive_window = ${YAMUX_MAX_CONNECTION_RECEIVE_WINDOW:-1073741824}
158+
max_num_streams = ${YAMUX_MAX_NUM_STREAMS:-512}
159+
read_after_close = ${YAMUX_READ_AFTER_CLOSE:-true}
160+
split_send_size = ${YAMUX_SPLIT_SEND_SIZE:-16384}
161+
ping_timeout = "${YAMUX_PING_TIMEOUT:-15s}"
153162
154163
[core.recycle]
155164
enabled = true

gateway/gateway.toml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,19 @@ connect_top_n = 3
6868
localhost_enabled = false
6969
app_address_ns_prefix = "_dstack-app-address"
7070
app_address_ns_compat = true
71-
# yamux_listen_port = 4433
7271
workers = 32
7372
external_port = 443
7473

74+
[core.proxy.yamux]
75+
listen_addr = "0.0.0.0"
76+
listen_port = 4433
77+
# max_connection_receive_window = 0 to disable limit
78+
max_connection_receive_window = "1G"
79+
max_num_streams = 4096
80+
read_after_close = true
81+
split_send_size = "16K"
82+
ping_timeout = "15s"
83+
7584
[core.proxy.timeouts]
7685
# Timeout for establishing a connection to the target app.
7786
connect = "5s"
@@ -94,8 +103,6 @@ write = "5s"
94103
shutdown = "5s"
95104
# Timeout for total connection duration.
96105
total = "5h"
97-
# Yamux ping timeout (0s disables).
98-
yamux_ping = "15s"
99106

100107
[core.recycle]
101108
enabled = true

gateway/src/config.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ pub struct ProxyConfig {
8585
pub workers: usize,
8686
pub app_address_ns_prefix: String,
8787
pub app_address_ns_compat: bool,
88-
pub yamux_listen_port: Option<u16>,
88+
#[serde(default)]
89+
pub yamux: YamuxConfig,
8990
}
9091

9192
#[derive(Debug, Clone, Deserialize)]
@@ -111,10 +112,35 @@ pub struct Timeouts {
111112
pub write: Duration,
112113
#[serde(with = "serde_duration")]
113114
pub shutdown: Duration,
115+
}
114116

117+
#[derive(Debug, Clone, Deserialize)]
118+
pub struct YamuxConfig {
119+
pub listen_addr: Ipv4Addr,
120+
pub listen_port: Option<u16>,
121+
#[serde(with = "size_parser::human_size")]
122+
pub max_connection_receive_window: usize,
123+
pub max_num_streams: usize,
124+
pub read_after_close: bool,
125+
#[serde(with = "size_parser::human_size")]
126+
pub split_send_size: usize,
115127
/// Ping timeout for yamux connections. Set to 0s to disable.
116128
#[serde(with = "serde_duration")]
117-
pub yamux_ping: Duration,
129+
pub ping_timeout: Duration,
130+
}
131+
132+
impl Default for YamuxConfig {
133+
fn default() -> Self {
134+
Self {
135+
listen_addr: Ipv4Addr::new(0, 0, 0, 0),
136+
listen_port: None,
137+
max_connection_receive_window: 1024 * 1024 * 1024,
138+
max_num_streams: 4096,
139+
read_after_close: true,
140+
split_send_size: 16 * 1024,
141+
ping_timeout: Duration::from_secs(15),
142+
}
143+
}
118144
}
119145

120146
#[derive(Debug, Clone, Deserialize, Serialize)]

gateway/src/proxy.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -257,16 +257,29 @@ pub async fn proxy_main(config: &ProxyConfig, proxy: Proxy) -> Result<()> {
257257
config.listen_addr, config.listen_port
258258
);
259259

260+
let yamux_cfg = &config.yamux;
260261
let mut yamux_config = yamux::Config::default();
261-
if config.timeouts.yamux_ping != Duration::ZERO {
262-
yamux_config.set_ping_timeout(Some(config.timeouts.yamux_ping));
262+
let max_conn_window = if yamux_cfg.max_connection_receive_window == 0 {
263+
None
264+
} else {
265+
Some(yamux_cfg.max_connection_receive_window)
266+
};
267+
yamux_config.set_max_connection_receive_window(max_conn_window);
268+
yamux_config.set_max_num_streams(yamux_cfg.max_num_streams);
269+
yamux_config.set_read_after_close(yamux_cfg.read_after_close);
270+
yamux_config.set_split_send_size(yamux_cfg.split_send_size);
271+
if yamux_cfg.ping_timeout != Duration::ZERO {
272+
yamux_config.set_ping_timeout(Some(yamux_cfg.ping_timeout));
263273
}
264274

265-
let yamux_listener = if let Some(yamux_port) = config.yamux_listen_port {
266-
let listener = TcpListener::bind(format!("0.0.0.0:{yamux_port}"))
275+
let yamux_listener = if let Some(yamux_port) = yamux_cfg.listen_port.filter(|p| *p != 0) {
276+
let listener = TcpListener::bind((yamux_cfg.listen_addr, yamux_port))
267277
.await
268-
.with_context(|| format!("failed to bind yamux port {yamux_port}"))?;
269-
info!("yamux bridge listening on TCP port {yamux_port}");
278+
.with_context(|| format!("failed to bind yamux {}:{yamux_port}", yamux_cfg.listen_addr))?;
279+
info!(
280+
"yamux bridge listening on {}:{}",
281+
yamux_cfg.listen_addr, yamux_port
282+
);
270283
Some(listener)
271284
} else {
272285
None

uds2yamux/src/main.rs

Lines changed: 121 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
//! Forward UDS connections to a remote yamux endpoint over TCP.
66
//!
7-
//! Maintains a single TCP connection and multiplexes each incoming UDS
7+
//! Maintains a pool of TCP connections and multiplexes each incoming UDS
88
//! connection onto a yamux stream.
99
1010
use anyhow::{Context, Result};
1111
use clap::Parser;
12+
use std::sync::atomic::{AtomicUsize, Ordering};
13+
use std::sync::Arc;
1214
use tokio::net::{TcpStream, UnixListener};
1315
use tokio::sync::{mpsc, oneshot};
1416
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
@@ -24,8 +26,14 @@ struct Args {
2426
/// Yamux server address (host:port)
2527
#[arg(long)]
2628
yamux_addr: String,
29+
30+
/// Number of TCP connections in the yamux pool
31+
#[arg(long, default_value = "1")]
32+
yamux_conns: usize,
2733
}
2834

35+
type YamuxRequest = oneshot::Sender<anyhow::Result<yamux::Stream>>;
36+
2937
async fn connect_yamux(
3038
addr: &str,
3139
) -> Result<yamux::Connection<tokio_util::compat::Compat<TcpStream>>> {
@@ -44,60 +52,135 @@ async fn connect_yamux(
4452
))
4553
}
4654

47-
async fn drive_yamux(
48-
mut conn: yamux::Connection<tokio_util::compat::Compat<TcpStream>>,
49-
mut requests: mpsc::Receiver<oneshot::Sender<anyhow::Result<yamux::Stream>>>,
50-
) {
55+
async fn drive_yamux(addr: String, mut requests: mpsc::Receiver<YamuxRequest>) {
56+
use futures::FutureExt;
57+
58+
let mut pending: Vec<YamuxRequest> = Vec::new();
59+
let mut backoff = std::time::Duration::from_secs(1);
60+
let max_backoff = std::time::Duration::from_secs(60);
61+
5162
loop {
52-
tokio::select! {
53-
inbound = std::future::poll_fn(|cx| conn.poll_next_inbound(cx)) => {
54-
match inbound {
55-
Some(Ok(stream)) => {
56-
info!("yamux: inbound stream {stream}");
57-
}
58-
Some(Err(e)) => {
59-
error!("yamux: inbound error: {e}");
60-
break;
61-
}
62-
None => {
63-
info!("yamux: connection closed by peer");
64-
break;
63+
let mut conn = loop {
64+
match connect_yamux(&addr).await {
65+
Ok(conn) => {
66+
backoff = std::time::Duration::from_secs(1);
67+
info!("yamux: connected to {addr}");
68+
break conn;
69+
}
70+
Err(e) => {
71+
error!("yamux: connect error: {e}");
72+
let delay = tokio::time::sleep(backoff);
73+
tokio::pin!(delay);
74+
tokio::select! {
75+
_ = &mut delay => {
76+
backoff = std::cmp::min(backoff * 2, max_backoff);
77+
}
78+
request = requests.recv() => {
79+
match request {
80+
Some(reply) => pending.push(reply),
81+
None => return,
82+
}
83+
}
6584
}
6685
}
6786
}
68-
request = requests.recv() => {
69-
let Some(reply) = request else {
70-
break;
71-
};
72-
let result = std::future::poll_fn(|cx| conn.poll_new_outbound(cx))
73-
.await
74-
.map_err(|e| anyhow::anyhow!("failed to open yamux stream: {e}"));
75-
let _ = reply.send(result);
87+
};
88+
89+
loop {
90+
let request_fut = if let Some(reply) = pending.pop() {
91+
futures::future::ready(Some(reply)).boxed()
92+
} else {
93+
requests.recv().boxed()
94+
};
95+
96+
tokio::select! {
97+
inbound = std::future::poll_fn(|cx| conn.poll_next_inbound(cx)) => {
98+
match inbound {
99+
Some(Ok(stream)) => {
100+
info!("yamux: inbound stream {stream}");
101+
}
102+
Some(Err(e)) => {
103+
error!("yamux: inbound error: {e}");
104+
break;
105+
}
106+
None => {
107+
info!("yamux: connection closed by peer");
108+
break;
109+
}
110+
}
111+
}
112+
request = request_fut => {
113+
let Some(reply) = request else {
114+
return;
115+
};
116+
let result = std::future::poll_fn(|cx| conn.poll_new_outbound(cx))
117+
.await
118+
.map_err(|e| anyhow::anyhow!("failed to open yamux stream: {e}"));
119+
let _ = reply.send(result);
120+
}
76121
}
77122
}
78123
}
79124
}
80125

126+
async fn spawn_yamux_driver(addr: &str) -> Result<mpsc::Sender<YamuxRequest>> {
127+
let (request_tx, request_rx) = mpsc::channel(128);
128+
tokio::spawn(drive_yamux(addr.to_string(), request_rx));
129+
Ok(request_tx)
130+
}
131+
132+
struct YamuxPool {
133+
senders: Vec<mpsc::Sender<YamuxRequest>>,
134+
next: AtomicUsize,
135+
}
136+
137+
impl YamuxPool {
138+
async fn new(addr: &str, num_conns: usize) -> Result<Self> {
139+
let mut senders = Vec::with_capacity(num_conns);
140+
for _ in 0..num_conns {
141+
senders.push(spawn_yamux_driver(addr).await?);
142+
}
143+
Ok(Self {
144+
senders,
145+
next: AtomicUsize::new(0),
146+
})
147+
}
148+
149+
async fn open_stream(&self) -> Result<yamux::Stream> {
150+
let idx = self.next.fetch_add(1, Ordering::Relaxed) % self.senders.len();
151+
let (reply_tx, reply_rx) = oneshot::channel();
152+
self.senders[idx]
153+
.send(reply_tx)
154+
.await
155+
.map_err(|_| anyhow::anyhow!("yamux driver closed"))?;
156+
reply_rx
157+
.await
158+
.map_err(|_| anyhow::anyhow!("yamux driver dropped response channel"))?
159+
}
160+
}
161+
81162
#[tokio::main]
82163
async fn main() -> Result<()> {
83164
tracing_subscriber::fmt::init();
84165
let args = Args::parse();
166+
if args.yamux_conns == 0 {
167+
anyhow::bail!("yamux_conns must be >= 1");
168+
}
85169

86170
let _ = std::fs::remove_file(&args.uds);
87171

88172
let listener = UnixListener::bind(&args.uds)
89173
.with_context(|| format!("failed to bind UDS at {}", args.uds))?;
90174

91-
let connection = connect_yamux(&args.yamux_addr)
92-
.await
93-
.context("failed to connect yamux")?;
94-
95-
let (request_tx, request_rx) = mpsc::channel(128);
96-
tokio::spawn(drive_yamux(connection, request_rx));
175+
let pool = Arc::new(
176+
YamuxPool::new(&args.yamux_addr, args.yamux_conns)
177+
.await
178+
.context("failed to connect yamux")?,
179+
);
97180

98181
info!(
99-
"listening on {}, forwarding to yamux {}",
100-
args.uds, args.yamux_addr
182+
"listening on {}, forwarding to yamux {} (pool size {})",
183+
args.uds, args.yamux_addr, args.yamux_conns
101184
);
102185

103186
loop {
@@ -106,24 +189,14 @@ async fn main() -> Result<()> {
106189
.await
107190
.context("failed to accept UDS connection")?;
108191

109-
let request_tx = request_tx.clone();
192+
let pool = pool.clone();
110193
tokio::spawn(async move {
111-
let (reply_tx, reply_rx) = oneshot::channel();
112-
if request_tx.send(reply_tx).await.is_err() {
113-
error!("yamux driver closed");
114-
return;
115-
}
116-
117-
let stream = match reply_rx.await {
118-
Ok(Ok(stream)) => stream,
119-
Ok(Err(e)) => {
194+
let stream = match pool.open_stream().await {
195+
Ok(stream) => stream,
196+
Err(e) => {
120197
error!("failed to open yamux stream: {e}");
121198
return;
122199
}
123-
Err(_) => {
124-
error!("yamux driver dropped response channel");
125-
return;
126-
}
127200
};
128201

129202
let stream = stream.compat();

0 commit comments

Comments
 (0)