using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; using ServerCore; using ServerBase; using ServerCommon; using ServerCommon.BusinessLogDomain; using MetaAssets; namespace GameServer; public class CaliumEventAction : EntityActionBase { private enum ReTryStatus { None = 0, Success = 1, Fail = 2 } public CaliumEventAction(CaliumStorageEntity owner) : base(owner) { } public override async Task onInit() => await Task.FromResult(new Result()); public override void onClear() {} public async Task sendCaliumEventFromDB(CaliumEventDoc sendDoc) { var owner = getOwner() as CaliumStorageEntity; NullReferenceCheckHelper.throwIfNull(owner, () => "owner is null !!!"); var web3_action = owner.getEntityAction(); NullReferenceCheckHelper.throwIfNull(web3_action, () => $"calium_web3_action is null !!! - {getOwner().toBasicString()}"); // 0. 이벤트 전송 var request = CaliumStorageHelper.makeCaliumEventRequest(sendDoc); var (result, response) = await web3_action.postCaliumEvent(request); // 1. 성공시 리턴 ( 상태 수정 : Success ) if (result.isSuccess()) { await updateCaliumEventStatus(sendDoc, CaliumEventStatus.Sending, CaliumEventStatus.Success); return result; } var res = checkRetryFail(result.ErrorCode, response?.m_code); // 2. 이미 처리된 데이터 ( 상태 수정 : Success ) if (res == ReTryStatus.Success) { await updateCaliumEventStatus(sendDoc, CaliumEventStatus.Sending, CaliumEventStatus.Success); return new Result(); } // 3. 실패시 재시도 (상태 수정 : Regist) if (res == ReTryStatus.None) { await updateCaliumEventStatus(sendDoc, CaliumEventStatus.Sending, CaliumEventStatus.Regist); return result; } // 3. 재시도 불가 기록 (상태 수정 : Failed) await updateCaliumEventStatus(sendDoc, CaliumEventStatus.Sending, CaliumEventStatus.Failed); // 4. 로그 기록 var log = new CaliumEchoSystemFailLogData { EventData = request }; log.FailCode = response?.m_code ?? string.Empty; log.FailMessages = response?.m_messages ?? new(); log.EventData = request; CaliumStorageHelper.writeFailEchoSystemLog(owner, log); Log.getLogger().error($"Failed to send Calium Event !! : {owner.toBasicString()}"); return new Result(); } /// /// 칼리움 소각시 Web3 통보 함수 /// /// 소각 행위자 /// Transaction ID /// 유저 닉네임 /// 소각 행위 구분 /// 칼리움 변화량 ( 음수값 ) /// public async Task sendCaliumBurnEvent(IWithLogActor actor, string eventGuid, string userNickname, string subType, double caliumDelta) { var result = new Result(); if (ServerCore.TypeHelper.NumericSignType.Positive == ServerCore.TypeHelper.checkNumericSignType(caliumDelta)) { caliumDelta *= -1; } var send = await sendCaliumEvent(actor, CaliumEventType.calium_burn, eventGuid, userNickname, subType, 0, caliumDelta, true); if (false == send.is_success) { var err_msg = $"fail to send calium event !! : eventGuid[{eventGuid}] subtype[{subType}] userNickanme[{userNickname}] caliumDelta[{caliumDelta}] / {nameof(sendCaliumEventFromPlayer)}"; result.setFail(ServerErrorCode.FailToSendEchoSystem, err_msg); Log.getLogger().error(result.toBasicString()); } return result; } public async Task<(bool is_success, CaliumEventData event_data)> sendCaliumEventFromPlayer(Player player, CaliumEventType type, string subType, double sapphireDelta, double caliumdelta, bool isReTry = true) { return await sendCaliumEvent(player, type, player.getUserGuid(), player.getUserNickname(), subType, sapphireDelta, caliumdelta, isReTry); } public async Task<(Result result, CaliumEventDoc? change_doc)> updateCaliumEventStatus(CaliumEventDoc targetDoc, CaliumEventStatus beforeStatus, CaliumEventStatus afterStatus) { var result = new Result(); var server_logic = GameServerApp.getServerLogic(); var dynamoDb_client = server_logic.getDynamoDbClient(); NullReferenceCheckHelper.throwIfNull(dynamoDb_client, () => $"dynamodb client is null !!! "); var make_primary_key = targetDoc.getPrimaryKey(); var updateRequest = makeUpdateItemRequest(targetDoc, make_primary_key.toKeyWithAttributeValue(), nameof(CaliumEventAttrib.Status), beforeStatus, afterStatus); if (updateRequest.result.isFail() || null == updateRequest.request) { if (updateRequest.result.isSuccess()) { result.setFail(ServerErrorCode.FailToGetEchoSystemException, $"failed to make item request!!! - {getOwner().toBasicString()}"); } return (result, null); } (result, var change_doc) = await dynamoDb_client.simpleQueryDocTypesWithUpdateItemRequest(updateRequest.request); return (result, change_doc); } private async Task<(bool is_success, CaliumEventData event_data)> sendCaliumEvent(IWithLogActor actor, CaliumEventType type, string eventGuid, string userNickname, string subType, double sapphireDelta, double caliumDelta, bool isRetry) { var result = new Result(); var owner = getOwner() as CaliumStorageEntity; NullReferenceCheckHelper.throwIfNull(owner, () => "owner is null !!!"); var web3_action = owner.getEntityAction(); NullReferenceCheckHelper.throwIfNull(web3_action, () => $"calium_web3_action is null !!! - {getOwner().toBasicString()}"); // 0. echoSystem 전송 var request = await CaliumStorageHelper.makeCaliumEventRequest(owner, type, subType, userNickname, sapphireDelta, caliumDelta); (result, var response) = await web3_action.postCaliumEvent(request); // 1. 성공시 리턴 if (result.isSuccess()) return (true, request); // 2. 실패시 DB 기록 및 Retry if (isRetry) { var event_guid = await retryPostCaliumEvent(eventGuid, result.ErrorCode, request, response); result.setSuccess(); return (true, request); } // 3. 실패시 실패 로그 기록 var fail_log = new CaliumEchoSystemFailLogData(); fail_log.FailCode = response?.m_code ?? string.Empty; fail_log.FailMessages = response?.m_messages ?? new(); fail_log.ReTry = false; fail_log.EventData = request; CaliumStorageHelper.writeFailEchoSystemLog(actor, fail_log); return (false, request); } private async Task retryPostCaliumEvent(string userGuid, ServerErrorCode errCode, CaliumEventRequest request, EchoSystemBaseResponse? response) { var calium_entity = getOwner() as CaliumStorageEntity; NullReferenceCheckHelper.throwIfNull(calium_entity, () => "calium_entity is null !!!"); string event_guid; var res = checkRetryFail(errCode, response?.m_code); // 0. 이미 처리된 메시지 if (res == ReTryStatus.Success) { event_guid = await saveCaliumEventToDB(userGuid, request, CaliumEventStatus.Success); return event_guid; } // 1. 실패시 재시도 저장 if (res != ReTryStatus.Fail) { event_guid = await saveCaliumEventToDB(userGuid, request, CaliumEventStatus.Regist); return event_guid; } // 2. 재시도 불가 기록 event_guid = await saveCaliumEventToDB(userGuid, request, CaliumEventStatus.Failed); // 3. 로그 기록 ( 비즈니스? DB? ) var fail_log = new CaliumEchoSystemFailLogData(); fail_log.FailCode = response?.m_code ?? string.Empty; fail_log.FailMessages = response?.m_messages ?? new(); fail_log.ReTry = true; fail_log.EventData = request; CaliumStorageHelper.writeFailEchoSystemLog(calium_entity, fail_log); return event_guid; } private ReTryStatus checkRetryFail(ServerErrorCode errCode, string? code) { // 1. http 에러인 경우 if (ServerErrorCode.FailToGetEchoSystemHttpError == errCode) return ReTryStatus.Fail; // 2. DB insert fail 인 경우 if (ServerErrorCode.GetFailEchoSystemResponse == errCode && code == "RET.WCE.101") return ReTryStatus.None; // 3. 이미 처리된 Message 인 경우 ( 성공 처리 ) if (ServerErrorCode.GetFailEchoSystemResponse == errCode && code == "FIN_WCE_200") return ReTryStatus.Success; return ReTryStatus.Fail; } private async Task saveCaliumEventToDB(string userGuid, CaliumEventRequest request, CaliumEventStatus isRetry) { var doc = new CaliumEventDoc(request.m_event_id); var attrib = doc.getAttrib(); NullReferenceCheckHelper.throwIfNull(attrib, () => $"calium event attrib is null !!! - userGuid[{userGuid}] / {getOwner().toBasicString()}"); attrib.UserGuid = userGuid; attrib.EventData.m_server_type = request.m_server_type; attrib.EventData.m_event_type = request.m_event_type; attrib.EventData.m_sub_type = request.m_sub_type; attrib.EventData.m_div_type = request.m_div_type; attrib.EventData.m_div_id = request.m_div_id; attrib.EventData.m_calium_delta = request.m_calium_delta; attrib.EventData.m_sapphire_delta = request.m_sapphire_delta; attrib.EventData.m_current_epoch = request.m_current_epoch; attrib.EventData.m_current_inflation_rate = request.m_current_inflation_rate; attrib.Status = isRetry; var dynamoDb_client = GameServerApp.getServerLogic().getDynamoDbClient(); await dynamoDb_client.simpleUpsertDocumentWithDocType(doc); return request.m_event_id; } private (Result result, UpdateItemRequest? request) makeUpdateItemRequest( CaliumEventDoc target_doc, Dictionary attributeValueWithPrimaryKey , string targetAttribName , CaliumEventStatus beforeStatus , CaliumEventStatus afterStatus ) { var server_logic = GameServerApp.getServerLogic(); var dynamoDb_client = server_logic.getDynamoDbClient(); NullReferenceCheckHelper.throwIfNull(dynamoDb_client, () => $"dynamodb client is null !!! "); var result = new Result(); var query_builder = new DynamoDbItemRequestHelper.UpdateItemRequestBuilder(dynamoDb_client.getTableFullName(target_doc.TableName)); query_builder.withKeys(attributeValueWithPrimaryKey); var attrib_path_json_string = target_doc.toJsonStringOfAttribs(); var target_key = JsonHelper.getJsonPropertyName(targetAttribName); var (is_success, attribute_expression) = DynamoDbClientHelper.toAttributeExpressionFromJson(attrib_path_json_string, target_key); if (false == is_success) { var err_msg = $"Failed to DynamoDbClientHelper.toAttributeExpressionFromJson() !!! : attribPath:{attrib_path_json_string}, targetKey:{target_key}"; result.setFail(ServerErrorCode.AttribPathMakeFailed, err_msg); Log.getLogger().error(result.toBasicString()); return (result, null); } query_builder.withExpressionAttributeNames(DynamoDbClientHelper.toExpressionAttributeNamesFromJson(attrib_path_json_string, target_key)); // 변경값 설정 var update_expression = $"SET {attribute_expression} = :changeValue"; query_builder.withUpdateExpression(update_expression); // 조건 추가 query_builder.withConditionExpression($"{attribute_expression} = :beforeValue"); // 설정값 등록 var expression_attribute_values = new Dictionary { { ":changeValue", new AttributeValue { S = afterStatus.ToString() } }, { ":beforeValue", new AttributeValue { S = beforeStatus.ToString() } } }; query_builder.withExpressionAttributeValues(expression_attribute_values); query_builder.withReturnValues(ReturnValue.ALL_NEW); return query_builder.build(); } }