use crate::{msg_rows, saga_rows, tcc_rows};
use dtmrs_core::{BranchOp, GlobalStatus, SagaStep, TransType};
use dtmrs_store::{Store, SubmitOutcome};
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,
inline: Option<crate::driver::Driver>,
}
impl Api {
pub fn new(store: Store) -> Self {
Self {
store,
inline: None,
}
}
pub fn with_inline_driver(mut self, d: crate::driver::Driver) -> Self {
self.inline = Some(d);
self
}
fn claim_for_inline(&self, g: &mut dtmrs_store::GlobalRow) -> bool {
let Some(d) = &self.inline else { return false };
g.owner = d.owner.clone();
g.next_cron_time = dtmrs_store::now() + d.lease;
true
}
async fn claim_and_submit(&self, gid: &str) -> Result<SubmitOutcome> {
let (owner, nct) = match &self.inline {
Some(d) => (d.owner.clone(), dtmrs_store::now() + d.lease),
None => (String::new(), dtmrs_store::now()),
};
self.store
.submit_prepared(gid, &owner, nct)
.await
.map_err(internal)
}
fn drive_detached(&self, g: dtmrs_store::GlobalRow) {
let Some(d) = self.inline.clone() else { return };
tokio::spawn(async move {
if let Err(e) = d.process(&g).await {
tracing::warn!(gid = %g.gid, error = %e, "提交后直接推进出错,等租约到期重试");
}
});
}
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()));
};
match tt {
TransType::Saga => {
if steps.is_empty() {
return match self.claim_and_submit(gid).await? {
SubmitOutcome::Advanced(g) => {
self.drive_detached(*g);
Ok(())
}
SubmitOutcome::Already => Ok(()),
SubmitOutcome::Missing => {
Err(ApiError::BadRequest("saga 的 steps 不能为空".into()))
}
};
}
let (mut g, branches) = saga_rows(gid, steps);
let claimed = self.claim_for_inline(&mut g);
if self
.store
.create_global(&g, &branches)
.await
.map_err(internal)?
{
if claimed {
self.drive_detached(g);
}
return Ok(());
}
if let SubmitOutcome::Advanced(g) = self.claim_and_submit(gid).await? {
self.drive_detached(*g);
}
Ok(())
}
TransType::Tcc | TransType::Msg | TransType::Xa => {
match self.claim_and_submit(gid).await? {
SubmitOutcome::Advanced(g) => {
self.drive_detached(*g);
Ok(())
}
SubmitOutcome::Already => Ok(()),
SubmitOutcome::Missing => {
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 retry(&self, gid: &str) -> Result<()> {
match self.store.get_global(gid).await {
Ok(Some(g)) if !g.status.is_final() => {
self.store.schedule_now(gid).await.map_err(internal)?;
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()
}
}