1use crate::{msg_rows, saga_rows, tcc_rows};
20use dtmrs_core::{BranchOp, GlobalStatus, SagaStep, TransType};
21use dtmrs_store::{Store, SubmitOutcome};
22use serde::Serialize;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum ApiError {
26 BadRequest(String),
27 NotFound(String),
28 Conflict(String),
30 Internal(String),
31}
32
33impl ApiError {
34 pub fn message(&self) -> &str {
35 match self {
36 Self::BadRequest(m) | Self::NotFound(m) | Self::Conflict(m) | Self::Internal(m) => m,
37 }
38 }
39}
40
41impl std::fmt::Display for ApiError {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 f.write_str(self.message())
44 }
45}
46
47pub type Result<T> = std::result::Result<T, ApiError>;
48
49fn internal(e: impl std::fmt::Display) -> ApiError {
50 ApiError::Internal(e.to_string())
51}
52
53#[derive(Debug, Clone, Serialize)]
54pub struct BranchView {
55 pub branch_id: String,
56 pub op: String,
57 pub url: String,
58 pub status: String,
59}
60
61#[derive(Debug, Clone, Serialize)]
62pub struct TransView {
63 pub gid: String,
64 pub trans_type: String,
65 pub status: String,
66 pub rollback_reason: String,
67 pub create_time: i64,
68 pub finish_time: Option<i64>,
69 pub branches: Vec<BranchView>,
70}
71
72#[derive(Debug, Clone, Default)]
74pub struct RegisterBranch {
75 pub gid: String,
76 pub branch_id: String,
77 pub confirm: String,
78 pub cancel: String,
79 pub r#try: String,
80 pub commit: String,
81 pub rollback: String,
82}
83
84#[derive(Clone)]
85pub struct Api {
86 pub store: Store,
87 inline: Option<crate::driver::Driver>,
90}
91
92impl Api {
93 pub fn new(store: Store) -> Self {
94 Self {
95 store,
96 inline: None,
97 }
98 }
99
100 pub fn with_inline_driver(mut self, d: crate::driver::Driver) -> Self {
123 self.inline = Some(d);
124 self
125 }
126
127 fn claim_for_inline(&self, g: &mut dtmrs_store::GlobalRow) -> bool {
129 let Some(d) = &self.inline else { return false };
130 g.owner = d.owner.clone();
131 g.next_cron_time = dtmrs_store::now() + d.lease;
132 true
133 }
134
135 async fn claim_and_submit(&self, gid: &str) -> Result<SubmitOutcome> {
140 let (owner, nct) = match &self.inline {
141 Some(d) => (d.owner.clone(), dtmrs_store::now() + d.lease),
142 None => (String::new(), dtmrs_store::now()),
143 };
144 self.store
145 .submit_prepared(gid, &owner, nct)
146 .await
147 .map_err(internal)
148 }
149
150 fn drive_detached(&self, g: dtmrs_store::GlobalRow) {
152 let Some(d) = self.inline.clone() else { return };
153 tokio::spawn(async move {
154 if let Err(e) = d.process(&g).await {
155 tracing::warn!(gid = %g.gid, error = %e, "提交后直接推进出错,等租约到期重试");
157 }
158 });
159 }
160
161 pub fn new_gid(&self) -> String {
164 use std::sync::atomic::{AtomicU64, Ordering};
165 static SEQ: AtomicU64 = AtomicU64::new(0);
166 let n = SEQ.fetch_add(1, Ordering::Relaxed);
167 format!("{}-{}", dtmrs_store::now(), n)
168 }
169
170 pub async fn submit(&self, gid: &str, trans_type: &str, steps: &[SagaStep]) -> Result<()> {
175 if gid.is_empty() {
176 return Err(ApiError::BadRequest("gid 不能为空".into()));
177 }
178 let Some(tt) = TransType::parse(trans_type) else {
179 return Err(ApiError::BadRequest("未知 trans_type".into()));
180 };
181
182 match tt {
183 TransType::Saga => {
184 if steps.is_empty() {
185 return match self.claim_and_submit(gid).await? {
189 SubmitOutcome::Advanced(g) => {
190 self.drive_detached(*g);
191 Ok(())
192 }
193 SubmitOutcome::Already => Ok(()),
194 SubmitOutcome::Missing => {
195 Err(ApiError::BadRequest("saga 的 steps 不能为空".into()))
196 }
197 };
198 }
199 let (mut g, branches) = saga_rows(gid, steps);
200 let claimed = self.claim_for_inline(&mut g);
203 if self
208 .store
209 .create_global(&g, &branches)
210 .await
211 .map_err(internal)?
212 {
213 if claimed {
214 self.drive_detached(g);
215 }
216 return Ok(());
217 }
218 if let SubmitOutcome::Advanced(g) = self.claim_and_submit(gid).await? {
222 self.drive_detached(*g);
223 }
224 Ok(())
225 }
226 TransType::Tcc | TransType::Msg | TransType::Xa => {
227 match self.claim_and_submit(gid).await? {
232 SubmitOutcome::Advanced(g) => {
236 self.drive_detached(*g);
237 Ok(())
238 }
239 SubmitOutcome::Already => Ok(()),
241 SubmitOutcome::Missing => {
242 Err(ApiError::BadRequest("tcc/xa/msg 要先调 prepare".into()))
243 }
244 }
245 }
246 TransType::Workflow => Err(ApiError::BadRequest(
250 "workflow 模式只能在嵌入式形态下提交(步骤是进程内的函数,不是 URL)".into(),
251 )),
252 }
253 }
254
255 pub async fn prepare(
257 &self,
258 gid: &str,
259 trans_type: &str,
260 actions: &[String],
261 query_prepared: &str,
262 grace_secs: Option<i64>,
263 ) -> Result<()> {
264 if gid.is_empty() {
265 return Err(ApiError::BadRequest("gid 不能为空".into()));
266 }
267 match TransType::parse(trans_type) {
268 Some(TransType::Msg) => {
269 if actions.is_empty() {
270 return Err(ApiError::BadRequest("msg 的 actions 不能为空".into()));
271 }
272 if query_prepared.is_empty() {
273 return Err(ApiError::BadRequest(
276 "msg 必须提供 query_prepared,否则崩溃后无法决断".into(),
277 ));
278 }
279 let (g, br) = msg_rows(gid, actions, query_prepared, grace_secs.unwrap_or(10));
280 self.store.create_global(&g, &br).await.map_err(internal)?;
281 Ok(())
282 }
283 Some(tt @ (TransType::Tcc | TransType::Xa)) => {
284 let mut g = tcc_rows(gid);
285 g.trans_type = tt;
286 self.store.create_global(&g, &[]).await.map_err(internal)?;
287 Ok(())
288 }
289 _ => Err(ApiError::BadRequest(
290 "prepare 支持 tcc / xa / msg;saga 直接 submit".into(),
291 )),
292 }
293 }
294
295 pub async fn register_branch(&self, r: &RegisterBranch) -> Result<()> {
299 if r.gid.is_empty() || r.branch_id.is_empty() {
300 return Err(ApiError::BadRequest("gid / branch_id 不能为空".into()));
301 }
302 let tt = match self.store.get_global(&r.gid).await {
303 Ok(Some(g)) if matches!(g.status, GlobalStatus::Aborting) || g.status.is_final() => {
317 return Err(ApiError::Conflict(format!(
318 "事务处于 {} 状态,不能再登记分支(登记后的一阶段将无人收尾)",
319 g.status.as_str()
320 )));
321 }
322 Ok(Some(g)) => g.trans_type,
323 Ok(None) => return Err(ApiError::NotFound("gid 不存在,先 prepare".into())),
324 Err(e) => return Err(internal(e)),
325 };
326
327 let mut ops = Vec::new();
328 match tt {
329 TransType::Tcc => {
330 if r.confirm.is_empty() || r.cancel.is_empty() {
331 return Err(ApiError::BadRequest(
332 "tcc 分支必须提供 confirm 和 cancel".into(),
333 ));
334 }
335 ops.push((BranchOp::Confirm, r.confirm.clone()));
336 ops.push((BranchOp::Cancel, r.cancel.clone()));
337 if !r.r#try.is_empty() {
338 ops.push((BranchOp::Try, r.r#try.clone()));
339 }
340 }
341 TransType::Xa => {
342 if r.commit.is_empty() || r.rollback.is_empty() {
343 return Err(ApiError::BadRequest(
345 "xa 分支必须提供 commit 和 rollback".into(),
346 ));
347 }
348 ops.push((BranchOp::Commit, r.commit.clone()));
349 ops.push((BranchOp::Rollback, r.rollback.clone()));
350 }
351 _ => return Err(ApiError::BadRequest("只有 tcc 和 xa 需要登记分支".into())),
352 }
353
354 self.store
355 .register_branch(&r.gid, &r.branch_id, &ops)
356 .await
357 .map_err(internal)
358 }
359
360 pub async fn abort(&self, gid: &str) -> Result<()> {
362 match self.store.get_global(gid).await {
363 Ok(Some(g)) if !g.status.is_final() => {
364 self.store
365 .set_global_status(gid, GlobalStatus::Aborting, g.trans_type, "调用方主动中止")
366 .await
367 .map_err(internal)?;
368 let _ = self.store.schedule_now(gid).await;
369 Ok(())
370 }
371 Ok(Some(_)) => Err(ApiError::Conflict("事务已终结,无法中止".into())),
372 Ok(None) => Err(ApiError::NotFound("gid 不存在".into())),
373 Err(e) => Err(internal(e)),
374 }
375 }
376
377 pub async fn retry(&self, gid: &str) -> Result<()> {
382 match self.store.get_global(gid).await {
383 Ok(Some(g)) if !g.status.is_final() => {
384 self.store.schedule_now(gid).await.map_err(internal)?;
385 Ok(())
386 }
387 Ok(Some(_)) => Err(ApiError::Conflict("事务已终结,无需重试".into())),
388 Ok(None) => Err(ApiError::NotFound("gid 不存在".into())),
389 Err(e) => Err(internal(e)),
390 }
391 }
392
393 pub async fn query(&self, gid: &str) -> Result<TransView> {
394 let g = self
395 .store
396 .get_global(gid)
397 .await
398 .map_err(internal)?
399 .ok_or_else(|| ApiError::NotFound("gid 不存在".into()))?;
400 let branches = self.store.list_branches(gid).await.map_err(internal)?;
401 Ok(TransView {
402 gid: g.gid,
403 trans_type: g.trans_type.to_string(),
404 status: g.status.as_str().into(),
405 rollback_reason: g.rollback_reason,
406 create_time: g.create_time,
407 finish_time: g.finish_time,
408 branches: branches
409 .into_iter()
410 .map(|b| BranchView {
411 branch_id: b.branch_id,
412 op: b.op.as_str().into(),
413 url: b.url,
414 status: b.status.as_str().into(),
415 })
416 .collect(),
417 })
418 }
419
420 pub async fn list_recent(&self, limit: i64) -> Vec<TransView> {
421 self.store
422 .list_recent(limit)
423 .await
424 .unwrap_or_default()
425 .into_iter()
426 .map(|g| TransView {
427 gid: g.gid,
428 trans_type: g.trans_type.to_string(),
429 status: g.status.as_str().into(),
430 rollback_reason: g.rollback_reason,
431 create_time: g.create_time,
432 finish_time: g.finish_time,
433 branches: Vec::new(),
434 })
435 .collect()
436 }
437}