Skip to main content

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

  1. Invoke CreateEct to create an ECT.
  2. Execute actions according to your actions and invoke UpdateEctActionStatus to update the status.
  3. Retry the ECT if failure. The game executes the failed actions again with the idempotency_token and then invokes UpdateEctActionStatus 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.
sequenceDiagram participant GS as Game Server participant SDK as PGOS SDK participant B as PGOS Backend participant VS as PGOS VS GS->>SDK: CreateEct(actions, auto_retry) SDK->>B: CreateEct(actions, auto_retry) B-->>SDK: return create result SDK-->>GS: return create result(ECT:Uncompleted, Actions:Init) GS->>GS: perform the operations corresponding to all actions and record the results. GS->>SDK: UpdateEctActions(status, results) SDK->>B: UpdateEctActions(status, results) alt all actions success B->>B: ECT status => Done B-->>SDK: return update result SDK-->>GS: return update result(ECT:Done) else some actions failed, auto_retry=true B-->>SDK: return update result SDK-->>GS: return update result(ECT:Uncompleted) Note right of B: waiting for the specified time loop specified frequency B-->>VS: Trigger the VS event VS-->>VS: The developer retries in the VS event. end else some actions failed, auto_retry=false B-->>SDK: return update result SDK-->>GS: return update result(ECT:Uncompleted) Note right of GS: The developer waits for the right moment to retry. loop GS->>SDK: GetPlayerUncompletedEcts(player_id) SDK->>B: GetPlayerUncompletedEcts(player_id) B-->>SDK: return get result SDK-->>GS: return get result GS->>GS: perform the operations corresponding to failed actions and record the results. GS->>SDK: UpdateEctActions(status, results) SDK->>B: UpdateEctActions(status, results) B-->>SDK: return update result SDK-->>GS: return update result end end Note over GS, VS: The developer thinks that the ECT should be canceled GS->>SDK: CancelEct(id) SDK->>B: CancelEct(id) B-->>SDK: return cancel result SDK-->>GS: return cancel result(ECT:Canceled)

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.

NOTE

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;
};
NOTE

The parameters for CreateEct must meet the following requirements:

FieldRequirements
idMust not be empty or duplicated
payloadMust not exceed 500KB
player_idsMust not contain more than 100 items or duplicates
expiration_and_retry_policy.expiration_durationMust be between 60 seconds and 7 days
expiration_and_retry_policy.auto_retry_intervalMust be between 60 seconds and 7 days
expiration_and_retry_policy.max_auto_retry_countMust be between 0 and 100
actionsMust not contain more than 100 items
actions.payloadMust 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.
NOTE

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.
NOTE

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.

NOTE

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 CodeRelevant APIHandling Suggestion
kBackendECTIDConflictCreateEctECTID should be a globally unique string. In practical use, it must not conflict with existing ECTIDs.
kBackendECTIDNotFoundGetEctInfoCannot find an ECT record associated with specified ECTID.
kBackendECTUpdateFailedUpdateEctThis 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
CreateEctECT creation failed due to multiple reasons. Please check the parameter requirements of the CreateEct interface.