ECT
1. Overview
ECT(Eventual Consistent Transaction) is used for tracking the game transaction. When a game needs to execute a transaction that contains several ordered actions to modify player data or inventory, the game can create an ECT Instance to track the transaction status on the backend side(game server or virtual server). After ECT is created, the game will execute every action in it one by one. In most cases, the actions would be done successfully. If some of the actions fail, an ECT retry Event will be triggered in a reasonable time until ECT is successfully done or the max retry limitation is exceeded.
After completing this tutorial, developers will know the following:
- What is the ECT?
- The general sequence to use ECT.
2. General Steps
- Invoke
CreateEct
to create an ECT. - Execute actions according to your actions and invoke
UpdateEctActionStatus
to update the status. - Retry the ECT if failure. The game executes the failed actions again with the
idempotency_token
and then invokesUpdateEctActionStatus
to update the status. There are two ways to trigger the retry:- Manually retry: The game decides when to retry.
- Automatically trigger retry: At a reasonable time, the PGOS backend will trigger the retry event and the Virtual Server can handle the retry.
3. Keyword Definition
ECT
ECT is the tracking of the status of transaction. Each ECT contains multiple actions. An ECT can be associated with multiple players.
Field description for ECT:
- id: The ID of ECT.
- name: The name of ECT.
- payload: The userdata of ECT set by game.
- player_ids: All players associated with the operation in this ECT.
- status: The status of ECT.
- Uncompleted: The ECT is 'Uncompleted', indicating that it has not yet reached the 'Done', 'Canceled', or 'Expired' state.
- Done: The status of all the actions was updated to 'SUCCESS'; consequently, the ECT's status will become 'Done'.
- Canceled: The ECT is canceled.
- Expired: The ECT is Expired.
- expiration_and_retry_policy: Whether the retry event of ECT is triggered to VS by PGOS if the ECT is not done.
- expiration_duration: The expiration duration for an ECT is the period in seconds from its creation until it expires. The ECT will be marked as expired if the duration has elapsed and the ECT is uncompleted. Minimum: 60 seconds; maximum: 604800 seconds (7 days).
- trigger_auto_retry_event: Whether the retry event of ECT is triggered to VS by PGOS if the ECT is uncompleted.
- max_auto_retry_count: Maximum number of retry attempts. This is only meaningful when the 'trigger_auto_retry_event' is set to true.
- auto_retry_interval: The time interval in seconds between each retry attempt. The timing of the first try starts counting from the creation of the ECT. This is only meaningful when the 'trigger_auto_retry_event' is set to true. Minimum: 60 seconds; maximum: 86400 seconds (24 hours).
- created_time: The created time of ECT.
- updated_time: The last updated time of ECT.
- actions: The actions of ECT.
Action
Action is the idempotent operation, which can prevent operation replay when it is executed repeatedly in the medium term.
Field description for action:
- id: The ID of ECT action.
- name: The name of ECT action.
- idempotency_token: Token for idempotency operation.
- payload: The userdata of operation set by the game, such as parameters in JSON.
- result: The result of the operation set by the game.
- status: The status of ECT action.
- Init: The default action status when an ECT is created.
- Success: The action has succeeded.
- Failed: The action is failed.
- updated_time: The last updated time of ECT action.
4. Using ECT with Server APIs
4.1 Create ECT
Before the game backend executes a transaction, an ECT needs to be created in advance. Once ECT is created, the status of the ECT will be 'Uncompleted' and the actions will be 'Init'. Game developers can also set the name and payload for every action, which is used for storing the data or parameters that the action needs to execute.
The ECT ID is an input parameter in CreateEct
.
Why it's designed like this is because the ECT creating needs to be idempotent. Think about a case: If the ECT ID is generated in CreateEct
. The game gets a failure response after invoking CreateEct
due to timeout. It's hard for the game to find out if the ECT is really created. Most of the time, the game has to retry to invoke CreateEct
. It's possible two ECTs created eventually. The first ECT will trigger an unexpected retry a while later.
If the ECT ID is an input parameter, there is another benefit that the game may need a specific ECT ID instead of a random one. For example, in the case of upgrading the inventory item, the game can composite the item ID and level as an ECT ID (e.g. upgraditem_cj8k00yz_level2to3) and don't need to store the ECT ID for retry later(especially retry after game client crashes). This way makes game logic simple.
So, please note that you need to use the same ECT ID for retrying CreateEct
.
Interface prototype:
/**
* Create an ECT. Once created, the status of the ECT will be 'Uncompleted' and the actions will be 'Init'.
*/
void CreateEct(
const FServerCreateEctParams& Params,
TFunction<void(const FPgosResult& Ret, const FServerEctInfo* Data)> Callback) const;
struct FServerCreateEctParams
{
/** The ID of ECT. */
FString id;
/** The name of ECT. */
FString name;
/** The userdata of ECT set by game. */
FString payload;
/** All players associated with the operation in this ECT. */
TArray<FString> player_ids;
/** Whether the retry event of ECT will be triggered to VS by PGOS if the ECT is uncompleted. */
FServerEctExpirationAndRetryPolicy expiration_and_retry_policy;
/** The actions of ECT. */
TArray<FServerEctActionCreateParams> actions;
};
struct FServerEctActionCreateParams
{
/** The name of ECT action. */
FString name;
/** The userdata of operation set by game, such as parameters in JSON. */
FString payload;
/** Token for idempotency operation. */
FString idempotency_token;
};
The parameters for CreateEct must meet the following requirements:
Field | Requirements |
---|---|
id | Must not be empty or duplicated |
payload | Must not exceed 500KB |
player_ids | Must not contain more than 100 items or duplicates |
expiration_and_retry_policy.expiration_duration | Must be between 60 seconds and 7 days |
expiration_and_retry_policy.auto_retry_interval | Must be between 60 seconds and 7 days |
expiration_and_retry_policy.max_auto_retry_count | Must be between 0 and 100 |
actions | Must not contain more than 100 items |
actions.payload | Must not exceed 100KB |
Example Code:
#include "PgosSDKCpp.h"
void SomeUObjectClass::SomeFunction()
{
auto Ect = IPgosSDKCpp::Get().GetServerEctAPI();
if (Ect)
{
FServerCreateEctParams Params;
// Fill Params
Ect->CreateEct(Params, [](const FPgosResult& Ret, const FServerEctInfo* Data) {
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("CreateEct Success"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("CreateEct Failed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg);
}
});
}
}
4.2 Update Actions
After ECT is created, the game backend then executes every action of ECT one by one. What specific logic to execute at each action is determined by game developer.
After all actions are executed, the game developer needs to update the ECT status and action result. There are several cases:
- Case 1: If all actions succeeded, the game developer updates the ECT Status with DONE.
- Case 2: If some actions failed, the game developer updates the ECT Status with Uncompleted, the failed actions Status with FAILED, and the successful actions with SUCCESS.
- Case 3: After ECT is created, the game backend routine aborts or crashes. The game backend isn't able to execute and update ECT at the same routine. The ECT must be retried a while later.
- Case 4: After the game backend executes a part of actions, it aborts or crashes. The game backend isn't able to execute the left actions and update ECT at the same routine. The ECT must be retried a while later.
The possible state transitions for action
are as follows:
Init->Failed, Init->Success,
Failed->Success, Failed->Failed,
Success->Success.
Any other state transition will result in an error.
Interface prototype:
/**
* Update the ECT's actions. The status of the ECT will be updated to 'Done' if the status of all actions are updated to 'Success'.
* The possible state transitions for `action` are as follows: Init->Failed, Init->Success, Failed->Success, Failed->Failed, Success->Success. Any other state transition will result in an error.
*/
void UpdateEctActions(
const FServerUpdateEctActionsParams& Params,
TFunction<void(const FPgosResult& Ret, const FServerEctInfo* Data)> Callback) const;
/**
* Update the ECT information and actions. The status of the ECT will be updated to 'Done' if the status of all actions were updated to 'Success'.
* The possible state transitions for `action` are as follows: Init->Failed, Init->Success, Failed->Success, Failed->Failed, Success->Success. Any other state transition will result in an error.
*/
void UpdateEct(
const FServerUpdateEctParams& Params,
TFunction<void(const FPgosResult& Ret, const FServerEctInfo* Data)> Callback) const;
struct FServerUpdateEctActionsParams
{
/** ECT ID. */
FString id;
/** Update the results and status of actions. key: action ID. */
TMap<FString, FServerEctActionUpdateParam> action_updates;
};
struct FServerEctActionUpdateParam
{
/** Update action's payload. */
FString payload;
/** Update action's result. */
FString result;
/** Update action's status. */
EServerEctActionStatus status = EServerEctActionStatus::Init;
};
struct FServerUpdateEctParams
{
/** ECT ID. */
FString id;
/** Update action's payload. */
FString payload;
/** Update the results and status of actions. key: action ID. */
TMap<FString, FServerEctActionUpdateParam> action_updates;
};
Example Code:
#include "PgosSDKCpp.h"
void SomeUObjectClass::SomeFunction()
{
auto Ect = IPgosSDKCpp::Get().GetServerEctAPI();
if (Ect)
{
FServerUpdateEctActionsParams Params;
// Fill Params
Ect->UpdateEctActions(Params, [](const FPgosResult& Ret, const FServerEctInfo* Data) {
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("UpdateEctActions Success"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("UpdateEctActions Failed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg);
}
});
}
}
4.3 Retry
No matter what case occurs at the execution stage, every uncompleted ECT needs to be retried after a while. Once the ECT retry is triggered, the game backend should check every action status and then execute all the uncompleted actions based on the fields Name, Payload, and IdemToken of actions. Fortunately, these actions can be executed safely because they are idempotent. Meanwhile, you can also control the maximum of retrying if you need it.
There are two ways to trigger retry:
- Manually retry: The game decides when to retry.
- Automatically trigger retry: At a reasonable time, the PGOS backend will trigger the retry event
event_ect_retry
and the Virtual Server can handle the retry.
Retry policy can be configured when creating ECT:
trigger_auto_retry_event: Whether the retry event of ECT is triggered to VS by PGOS if the ECT is uncompleted.
max_auto_retry_count: Maximum number of retry attempts. This is only meaningful when the 'trigger_auto_retry_event' is set to true.
auto_retry_interval: The time interval in seconds between each retry attempt. The timing of the first try starts counting from the creation of the ECT. This is only meaningful when the 'trigger_auto_retry_event' is set to true. Minimum: 60 seconds; maximum: 86400 seconds (24 hours).
4.4 Cancel ECT
There are some cases you need to cancel the ECT when you are doing an ECT retry. For example, if the player doesn't have sufficient Virtual Currency to spend, you can cancel the ECT(Set ECT Status to Canceled) but shouldn't retry again and again. The retry event of these canceled ECTs will not be triggered again.
Interface prototype:
/**
* Cancel the ECT. Once canceled, it will not trigger an auto retry event, and the game will not be able to update it either.
*/
void CancelEct(
const FServerCancelEctParams& Params,
TFunction<void(const FPgosResult& Ret, const FServerEctInfo* Data)> Callback) const;
struct FServerCancelEctParams
{
/** ECT ID. */
FString id;
/** Reason for cancellation. */
FString reason;
};
Example Code:
#include "PgosSDKCpp.h"
void SomeUObjectClass::SomeFunction()
{
auto Ect = IPgosSDKCpp::Get().GetServerEctAPI();
if (Ect)
{
FServerCancelEctParams Params;
// Fill Params
Ect->CancelEct(Params, [](const FPgosResult& Ret, const FServerEctInfo* Data) {
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("CancelEct Success"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("CancelEct Failed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg);
}
});
}
}
4.5 Query Player's ECTs
Benefiting from persistently storing, all ECTs can be tracked and queried. You can query them on the Portal or uncompleted ECTs by the API GetPlayerUncompletedEcts
for the specific player. You can also get information about the specified ECT by the API GetEctInfo
.
This interface supports querying up to 100 ECT Info for a single player at a time.
Interface prototype:
/**
* Retrieve ECTs of a player that are in an `Uncompleted` status.
*/
void GetPlayerUncompletedEcts(
const FServerGetPlayerUncompletedEctsParams& Params,
TFunction<void(const FPgosResult& Ret, const FServerGetPlayerUncompletedEctsResult* Data)> Callback) const;
/**
* Get information about the specified ECT.
* When the ECT status is `Uncompleted` it can always be queried.
* If the ECT status changes to a terminal state (`Done`, `Canceled`, or `Expired`) and more than a few days pass, it can no longer be queried. However, the ECT details can still be viewed on the Portal at this time.
*/
void GetEctInfo(
const FServerGetEctInfoParams& Params,
TFunction<void(const FPgosResult& Ret, const FServerEctInfo* Data)> Callback) const;
struct FServerGetPlayerUncompletedEctsParams
{
/** The ID of the specified player. */
FString player_id;
/** The start position. */
int32 offset = 0;
/** The limit of the ECTs request. */
int32 count = 50;
};
struct FServerGetEctInfoParams
{
/** ECT ID. */
FString id;
};
Example Code:
#include "PgosSDKCpp.h"
void SomeUObjectClass::SomeFunction1()
{
auto Ect = IPgosSDKCpp::Get().GetServerEctAPI();
if (Ect)
{
FServerGetPlayerUncompletedEctsParams Params;
// Fill Params
Ect->GetPlayerUncompletedEcts(Params, [](const FPgosResult& Ret, const FServerGetPlayerUncompletedEctsResult* Data) {
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("GetPlayerUncompletedEcts Success"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("GetPlayerUncompletedEcts Failed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg);
}
});
}
}
void SomeUObjectClass::SomeFunction2()
{
auto Ect = IPgosSDKCpp::Get().GetServerEctAPI();
if (Ect)
{
FServerGetEctInfoParams Params;
// Fill Params
Ect->GetEctInfo(Params, [](const FPgosResult& Ret, const FServerEctInfo* Data) {
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess)
{
UE_LOG(LogTemp, Log, TEXT("GetEctInfo Success"));
}
else
{
UE_LOG(LogTemp, Log, TEXT("GetEctInfo Failed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg);
}
});
}
}
5. HTTP APIs with Idempotent Operation Support
ECTActionInfo includes an optional field idempotency_token
that you can use to build an ECTAction supporting idempotent operations. When performing idempotent operations, you need to choose APIs in PGOS that also support the idempotency_token
field.
Please check out the this doc for idempotent HTTP APIs currently supported by PGOS.
6. Key Errors Handling
Error Code | Relevant API | Handling Suggestion |
---|---|---|
kBackendECTIDConflict | CreateEct | ECTID should be a globally unique string. In practical use, it must not conflict with existing ECTIDs. |
kBackendECTIDNotFound | GetEctInfo | Cannot find an ECT record associated with specified ECTID. |
kBackendECTUpdateFailed | UpdateEct | This error code indicates that the ECT information could not be updated as expected. Possible reasons include: the ECT is already in one of terminated states(done, canceled or expired), the Action cannot be transitioned from success to failure state, or the Action cannot be transitioned from failure to initial state, etc. |
kBackendECTExpirationOutOfRange kBackendECTRetryIntervalOutOfRange kBackendECTMaxRetryCountOutOfRange kBackendECTPlayerIDsCountOutOfRange kBackendEctActionsCountOutOfRange kBackendECTPlayerIDsIllegal kBackendECTPlayerIDsRepeated | CreateEct | ECT creation failed due to multiple reasons. Please check the parameter requirements of the CreateEct interface. |