Skip to content

Commit 6a52d2e

Browse files
committed
fix(gateway): support skip ready wait routing
1 parent 7f7a368 commit 6a52d2e

23 files changed

Lines changed: 295 additions & 215 deletions

File tree

engine/artifacts/errors/guard.invalid_header.json

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

engine/packages/guard/src/errors.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@ pub struct MissingHeader {
1313
pub header: String,
1414
}
1515

16+
#[derive(RivetError, Serialize)]
17+
#[error(
18+
"guard",
19+
"invalid_header",
20+
"Invalid header value.",
21+
"Invalid {header} header: {detail}."
22+
)]
23+
pub struct InvalidHeader {
24+
pub header: String,
25+
pub detail: String,
26+
}
27+
1628
#[derive(RivetError, Serialize)]
1729
#[error(
1830
"guard",

engine/packages/guard/src/routing/actor_path.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ pub enum QueryActorQuery {
3030
namespace: String,
3131
name: String,
3232
key: Vec<String>,
33-
bypass_connectable: bool,
33+
skip_ready_wait: bool,
3434
},
3535
GetOrCreate {
3636
namespace: String,
@@ -40,19 +40,19 @@ pub enum QueryActorQuery {
4040
input: Option<Vec<u8>>,
4141
region: Option<String>,
4242
crash_policy: Option<CrashPolicy>,
43-
bypass_connectable: bool,
43+
skip_ready_wait: bool,
4444
},
4545
}
4646

4747
impl QueryActorQuery {
48-
pub fn bypass_connectable(&self) -> bool {
48+
pub fn skip_ready_wait(&self) -> bool {
4949
match self {
5050
QueryActorQuery::Get {
51-
bypass_connectable, ..
51+
skip_ready_wait, ..
5252
}
5353
| QueryActorQuery::GetOrCreate {
54-
bypass_connectable, ..
55-
} => *bypass_connectable,
54+
skip_ready_wait, ..
55+
} => *skip_ready_wait,
5656
}
5757
}
5858
}
@@ -97,8 +97,8 @@ struct RvtParams {
9797
crash_policy: Option<String>,
9898
#[serde(default)]
9999
token: Option<String>,
100-
#[serde(default)]
101-
bypass_connectable: bool,
100+
#[serde(default, rename = "skip-ready-wait")]
101+
skip_ready_wait: bool,
102102
}
103103

