GS2-Distributor
GS2-Distributor is the core service for executing transactions that span across multiple GS2 microservices. It provides the foundation for safely executing a sequence of operations that span multiple microservices, such as “consume an item to receive a reward” or “consume stamina to start a quest”.
In GS2, operations that are disadvantageous to the player are called “consume actions”, and operations that are advantageous are called “acquire actions”.
GS2-Distributor receives a “transaction” that bundles these actions together, then forwards and executes them against the appropriate microservices, allowing all of the processing that makes up the game cycle to be handled in a consistent manner.
For more details on how transactions work, see Transaction.
Actions that compose a transaction
A GS2 transaction is composed of the following three types of actions.
graph LR Issue["Transaction Issued<br/>(Store / Quest / Mission, etc.)"] --> Verify["Verify Action"] Verify --> Consume["Consume Action"] Consume --> Acquire["Acquire Action"] Acquire --> Done["Done"]
Verify Action
An action that checks in advance, before starting transaction execution, whether the consume actions and acquire actions can be performed.
For example, conditions such as “must possess a specific item”, “rank must be at or above a certain value”, or “a specific quest must already be cleared” can be checked before any consumption occurs.
If verification fails, the consume and acquire actions are not executed.
Consume Action
An action that represents processing that is disadvantageous to the player.
This includes consuming items, consuming currency, consuming stamina, incrementing a usage limit counter, and similar operations.
When a consume action is executed, each microservice issues a signature proving that “it has been executed”. This signature is verified when the next acquire action is executed, and the acquire action cannot run unless all consume actions have been passed.
Acquire Action
An action that represents processing that is advantageous to the player.
This includes acquiring items, acquiring currency, gaining experience, starting a quest, and similar operations.
An acquire action is executed only when the signatures indicating that all related consume actions have completed successfully are collected.
Structure of a transaction
A transaction contains lists of verify, consume, and acquire actions. A transaction can include multiple acquire actions; the execution setting determines how they are processed.
For new namespaces, use transactionSettingV2. The server executes the transaction as part of the API that issues it, commits all of its actions together, and completes execution before returning the response. The default is sequential execution. See Transaction Settings for limits and parallel execution.
Older namespaces using transactionSetting may instead return a client-run stamp sheet, or execute transactions asynchronously. The legacy stamp sheet format has multiple consume actions and one acquire action; multiple rewards may be folded into job registration. These properties of legacy stamp sheets do not describe all GS2 transactions.
The Game Engine SDK returns a TransactionDomain from transaction issuance APIs. Call WaitAsync (Unity), Wait (Unreal Engine 5), or wait (Godot) to obtain the execution result. This handles the namespace’s configured execution mode: a completed server transaction does not require the client to execute the actions again.
DistributorModel
A DistributorModel is a unit of resource distribution configuration registered within a namespace.
The following can be configured for a DistributorModel:
inboxNamespaceId: GRN of the GS2-Inbox namespace to which contents are automatically forwarded if they overflow.whiteListTargetIds: Whitelist of actions allowed to be executed via transactions.
By preparing multiple DistributorModels, you can use different forwarding rules for different purposes such as store, quest, and mission.
Overflow handling for distributions
When executing an acquire action, the result may not be receivable on the spot due to reasons such as exceeding the player’s holding limit.
To prepare for such cases, setting the inboxNamespaceId of the DistributorModel will automatically forward the undelivered content as a message to GS2-Inbox, allowing the player to receive it later.
This prevents situations where rewards are lost because they could not be received.
graph TD
Acquire["Execute Acquire Action"] --> Check{"Grantable to player?"}
Check -- Yes --> Granted["Grant to player"]
Check -- "No (limit exceeded, etc.)" --> Inbox["Forward to GS2-Inbox"]
Inbox --> Receive["Player receives later"]Batch Request
GS2-Distributor provides a “batch request” feature that bundles multiple GS2 API calls into a single request for execution.
Since multiple service queries can be completed in a single API call, this reduces network round trips and improves response time. It is especially useful in cases such as fetching initial data from multiple microservices at game startup.
Each request included in a batch request is processed independently, and a response is returned for each.
Master Data Management
Registering master data allows you to configure data and behaviors available to the microservice.
Master data types include:
DistributorModel: Transaction forwarding rules and the forwarding destination on overflow
Below is a JSON example of master data:
{
"version": "2019-09-09",
"distributorModels": [
{
"name": "default",
"metadata": "default distributor",
"inboxNamespaceId": "grn:gs2:{region}:{ownerId}:inbox:namespace-0001"
}
]
}Master data can be registered via the management console, imported from GitHub, or registered from CI using GS2-Deploy.
Transaction Actions
GS2-Distributor is the service that executes transactions issued by other services, and it does not itself issue consume or acquire actions.
Example Implementation
Executing a transaction
In the GS2 SDK, calling WaitAsync on the TransactionDomain returned by each service’s transaction issuance API automatically executes the transaction through GS2-Distributor.
Below is an example of executing a transaction using the GS2-Mission reward receipt process.
var transaction = await gs2.Mission.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Complete(
missionGroupName: "group-0001"
).ReceiveRewardsAsync(
missionTaskName: "task-0001"
);
await transaction.WaitAsync(); const auto Future = Gs2->Mission->Namespace(
"namespace-0001" // namespaceName
)->Me(
GameSession
)->Complete(
"group-0001" // missionGroupName
)->ReceiveRewards(
"task-0001" // missionTaskName
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;
const auto Transaction = Future->GetTask().Result();
const auto Future2 = Transaction->Wait();
Future2->StartSynchronousTask();
if (Future2->GetTask().IsError()) return false;var transaction = await ez.mission.namespace_(
"namespace-0001"
).me(game_session).complete("missionGroup-0001").receive_rewards(
"missionTask-0001"
)
if transaction.error != null:
push_error(str(transaction.error))
return
var async_result = await transaction.result.wait()
if async_result.error != null:
push_error(str(async_result.error))
returnExecuting a batch request
Multiple GS2 API calls are executed in a single round trip.
var result = await gs2.Distributor.Namespace(
namespaceName: "namespace-0001"
).BatchExecuteApiAsync(
requestPayloads: new [] {
new Gs2.Unity.Gs2Distributor.Model.EzBatchRequestPayload
{
RequestId = "1",
Service = "inventory",
MethodName = "describe_inventories",
Parameter = "{\"namespaceName\":\"namespace-0001\"}",
},
new Gs2.Unity.Gs2Distributor.Model.EzBatchRequestPayload
{
RequestId = "2",
Service = "experience",
MethodName = "describe_statuses",
Parameter = "{\"namespaceName\":\"namespace-0001\"}",
},
}
); const auto Future = Gs2->Distributor->Namespace(
"namespace-0001" // namespaceName
)->BatchExecuteApi(
[]
{
const auto v = MakeShared<TArray<Gs2::UE5::Distributor::Model::FEzBatchRequestPayloadPtr>>();
v->Add(MakeShared<Gs2::UE5::Distributor::Model::FEzBatchRequestPayload>()
->WithRequestId(TOptional<FString>("1"))
->WithService(TOptional<FString>("inventory"))
->WithMethodName(TOptional<FString>("describe_inventories"))
->WithParameter(TOptional<FString>("{\"namespaceName\":\"namespace-0001\"}")));
v->Add(MakeShared<Gs2::UE5::Distributor::Model::FEzBatchRequestPayload>()
->WithRequestId(TOptional<FString>("2"))
->WithService(TOptional<FString>("experience"))
->WithMethodName(TOptional<FString>("describe_statuses"))
->WithParameter(TOptional<FString>("{\"namespaceName\":\"namespace-0001\"}")));
return v;
}()
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;var domain = ez.distributor.namespace_(
null
)
var async_result = await domain.batch_execute_api(
[
Gs2DistributorEzBatchRequestPayload.new()
.with_service("inventory")
.with_method_name("describeSimpleItems")
.with_parameter("{\"namespaceName\": \"namespace-0001\", \"inventoryName\": \"inventory-0001\", \"accessToken\": \"accessToken-0001\"}"),
Gs2DistributorEzBatchRequestPayload.new()
.with_service("exchange")
.with_method_name("describeRateModels")
.with_parameter("{\"namespaceName\": \"namespace-0001\"}"),
] # request_payloads
)
if async_result.error != null:
push_error(str(async_result.error))
return
var result = async_result.resultFreezing master data
If master data is updated after a transaction has been issued, inconsistencies may arise between the issued transaction and the current master data.
By calling FreezeMasterData of GS2-Distributor, the master data version used for the logged-in player is fixed, avoiding inconsistencies caused by switching master data during gameplay.
await gs2.Distributor.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).FreezeMasterDataAsync(
); // FreezeMasterData is not available in the Unreal Engine SDK.
// Use the management console / GS2 CLI / general-purpose SDKs for various languages (C# / Go / Python / TypeScript / PHP / Java).
var async_result = await Gs2DistributorClient.new(connection).freeze_master_data(
game_session, "namespace-0001"
)
if async_result.error != null:
push_error(str(async_result.error))
return