csharp/2881099/FreeSql.Cloud/src/FreeSql.Cloud/Tcc/TccMaster.cs

TccMaster.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.Tcc
{
    public clast TccMaster
    {
        FreeSqlCloud _cloud;
        string _tid;
        string _satle;
        TccOptions _options;
        List _thenUnitInfos = new List();
        List _thenUnits = new List();

        internal TccMaster(FreeSqlCloud cloud, string tid, string satle, TccOptions options)
        {
            if (string.IsNullOrWhiteSpace(tid)) throw new ArgumentNullException(nameof(tid));
            _cloud = cloud;
            _tid = tid;
            _satle = satle;
            if (options == null) options = new TccOptions();
            _options = new TccOptions
            {
                MaxRetryCount = options.MaxRetryCount,
                RetryInterval = options.RetryInterval
            };
        }

        public TccMaster Then() where TUnit : ITccUnit => Then(typeof(TUnit), null);
        public TccMaster Then(object state) where TUnit : ITccUnit => Then(typeof(TUnit), state);

        TccMaster Then(Type tccUnitType, object state)
        {
            if (tccUnitType == null) throw new ArgumentNullException(nameof(tccUnitType));
            var unitTypeBase = typeof(TccUnit);
            if (state == null && tccUnitType.BaseType.GetGenericTypeDefinition() == typeof(TccUnit)) unitTypeBase = unitTypeBase.MakeGenericType(tccUnitType.BaseType.GetGenericArguments()[0]);
            else unitTypeBase = unitTypeBase.MakeGenericType(state.GetType());
            if (unitTypeBase.IsastignableFrom(tccUnitType) == false) throw new ArgumentException($"{tccUnitType.DisplayCsharp(false)} 必须继承 {unitTypeBase.DisplayCsharp(false)}");
            var unitCtors = tccUnitType.GetConstructors();
            if (unitCtors.Length != 1 && unitCtors[0].GetParameters().Length > 0) throw new ArgumentException($"{tccUnitType.FullName} 不能使用构造函数");

            var unitTypeConved = Type.GetType(tccUnitType.astemblyQualifiedName);
            if (unitTypeConved == null) throw new ArgumentException($"{tccUnitType.FullName} 无效");
            var unit = unitTypeConved.CreateInstanceGetDefaultValue() as ITccUnit;
            (unit as ITccUnitSetter)?.SetState(state);
            _thenUnits.Add(unit);
            _thenUnitInfos.Add(new TccUnitInfo
            {
                Description = unitTypeConved.GetDescription(),
                Index = _thenUnitInfos.Count + 1,
                Stage = TccUnitStage.Try,
                State = state == null ? null : Newtonsoft.Json.JsonConvert.SerializeObject(state),
                StateTypeName = state?.GetType().astemblyQualifiedName,
                Tid = _tid,
                TypeName = tccUnitType.astemblyQualifiedName,
            });
            return this;
        }

        /// 
        /// 执行 TCC 事务
        /// 返回值 true: 事务完成并且 Confirm 成功
        /// 返回值 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 TccMasterInfo
            {
                Tid = _tid,
                satle = _satle,
                Total = _thenUnitInfos.Count,
                Status = TccMasterStatus.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($"TCC ({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 ITccUnitSetter)?.SetUnit(_thenUnitInfos[idx]);
                            var fsql = FreeSqlTransaction.Create(ormMaster, () => tran);
#if net40
                            fsql.Insert(_thenUnitInfos[idx]).ExecuteAffrows();
                            units[idx].Try();
#else
                            await fsql.Insert(_thenUnitInfos[idx]).ExecuteAffrowsAsync();
                            await units[idx].Try();
#endif
                            tran.Commit();
                            tranIsCommited = true;
                            unitInfos.Add(_thenUnitInfos[idx]);
                        }
                        finally
                        {
                            if (tranIsCommited == false)
                                tran.Rollback();
                        }
                    }
                    if (_cloud._distributeTraceEnable) _cloud._distributedTraceCall($"TCC ({masterInfo.Tid}, {masterInfo.satle}) Unit{_thenUnitInfos[idx].Index}{(string.IsNullOrWhiteSpace(_thenUnitInfos[idx].Description) ? "" : $"({_thenUnitInfos[idx].Description})")} TRY 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($"TCC ({masterInfo.Tid}, {masterInfo.satle}) Unit{_thenUnitInfos[idx].Index}{(string.IsNullOrWhiteSpace(_thenUnitInfos[idx].Description) ? "" : $"({_thenUnitInfos[idx].Description})")} TRY failed, ready to CANCEL, -ERR {unitException.Message}\r\n    State: {_thenUnitInfos[idx].State}\r\n    Type:  {_thenUnitInfos[idx].TypeName}");
                    break;
                }
            }
#if net40
            return ConfimCancel(_cloud, masterInfo, unitInfos, units, true);
#else
            return await ConfimCancelAsync(_cloud, masterInfo, unitInfos, units,  true);
#endif
        }


        static void SetTccState(ITccUnit unit, TccUnitInfo unitInfo)
        {
            if (string.IsNullOrWhiteSpace(unitInfo.StateTypeName)) return;
            if (unitInfo.State == null) return;
            var stateType = Type.GetType(unitInfo.StateTypeName);
            if (stateType == null) return;
            (unit as ITccUnitSetter)?.SetState(Newtonsoft.Json.JsonConvert.DeserializeObject(unitInfo.State, stateType));
        }