104104
/// Parse actor routing information from path.
@@ -244,7 +244,7 @@ fn extract_rvt_params(rvt_params: &[(String, String)]) -> Result<RvtParams> {
244244
.build());
245245
}
246246
let value = match stripped {
247-
"bypass_connectable" => parse_query_bool(value)
247+
"skip-ready-wait" => parse_query_bool(value)
248248
.map(serde_json::Value::Bool)
249249
.unwrap_or_else(|| serde_json::Value::String(value.clone())),
250250
_ => serde_json::Value::String(value.clone()),
@@ -294,7 +294,7 @@ fn build_actor_query(name: &str, rvt: RvtParams) -> Result<QueryActorQuery> {
294294
namespace: rvt.namespace,
295295
name: name.to_string(),
296296
key,
297-
bypass_connectable: rvt.bypass_connectable,
297+
skip_ready_wait: rvt.skip_ready_wait,
298298
})
299299
}
300300
"getOrCreate" => {
@@ -319,7 +319,7 @@ fn build_actor_query(name: &str, rvt: RvtParams) -> Result<QueryActorQuery> {
319319
input,
320320
region: rvt.region,
321321
crash_policy,
322-
bypass_connectable: rvt.bypass_connectable,
322+
skip_ready_wait: rvt.skip_ready_wait,
323323
})
324324
}
325325
other => Err(errors::QueryInvalidParams {

engine/packages/guard/src/routing/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ mod ws_health;
1616

1717
pub(crate) const X_RIVET_TARGET: HeaderName = HeaderName::from_static("x-rivet-target");
1818
pub(crate) const X_RIVET_TOKEN: HeaderName = HeaderName::from_static("x-rivet-token");
19-
pub(crate) const X_RIVET_BYPASS_CONNECTABLE: HeaderName =
20-
HeaderName::from_static("x-rivet-bypass-connectable");
19+
pub(crate) const X_RIVET_SKIP_READY_WAIT: HeaderName =
20+
HeaderName::from_static("x-rivet-skip-ready-wait");
2121
pub(crate) const SEC_WEBSOCKET_PROTOCOL: HeaderName =
2222
HeaderName::from_static("sec-websocket-protocol");
2323
pub(crate) const WS_PROTOCOL_TARGET: &str = "rivet_target.";
2424
pub(crate) const WS_PROTOCOL_ACTOR: &str = "rivet_actor.";
2525
pub(crate) const WS_PROTOCOL_TOKEN: &str = "rivet_token.";
26-
pub(crate) const WS_PROTOCOL_BYPASS_CONNECTABLE: &str = "rivet_bypass_connectable";
26+
pub(crate) const WS_PROTOCOL_SKIP_READY_WAIT: &str = "rivet_skip_ready_wait";
2727

2828
/// Creates the main routing function that handles all incoming requests
2929
#[tracing::instrument(skip_all)]

engine/packages/guard/src/routing/pegboard_gateway/mod.rs

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ use hyper::header::HeaderName;
99
use rivet_guard_core::{RouteConfig, RouteTarget, RoutingOutput, request_context::RequestContext};
1010

1111
use super::{
12-
SEC_WEBSOCKET_PROTOCOL, WS_PROTOCOL_ACTOR, WS_PROTOCOL_BYPASS_CONNECTABLE, WS_PROTOCOL_TOKEN,
13-
X_RIVET_BYPASS_CONNECTABLE, X_RIVET_TOKEN, actor_path::ParsedActorPath,
12+
SEC_WEBSOCKET_PROTOCOL, WS_PROTOCOL_ACTOR, WS_PROTOCOL_SKIP_READY_WAIT, WS_PROTOCOL_TOKEN,
13+
X_RIVET_SKIP_READY_WAIT, X_RIVET_TOKEN, actor_path::ParsedActorPath,
1414
};
1515
use crate::{
1616
errors,
@@ -70,22 +70,21 @@ pub async fn route_request_path_based_inner(
7070

7171
tracing::debug!(?actor_path, "routing using path-based actor routing");
7272

73-
let (actor_id, token, stripped_path, bypass_connectable) = match actor_path {
73+
let (actor_id, token, stripped_path, skip_ready_wait) = match actor_path {
7474
ParsedActorPath::Direct(path) => (
7575
Id::parse(&path.actor_id).context("invalid actor id in path")?,
7676
read_gateway_token_for_path_based(req_ctx, path.token.as_deref())?
7777
.map(ToOwned::to_owned),
7878
path.stripped_path.clone(),
79-
// TODO:
80-
false,
79+
read_skip_ready_wait_for_path_based(req_ctx)?,
8180
),
8281
ParsedActorPath::Query(path) => match resolve_query(ctx, &path.query).await? {
8382
ResolveQueryActorResult::Found { actor_id } => (
8483
actor_id,
8584
read_gateway_token_for_path_based(req_ctx, path.token.as_deref())?
8685
.map(ToOwned::to_owned),
8786
path.stripped_path.clone(),
88-
path.query.bypass_connectable(),
87+
path.query.skip_ready_wait(),
8988
),
9089
ResolveQueryActorResult::Forward { dc_label } => {
9190
let peer_dc = ctx
@@ -116,7 +115,7 @@ pub async fn route_request_path_based_inner(
116115
actor_id,
117116
&stripped_path,
118117
token.as_deref(),
119-
bypass_connectable,
118+
skip_ready_wait,
120119
)
121120
.await
122121
.map(Some)
@@ -148,7 +147,7 @@ pub async fn route_request(
148147
set_non_preflight_cors(req_ctx);
149148

150149
// Extract actor ID and token from WebSocket protocol or HTTP headers
151-
let (actor_id_str, token, bypass_connectable) = if req_ctx.is_websocket() {
150+
let (actor_id_str, token, skip_ready_wait) = if req_ctx.is_websocket() {
152151
// For WebSocket, parse the sec-websocket-protocol header
153152
let protocols_header = req_ctx
154153
.headers()
@@ -182,11 +181,11 @@ pub async fn route_request(
182181
.find_map(|p| p.strip_prefix(WS_PROTOCOL_TOKEN))
183182
.map(ToOwned::to_owned);
184183

185-
let bypass_connectable = protocols
184+
let skip_ready_wait = protocols
186185
.iter()
187-
.any(|p| p == &WS_PROTOCOL_BYPASS_CONNECTABLE);
186+
.any(|p| p == &WS_PROTOCOL_SKIP_READY_WAIT);
188187

189-
(actor_id, token, bypass_connectable)
188+
(actor_id, token, skip_ready_wait)
190189
} else {
191190
// For HTTP, use headers
192191
let actor_id = req_ctx
@@ -210,9 +209,9 @@ pub async fn route_request(
210209
.context("invalid x-rivet-token header")?
211210
.map(ToOwned::to_owned);
212211

213-
let bypass_connectable = req_ctx.headers().contains_key(X_RIVET_BYPASS_CONNECTABLE);
212+
let skip_ready_wait = read_skip_ready_wait_header(req_ctx)?;
214213

215-
(actor_id.to_string(), token, bypass_connectable)
214+
(actor_id.to_string(), token, skip_ready_wait)
216215
};
217216

218217
// Find actor to route to
@@ -226,7 +225,7 @@ pub async fn route_request(
226225
actor_id,
227226
&stripped_path,
228227
token.as_deref(),
229-
bypass_connectable,
228+
skip_ready_wait,
230229
)
231230
.await
232231
.map(Some)
@@ -247,7 +246,7 @@ async fn route_request_inner(
247246
actor_id: Id,
248247
stripped_path: &str,
249248
_token: Option<&str>,
250-
bypass_connectable: bool,
249+
skip_ready_wait: bool,
251250
) -> Result<RoutingOutput> {
252251
// NOTE: Token validation implemented in EE
253252

@@ -323,7 +322,7 @@ async fn route_request_inner(
323322
actor_id,
324323
actor,
325324
stripped_path,
326-
bypass_connectable,
325+
skip_ready_wait,
327326
ready_sub2,
328327
stopped_sub2,
329328
fail_sub2,
@@ -338,7 +337,7 @@ async fn route_request_inner(
338337
actor_id,
339338
actor,
340339
stripped_path,
341-
bypass_connectable,
340+
skip_ready_wait,
342341
ready_sub,
343342
stopped_sub,
344343
fail_sub,
@@ -361,7 +360,7 @@ async fn handle_actor_v2(
361360
actor_id: Id,
362361
actor: pegboard::ops::actor::get_for_gateway::Output,
363362
stripped_path: &str,
364-
bypass_connectable: bool,
363+
skip_ready_wait: bool,
365364
mut ready_sub: SubscriptionHandle<pegboard::workflows::actor2::Ready>,
366365
mut stopped_sub: SubscriptionHandle<pegboard::workflows::actor2::Stopped>,
367366
mut fail_sub: SubscriptionHandle<pegboard::workflows::actor2::Failed>,
@@ -378,7 +377,7 @@ async fn handle_actor_v2(
378377
}
379378

380379
let envoy_key = if let (Some(envoy_key), true) =
381-
(actor.envoy_key, actor.connectable || bypass_connectable)
380+
(actor.envoy_key, actor.connectable || skip_ready_wait)
382381
{
383382
envoy_key
384383
} else {
@@ -464,7 +463,7 @@ async fn handle_actor_v1(
464463
actor_id: Id,
465464
actor: pegboard::ops::actor::get_for_gateway::Output,
466465
stripped_path: &str,
467-
bypass_connectable: bool,
466+
skip_ready_wait: bool,
468467
mut ready_sub: SubscriptionHandle<pegboard::workflows::actor::Ready>,
469468
mut stopped_sub: SubscriptionHandle<pegboard::workflows::actor::Stopped>,
470469
mut fail_sub: SubscriptionHandle<pegboard::workflows::actor::Failed>,
@@ -490,7 +489,7 @@ async fn handle_actor_v1(
490489
}
491490

492491
let runner_id = if let (Some(runner_id), true) =
493-
(actor.runner_id, actor.connectable || bypass_connectable)
492+
(actor.runner_id, actor.connectable || skip_ready_wait)
494493
{
495494
runner_id
496495
} else {
@@ -555,7 +554,7 @@ async fn handle_actor_v1(
555554
actor_id,
556555
actor,
557556
stripped_path,
558-
bypass_connectable,
557+
skip_ready_wait,
559558
ready_sub2,
560559
stopped_sub2,
561560
fail_sub2,
@@ -626,6 +625,46 @@ fn read_gateway_token_for_path_based<'a>(
626625
}
627626
}
628627

628+
fn read_skip_ready_wait_for_path_based(req_ctx: &RequestContext) -> Result<bool> {
629+
if req_ctx.is_websocket() {
630+
Ok(req_ctx
631+
.headers()
632+
.get(SEC_WEBSOCKET_PROTOCOL)
633+
.and_then(|protocols| protocols.to_str().ok())
634+
.is_some_and(|protocols| {
635+
protocols
636+
.split(',')
637+
.map(|p| p.trim())
638+
.any(|p| p == WS_PROTOCOL_SKIP_READY_WAIT)
639+
}))
640+
} else {
641+
read_skip_ready_wait_header(req_ctx)
642+
}
643+
}
644+
645+
fn read_skip_ready_wait_header(req_ctx: &RequestContext) -> Result<bool> {
646+
let Some(value) = req_ctx.headers().get(X_RIVET_SKIP_READY_WAIT) else {
647+
return Ok(false);
648+
};
649+
650+
let value = value.to_str().context("invalid x-rivet-skip-ready-wait header")?;
651+
parse_skip_ready_wait_bool(value).ok_or_else(|| {
652+
crate::errors::InvalidHeader {
653+
header: X_RIVET_SKIP_READY_WAIT.to_string(),
654+
detail: "expected true, false, 1, or 0".to_string(),
655+
}
656+
.build()
657+
})
658+
}
659+
660+
fn parse_skip_ready_wait_bool(value: &str) -> Option<bool> {
661+
match value {
662+
"true" | "1" => Some(true),
663+
"false" | "0" => Some(false),
664+
_ => None,
665+
}
666+
}
667+
629668
/// Waits for initial delay, then periodically checks for runner pool errors.
630669
///
631670
/// Returns `true` if the pool has an active error, `false` otherwise.

0 commit comments

Comments
 (0)