Skip to content

Commit 1e3e8db

Browse files
authored
Database-specific Timestamp route (#2864)
1 parent 193c0d4 commit 1e3e8db

5 files changed

Lines changed: 58 additions & 29 deletions

File tree

crates/client-api/src/routes/database.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ use spacetimedb::messages::control_db::{Database, HostType};
2929
use spacetimedb_client_api_messages::name::{self, DatabaseName, DomainName, PublishOp, PublishResult};
3030
use spacetimedb_lib::db::raw_def::v9::RawModuleDefV9;
3131
use spacetimedb_lib::identity::AuthCtx;
32-
use spacetimedb_lib::sats;
32+
use spacetimedb_lib::{sats, Timestamp};
3333

3434
use super::subscribe::handle_websocket;
3535

@@ -371,7 +371,7 @@ fn mime_ndjson() -> mime::Mime {
371371
"application/x-ndjson".parse().unwrap()
372372
}
373373

374-
async fn worker_ctx_find_database(
374+
pub(crate) async fn worker_ctx_find_database(
375375
worker_ctx: &(impl ControlStateDelegate + ?Sized),
376376
database_identity: &Identity,
377377
) -> axum::response::Result<Option<Database>> {
@@ -732,6 +732,33 @@ pub async fn set_names<S: ControlStateDelegate>(
732732
Ok((status, axum::Json(response)))
733733
}
734734

735+
#[derive(serde::Deserialize)]
736+
pub struct TimestampParams {
737+
name_or_identity: NameOrIdentity,
738+
}
739+
740+
/// Returns the database's view of the current time,
741+
/// as a SATS-JSON encoded [`Timestamp`].
742+
///
743+
/// Takes a particular database's [`NameOrIdentity`] as an argument
744+
/// because in a clusterized SpacetimeDB-cloud deployment,
745+
/// this request will be routed to the node running the requested database.
746+
async fn get_timestamp<S: ControlStateDelegate>(
747+
State(worker_ctx): State<S>,
748+
Path(TimestampParams { name_or_identity }): Path<TimestampParams>,
749+
) -> axum::response::Result<impl IntoResponse> {
750+
let db_identity = name_or_identity.resolve(&worker_ctx).await?;
751+
752+
let _database = worker_ctx_find_database(&worker_ctx, &db_identity)
753+
.await?
754+
.ok_or_else(|| {
755+
log::error!("Could not find database: {}", db_identity.to_hex());
756+
NO_SUCH_DATABASE
757+
})?;
758+
759+
Ok(axum::Json(sats::serde::SerdeWrapper(Timestamp::now())).into_response())
760+
}
761+
735762
/// This struct allows the edition to customize `/database` routes more meticulously.
736763
pub struct DatabaseRoutes<S> {
737764
/// POST /database
@@ -760,6 +787,9 @@ pub struct DatabaseRoutes<S> {
760787
pub logs_get: MethodRouter<S>,
761788
/// POST: /database/:name_or_identity/sql
762789
pub sql_post: MethodRouter<S>,
790+
791+
/// GET: /database/: name_or_identity/unstable/timestamp
792+
pub timestamp_get: MethodRouter<S>,
763793
}
764794

765795
impl<S> Default for DatabaseRoutes<S>
@@ -782,6 +812,7 @@ where
782812
schema_get: get(schema::<S>),
783813
logs_get: get(logs::<S>),
784814
sql_post: post(sql::<S>),
815+
timestamp_get: get(get_timestamp::<S>),
785816
}
786817
}
787818
}
@@ -803,7 +834,8 @@ where
803834
.route("/call/:reducer", self.call_reducer_post)
804835
.route("/schema", self.schema_get)
805836
.route("/logs", self.logs_get)
806-
.route("/sql", self.sql_post);
837+
.route("/sql", self.sql_post)
838+
.route("/unstable/timestamp", self.timestamp_get);
807839

808840
axum::Router::new()
809841
.route("/", self.root_post)

crates/client-api/src/routes/mod.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ mod internal;
1212
pub mod metrics;
1313
pub mod prometheus;
1414
pub mod subscribe;
15-
pub mod unstable;
1615

1716
/// This API call is just designed to allow clients to determine whether or not they can
1817
/// establish a connection to SpacetimeDB. This API call doesn't actually do anything.
@@ -41,5 +40,4 @@ where
4140
axum::Router::new()
4241
.nest("/v1", router.layer(cors))
4342
.nest("/internal", internal::router())
44-
.nest("/unstable", unstable::router())
4543
}

crates/client-api/src/routes/unstable.rs

Lines changed: 0 additions & 21 deletions
This file was deleted.

smoketests/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,10 @@ def run():
253253
# and **not raise any exceptions to the caller**.
254254
return ReturnThread(run).join
255255

256+
# Make an HTTP call with `method` to `path`.
257+
#
258+
# If the response is 200, return the body.
259+
# Otherwise, throw an `Exception` constructed with two arguments, the response object and the body.
256260
def api_call(self, method, path, body = None, headers = {}):
257261
with open(self.config_path, "rb") as f:
258262
config = tomllib.load(f)
@@ -267,7 +271,7 @@ def api_call(self, method, path, body = None, headers = {}):
267271
body = resp.read()
268272
logging.debug(f"{resp.status} {body}")
269273
if resp.status != 200:
270-
raise resp
274+
raise Exception(resp, body)
271275
return body
272276

273277

smoketests/tests/timestamp_route.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,29 @@
66
TIMESTAMP_TAG = "__timestamp_micros_since_unix_epoch__"
77

88
class TimestampRoute(Smoketest):
9-
AUTOPUBLISH = False
9+
AUTO_PUBLISH = False
1010

1111
def test_timestamp_route(self):
12+
name = random_string()
13+
14+
# A request for the timestamp at a non-existent database is an error...
15+
with self.assertRaises(Exception) as err:
16+
self.api_call(
17+
"GET",
18+
f"/v1/database/{name}/unstable/timestamp",
19+
)
20+
# ... with code 404.
21+
self.assertEqual(err.exception.args[0].status, 404)
22+
23+
self.publish_module(name)
24+
25+
# A request for the timestamp at an extant database is a success...
1226
resp = self.api_call(
1327
"GET",
14-
"/unstable/timestamp",
28+
f"/v1/database/{name}/unstable/timestamp",
1529
)
30+
31+
# ... and the response body is a SATS-JSON encoded `Timestamp`.
1632
timestamp = json.load(io.BytesIO(resp))
1733
self.assertIsInstance(timestamp, dict)
1834
self.assertIn(TIMESTAMP_TAG, timestamp)

0 commit comments

Comments
 (0)