Skip to content

Commit bd8288d

Browse files
committed
fix(web): consolidate gw.inner read locks in build_gon_data to prevent deadlock
build_gon_data previously acquired gw.inner.read() three times across build_nav_counts (hooks count) and the main function (heartbeat_config, channels_offered, update). With tokio's fair RwLock, if a concurrent WS handler requests a write lock between these reads, the subsequent read blocks indefinitely — causing page.goto to hang with zero HTTP response on CI. Consolidated all three reads into a single lock acquisition at the start of build_gon_data, eliminating the deadlock window.
1 parent f773171 commit bd8288d

1 file changed

Lines changed: 18 additions & 12 deletions

File tree

crates/web/src/templates.rs

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -358,16 +358,14 @@ pub(crate) async fn build_nav_counts(gw: &GatewayState) -> NavCounts {
358358
})
359359
.unwrap_or(0);
360360

361-
let hooks = gw.inner.read().await.discovered_hooks.len();
362-
363361
NavCounts {
364362
projects,
365363
providers,
366364
channels,
367365
skills,
368366
mcp,
369367
crons,
370-
hooks,
368+
hooks: 0, // placeholder — set from a single inner.read() in build_gon_data
371369
}
372370
}
373371

@@ -386,7 +384,22 @@ pub(crate) async fn build_gon_data(gw: &GatewayState) -> GonData {
386384
.and_then(|v| serde_json::from_value(v).ok())
387385
.unwrap_or_default();
388386

389-
let counts = build_nav_counts(gw).await;
387+
let mut counts = build_nav_counts(gw).await;
388+
389+
// Read all fields from gw.inner in a SINGLE lock acquisition to avoid
390+
// deadlocks with concurrent write-lock requests (tokio's fair RwLock
391+
// blocks new reads when a write is queued).
392+
let (hooks_count, heartbeat_config, cached_channels_offered, update) = {
393+
let inner = gw.inner.read().await;
394+
(
395+
inner.discovered_hooks.len(),
396+
inner.heartbeat_config.clone(),
397+
inner.channels_offered.clone(),
398+
inner.update.clone(),
399+
)
400+
};
401+
counts.hooks = hooks_count;
402+
390403
let (crons, cron_status, webhooks_val, webhook_profiles_val) = tokio::join!(
391404
gw.services.cron.list(),
392405
gw.services.cron.status(),
@@ -409,13 +422,6 @@ pub(crate) async fn build_gon_data(gw: &GatewayState) -> GonData {
409422
.ok()
410423
.and_then(|v| serde_json::from_value(v).ok())
411424
.unwrap_or_default();
412-
let (heartbeat_config, cached_channels_offered) = {
413-
let inner = gw.inner.read().await;
414-
(
415-
inner.heartbeat_config.clone(),
416-
inner.channels_offered.clone(),
417-
)
418-
};
419425
let channels_offered = resolve_channels_offered(cached_channels_offered);
420426
let channel_descriptors: Vec<moltis_channels::ChannelDescriptor> = channels_offered
421427
.iter()
@@ -491,7 +497,7 @@ pub(crate) async fn build_gon_data(gw: &GatewayState) -> GonData {
491497
.join("moltis.db")
492498
.display()
493499
.to_string(),
494-
update: gw.inner.read().await.update.clone(),
500+
update,
495501
sandbox,
496502
routes: SPA_ROUTES.clone(),
497503
started_at: *PROCESS_STARTED_AT_MS,

0 commit comments

Comments
 (0)