pub mod dialect;
pub use dialect::Backend;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BranchResult {
Success,
Failure,
Ongoing,
Unknown,
}
impl BranchResult {
pub fn from_http(status: u16, body: &str) -> Self {
if body.contains("FAILURE") {
return Self::Failure;
}
if body.contains("ONGOING") {
return Self::Ongoing;
}
match status {
200..=299 => Self::Success,
409 => Self::Failure,
425 => Self::Ongoing,
_ => Self::Unknown,
}
}
pub fn from_grpc(code: i32) -> Self {
match code {
GRPC_OK => Self::Success,
GRPC_ABORTED => Self::Failure,
GRPC_FAILED_PRECONDITION => Self::Ongoing,
_ => Self::Unknown,
}
}
}
pub const GRPC_OK: i32 = 0;
pub const GRPC_ABORTED: i32 = 10;
pub const GRPC_FAILED_PRECONDITION: i32 = 9;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TransType {
Saga,
Tcc,
Msg,
Xa,
Workflow,
}
impl fmt::Display for TransType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Saga => "saga",
Self::Tcc => "tcc",
Self::Msg => "msg",
Self::Xa => "xa",
Self::Workflow => "workflow",
};
f.write_str(s)
}
}
impl TransType {
pub fn parse(s: &str) -> Option<Self> {
match s {
"saga" => Some(Self::Saga),
"tcc" => Some(Self::Tcc),
"msg" => Some(Self::Msg),
"xa" => Some(Self::Xa),
"workflow" => Some(Self::Workflow),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum GlobalStatus {
Prepared,
Submitted,
Aborting,
Succeed,
Failed,
}
impl GlobalStatus {
pub fn as_str(&self) -> &'static str {
match self {
Self::Prepared => "prepared",
Self::Submitted => "submitted",
Self::Aborting => "aborting",
Self::Succeed => "succeed",
Self::Failed => "failed",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"prepared" => Some(Self::Prepared),
"submitted" => Some(Self::Submitted),
"aborting" => Some(Self::Aborting),
"succeed" => Some(Self::Succeed),
"failed" => Some(Self::Failed),
_ => None,
}
}
pub fn is_final(&self) -> bool {
matches!(self, Self::Succeed | Self::Failed)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BranchStatus {
Prepared,
Succeed,
Failed,
}
impl BranchStatus {
pub fn as_str(&self) -> &'static str {
match self {
Self::Prepared => "prepared",
Self::Succeed => "succeed",
Self::Failed => "failed",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"prepared" => Some(Self::Prepared),
"succeed" => Some(Self::Succeed),
"failed" => Some(Self::Failed),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BranchOp {
Action,
Compensate,
Try,
Confirm,
Cancel,
Commit,
Rollback,
}
impl BranchOp {
pub fn as_str(&self) -> &'static str {
match self {
Self::Action => "action",
Self::Compensate => "compensate",
Self::Try => "try",
Self::Confirm => "confirm",
Self::Cancel => "cancel",
Self::Commit => "commit",
Self::Rollback => "rollback",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"action" => Some(Self::Action),
"compensate" => Some(Self::Compensate),
"try" => Some(Self::Try),
"confirm" => Some(Self::Confirm),
"cancel" => Some(Self::Cancel),
"commit" => Some(Self::Commit),
"rollback" => Some(Self::Rollback),
_ => None,
}
}
pub fn origin_op(&self) -> Option<BranchOp> {
match self {
Self::Cancel => Some(Self::Try),
Self::Compensate => Some(Self::Action),
Self::Rollback => Some(Self::Action),
_ => None,
}
}
pub fn is_compensating(&self) -> bool {
self.origin_op().is_some()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SagaStep {
pub action: String,
pub compensate: String,
#[serde(default)]
pub payload: String,
}
impl SagaStep {
pub fn new(action: &str, compensate: &str) -> Self {
Self {
action: action.to_string(),
compensate: compensate.to_string(),
payload: String::new(),
}
}
pub fn with_payload(action: &str, compensate: &str, payload: &str) -> Self {
Self {
action: action.to_string(),
compensate: compensate.to_string(),
payload: payload.to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Advance {
Call { index: usize, op: BranchOp },
Finish(GlobalStatus),
Wait,
RunWorkflow,
}
pub fn saga_advance(
status: GlobalStatus,
actions: &[BranchStatus],
compensates: &[BranchStatus],
) -> Advance {
debug_assert_eq!(actions.len(), compensates.len());
match status {
GlobalStatus::Submitted => {
for (i, st) in actions.iter().enumerate() {
match st {
BranchStatus::Succeed => continue,
BranchStatus::Prepared => {
return Advance::Call {
index: i,
op: BranchOp::Action,
}
}
BranchStatus::Failed => return Advance::Finish(GlobalStatus::Aborting),
}
}
Advance::Finish(GlobalStatus::Succeed)
}
GlobalStatus::Aborting => {
for i in (0..compensates.len()).rev() {
if compensates[i] == BranchStatus::Prepared {
return Advance::Call {
index: i,
op: BranchOp::Compensate,
};
}
}
Advance::Finish(GlobalStatus::Failed)
}
GlobalStatus::Prepared => Advance::Wait,
s => Advance::Finish(s),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RetryPolicy {
pub initial: i64,
pub max: i64,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
initial: 10,
max: 300,
}
}
}
impl RetryPolicy {
pub fn from_env() -> Self {
let d = Self::default();
let get = |k: &str, fallback: i64| {
std::env::var(k)
.ok()
.and_then(|v| v.parse::<i64>().ok())
.filter(|v| *v > 0)
.unwrap_or(fallback)
};
let initial = get("DTMRS_RETRY_INTERVAL", d.initial);
let max = get("DTMRS_RETRY_MAX_INTERVAL", d.max);
Self {
initial,
max: max.max(initial),
}
}
}
pub fn next_interval_with(cur: i64, p: RetryPolicy) -> i64 {
if cur <= 0 {
return p.initial;
}
(cur * 2).min(p.max)
}
pub fn next_interval(cur: i64) -> i64 {
next_interval_with(cur, RetryPolicy::default())
}
#[cfg(test)]
mod tests {
use super::*;
use BranchStatus::{Failed, Prepared, Succeed};
#[test]
fn 超时不能当失败() {
assert_eq!(BranchResult::from_http(504, ""), BranchResult::Unknown);
assert_eq!(BranchResult::from_http(500, ""), BranchResult::Unknown);
assert_eq!(BranchResult::from_http(409, ""), BranchResult::Failure);
assert_eq!(
BranchResult::from_http(200, r#"{"dtm_result":"FAILURE"}"#),
BranchResult::Failure
);
assert_eq!(BranchResult::from_http(425, ""), BranchResult::Ongoing);
assert_eq!(BranchResult::from_http(200, "ok"), BranchResult::Success);
}
#[test]
fn grpc只有aborted才算失败() {
assert_eq!(BranchResult::from_grpc(0), BranchResult::Success);
assert_eq!(BranchResult::from_grpc(10), BranchResult::Failure);
assert_eq!(BranchResult::from_grpc(9), BranchResult::Ongoing);
for code in 0..=15 {
let want = match code {
0 => BranchResult::Success,
10 => BranchResult::Failure,
9 => BranchResult::Ongoing,
_ => BranchResult::Unknown,
};
assert_eq!(BranchResult::from_grpc(code), want, "gRPC 码 {code} 判错了");
}
assert_eq!(
BranchResult::from_grpc(1),
BranchResult::Unknown,
"CANCELLED 是我们自己放弃,不是对方拒绝"
);
assert_eq!(
BranchResult::from_grpc(4),
BranchResult::Unknown,
"DEADLINE_EXCEEDED 绝不能当失败"
);
assert_eq!(
BranchResult::from_grpc(14),
BranchResult::Unknown,
"UNAVAILABLE 绝不能当失败"
);
assert_eq!(BranchResult::from_grpc(99), BranchResult::Unknown);
assert_eq!(BranchResult::from_grpc(-1), BranchResult::Unknown);
}
#[test]
fn grpc与http的判定语义一致() {
for (http, grpc) in [(200u16, 0i32), (409, 10), (425, 9), (500, 13), (503, 14)] {
assert_eq!(
BranchResult::from_http(http, ""),
BranchResult::from_grpc(grpc),
"HTTP {http} 与 gRPC {grpc} 应当判定一致"
);
}
}
#[test]
fn 正向按序推进() {
let a = [Prepared, Prepared];
let c = [Prepared, Prepared];
assert_eq!(
saga_advance(GlobalStatus::Submitted, &a, &c),
Advance::Call {
index: 0,
op: BranchOp::Action
}
);
let a = [Succeed, Prepared];
assert_eq!(
saga_advance(GlobalStatus::Submitted, &a, &c),
Advance::Call {
index: 1,
op: BranchOp::Action
}
);
let a = [Succeed, Succeed];
assert_eq!(
saga_advance(GlobalStatus::Submitted, &a, &c),
Advance::Finish(GlobalStatus::Succeed)
);
}
#[test]
fn 补偿必须逆序() {
let a = [Succeed, Failed];
let c = [Prepared, Prepared];
assert_eq!(
saga_advance(GlobalStatus::Aborting, &a, &c),
Advance::Call {
index: 1,
op: BranchOp::Compensate
}
);
let c = [Prepared, Succeed];
assert_eq!(
saga_advance(GlobalStatus::Aborting, &a, &c),
Advance::Call {
index: 0,
op: BranchOp::Compensate
}
);
let c = [Succeed, Succeed];
assert_eq!(
saga_advance(GlobalStatus::Aborting, &a, &c),
Advance::Finish(GlobalStatus::Failed)
);
}
#[test]
fn 没跑过的分支也要补偿() {
let a = [Prepared, Prepared];
let c = [Prepared, Prepared];
assert_eq!(
saga_advance(GlobalStatus::Aborting, &a, &c),
Advance::Call {
index: 1,
op: BranchOp::Compensate
}
);
}
#[test]
fn 终态不再推进() {
for s in [GlobalStatus::Succeed, GlobalStatus::Failed] {
assert!(s.is_final());
assert_eq!(saga_advance(s, &[], &[]), Advance::Finish(s));
}
}
#[test]
fn 补偿操作能找到正向操作() {
assert_eq!(BranchOp::Compensate.origin_op(), Some(BranchOp::Action));
assert_eq!(BranchOp::Cancel.origin_op(), Some(BranchOp::Try));
assert_eq!(BranchOp::Action.origin_op(), None);
assert!(BranchOp::Compensate.is_compensating());
assert!(!BranchOp::Try.is_compensating());
}
#[test]
fn 退避有上限() {
assert_eq!(next_interval(0), 10);
assert_eq!(next_interval(10), 20);
assert_eq!(next_interval(200), 300);
assert_eq!(next_interval(300), 300);
}
#[test]
fn 退避策略可配且默认值不变() {
let d = RetryPolicy::default();
assert_eq!((d.initial, d.max), (10, 300));
let fast = RetryPolicy { initial: 1, max: 5 };
assert_eq!(next_interval_with(0, fast), 1);
assert_eq!(next_interval_with(1, fast), 2);
assert_eq!(next_interval_with(4, fast), 5, "封顶");
assert_eq!(next_interval_with(5, fast), 5);
}
#[test]
fn 上限小于初始值时不让退避反向增长() {
let p = RetryPolicy {
initial: 60,
max: 10,
};
let fixed = RetryPolicy {
initial: p.initial,
max: p.max.max(p.initial),
};
assert_eq!(next_interval_with(0, fixed), 60);
assert_eq!(next_interval_with(60, fixed), 60, "不该缩到 10");
}
#[test]
fn 步骤的payload默认为空且能带数据() {
let a = SagaStep::new("http://a", "http://c");
assert_eq!(a.payload, "");
let b = SagaStep::with_payload("http://a", "http://c", r#"{"amount":100}"#);
assert_eq!(b.payload, r#"{"amount":100}"#);
let old: SagaStep =
serde_json::from_str(r#"{"action":"http://a","compensate":"http://c"}"#)
.expect("老格式必须能解,否则升级后存量事务全推不动");
assert_eq!(old.payload, "");
}
}
pub fn tcc_advance(
status: GlobalStatus,
confirms: &[BranchStatus],
cancels: &[BranchStatus],
) -> Advance {
debug_assert_eq!(confirms.len(), cancels.len());
match status {
GlobalStatus::Prepared => Advance::Wait,
GlobalStatus::Submitted => {
for (i, st) in confirms.iter().enumerate() {
match st {
BranchStatus::Succeed => continue,
BranchStatus::Prepared | BranchStatus::Failed => {
return Advance::Call {
index: i,
op: BranchOp::Confirm,
}
}
}
}
Advance::Finish(GlobalStatus::Succeed)
}
GlobalStatus::Aborting => {
for i in (0..cancels.len()).rev() {
if cancels[i] != BranchStatus::Succeed {
return Advance::Call {
index: i,
op: BranchOp::Cancel,
};
}
}
Advance::Finish(GlobalStatus::Failed)
}
s => Advance::Finish(s),
}
}
pub fn msg_advance(status: GlobalStatus, actions: &[BranchStatus]) -> Advance {
match status {
GlobalStatus::Prepared => Advance::Wait,
GlobalStatus::Submitted => {
for (i, st) in actions.iter().enumerate() {
if *st != BranchStatus::Succeed {
return Advance::Call {
index: i,
op: BranchOp::Action,
};
}
}
Advance::Finish(GlobalStatus::Succeed)
}
GlobalStatus::Aborting => Advance::Finish(GlobalStatus::Failed),
s => Advance::Finish(s),
}
}
#[cfg(test)]
mod tcc_msg_tests {
use super::*;
use BranchStatus::{Failed, Prepared, Succeed};
#[test]
fn tcc的try阶段tc不插手() {
assert_eq!(
tcc_advance(GlobalStatus::Prepared, &[Prepared], &[Prepared]),
Advance::Wait
);
}
#[test]
fn tcc按序confirm() {
let c = [Prepared, Prepared];
let x = [Prepared, Prepared];
assert_eq!(
tcc_advance(GlobalStatus::Submitted, &c, &x),
Advance::Call {
index: 0,
op: BranchOp::Confirm
}
);
assert_eq!(
tcc_advance(GlobalStatus::Submitted, &[Succeed, Prepared], &x),
Advance::Call {
index: 1,
op: BranchOp::Confirm
}
);
assert_eq!(
tcc_advance(GlobalStatus::Submitted, &[Succeed, Succeed], &x),
Advance::Finish(GlobalStatus::Succeed)
);
}
#[test]
fn confirm失败绝不能触发cancel() {
let c = [Succeed, Failed];
let x = [Prepared, Prepared];
assert_eq!(
tcc_advance(GlobalStatus::Submitted, &c, &x),
Advance::Call {
index: 1,
op: BranchOp::Confirm
},
"confirm 失败要继续重试 confirm,不能转 cancel"
);
for a in [Prepared, Succeed, Failed] {
for b in [Prepared, Succeed, Failed] {
let r = tcc_advance(GlobalStatus::Submitted, &[a, b], &x);
assert_ne!(r, Advance::Finish(GlobalStatus::Aborting));
assert_ne!(r, Advance::Finish(GlobalStatus::Failed));
}
}
}
#[test]
fn tcc逆序cancel() {
let c = [Prepared, Prepared];
assert_eq!(
tcc_advance(GlobalStatus::Aborting, &c, &[Prepared, Prepared]),
Advance::Call {
index: 1,
op: BranchOp::Cancel
}
);
assert_eq!(
tcc_advance(GlobalStatus::Aborting, &c, &[Prepared, Succeed]),
Advance::Call {
index: 0,
op: BranchOp::Cancel
}
);
assert_eq!(
tcc_advance(GlobalStatus::Aborting, &c, &[Succeed, Succeed]),
Advance::Finish(GlobalStatus::Failed)
);
assert_eq!(
tcc_advance(GlobalStatus::Aborting, &c, &[Succeed, Failed]),
Advance::Call {
index: 1,
op: BranchOp::Cancel
}
);
}
#[test]
fn msg等回查而不是自己推() {
assert_eq!(
msg_advance(GlobalStatus::Prepared, &[Prepared]),
Advance::Wait
);
}
#[test]
fn msg只往前不补偿() {
assert_eq!(
msg_advance(GlobalStatus::Submitted, &[Prepared, Prepared]),
Advance::Call {
index: 0,
op: BranchOp::Action
}
);
assert_eq!(
msg_advance(GlobalStatus::Submitted, &[Failed]),
Advance::Call {
index: 0,
op: BranchOp::Action
}
);
assert_eq!(
msg_advance(GlobalStatus::Submitted, &[Succeed, Succeed]),
Advance::Finish(GlobalStatus::Succeed)
);
assert_eq!(
msg_advance(GlobalStatus::Aborting, &[Succeed]),
Advance::Finish(GlobalStatus::Failed)
);
}
}
pub fn xa_advance(
status: GlobalStatus,
commits: &[BranchStatus],
rollbacks: &[BranchStatus],
) -> Advance {
debug_assert_eq!(commits.len(), rollbacks.len());
match status {
GlobalStatus::Prepared => Advance::Wait,
GlobalStatus::Submitted => {
for (i, st) in commits.iter().enumerate() {
if *st != BranchStatus::Succeed {
return Advance::Call {
index: i,
op: BranchOp::Commit,
};
}
}
Advance::Finish(GlobalStatus::Succeed)
}
GlobalStatus::Aborting => {
for i in (0..rollbacks.len()).rev() {
if rollbacks[i] != BranchStatus::Succeed {
return Advance::Call {
index: i,
op: BranchOp::Rollback,
};
}
}
Advance::Finish(GlobalStatus::Failed)
}
s => Advance::Finish(s),
}
}
pub fn workflow_advance(status: GlobalStatus, compensates: &[BranchStatus]) -> Advance {
match status {
GlobalStatus::Prepared => Advance::Wait,
GlobalStatus::Submitted => Advance::RunWorkflow,
GlobalStatus::Aborting => {
for i in (0..compensates.len()).rev() {
if compensates[i] != BranchStatus::Succeed {
return Advance::Call {
index: i,
op: BranchOp::Compensate,
};
}
}
Advance::Finish(GlobalStatus::Failed)
}
s => Advance::Finish(s),
}
}
#[cfg(test)]
mod workflow_tests {
use super::*;
use BranchStatus::{Failed, Prepared, Succeed};
#[test]
fn submitted就是去跑函数() {
assert_eq!(
workflow_advance(GlobalStatus::Submitted, &[]),
Advance::RunWorkflow
);
assert_eq!(
workflow_advance(GlobalStatus::Submitted, &[Succeed, Prepared]),
Advance::RunWorkflow
);
}
#[test]
fn 回滚时逆序补偿() {
assert_eq!(
workflow_advance(GlobalStatus::Aborting, &[Prepared, Prepared]),
Advance::Call {
index: 1,
op: BranchOp::Compensate
},
"后执行的先回滚"
);
assert_eq!(
workflow_advance(GlobalStatus::Aborting, &[Prepared, Succeed]),
Advance::Call {
index: 0,
op: BranchOp::Compensate
}
);
assert_eq!(
workflow_advance(GlobalStatus::Aborting, &[Succeed, Succeed]),
Advance::Finish(GlobalStatus::Failed)
);
assert_eq!(
workflow_advance(GlobalStatus::Aborting, &[Succeed, Failed]),
Advance::Call {
index: 1,
op: BranchOp::Compensate
}
);
}
#[test]
fn 一个分支都没登记就回滚是直接失败() {
assert_eq!(
workflow_advance(GlobalStatus::Aborting, &[]),
Advance::Finish(GlobalStatus::Failed)
);
}
#[test]
fn 终态不再推进() {
for s in [GlobalStatus::Succeed, GlobalStatus::Failed] {
assert_eq!(workflow_advance(s, &[]), Advance::Finish(s));
}
}
#[test]
fn workflow是一种事务类型() {
assert_eq!(TransType::parse("workflow"), Some(TransType::Workflow));
assert_eq!(TransType::Workflow.to_string(), "workflow");
}
}
#[cfg(test)]
mod xa_tests {
use super::*;
use BranchStatus::{Failed, Prepared, Succeed};
#[test]
fn xa的prepare阶段tc不插手() {
assert_eq!(
xa_advance(GlobalStatus::Prepared, &[Prepared], &[Prepared]),
Advance::Wait
);
}
#[test]
fn xa按序commit() {
let r = [Prepared, Prepared];
assert_eq!(
xa_advance(GlobalStatus::Submitted, &[Prepared, Prepared], &r),
Advance::Call {
index: 0,
op: BranchOp::Commit
}
);
assert_eq!(
xa_advance(GlobalStatus::Submitted, &[Succeed, Prepared], &r),
Advance::Call {
index: 1,
op: BranchOp::Commit
}
);
assert_eq!(
xa_advance(GlobalStatus::Submitted, &[Succeed, Succeed], &r),
Advance::Finish(GlobalStatus::Succeed)
);
}
#[test]
fn commit失败绝不能转rollback() {
let r = [Prepared, Prepared];
assert_eq!(
xa_advance(GlobalStatus::Submitted, &[Succeed, Failed], &r),
Advance::Call {
index: 1,
op: BranchOp::Commit
},
"commit 失败要继续重试 commit"
);
for a in [Prepared, Succeed, Failed] {
for b in [Prepared, Succeed, Failed] {
let got = xa_advance(GlobalStatus::Submitted, &[a, b], &r);
assert_ne!(got, Advance::Finish(GlobalStatus::Aborting));
assert_ne!(got, Advance::Finish(GlobalStatus::Failed));
}
}
}
#[test]
fn xa逆序rollback且失败也要重试() {
let c = [Prepared, Prepared];
assert_eq!(
xa_advance(GlobalStatus::Aborting, &c, &[Prepared, Prepared]),
Advance::Call {
index: 1,
op: BranchOp::Rollback
}
);
assert_eq!(
xa_advance(GlobalStatus::Aborting, &c, &[Succeed, Succeed]),
Advance::Finish(GlobalStatus::Failed)
);
assert_eq!(
xa_advance(GlobalStatus::Aborting, &c, &[Succeed, Failed]),
Advance::Call {
index: 1,
op: BranchOp::Rollback
}
);
}
#[test]
fn commit操作没有反向映射() {
assert_eq!(BranchOp::Commit.origin_op(), None);
assert!(!BranchOp::Commit.is_compensating());
assert_eq!(BranchOp::parse("commit"), Some(BranchOp::Commit));
assert_eq!(BranchOp::Commit.as_str(), "commit");
}
}