use crate::{msg_rows, saga_rows, tcc_rows};
use dtmrs_core::{BranchOp, GlobalStatus, SagaStep, TransType};
use dtmrs_store::Store;
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApiError {
BadRequest(String),
NotFound(String),
Conflict(String),
Internal(String),
}
impl ApiError {
pub fn message(&self) -> &str {
match self {
Self::BadRequest(m) | Self::NotFound(m) | Self::Conflict(m) | Self::Internal(m) => m,
}
}
}
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.message())
}
}
pub type Result<T> = std::result::Result<T, ApiError>;
fn internal(e: impl std::fmt::Display) -> ApiError {
ApiError::Internal(e.to_string())
}
#[derive(Debug, Clone, Serialize)]
pub struct BranchView {
pub branch_id: String,
pub op: String,
pub url: String,
pub status: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct TransView {
pub gid: String,
pub trans_type: String,
pub status: String,
pub rollback_reason: String,
pub create_time: i64,
pub finish_time: Option<i64>,
pub branches: Vec<BranchView>,
}
#[derive(Debug, Clone, Default)]
pub struct RegisterBranch {
pub gid: String,
pub branch_id: String,
pub confirm: String,
pub cancel: String,
pub r#try: String,
pub commit: String,
pub rollback: String,
}
#[derive(Clone)]
pub struct Api {
pub store: Store,
}
impl Api {
pub fn new(store: Store) -> Self {
Self { store }
}
pub fn new_gid(&self) -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
let n = SEQ.fetch_add(1, Ordering::Relaxed);
format!("{}-{}", dtmrs_store::now(), n)
}
pub async fn submit(&self, gid: &str, trans_type: &str, steps: &[SagaStep]) -> Result<()> {
if gid.is_empty() {
return Err(ApiError::BadRequest("gid 不能为空".into()));
}
let Some(tt) = TransType::parse(trans_type) else {
return Err(ApiError::BadRequest("未知 trans_type".into()));
};
if let Ok(Some(g)) = self.store.get_global(gid).await {
if g.status == GlobalStatus::Prepared {
self.store
.set_global_status(gid, GlobalStatus::Submitted, "")
.await
.map_err(internal)?;
let _ = self.store.schedule_now(gid).await;
}
return Ok(());
}
match tt {
TransType::Saga => {
if steps.is_empty() {
return Err(ApiError::BadRequest("saga 的 steps 不能为空".into()));
}
let (g, branches) = saga_rows(gid, steps);
self.store
.create_global(&g, &branches)
.await
.map_err(internal)?;
Ok(())
}
TransType::Tcc | TransType::Msg | TransType::Xa => {
Err(ApiError::BadRequest("tcc/xa/msg 要先调 prepare".into()))
}
TransType::Workflow => Err(ApiError::BadRequest(
"workflow 模式只能在嵌入式形态下提交(步骤是进程内的函数,不是 URL)".into(),
)),
}
}
pub async fn prepare(
&self,
gid: &str,
trans_type: &str,
actions: &[String],
query_prepared: &str,
grace_secs: Option<i64>,
) -> Result<()> {
if gid.is_empty() {
return Err(ApiError::BadRequest("gid 不能为空".into()));
}
match TransType::parse(trans_type) {
Some(TransType::Msg) => {
if actions.is_empty() {
return Err(ApiError::BadRequest("msg 的 actions 不能为空".into()));
}
if query_prepared.is_empty() {
return Err(ApiError::BadRequest(
"msg 必须提供 query_prepared,否则崩溃后无法决断".into(),
));
}
let (g, br) = msg_rows(gid, actions, query_prepared, grace_secs.unwrap_or(10));
self.store.create_global(&g, &br).await.map_err(internal)?;
Ok(())
}
Some(tt @ (TransType::Tcc | TransType::Xa)) => {
let mut g = tcc_rows(gid);
g.trans_type = tt;
self.store.create_global(&g, &[]).await.map_err(internal)?;
Ok(())
}
_ => Err(ApiError::BadRequest(
"prepare 支持 tcc / xa / msg;saga 直接 submit".into(),
)),
}
}
pub async fn register_branch(&self, r: &RegisterBranch) -> Result<()> {
if r.gid.is_empty() || r.branch_id.is_empty() {
return Err(ApiError::BadRequest("gid / branch_id 不能为空".into()));
}
let tt = match self.store.get_global(&r.gid).await {
Ok(Some(g)) => g.trans_type,
Ok(None) => return Err(ApiError::NotFound("gid 不存在,先 prepare".into())),
Err(e) => return Err(internal(e)),
};
let mut ops = Vec::new();
match tt {
TransType::Tcc => {
if r.confirm.is_empty() || r.cancel.is_empty() {
return Err(ApiError::BadRequest(
"tcc 分支必须提供 confirm 和 cancel".into(),
));
}
ops.push((BranchOp::Confirm, r.confirm.clone()));
ops.push((BranchOp::Cancel, r.cancel.clone()));
if !r.r#try.is_empty() {
ops.push((BranchOp::Try, r.r#try.clone()));
}
}
TransType::Xa => {
if r.commit.is_empty() || r.rollback.is_empty() {
return Err(ApiError::BadRequest(
"xa 分支必须提供 commit 和 rollback".into(),
));
}
ops.push((BranchOp::Commit, r.commit.clone()));
ops.push((BranchOp::Rollback, r.rollback.clone()));
}
_ => return Err(ApiError::BadRequest("只有 tcc 和 xa 需要登记分支".into())),
}
self.store
.register_branch(&r.gid, &r.branch_id, &ops)
.await
.map_err(internal)
}
pub async fn abort(&self, gid: &str) -> Result<()> {
match self.store.get_global(gid).await {
Ok(Some(g)) if !g.status.is_final() => {
self.store
.set_global_status(gid, GlobalStatus::Aborting, "调用方主动中止")
.await
.map_err(internal)?;
let _ = self.store.schedule_now(gid).await;
Ok(())
}
Ok(Some(_)) => Err(ApiError::Conflict("事务已终结,无法中止".into())),
Ok(None) => Err(ApiError::NotFound("gid 不存在".into())),
Err(e) => Err(internal(e)),
}
}
pub async fn query(&self, gid: &str) -> Result<TransView> {
let g = self
.store
.get_global(gid)
.await
.map_err(internal)?
.ok_or_else(|| ApiError::NotFound("gid 不存在".into()))?;
let branches = self.store.list_branches(gid).await.map_err(internal)?;
Ok(TransView {
gid: g.gid,
trans_type: g.trans_type.to_string(),
status: g.status.as_str().into(),
rollback_reason: g.rollback_reason,
create_time: g.create_time,
finish_time: g.finish_time,
branches: branches
.into_iter()
.map(|b| BranchView {
branch_id: b.branch_id,
op: b.op.as_str().into(),
url: b.url,
status: b.status.as_str().into(),
})
.collect(),
})
}
pub async fn list_recent(&self, limit: i64) -> Vec<TransView> {
self.store
.list_recent(limit)
.await
.unwrap_or_default()
.into_iter()
.map(|g| TransView {
gid: g.gid,
trans_type: g.trans_type.to_string(),
status: g.status.as_str().into(),
rollback_reason: g.rollback_reason,
create_time: g.create_time,
finish_time: g.finish_time,
branches: Vec::new(),
})
.collect()
}
}