diff --git a/crates/client-api/src/lib.rs b/crates/client-api/src/lib.rs index 92fb8887b70..9cb494691ef 100644 --- a/crates/client-api/src/lib.rs +++ b/crates/client-api/src/lib.rs @@ -107,6 +107,29 @@ impl Host { self.host_controller.get_module_host(self.replica_id).await } + /// Wait for the module host to become available, retrying with backoff. + /// + /// This is useful for routes like `/schema` that may be called while the + /// database is still loading. Instead of returning an immediate 500, we + /// poll for up to `timeout` before giving up. + pub async fn wait_for_module(&self, timeout: std::time::Duration) -> Result { + let deadline = tokio::time::Instant::now() + timeout; + let mut interval = tokio::time::Duration::from_millis(100); + loop { + match self.host_controller.get_module_host(self.replica_id).await { + Ok(module) => return Ok(module), + Err(NoSuchModule) => { + if tokio::time::Instant::now() >= deadline { + return Err(NoSuchModule); + } + tokio::time::sleep(interval).await; + // Exponential backoff: 100ms, 200ms, 400ms, 800ms, 1s, 1s, ... + interval = (interval * 2).min(tokio::time::Duration::from_secs(1)); + } + } + } + } + pub async fn module_watcher(&self) -> Result, NoSuchModule> { self.host_controller.watch_module_host(self.replica_id).await } diff --git a/crates/client-api/src/routes/database.rs b/crates/client-api/src/routes/database.rs index 4c4ce647be7..20e6196c78b 100644 --- a/crates/client-api/src/routes/database.rs +++ b/crates/client-api/src/routes/database.rs @@ -341,7 +341,14 @@ pub async fn schema( where S: ControlStateDelegate + NodeDelegate, { - let (module, _) = find_module_and_database(&worker_ctx, name_or_identity).await?; + let (leader, _) = find_leader_and_database(&worker_ctx, name_or_identity).await?; + // Wait for the module to finish loading rather than returning an immediate + // 500 error. The database may still be initializing (replaying the log, + // running init reducers, etc.). + let module = leader + .wait_for_module(std::time::Duration::from_secs(10)) + .await + .map_err(log_and_500)?; let module_def = &module.info.module_def; let response_json = match version {