#if net40
        static void ConfimCancel(FreeSqlCloud cloud, string tid, bool retry)
        {
            var masterInfo = cloud._ormMaster.Select().Where(a => a.Tid == tid && a.Status == TccMasterStatus.Pending && a.RetryCount  a.Tid == tid).OrderBy(a => a.Index).ToList();
#else
        async static Task ConfimCancelAsync(FreeSqlCloud cloud, string tid, bool retry)
        {
            var masterInfo = await cloud._ormMaster.Select().Where(a => a.Tid == tid && a.Status == TccMasterStatus.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 ITccUnit;
                    if (unitTypeDefault == null)
                    {
                        if (cloud._distributeTraceEnable) cloud._distributedTraceCall($"TCC ({masterInfo.Tid}, {masterInfo.satle}) Data error, cannot create as ITccUnit, {unitInfo.TypeName}");
                        throw new ArgumentException($"TCC ({masterInfo.Tid}, {masterInfo.satle}) Data error, cannot create as ITccUnit, {unitInfo.TypeName}");
                    }
                    return unitTypeDefault;
                }
                catch
                {
                    if (cloud._distributeTraceEnable) cloud._distributedTraceCall($"TCC ({masterInfo.Tid}, {masterInfo.satle}) Data error, cannot create as ITccUnit, {unitInfo.TypeName}");
                    throw new ArgumentException($"TCC ({masterInfo.Tid}, {masterInfo.satle}) Data error, cannot create as ITccUnit, {unitInfo.TypeName}");
                }
            })
            .ToArray();

#if net40
            ConfimCancel(cloud, masterInfo, unitInfos, units, retry);
#else
            await ConfimCancelAsync(cloud, masterInfo, unitInfos, units, retry);
#endif
        }

#if net40
        static bool? ConfimCancel(FreeSqlCloud cloud, TccMasterInfo masterInfo, List unitInfos, ITccUnit[] units, bool retry)
#else
        async static Task ConfimCancelAsync(FreeSqlCloud cloud, TccMasterInfo masterInfo, List unitInfos, ITccUnit[] units, bool retry)
