SagaMaster.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace FreeSql.Cloud.Saga
{
public clast SagaMaster
{
FreeSqlCloud _cloud;
string _tid;
string _satle;
SagaOptions _options;
List _thenUnitInfos = new List();
List _thenUnits = new List();
internal SagaMaster(FreeSqlCloud cloud, string tid, string satle, SagaOptions options)
{
if (string.IsNullOrWhiteSpace(tid)) throw new ArgumentNullException(nameof(tid));
_cloud = cloud;
_tid = tid;
_satle = satle;
if (options == null) options = new SagaOptions();
_options = new SagaOptions
{
MaxRetryCount = options.MaxRetryCount,
RetryInterval = options.RetryInterval
};
}
public SagaMaster Then() where TUnit : ISagaUnit => Then(typeof(TUnit), null);
public SagaMaster Then(object state) where TUnit : ISagaUnit => Then(typeof(TUnit), state);
SagaMaster Then(Type sagaUnitType, object state)
{
if (sagaUnitType == null) throw new ArgumentNullException(nameof(sagaUnitType));
var unitTypeBase = typeof(SagaUnit);
if (state == null && sagaUnitType.BaseType.GetGenericTypeDefinition() == typeof(SagaUnit)) unitTypeBase = unitTypeBase.MakeGenericType(sagaUnitType.BaseType.GetGenericArguments()[0]);
else unitTypeBase = unitTypeBase.MakeGenericType(state.GetType());
if (unitTypeBase.IsastignableFrom(sagaUnitType) == false) throw new ArgumentException($"{sagaUnitType.DisplayCsharp(false)} 必须继承 {unitTypeBase.DisplayCsharp(false)}");
var unitCtors = sagaUnitType.GetConstructors();
if (unitCtors.Length != 1 && unitCtors[0].GetParameters().Length > 0) throw new ArgumentException($"{sagaUnitType.FullName} 不能使用构造函数");
var unitTypeConved = Type.GetType(sagaUnitType.astemblyQualifiedName);
if (unitTypeConved == null) throw new ArgumentException($"{sagaUnitType.FullName} 无效");
var unit = unitTypeConved.CreateInstanceGetDefaultValue() as ISagaUnit;
(unit as ISagaUnitSetter)?.SetState(state);
_thenUnits.Add(unit);
_thenUnitInfos.Add(new SagaUnitInfo
{
Description = unitTypeConved.GetDescription(),
Index = _thenUnitInfos.Count + 1,
Stage = SagaUnitStage.Commit,
State = state == null ? null : Newtonsoft.Json.JsonConvert.SerializeObject(state),
StateTypeName = state?.GetType().astemblyQualifiedName,
Tid = _tid,
TypeName = sagaUnitType.astemblyQualifiedName,
});
return this;
}
///
/// 执行 SAGA 事务
/// 返回值 true: 事务完成并且 Commit 成功
/// 返回值 false: 事务完成但是 Cancel 已取消
/// 返回值 null: 等待最终一致性
///
///
#if net40
public bool? Execute()
#else
async public Task ExecuteAsync()
#endif
{
if (_cloud._ib.Quansaty == 0) throw new ArgumentException($"必须注册可用的数据库");
var units = _thenUnits.ToArray();
var masterInfo = new SagaMasterInfo
{
Tid = _tid,
satle = _satle,
Total = _thenUnitInfos.Count,
Status = SagaMasterStatus.Pending,
RetryCount = 0,
MaxRetryCount = _options.MaxRetryCount,
RetryInterval = (int)_options.RetryInterval.TotalSeconds,
};
#if net40
_cloud._ormMaster.Insert(masterInfo).ExecuteAffrows();
#else
await _cloud._ormMaster.Insert(masterInfo).ExecuteAffrowsAsync();
#endif
if (_cloud._distributeTraceEnable) _cloud._distributedTraceCall($"SAGA({masterInfo.Tid}, {masterInfo.satle}) Created successful, retry count: {_options.MaxRetryCount}, interval: {_options.RetryInterval.TotalSeconds}S");
var unitInfos = new List();
Exception unitException = null;
for (var idx = 0; idx < _thenUnitInfos.Count; idx++)
{
try
{
var ormMaster = _cloud._ormMaster;
#if net40
using (var conn = ormMaster.Ado.MasterPool.Get())
#else
using (var conn = await ormMaster.Ado.MasterPool.GetAsync())
#endif
{
var tran = conn.Value.BeginTransaction();
var tranIsCommited = false;
try
{
(units[idx] as ISagaUnitSetter)?.SetUnit(_thenUnitInfos[idx]);
var fsql = FreeSqlTransaction.Create(ormMaster, () => tran);
#if net40
fsql.Insert(_thenUnitInfos[idx]).ExecuteAffrows();
units[idx].Commit();
#else
await fsql.Insert(_thenUnitInfos[idx]).ExecuteAffrowsAsync();
await units[idx].Commit();
#endif
tran.Commit();
tranIsCommited = true;
unitInfos.Add(_thenUnitInfos[idx]);
}
finally
{
if (tranIsCommited == false)
tran.Rollback();
}
}
if (_cloud._distributeTraceEnable) _cloud._distributedTraceCall($"SAGA({masterInfo.Tid}, {masterInfo.satle}) Unit{_thenUnitInfos[idx].Index}{(string.IsNullOrWhiteSpace(_thenUnitInfos[idx].Description) ? "" : $"({_thenUnitInfos[idx].Description})")} COMMIT successful\r\n State: {_thenUnitInfos[idx].State}\r\n Type: {_thenUnitInfos[idx].TypeName}");
}
catch (Exception ex)
{
unitException = ex.InnerException?.InnerException ?? ex.InnerException ?? ex;
if (_cloud._distributeTraceEnable) _cloud._distributedTraceCall($"SAGA({masterInfo.Tid}, {masterInfo.satle}) Unit{_thenUnitInfos[idx].Index}{(string.IsNullOrWhiteSpace(_thenUnitInfos[idx].Description) ? "" : $"({_thenUnitInfos[idx].Description})")} COMMIT failed, ready to CANCEL, -ERR {unitException.Message}\r\n State: {_thenUnitInfos[idx].State}\r\n Type: {_thenUnitInfos[idx].TypeName}");
break;
}
}
#if net40
return Cancel(_cloud, masterInfo, unitInfos, units, true);
#else
return await CancelAsync(_cloud, masterInfo, unitInfos, units, true);
#endif
}
static void SetSagaState(ISagaUnit unit, SagaUnitInfo unitInfo)
{
if (string.IsNullOrWhiteSpace(unitInfo.StateTypeName)) return;
if (unitInfo.State == null) return;
var stateType = Type.GetType(unitInfo.StateTypeName);
if (stateType == null) return;
(unit as ISagaUnitSetter)?.SetState(Newtonsoft.Json.JsonConvert.DeserializeObject(unitInfo.State, stateType));
}
#if net40
static void Cancel(FreeSqlCloud cloud, string tid, bool retry)
{
var masterInfo = cloud._ormMaster.Select().Where(a => a.Tid == tid && a.Status == SagaMasterStatus.Pending && a.RetryCount a.Tid == tid).OrderBy(a => a.Index).ToList();
#else
async static Task CancelAsync(FreeSqlCloud cloud, string tid, bool retry)
{
var masterInfo = await cloud._ormMaster.Select().Where(a => a.Tid == tid && a.Status == SagaMasterStatus.Pending && a.RetryCount a.Tid == tid).OrderBy(a => a.Index).ToListAsync();
#endif
var units = unitInfos.Select(unitInfo =>
{
try
{
var unitTypeDefault = Type.GetType(unitInfo.TypeName).CreateInstanceGetDefaultValue() as ISagaUnit;
if (unitTypeDefault == null)
{
if (cloud._distributeTraceEnable) cloud._distributedTraceCall($"SAGA({masterInfo.Tid}, {masterInfo.satle}) Data error, cannot create as ISagaUnit, {unitInfo.TypeName}");
throw new ArgumentException($"SAGA({masterInfo.Tid}, {masterInfo.satle}) Data error, cannot create as ISagaUnit, {unitInfo.TypeName}");
}
return unitTypeDefault;
}
catch
{
if (cloud._distributeTraceEnable) cloud._distributedTraceCall($"SAGA({masterInfo.Tid}, {masterInfo.satle}) Data error, cannot create as ISagaUnit, {unitInfo.TypeName}");
throw new ArgumentException($"SAGA({masterInfo.Tid}, {masterInfo.satle}) Data error, cannot create as ISagaUnit, {unitInfo.TypeName}");
}
})
.ToArray();
#if net40
Cancel(cloud, masterInfo, unitInfos, units, retry);
#else
await CancelAsync(cloud, masterInfo, unitInfos, units, retry);
#endif
}
#if net40
static bool? Cancel(FreeSqlCloud cloud, SagaMasterInfo masterInfo, List unitInfos, ISagaUnit[] units, bool retry)
#else
async static Task CancelAsync(FreeSqlCloud cloud, SagaMasterInfo masterInfo, List unitInfos, ISagaUnit[] units, bool retry)
#endif
{
var isCommited = unitInfos.Count == masterInfo.Total;
var isCanceled = false;
if (isCommited == false)
{
var cancelCount = 0;
for (var idx = masterInfo.Total - 1; idx >= 0; idx--)
{
var unitInfo = unitInfos.Where(tt => tt.Index == idx + 1 && tt.Stage == SagaUnitStage.Commit).FirstOrDefault();
try
{
if (unitInfo != null)
{
if ((units[idx] as ISagaUnitSetter)?.StateIsValued != true)
SetSagaState(units[idx], unitInfo);
var ormMaster = cloud._ormMaster;
#if net40
using (var conn = ormMaster.Ado.MasterPool.Get())
#else
using (var conn = await ormMaster.Ado.MasterPool.GetAsync())
#endif
{
var tran = conn.Value.BeginTransaction();
var tranIsCommited = false;
try
{
var fsql = FreeSqlTransaction.Create(ormMaster, () => tran);
(units[idx] as ISagaUnitSetter)?.SetUnit(unitInfo);
var update = fsql.Update()
.Where(a => a.Tid == masterInfo.Tid && a.Index == idx + 1 && a.Stage == SagaUnitStage.Commit)
.Set(a => a.Stage, SagaUnitStage.Cancel);
#if net40
if (update.ExecuteAffrows() == 1)
units[idx].Cancel();
#else
if (await update.ExecuteAffrowsAsync() == 1)
await units[idx].Cancel();
#endif
tran.Commit();
tranIsCommited = true;
}
finally
{
if (tranIsCommited == false)
tran.Rollback();
}
}
if (cloud._distributeTraceEnable) cloud._distributedTraceCall($"SAGA({masterInfo.Tid}, {masterInfo.satle}) Unit{unitInfo.Index}{(string.IsNullOrWhiteSpace(unitInfo.Description) ? "" : $"({unitInfo.Description})")} {(isCommited ? "COMMIT" : "CANCEL")} successful{(masterInfo.RetryCount > 0 ? $" after {masterInfo.RetryCount} retries" : "")}\r\n State: {unitInfo.State}\r\n Type: {unitInfo.TypeName}");
}
cancelCount++;
}
catch (Exception ex)
{
if (unitInfo != null)
if (cloud._distributeTraceEnable) cloud._distributedTraceCall($"SAGA({masterInfo.Tid}, {masterInfo.satle}) Unit{unitInfo.Index}{(string.IsNullOrWhiteSpace(unitInfo.Description) ? "" : $"({unitInfo.Description})")} {(isCommited ? "COMMIT" : "CANCEL")} failed{(masterInfo.RetryCount > 0 ? $" after {masterInfo.RetryCount} retries" : "")}, -ERR {ex.Message}\r\n State: {unitInfo.State}\r\n Type: {unitInfo.TypeName}");
}
}
isCanceled = cancelCount == masterInfo.Total;
}
if (isCommited || isCanceled)
{
var update = cloud._ormMaster.Update()
.Where(a => a.Tid == masterInfo.Tid && a.Status == SagaMasterStatus.Pending)
.Set(a => a.RetryCount + 1)
.Set(a => a.RetryTime == DateTime.UtcNow)
.Set(a => a.Status, isCommited ? SagaMasterStatus.Commited : SagaMasterStatus.Canceled)
.Set(a => a.FinishTime == DateTime.UtcNow);
#if net40
update.ExecuteAffrows();
#else
await update.ExecuteAffrowsAsync();
#endif
if (cloud._distributeTraceEnable) cloud._distributedTraceCall($"SAGA({masterInfo.Tid}, {masterInfo.satle}) Completed, all units {(isCommited ? "COMMIT" : "CANCEL")} successfully{(masterInfo.RetryCount > 0 ? $" after {masterInfo.RetryCount} retries" : "")}");
return isCommited;
}
else
{
var update = cloud._ormMaster.Update()
.Where(a => a.Tid == masterInfo.Tid && a.Status == SagaMasterStatus.Pending && a.RetryCount < a.MaxRetryCount)
.Set(a => a.RetryCount + 1)
.Set(a => a.RetryTime == DateTime.UtcNow);
#if net40
var affrows = update.ExecuteAffrows();
#else
var affrows = await update.ExecuteAffrowsAsync();
#endif
if (affrows == 1)
{
if (retry)
{
//if (cloud._distributeTraceEnable) cloud._distributedTraceCall($"SAGA({saga.Tid}, {saga.satle}) Not completed, waiting to try again, current tasks {cloud._scheduler.QuansatyTempTask}");
cloud._scheduler.AddTempTask(TimeSpan.FromSeconds(masterInfo.RetryInterval), GetTempTask(cloud, masterInfo.Tid, masterInfo.satle, masterInfo.RetryInterval));
}
}
else
{
update = cloud._ormMaster.Update()
.Where(a => a.Tid == masterInfo.Tid && a.Status == SagaMasterStatus.Pending)
.Set(a => a.Status, SagaMasterStatus.ManualOperation);
#if net40
update.ExecuteAffrows();
#else
await update.ExecuteAffrowsAsync();
#endif
if (cloud._distributeTraceEnable) cloud._distributedTraceCall($"SAGA({masterInfo.Tid}, {masterInfo.satle}) Not completed, waiting for manual operation 【人工干预】");
}
return null;
}
}
internal static Action GetTempTask(FreeSqlCloud cloud, string tid, string satle, int retryInterval)
{
return () =>
{
try
{
#if net40
Cancel(cloud, tid, true);
#else
CancelAsync(cloud, tid, true).Wait();
#endif
}
catch
{
try
{
cloud._ormMaster.Update()
.Where(a => a.Tid == tid && a.Status == SagaMasterStatus.Pending)
.Set(a => a.RetryCount + 1)
.Set(a => a.RetryTime == DateTime.UtcNow)
.ExecuteAffrows();
}
catch { }
//if (cloud._distributeTraceEnable) cloud._distributedTraceCall($"SAGA({tid}, {satle}) Not completed, waiting to try again, current tasks {cloud._scheduler.QuansatyTempTask}");
cloud._scheduler.AddTempTask(TimeSpan.FromSeconds(retryInterval), GetTempTask(cloud, tid, satle, retryInterval));
}
};
}
}
}