-
Notifications
You must be signed in to change notification settings - Fork 109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Server-side implementation of incremental subscription changes #2030
base: master
Are you sure you want to change the base?
Changes from 10 commits
5c415fe
4feeae3
d6107bc
05d4c30
4c53a42
24bec7d
a29bc27
65b6400
f46c6b3
2228b65
4c31994
32c1feb
2abffbf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,7 +31,7 @@ use spacetimedb_sats::{ | |
de::{Deserialize, Error}, | ||
impl_deserialize, impl_serialize, impl_st, | ||
ser::{serde::SerializeWrapper, Serialize}, | ||
AlgebraicType, SpacetimeType, | ||
u256, AlgebraicType, SpacetimeType, | ||
}; | ||
use std::{ | ||
io::{self, Read as _, Write as _}, | ||
|
@@ -90,6 +90,10 @@ pub enum ClientMessage<Args> { | |
Subscribe(Subscribe), | ||
/// Send a one-off SQL query without establishing a subscription. | ||
OneOffQuery(OneOffQuery), | ||
/// Register a SQL query to to subscribe to updates. This does not affect other subscriptions. | ||
SubscribeSingle(SubscribeSingle), | ||
/// Remove a subscription to a SQL query that was added with SubscribeSingle. | ||
Unsubscribe(Unsubscribe), | ||
} | ||
|
||
impl<Args> ClientMessage<Args> { | ||
|
@@ -106,8 +110,10 @@ impl<Args> ClientMessage<Args> { | |
request_id, | ||
flags, | ||
}), | ||
ClientMessage::Subscribe(x) => ClientMessage::Subscribe(x), | ||
ClientMessage::OneOffQuery(x) => ClientMessage::OneOffQuery(x), | ||
ClientMessage::SubscribeSingle(x) => ClientMessage::SubscribeSingle(x), | ||
ClientMessage::Unsubscribe(x) => ClientMessage::Unsubscribe(x), | ||
ClientMessage::Subscribe(x) => ClientMessage::Subscribe(x), | ||
} | ||
} | ||
} | ||
|
@@ -162,6 +168,19 @@ impl_deserialize!([] CallReducerFlags, de => match de.deserialize_u8()? { | |
x => Err(D::Error::custom(format_args!("invalid call reducer flag {x}"))), | ||
}); | ||
|
||
/// A hash of a query. I'm not sure this has a use in the client other than for debugging. | ||
#[derive(SpacetimeType, Clone, Debug)] | ||
#[sats(crate = spacetimedb_lib)] | ||
pub struct QueryId { | ||
pub hash: u256, | ||
} | ||
|
||
impl QueryId { | ||
pub fn new(hash: u256) -> Self { | ||
Self { hash } | ||
} | ||
} | ||
|
||
/// Sent by client to database to register a set of queries, about which the client will | ||
/// receive `TransactionUpdate`s. | ||
/// | ||
|
@@ -184,6 +203,39 @@ pub struct Subscribe { | |
pub request_id: u32, | ||
} | ||
|
||
/// Sent by client to register a subscription to single query, for which the client should receive | ||
/// receive relevant `TransactionUpdate`s. | ||
/// | ||
/// After issuing a `SubscribeSingle` message, the client will receive a single | ||
/// `SubscribeApplied` message containing every current row which matches the query. Then, any | ||
/// time a reducer updates the query's results, the client will receive a `TransactionUpdate` | ||
/// containing the relevant updates. | ||
/// | ||
/// If a client subscribes to queries with overlapping results, the client will receive | ||
/// multiple copies of rows that appear in multiple queries. | ||
#[derive(SpacetimeType)] | ||
#[sats(crate = spacetimedb_lib)] | ||
pub struct SubscribeSingle { | ||
/// A single SQL `SELECT` query to subscribe to. | ||
pub query: Box<str>, | ||
/// An identifier for a client request. | ||
/// This should not be reused for any other subscriptions on the same connection. | ||
/// TODO: Can we call this subscription_id? It feels odd that this request_id will be used for multiple messages (the Unsubscribe call). | ||
pub request_id: u32, | ||
} | ||
|
||
/// Client request for removing a query from a subscription. | ||
#[derive(SpacetimeType)] | ||
#[sats(crate = spacetimedb_lib)] | ||
pub struct Unsubscribe { | ||
/// An identifier for a client request. | ||
pub request_id: u32, | ||
/// The ID returned in the [`SubscribeApplied`] message. | ||
/// An optimization to avoid reparsing and normalizing the query string. | ||
/// TODO: I assume this is just used for debugging? | ||
pub query_id: QueryId, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See previous comment. This |
||
} | ||
|
||
/// A one-off query submission. | ||
/// | ||
/// Query should be a "SELECT * FROM Table WHERE ...". Other types of queries will be rejected. | ||
|
@@ -213,6 +265,7 @@ pub const SERVER_MSG_COMPRESSION_TAG_GZIP: u8 = 2; | |
#[sats(crate = spacetimedb_lib)] | ||
pub enum ServerMessage<F: WebsocketFormat> { | ||
/// Informs of changes to subscribed rows. | ||
/// This will be removed when we switch to `SubscribeSingle`. | ||
InitialSubscription(InitialSubscription<F>), | ||
/// Upon reducer run. | ||
TransactionUpdate(TransactionUpdate<F>), | ||
|
@@ -222,6 +275,99 @@ pub enum ServerMessage<F: WebsocketFormat> { | |
IdentityToken(IdentityToken), | ||
/// Return results to a one off SQL query. | ||
OneOffQueryResponse(OneOffQueryResponse<F>), | ||
/// Sent in response to a `SubscribeSingle` message. This contains the initial matching rows. | ||
SubscribeApplied(SubscribeApplied<F>), | ||
/// Sent in response to an `Unsubscribe` message. This contains the matching rows. | ||
UnsubscribeApplied(UnsubscribeApplied<F>), | ||
/// Communicate an error in the subscription lifecycle. | ||
SubscriptionError(SubscriptionError), | ||
/// Informs of changes to subscribed rows. | ||
SubscriptionUpdate(SubscriptionUpdate<F>), | ||
jsdt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/// The matching rows of a subscription query. | ||
#[derive(SpacetimeType)] | ||
#[sats(crate = spacetimedb_lib)] | ||
pub struct SubscribeRows<F: WebsocketFormat> { | ||
/// The table ID of the query. | ||
pub table_id: TableId, | ||
/// The table name of the query. | ||
pub table_name: Box<str>, | ||
/// The BSATN row values. | ||
pub table_rows: TableUpdate<F>, | ||
} | ||
|
||
/// Response to [`Subscribe`] containing the initial matching rows. | ||
#[derive(SpacetimeType)] | ||
#[sats(crate = spacetimedb_lib)] | ||
pub struct SubscribeApplied<F: WebsocketFormat> { | ||
/// An identifier sent by the client in requests. | ||
/// The server will include the same request_id in the response. | ||
pub request_id: u32, | ||
/// The overall time between the server receiving a request and sending the response. | ||
pub total_host_execution_duration_micros: u64, | ||
/// An identifier for the subscribed query, allocated by the server. | ||
pub query_id: QueryId, | ||
/// The matching rows for this query. | ||
pub rows: SubscribeRows<F>, | ||
} | ||
|
||
/// Server response to a client [`Unsubscribe`] request. | ||
#[derive(SpacetimeType)] | ||
#[sats(crate = spacetimedb_lib)] | ||
pub struct UnsubscribeApplied<F: WebsocketFormat> { | ||
/// Provided by the client via the `Subscribe` message. | ||
/// TODO: switch to subscription id? | ||
pub request_id: u32, | ||
/// The overall time between the server receiving a request and sending the response. | ||
pub total_host_execution_duration_micros: u64, | ||
/// The ID included in the `SubscribeApplied` and `Unsubscribe` messages. | ||
pub query_id: QueryId, | ||
/// The matching rows for this query. | ||
/// TODO: Sending row ids could reduce bandwidth here. | ||
jsdt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
pub rows: SubscribeRows<F>, | ||
} | ||
|
||
/// Server response to an error at any point of the subscription lifecycle. | ||
/// TODO: How are we supposed to handle errors without a request_id? | ||
/// Should we drop all subscriptions? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
#[derive(SpacetimeType)] | ||
#[sats(crate = spacetimedb_lib)] | ||
pub struct SubscriptionError { | ||
/// The overall time between the server receiving a request and sending the response. | ||
pub total_host_execution_duration_micros: u64, | ||
/// Provided by the client via a [`Subscribe`] or [`Unsubscribe`] message. | ||
/// [`None`] if this occurred as the result of a [`TransactionUpdate`]. | ||
pub request_id: Option<u32>, | ||
/// The return table of the query in question. | ||
/// The server is not required to set this field. | ||
/// It has been added to avoid a breaking change post 1.0. | ||
/// | ||
/// If unset, an error results in the entire subscription being dropped. | ||
/// Otherwise only queries of this table type must be dropped. | ||
pub table_id: Option<TableId>, | ||
/// An error message describing the failure. | ||
/// | ||
/// This should reference specific fragments of the query where applicable, | ||
/// but should not include the full text of the query, | ||
/// as the client can retrieve that from the `request_id`. | ||
/// | ||
/// This is intended for diagnostic purposes. | ||
/// It need not have a predictable/parseable format. | ||
pub error: Box<str>, | ||
} | ||
|
||
/// Response to [`Subscribe`] containing the initial matching rows. | ||
#[derive(SpacetimeType)] | ||
#[sats(crate = spacetimedb_lib)] | ||
pub struct SubscriptionUpdate<F: WebsocketFormat> { | ||
/// A [`DatabaseUpdate`] containing only inserts, the rows which match the subscription queries. | ||
pub database_update: DatabaseUpdate<F>, | ||
/// An identifier sent by the client in requests. | ||
/// The server will include the same request_id in the response. | ||
pub request_id: u32, | ||
/// The overall time between the server receiving a request and sending the response. | ||
pub total_host_execution_duration_micros: u64, | ||
} | ||
|
||
/// Response to [`Subscribe`] containing the initial matching rows. | ||
|
@@ -397,6 +543,15 @@ impl<F: WebsocketFormat> TableUpdate<F> { | |
} | ||
} | ||
|
||
pub fn empty(table_id: TableId, table_name: Box<str>) -> Self { | ||
Self { | ||
table_id, | ||
table_name, | ||
num_rows: 0, | ||
updates: SmallVec::new(), | ||
} | ||
} | ||
|
||
pub fn push(&mut self, (update, num_rows): (F::QueryUpdate, u64)) { | ||
self.updates.push(update); | ||
self.num_rows += num_rows; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there's some confusion here due to a typo in the proposal. The intended design is:
request_id
, assigned by the client. It is not an error for the client to re-userequest_id
s, as their only purpose is to correlate a request/response pair; they have no semantic meaning to the host.Unsubscribe
gets a uniquerequest_id
distinct from the one inSubscribeSingle
.QueryId
included in theUnsubscribe
message to determine which query is being removed.It looks like at some point, someone (almost certainly me) made a copy-paste error in the proposal:
This should say, "Provided by the client via the
Unsubscribe
message." That is, the host treats it as an opaque nonce, and does not retain it or recognize it after sending its response.Sorry for the confusion!