use spiral_contract::call::CallError;
use crate::api::tables::objects::TableId;
use crate::api::tables::{CreateVersionRequest, TablesError};
use crate::resources::arbor::Client as ArborClient;
use crate::scandal::types::{PreparedWriteReceipt, TableWriteResult};
use crate::{Client, Error, Table};
impl Client {
pub async fn register_prepared(&self, receipt: PreparedWriteReceipt) -> Result<Table, Error> {
let result = register(&self.connection.arbor, receipt).await?;
Ok(self.table_by_id(result.resource_id, result.version_id))
}
}
pub(crate) async fn register(
client: &ArborClient,
receipt: PreparedWriteReceipt,
) -> Result<TableWriteResult, Error> {
let result = client
.create_version(
TableId(receipt.resource_id),
&CreateVersionRequest {
base_version_id: receipt.base_version,
version_id: receipt.version_id,
},
)
.await
.map_err(|error| match &error {
Error::Arbor(failure) if registration_may_have_committed(failure) => {
Error::RegistrationUnknown {
receipt: receipt.clone(),
message: error.to_string(),
}
}
_ => error,
})?;
if result.version_id != receipt.version_id
|| result.parent_version_id != receipt.base_version
|| result.seq_no.0 != receipt.sequence.0
{
return Err(Error::RegistrationUnknown {
receipt,
message: "registration did not match the prepared receipt".into(),
});
}
Ok(TableWriteResult {
resource_id: receipt.resource_id,
version_id: receipt.version_id,
})
}
fn registration_may_have_committed(error: &CallError<TablesError>) -> bool {
match error {
CallError::Service(failure) => failure.status() >= 500,
CallError::UnknownRemote(failure) => failure.status() >= 500,
CallError::Transport(failure) => failure.observed_status().is_none_or(uncertain_status),
CallError::Protocol(failure) => failure.observed_status().is_none_or(uncertain_status),
CallError::Internal { .. } => true,
}
}
fn uncertain_status(status: u16) -> bool {
(200..300).contains(&status) || status >= 500
}
#[cfg(test)]
mod tests {
use serde_json::json;
use uuid::Uuid;
use wiremock::matchers::{body_json, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use super::*;
use crate::auth::TokenSource;
#[tokio::test]
async fn registration_retains_the_receipt_when_the_outcome_is_uncertain() {
let receipt = PreparedWriteReceipt {
base_version: Some(Uuid::now_v7()),
resource_id: Uuid::new_v4(),
sequence: 2.into(),
version_id: Uuid::now_v7(),
};
for status in [200, 409, 500] {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path(format!(
"/api/tables/{}/versions",
receipt.resource_id
)))
.and(body_json(json!({
"baseVersionId": receipt.base_version,
"versionId": receipt.version_id,
})))
.respond_with(ResponseTemplate::new(status).set_body_json(json!({
"versionId": receipt.version_id,
"parentVersionId": receipt.base_version,
"seqNo": receipt.sequence,
})))
.expect(1)
.mount(&server)
.await;
let result = register(
&ArborClient::new(&server.uri())
.unwrap()
.with_token_source(TokenSource::fixed("test".into())),
receipt.clone(),
)
.await;
match status {
200 => assert_eq!(result.unwrap().version_id, receipt.version_id),
409 => assert!(matches!(result, Err(Error::Arbor(_)))),
_ => assert!(matches!(
result,
Err(Error::RegistrationUnknown { receipt: retained, .. }) if retained == receipt
)),
}
}
let unavailable = ArborClient::new("http://127.0.0.1:9")
.unwrap()
.with_token_source(TokenSource::fixed("test".into()));
assert!(matches!(
register(&unavailable, receipt.clone()).await,
Err(Error::RegistrationUnknown { receipt: retained, .. }) if retained == receipt
));
}
}