From f74c13d31a25711a5e38cec1c71cebae67cb7b6e Mon Sep 17 00:00:00 2001 From: Kim Altintop Date: Thu, 6 Aug 2026 13:39:51 +0200 Subject: [PATCH] client-api: Optionally include the current program hash in the database info Useful in certain circumstances, including observability. Including the information in the info endpoint is the obvious choice without introducing further machinery. It comes with some caveats, however: - If the database is not running, retrieving the module hash as implemented will start it. This may not be desirable, which is why the behavior is opt-in via the `current-hash` query parameter. - If the database is suspended, paused, or faulty, determining the hash is not possible. For the intended purpose, this is sufficient, as absence is an error condition. --- crates/client-api/src/routes/database.rs | 63 +++++++++++++++++++----- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/crates/client-api/src/routes/database.rs b/crates/client-api/src/routes/database.rs index b15f72a1e7c..89cf3d6e8c0 100644 --- a/crates/client-api/src/routes/database.rs +++ b/crates/client-api/src/routes/database.rs @@ -555,33 +555,62 @@ where )) } +/// Path parameters for the [db_info] handler. #[derive(Deserialize)] -pub struct DatabaseParam { +pub struct InfoParams { name_or_identity: NameOrIdentity, } -#[derive(sats::Serialize)] -struct DatabaseResponse { - database_identity: Identity, - owner_identity: Identity, - host_type: HostType, - initial_program: spacetimedb_lib::Hash, +/// Query parameters for the [db_info] handler. +#[derive(Deserialize)] +pub struct InfoQuery { + /// If set, try to determine the current module's program hash. + #[serde( + default, + alias = "current-hash", + alias = "current-program", + alias = "current_program" + )] + current_hash: bool, +} + +/// Response of the [db_info] handler. +#[derive(Debug, sats::Serialize, sats::Deserialize)] +pub struct InfoResponse { + pub database_identity: Identity, + pub owner_identity: Identity, + pub host_type: HostType, + pub initial_program: spacetimedb_lib::Hash, + /// The hash of the database's current module. + /// + /// This is determined on a best-effort basis. If it is `None`, one of the + /// following is true: + /// + /// - the `current_hash` query parameter was not set + /// - the current node doesn't host the leader of the database + /// - the database has no current leader + /// - the database is paused or suspended + /// - the module could not be instantiated + /// + pub current_program: Option, } -impl From for DatabaseResponse { +impl From for InfoResponse { fn from(db: Database) -> Self { - DatabaseResponse { + InfoResponse { database_identity: db.database_identity, owner_identity: db.owner_identity, host_type: db.host_type, initial_program: db.initial_program, + current_program: None, } } } -pub async fn db_info( +pub async fn db_info( State(worker_ctx): State, - Path(DatabaseParam { name_or_identity }): Path, + Path(InfoParams { name_or_identity }): Path, + Query(InfoQuery { current_hash }): Query, ) -> axum::response::Result { log::trace!("Trying to resolve database identity: {name_or_identity:?}"); let database_identity = name_or_identity.resolve(&worker_ctx).await?; @@ -591,7 +620,17 @@ pub async fn db_info( .ok_or(NO_SUCH_DATABASE)?; log::trace!("Fetched database from the worker db for database identity: {database_identity:?}"); - let response = DatabaseResponse::from(database); + let current_program = async { + current_hash.then_some(())?; + let host = worker_ctx.leader(database.id).await.ok()?; + let module = host.module().await.ok()?; + Some(module.info().module_hash) + } + .await; + + let mut response = InfoResponse::from(database); + response.current_program = current_program; + Ok(axum::Json(sats::serde::SerdeWrapper(response))) }