- 뒤끝 SDK 버전 :
5.11.3
- 프로젝트명 :
ttheroes
안녕하세요. 비동기 트랜잭션 처리 이슈에 대해 문의드립니다.
트랜잭션 관리 시나리오는 다음과 같습니다.
- 쓰기 요청마다 id를 받아 중복 키가 있으면 덮어쓰는 방식으로 딕셔너리에 보존
- 해소 요청이 들어오면 딕셔너리를 10개 단위로 묶어 트랜잭션 요청
- 요청한 항목들을 딕셔너리에서 제거
- 전체 해소 요청이 들어오면 딕셔너리 갯수가 0이 될 때까지 해소 요청을 반복
트랜잭션 해소 시나리오는 다음과 같습니다.
- 매 300초 마다 비동기 형식으로 트랜잭션 딕셔너리 전체 해소 요청 (SendQueue 또는 콜백)
- 앱이 종료될 때 동기 형식으로 트랜잭션 딕셔너리 전체 해소 요청 (콜백 없이 요청)
시나리오 별 에러는 다음과 같습니다.
- 실행 중 매 n초 마다 비동기 트랜잭션
* 스테이터스 코드 :400
* 에러 코드 :TransactionSizeError
* 에러 메시지 :Not Support Transaction Size: 0
- 종료 시 동기 트랜잭션
* 스테이터스 코드 :404
* 에러 코드 :NotFoundException
* 에러 메시지 :gameinfo not found, gameinfo을(를) 찾을 수 없습니다
관련해서 제가 작성한 트랜잭션 매니저 코드는 다음과 같습니다.
using System;
using System.Collections.Generic;
using UnityEngine;
using Sirenix.OdinInspector;
using BackEnd;
// ReSharper disable once CheckNamespace
public class BackEndTransactionManager : MonoBehaviour
{
#region Editor
[TitleGroup("Settings")]
[SerializeField] private DataHandlerSetContentMethod repeativeFlushMethod;
[SerializeField] private DataHandlerSetContentMethod closingFlushMethod;
[SerializeField] private bool flushAll = true;
[SerializeField] private float initialflushInterval = 60f;
[SerializeField] private float continuousflushInterval = 300f;
[SerializeField, ReadOnly] private float flushInterval;
[SerializeField, ReadOnly] private float flushDelta;
[TitleGroup("Dev")]
[SerializeField] private bool logAddingJob;
[SerializeField] private bool logReplacingJob;
[SerializeField] private bool logQueueingJobs;
[SerializeField] private bool logQueuedJobs;
#endregion
#region Lifecycle
public static BackEndTransactionManager Instance { get; private set; }
private void Awake()
{
Instance = this;
UnityEngine.Object.DontDestroyOnLoad(this.gameObject);
this.flushInterval = this.initialflushInterval;
}
private void Update()
{
if (this.flushDelta < this.flushInterval)
{
this.flushDelta += Time.unscaledDeltaTime;
return;
}
this.flushInterval = this.continuousflushInterval;
this.flushDelta = 0f;
if (this.flushAll)
this.FlushAll(this.repeativeFlushMethod);
else
this.Flush(this.repeativeFlushMethod);
}
private void OnApplicationQuit()
{
this.FlushAll(this.closingFlushMethod);
}
#endregion
#region Internal
private readonly Dictionary<string, TransactionRegistry> queue = new Dictionary<string, TransactionRegistry>();
private readonly List<string> keyBuffer = new List<string>();
private readonly List<TransactionValue> valueBuffer = new List<TransactionValue>();
private readonly List<Action<bool, object>> callbackBuffer = new List<Action<bool, object>>();
private class TransactionRegistry
{
public TransactionValue Value;
public Action<bool, object> Callback;
public TransactionRegistry(TransactionValue value, Action<bool, object> callback)
{
Value = value;
Callback = callback;
}
}
private void OnTransactionResult(DataHandlerSetContentMethod method, BackendReturnObject transactionResult, IEnumerable<Action<bool, object>> callbacks)
{
var result = transactionResult.IsSuccess();
var message = transactionResult.ToString();
foreach (var callback in callbacks)
callback?.Invoke(result, message);
if (!this.logQueuedJobs) return;
if (result)
UnityEngine.Debug.Log($"[{nameof(BackEndTransactionManager)}.{nameof(Flush)}][{method}] success : {transactionResult}");
else
UnityEngine.Debug.LogError($"[{nameof(BackEndTransactionManager)}.{nameof(Flush)}][{method}] fail. {transactionResult}");
}
#endregion
#region API
public void Enqueue(string id, TransactionValue job, Action<bool, object> callback)
{
if (this.queue.TryGetValue(id, out var duplicate))
{
duplicate.Value = job;
duplicate.Callback = callback;
if (this.logReplacingJob)
UnityEngine.Debug.Log($"[{nameof(BackEndTransactionManager)}.{nameof(Enqueue)}] replacing job : {id}");
}
else
{
this.queue.Add(id, new TransactionRegistry(job, callback));
if (this.logAddingJob)
UnityEngine.Debug.Log($"[{nameof(BackEndTransactionManager)}.{nameof(Enqueue)}] adding job : {id}");
}
}
public void Flush(DataHandlerSetContentMethod method)
{
if (this.logQueueingJobs)
UnityEngine.Debug.Log($"[{nameof(BackEndTransactionManager)}.{nameof(Flush)}] flushing transactions : {this.queue.Keys.ToArray().Join(",")}");
foreach (var (id, registry) in this.queue)
{
this.keyBuffer.Add(id);
this.valueBuffer.Add(registry.Value);
this.callbackBuffer.Add(registry.Callback);
if (this.valueBuffer.Count >= 10) break;
}
foreach (var id in this.keyBuffer) this.queue.Remove(id);
var callbacks = this.callbackBuffer.ToArray();
switch (method)
{
case DataHandlerSetContentMethod.Sync:
var syncedResult = Backend.GameData.TransactionWriteV2(this.valueBuffer);
this.OnTransactionResult(method, syncedResult, callbacks);
break;
case DataHandlerSetContentMethod.Async:
Backend.GameData.TransactionWriteV2(
this.valueBuffer,
(asyncedResult) => this.OnTransactionResult(method, asyncedResult, callbacks));
break;
case DataHandlerSetContentMethod.Queue:
SendQueue.Enqueue(
Backend.GameData.TransactionWriteV2,
this.valueBuffer,
(queuedResult) => this.OnTransactionResult(method, queuedResult, callbacks));
break;
}
this.keyBuffer.Clear();
this.valueBuffer.Clear();
this.callbackBuffer.Clear();
}
public void FlushAll(DataHandlerSetContentMethod method)
{
if (this.queue.Count < 1) return;
var count = this.queue.Count;
var i = 0;
while (this.queue.Count > 0)
{
i++;
this.Flush(method);
UnityEngine.Debug.LogWarning($"[{nameof(BackEndTransactionManager)}.{nameof(FlushAll)}] flushing loop {i}... ({this.queue.Count}/{count} jobs remaining)");
}
}
#endregion
}