using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Text; using System.Threading.Tasks; using System.Reflection; using NLog; using Amazon.Runtime; using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; using Amazon.DynamoDBv2.DataModel; using Amazon.DynamoDBv2.DocumentModel; using Amazon; using ServerCore; using ServerBase; using LOG_ACTION_TYPE = System.String; namespace ServerBase; //============================================================================================== // 배치 쿼리를 전담 처리해 주는 추상 클래스 이다. //============================================================================================== public abstract partial class QueryBatchBase { public enum QueryResultType { Success = 0, // 쿼리를 성공 했다. NotCalledQueryFunc = 1, // 쿼리 함수가 호출되지 않았다. CalledQueryFunc = 2, // 쿼리 함수를 호출 했다. QueryFailed = 3, // 쿼리를 실패 했다. } private DynamoDbClient m_dynamo_db_connector; private IQueryRunner m_query_runner; private bool m_is_use_transact = false; private readonly string m_trans_id = string.Empty; private UInt16 m_retry_delay_msec = 0; private UInt16 m_retry_count = 0; private readonly List m_querys = new(); private IWithLogActor m_log_actor; private readonly LogAction m_log_action; private readonly List m_business_logs = new(); public QueryBatchBase( IWithLogActor logActor, LOG_ACTION_TYPE logActionType , DynamoDbClient dbConnector , IQueryRunner queryRunner , bool isUseTransact = true, string transId = "" , UInt16 retryCount = 0, UInt16 retryDelayMSec = 0) { m_log_actor = logActor; m_dynamo_db_connector = dbConnector; m_query_runner = queryRunner; m_is_use_transact = isUseTransact; m_trans_id = string.IsNullOrEmpty(transId) ? Guid.NewGuid().ToString("N") : transId; m_log_action = new LogAction(logActionType); m_retry_count = retryCount; m_retry_delay_msec = retryDelayMSec; } public QueryBatchBase( IWithLogActor logActor, LogAction logAction , DynamoDbClient dbConnector , IQueryRunner queryRunner , bool isUseTransact = true, string transId = "" , UInt16 retryCount = 0, UInt16 retryDelayMSec = 0) { m_log_actor = logActor; m_dynamo_db_connector = dbConnector; m_query_runner = queryRunner; m_is_use_transact = isUseTransact; m_trans_id = string.IsNullOrEmpty(transId) ? Guid.NewGuid().ToString("N") : transId; m_log_action = logAction; m_retry_count = retryCount; m_retry_delay_msec = retryDelayMSec; } public bool addQuery( QueryExecutorBase queryContext , QueryExecutorBase.FnCommit? fnCommit = null , QueryExecutorBase.FnRollback? fnRollback = null ) { queryContext.bindCallback(fnCommit, fnRollback); queryContext.setQueryBatch(this); if(false == queryContext.onAddQueryRequests()) { return false; } m_querys.Add(queryContext); return true; } //========================================================================================= // DB 쿼리 주요 실행 함수들... //========================================================================================= public async Task prepareQueryWithStopwatch() { Stopwatch? stopwatch = null; var event_tid = string.Empty; var server_logic = ServerLogicApp.getServerLogicApp(); ArgumentNullException.ThrowIfNull(server_logic); var server_config = server_logic.getServerConfig(); ArgumentNullException.ThrowIfNull(server_config); if (true == server_config.PerformanceCheckEnable) { event_tid = string.IsNullOrEmpty(getTransId()) ? System.Guid.NewGuid().ToString("N") : getTransId(); stopwatch = Stopwatch.StartNew(); } var result = await prepareQuery(); if (null != stopwatch) { var elapsed_msec = stopwatch.ElapsedMilliseconds; stopwatch.Stop(); if (elapsed_msec > Constant.STOPWATCH_LOG_LIMIT_MSEC) { Log.getLogger().debug($"QueryBatch - prepareQuery Stopwatch Stop : ETID:{event_tid}, ElapsedMSec:{elapsed_msec}"); } } return result; } private async Task prepareQuery() { var result = new Result(); foreach(var query in m_querys) { result = await query.onPrepareQuery(); if (result.isFail()) { return result; } } return result; } public async Task doQueryWithStopwatch() { Stopwatch? stopwatch = null; var event_tid = string.Empty; var server_logic = ServerLogicApp.getServerLogicApp(); ArgumentNullException.ThrowIfNull(server_logic); var server_config = server_logic.getServerConfig(); ArgumentNullException.ThrowIfNull(server_config); if (true == server_config.PerformanceCheckEnable) { event_tid = string.IsNullOrEmpty(getTransId()) ? System.Guid.NewGuid().ToString("N") : getTransId(); stopwatch = Stopwatch.StartNew(); } var result = await doQuery(); if (null != stopwatch) { var elapsed_msec = stopwatch.ElapsedMilliseconds; stopwatch.Stop(); if (elapsed_msec > Constant.STOPWATCH_LOG_LIMIT_MSEC) { Log.getLogger().debug($"QueryBatch - doQuery Stopwatch Stop : ETID:{event_tid}, ElapsedMSec:{elapsed_msec}"); } } return result; } private async Task doQuery() { var result = new Result(); var err_msg = string.Empty; try { foreach (var query in m_querys) { result = await query.onQuery(); if (result.isFail()) { Log.getLogger().error(result.toBasicString()); break; } } } catch(System.Exception e) { err_msg = $"Exception !!!, doQuery(), Exception:{e} - {toBasicString()}"; result.setFail(ServerErrorCode.DynamoDbQueryException, err_msg); Log.getLogger().error(result.toBasicString()); } finally { if (result.isSuccess()) { result = await commitQueryAllWithStopWatch(); } else { await rollbackQueryAll(result); } onSendMsgByHandler(result); } return result; } //===================================================================================== // 1. 등록된 쿼리들을 등록 순서대로 처리해 준다. // 2. Non-Transaction 쿼리에 오류가 발생할 경우 이후 처리할 쿼리는 모두 실행하지 않는다. // 3. TransactionGetItem 쿼리에 오류가 발생할 경우 TransactionGetItemRequest에 // 등록된 모든 쿼리는 처리되지 않는다. // 4. TransactionWriteItem 쿼리에 오류가 발생할 경우 TransactionWriteItemRequest에 // 등록된 모든 쿼리는 처리되지 않는다. //===================================================================================== private async Task commitQueryAllWithStopWatch() { Stopwatch? stopwatch = null; var event_tid = string.Empty; var server_logic = ServerLogicApp.getServerLogicApp(); ArgumentNullException.ThrowIfNull(server_logic); var server_config = server_logic.getServerConfig(); ArgumentNullException.ThrowIfNull(server_config); if (true == server_config.PerformanceCheckEnable) { event_tid = string.IsNullOrEmpty(getTransId()) ? System.Guid.NewGuid().ToString("N") : getTransId(); stopwatch = Stopwatch.StartNew(); } var result = await commitQueryAll(); if (null != stopwatch) { var elapsed_msec = stopwatch.ElapsedMilliseconds; stopwatch.Stop(); if (elapsed_msec > 100) { Log.getLogger().debug($"QueryBatch - commitQueryAll Stopwatch Stop : ETID:{event_tid}, ElapsedMSec:{elapsed_msec}"); } } return result; } private async Task commitQueryAll() { var result = new Result(); var try_count = 0; while(true) { (result, bool is_retry_query) = await commitQueryRunner(); if (true == is_retry_query) { try_count++; if (m_retry_count <= try_count) { break; } await Task.Delay(m_retry_delay_msec); } else { break; } } if (result.isFail()) { await rollbackQueryAll(result); } else { foreach (var query in m_querys) { if (QueryBatchBase.QueryResultType.NotCalledQueryFunc == await query.doFnCommit()) { await query.onQueryResponseCommit(); } } } return result; } private async Task<(Result, bool)> commitQueryRunner() { var result = new Result(); var err_msg = string.Empty; try { result = await onCommitQueryRunner(); } catch (TransactionCanceledException e) { var error_code = ServerErrorCode.DynamoDbTransactionCanceledException; err_msg = $"Failed to commitQueryRunner() !!!, Transaction Canceled, implies a client issue, fix before retrying !!! : errorCode:{error_code}, {e.toExceptionString()} - {toBasicString()}"; result.setFail(error_code, err_msg); Log.getLogger().error(result.toBasicString()); (var dispatch_result, var exception_custom_result) = await onDispatchTransactionCancelException(e); if (dispatch_result.isFail()) { err_msg = $"Failed to onDispatchTransactionCancelException() !!! : {dispatch_result.toBasicString()}, {e.toExceptionString()} - {toBasicString()}"; Log.getLogger().error(err_msg); } else { // 재정의된 오류 코드가 설정된 경우 if (exception_custom_result.isFail()) { // 재정의한 예외 결과를 dispatch 했다면 오류 로그로 설정해 준다. !!! err_msg = $"QueryBatch.onDispatchTransactionCancelException() !!! : exceptionResult:{exception_custom_result.getResultString()} - {toBasicString()}"; result.setFail(exception_custom_result.getErrorCode(), exception_custom_result.getResultString()); Log.getLogger().error(result.toBasicString()); } } } catch (TransactionConflictException e) { var error_code = ServerErrorCode.DynamoDbTransactionConflictException; err_msg = $"TransactionConflictException !!!, Failed to perform in commitQueryRunner() !!!, Transaction conflict occurred !!! : errorCode:{error_code}, {e.toExceptionString()} - {toBasicString()}"; result.setFail(error_code, err_msg); Log.getLogger().error(result.toBasicString()); } catch (AmazonDynamoDBException e) { var error_code = ServerErrorCode.DynamoDbAmazonDynamoDbException; err_msg = $"AmazonDynamoDBException !!!, Failed to perform in commitQueryRunner() !!! : errorCode:{error_code}, {e.toExceptionString()} - {toBasicString()}"; result.setFail(error_code, err_msg); Log.getLogger().error(err_msg); } catch (AmazonServiceException e) { var error_code = ServerErrorCode.DynamoDbAmazonServiceException; err_msg = $"AmazonServiceException !!!, Failed to perform in commitQueryRunner() !!! : errorCode:{error_code}, {e.toExceptionString()} - {toBasicString()}"; result.setFail(error_code, err_msg); Log.getLogger().error(result.toBasicString()); return (result, true); } catch (System.Exception e) { var error_code = ServerErrorCode.TryCatchException; err_msg = $"Exception !!!, Failed to perform in commitQueryRunner() !!! : errorCode:{error_code}, exception:{e} - {toBasicString()}"; result.setFail(error_code, err_msg); Log.getLogger().error(result.toBasicString()); } return (result, false); } private async Task rollbackQueryAll(Result errorResult) { await onRollbackQueryRunnner(); foreach (var query in m_querys) { if (QueryBatchBase.QueryResultType.NotCalledQueryFunc == await query.doFnRollback(errorResult)) { await query.onQueryResponseRollback(errorResult); } } } protected abstract Task onCommitQueryRunner(); protected abstract Task onRollbackQueryRunnner(); public virtual void onSendMsgByHandler(Result errorResult) { return; } //============================================================================================== // 배치쿼리중에서 특정 쿼리를 찾는다. //============================================================================================== public TQuery? getQuery() where TQuery : QueryExecutorBase { return m_querys.Find(x => x is TQuery == true) as TQuery; } public List getQuerys() where TQuery : QueryExecutorBase { var querys = new List(); foreach (var each in m_querys) { if (each is TQuery query) { querys.Add(query); } } return querys; } public void appendBusinessLogs(List logs) { foreach(var log in logs) { m_business_logs.Add(log); } } public void appendBusinessLog(ILogInvoker log) { m_business_logs.Add(log); } private bool hasBusinessLog() { var log_actor = getLogActor(); if (null != log_actor && 0 < m_business_logs.Count) { return true; } return false; } public void sendBusinessLog() { if (false == hasBusinessLog()) { return; } BusinessLogger.collectLogs(m_log_action, m_log_actor, m_business_logs); } public string toBasicString() { return $"{this.getTypeName()}, LogActionType:{m_log_action.getLogActionType()}, TransId:{m_trans_id}"; } protected abstract Task<(Result, Result)> onDispatchTransactionCancelException(TransactionCanceledException exception); }//QueryBatchBase //============================================================================================== // 배치 쿼리를 전담 처리해 주는 제네릭 클래스 이다. //============================================================================================== public partial class QueryBatch : QueryBatchBase where TQueryRunner : IQueryRunner, new() { public QueryBatch( IWithLogActor logActor , LOG_ACTION_TYPE logActionType , DynamoDbClient dbClient , bool isUseTransact = true, string transId = "" , UInt16 retryCount = 3, UInt16 retryDelayMSec = 1000, string eventTid = "" ) : base( logActor, logActionType, dbClient, new TQueryRunner() , isUseTransact, transId, retryCount, retryDelayMSec) { getQueryRunner().setQueryBatch(this); } public QueryBatch( IWithLogActor logActor , LogAction logAction , DynamoDbClient dbClient , bool isUseTransact = true, string transId = "" , UInt16 retryCount = 3, UInt16 retryDelayMSec = 1000, string eventTid = "" ) : base( logActor, logAction.getLogActionType(), dbClient, new TQueryRunner() , isUseTransact, transId, retryCount, retryDelayMSec) { getQueryRunner().setQueryBatch(this); } protected override async Task onCommitQueryRunner() { var query_runner = getQueryRunner(); var result = await query_runner.onTryCommitQueryRunner(); if (result.isFail()) { return result; } return await getQueryRunner().onCommitQueryRunner(); } protected override async Task onRollbackQueryRunnner() { await getQueryRunner().onRollbackQueryRunner(); } protected override async Task<(Result, Result)> onDispatchTransactionCancelException(TransactionCanceledException exception) { await Task.CompletedTask; var log_actor = getLogActor(); ArgumentNullException.ThrowIfNull(log_actor, $"log_actor is null !!!"); ArgumentNullException.ThrowIfNull(exception, $"exception is null !!! - {log_actor.getTypeName()}"); return await getQueryRunner().onHandleTransactionCancelException(exception); } }//QueryBatch //============================================================================================== // 배치 쿼리 + 모든 쿼리 처리후 TMsg 를 결과로 전송해 주는 제네릭 클래스 이다. //============================================================================================== public class QueryBatch : QueryBatch where TQueryRunner : IQueryRunner, new() where TMsg : class, new() { protected readonly TMsg m_reserved_msg ; // 전송하고자 하는 패킷 protected readonly object m_sender; // 전송객체 : setError(ErrorCode error), sendPacketAsync(TMsg msg) 맴버함수가 있어야 한다. !!! public QueryBatch( IWithLogActor logActor , LOG_ACTION_TYPE logActionType , DynamoDbClient dbClient , object sender , TMsg? msg = null , bool isUseTransact = true ) : base(logActor, logActionType, dbClient, isUseTransact ) { if (msg == null) { m_reserved_msg = new TMsg(); } else { m_reserved_msg = msg; } m_sender = sender; } public QueryBatch( IWithLogActor logActor , LogAction logAction , DynamoDbClient dbClient , object sender , TMsg? msg = null , bool isUseTransact = true ) : base(logActor, logAction.getLogActionType(), dbClient, isUseTransact ) { if (msg == null) { m_reserved_msg = new TMsg(); } else { m_reserved_msg = msg; } m_sender = sender; } public TMsg Msg { get { return m_reserved_msg; } } private void setErrorToMsg(Result errorResult) { } public override void onSendMsgByHandler(Result errorResult) { if (m_reserved_msg != null && m_sender != null) { setErrorToMsg(errorResult); } } }//QueryBatch