#endif
        {
            var isConfirm = unitInfos.Count == masterInfo.Total;
            var successCount = 0;
            for (var idx = masterInfo.Total - 1; idx >= 0; idx--)
            {
                var unitInfo = unitInfos.Where(tt => tt.Index == idx + 1 && tt.Stage == TccUnitStage.Try).FirstOrDefault();
                try
                {
                    if (unitInfo != null)
                    {
                        if ((units[idx] as ITccUnitSetter)?.StateIsValued != true)
                            SetTccState(units[idx], unitInfo);
                        var ormMaster = cloud._ormMaster;
#if net40
                        using (var conn = cloud.Ado.MasterPool.Get())
#else
                        using (var conn = await cloud.Ado.MasterPool.GetAsync())
#endif
                        {
                            var tran = conn.Value.BeginTransaction();
                            var tranIsCommited = false;
                            try
                            {
                                var fsql = FreeSqlTransaction.Create(ormMaster, () => tran);
                                (units[idx] as ITccUnitSetter)?.SetUnit(unitInfo);
                                var update = fsql.Update()
                                    .Where(a => a.Tid == masterInfo.Tid && a.Index == idx + 1 && a.Stage == TccUnitStage.Try)
                                    .Set(a => a.Stage, isConfirm ? TccUnitStage.Confirm : TccUnitStage.Cancel);
#if net40
                                if (update.ExecuteAffrows() == 1)
                                {
                                    if (isConfirm) units[idx].Confirm();
                                    else units[idx].Cancel();
                                }
#else
                                if (await update.ExecuteAffrowsAsync() == 1)
                                {
                                    if (isConfirm) await units[idx].Confirm();
                                    else await units[idx].Cancel();
                                }
#endif
                                tran.Commit();
                                tranIsCommited = true;
                            }
                            finally
                            {
                                if (tranIsCommited == false)
                                    tran.Rollback();
                            }
                        }
                        if (cloud._distributeTraceEnable) cloud._distributedTraceCall($"TCC ({masterInfo.Tid}, {masterInfo.satle}) Unit{unitInfo.Index}{(string.IsNullOrWhiteSpace(unitInfo.Description) ? "" : $"({unitInfo.Description})")} {(isConfirm ? "CONFIRM" : "CANCEL")} successful{(masterInfo.RetryCount > 0 ? $" after {masterInfo.RetryCount} retries" : "")}\r\n    State: {unitInfo.State}\r\n    Type:  {unitInfo.TypeName}");
                    }
                    successCount++;
                }
                catch(Exception ex)
                {
                    if (unitInfo != null)
                        if (cloud._distributeTraceEnable) cloud._distributedTraceCall($"TCC ({masterInfo.Tid}, {masterInfo.satle}) Unit{unitInfo.Index}{(string.IsNullOrWhiteSpace(unitInfo.Description) ? "" : $"({unitInfo.Description})")} {(isConfirm ? "CONFIRM" : "CANCEL")} failed{(masterInfo.RetryCount > 0 ? $" after {masterInfo.RetryCount} retries" : "")}, -ERR {ex.Message}\r\n    State: {unitInfo.State}\r\n    Type:  {unitInfo.TypeName}");
                }
            }
            if (successCount == masterInfo.Total)
            {
                var update = cloud._ormMaster.Update()
                    .Where(a => a.Tid == masterInfo.Tid && a.Status == TccMasterStatus.Pending)
                    .Set(a => a.RetryCount + 1)
                    .Set(a => a.RetryTime == DateTime.UtcNow)
                    .Set(a => a.Status, isConfirm ? TccMasterStatus.Confirmed : TccMasterStatus.Canceled)
                    .Set(a => a.FinishTime == DateTime.UtcNow);
#if net40
                update.ExecuteAffrows();
#else
                await update.ExecuteAffrowsAsync();
#endif
                if (cloud._distributeTraceEnable) cloud._distributedTraceCall($"TCC ({masterInfo.Tid}, {masterInfo.satle}) Completed, all units {(isConfirm ? "CONFIRM" : "CANCEL")} successfully{(masterInfo.RetryCount > 0 ? $" after {masterInfo.RetryCount} retries" : "")}");
                return isConfirm;
            }
            else
            {
                var update = cloud._ormMaster.Update()
                    .Where(a => a.Tid == masterInfo.Tid && a.Status == TccMasterStatus.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.TccTraceEnable) cloud.OnTccTrace($"TCC ({tcc.Tid}, {tcc.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 == TccMasterStatus.Pending)
                        .Set(a => a.Status, TccMasterStatus.ManualOperation);
#if net40
                    update.ExecuteAffrows();
#else
                    await update.ExecuteAffrowsAsync();
#endif
                    if (cloud._distributeTraceEnable) cloud._distributedTraceCall($"TCC ({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
                    ConfimCancel(cloud, tid, true);
#else
                    ConfimCancelAsync(cloud, tid, true).Wait();
#endif
                }
                catch
                {
                    try
                    {
                        cloud._ormMaster.Update()
                            .Where(a => a.Tid == tid && a.Status == TccMasterStatus.Pending)
                            .Set(a => a.RetryCount + 1)
                            .Set(a => a.RetryTime == DateTime.UtcNow)
                            .ExecuteAffrows();
                    }
                    catch { }
                    //if (cloud.TccTraceEnable) cloud.OnTccTrace($"TCC ({tid}, {satle}) Not completed, waiting to try again, current tasks {cloud._scheduler.QuansatyTempTask}");
                    cloud._scheduler.AddTempTask(TimeSpan.FromSeconds(retryInterval), GetTempTask(cloud, tid, satle, retryInterval));
                }
            };
        }
    }
}