GS2-Quest Transaction Actions

Specification of verify/consume/acquire transaction actions

Combining actions, and concurrency

The background common to every service is collected in Combining Transaction Actions. Read that first; the rest of this section is what GS2-Quest adds to it.

GS2-Quest provides two transaction actions, both addressing the progress of one user, identified by the combination of namespace and user. There is one progress per user, which is what limits a player to one quest at a time.

OperationRepeated in one transactionAcross a nested transactionBoundary that separates targetsUnder sequential execution mode
Creating a progress
CreateProgressByUserId
Rejected when the transaction is issued, even if the requests are identicalFailsnamespace, userStill fails where it collides with a discard. A discard is a consume action and does run first, so the creation’s existence check passes, but the discard and the creation are then two writes to the same row that cannot be combined and the transaction fails (400). Naming a creation and a discard directly in one transaction is rejected when the transaction is issued in any case. Still fails where it collides with another creation as well: whichever one runs second now finds the progress already exists and its own creation condition fails, so the transaction still fails, just for that reason rather than for values never matching
Discarding a progress
DeleteProgressByUserId
Rejected when the transaction is issued, even if the requests are identicalCombined into onenamespace, userFails across a nested transaction, where it used to be gathered into one: the second discard runs against a progress that is already gone and is rejected as not found (404)

Creating and discarding are separate rows sharing a boundary, so they cannot go in one transaction whether or not a progress exists. The transaction is rejected when it is issued.

Creating a progress fails where another progress already exists. That check is made against the state at the start of the transaction, so where enableAtomicCommit is on, quite apart from the collision, a transaction cannot discard a progress and then start a new quest. Split the transaction. Turning on sequential execution mode (enableSequentialExecution or TransactionSettingV2) does not remove this problem. A creation does now see the discard that ran before it in the same transaction (consume actions run before acquire actions), so the existence check passes — but the discard and the creation are two writes to the same row that cannot be combined, and the transaction fails (400) at the point they are put together. Discarding a progress and starting a new quest still takes two transactions. The rule that a creation and a discard cannot both be named directly in one transaction is unaffected either — that rejection happens when the transaction is issued, before any action runs.

Because there is only one progress per user, placing two creations for the same user in one transaction is a contradictory instruction in itself. There is nothing to decide which of them should run, so it is rejected when the transaction is issued, even where the questModelId, the force flag and the settings are all identical. Treat “buy three and start the quest three times” as something that cannot be expressed. The same goes for two discards: however many are placed, the transaction is rejected when it is issued.

Quest rewards are issued separately

Completing or failing a quest is not a transaction action. When a quest ends, GS2-Quest issues its rewards as a transaction of its own, so the restrictions of whichever services those rewards belong to apply, and so does the section below.

That transaction always contains a discard of the progress. Where enableAtomicCommit is on, a reward therefore cannot hold a CreateProgressByUserId against the same namespace to start the next quest automatically: even a single creation sits together with the discard, and the transaction is rejected when it is issued. Split the transaction where you want to chain quests.

Take care with nested transactions

A progress created from the inside collides with a progress discarded from the outside, and the transaction fails. Two creations write to the same progress twice and fail as well; creating a progress writes a fresh identifier and a fresh random seed every time, so the values written never match even where the requests are identical, and the combining that applies to matching values does not apply here. Only two discards do not fail: deleting the same progress is idempotent, so they are combined into one even across a nest.

Turning on sequential execution mode removes the first failure: a discard is a consume action and always runs before the acquire action that triggers the nested transaction, so the discard has already happened by the time the inner creation runs, and the two no longer collide. It does not remove the second: whichever of the two creations runs later still finds the progress already exists, so its own creation still fails and the transaction still fails, even though the reason is now an ordinary condition failure rather than two values that can never match.

If you want to avoid these restrictions

Creating a progress is an acquire action and discarding one is a consume action. Two creations, or two discards, are checked when the transaction is issued, before either setting is applied, so neither turning acquireActionUseJobQueue on nor turning enableAtomicCommit off clears them; reduce them to one, or split the transaction.

A creation together with a discard is cleared by turning enableAtomicCommit off. Consume actions then run before acquire actions, so discarding and starting a new quest in one transaction starts to work as well. acquireActionUseJobQueue separates acquire actions from each other, so do not rely on it here: where there is only one acquire action it is not put on the queue, and it stays in the same transaction as the discard.

Concurrency and retries

Where several requests create or discard the same user’s progress at the same time, the one confirmed later returns a conflict (409). Retrying re-evaluates against the latest state, so it succeeds if the progress is in the expected state and returns an error if another quest has already started.


Consume Action

Gs2Quest:DeleteProgressByUserId

Delete Quest Progress by User ID

Deletes the current quest progress for the specified user. This cancels the quest in progress and allows starting a new quest.

Quantity specification supported: NO

Reversible action: NO

TypeConditionRequiredDefaultValue LimitsDescription
namespaceNamestring
~ 128 charsNamespace name
Unique Namespace name. Specified using alphanumeric characters, hyphens (-), underscores (_), and periods (.).
userIdstring
~ 128 charsUser ID
Specify #{userId} to substitute the currently logged-in user’s ID.
timeOffsetTokenstring~ 1024 charsTime offset token
{
    "action": "Gs2Quest:DeleteProgressByUserId",
    "request": {
        "namespaceName": "[string]Namespace name",
        "userId": "[string]User ID",
        "timeOffsetToken": "[string]Time offset token"
    }
}
action: Gs2Quest:DeleteProgressByUserId
request:
  namespaceName: "[string]Namespace name"
  userId: "[string]User ID"
  timeOffsetToken: "[string]Time offset token"
transaction.service("quest").consume.delete_progress_by_user_id({
    namespaceName="[string]Namespace name",
    userId="[string]User ID",
    timeOffsetToken="[string]Time offset token",
})

Acquire Action

Gs2Quest:CreateProgressByUserId

Create Quest Progress by User ID

Creates a quest progress by looking up the quest model, performing a lottery on the quest content, and starting the quest. If a quest is already in progress, it returns an error unless the force flag is set.

Quantity specification supported: NO

Reversible action: YES

TypeConditionRequiredDefaultValue LimitsDescription
namespaceNamestring
~ 128 charsNamespace name
Unique Namespace name. Specified using alphanumeric characters, hyphens (-), underscores (_), and periods (.).
userIdstring
~ 128 charsUser ID
Specify #{userId} to substitute the currently logged-in user’s ID.
questModelIdstring
~ 1024 charsQuest Model GRN to Start
forceboolfalseIf have a quest already started, you can discard it and start it
configList<Config>[]0 ~ 32 itemsConfiguration values applied to transaction variables
timeOffsetTokenstring~ 1024 charsTime offset token
{
    "action": "Gs2Quest:CreateProgressByUserId",
    "request": {
        "namespaceName": "[string]Namespace name",
        "userId": "[string]User ID",
        "questModelId": "[string]Quest Model GRN to Start",
        "force": "[bool]If have a quest already started, you can discard it and start it",
        "config": [
            {
                "key": "[string]Name",
                "value": "[string]Value"
            }
        ],
        "timeOffsetToken": "[string]Time offset token"
    }
}
action: Gs2Quest:CreateProgressByUserId
request:
  namespaceName: "[string]Namespace name"
  userId: "[string]User ID"
  questModelId: "[string]Quest Model GRN to Start"
  force: "[bool]If have a quest already started, you can discard it and start it"
  config: 
    - key: "[string]Name"
      value: "[string]Value"
  timeOffsetToken: "[string]Time offset token"
transaction.service("quest").acquire.create_progress_by_user_id({
    namespaceName="[string]Namespace name",
    userId="[string]User ID",
    questModelId="[string]Quest Model GRN to Start",
    force="[bool]If have a quest already started, you can discard it and start it",
    config={
        {
            key="[string]Name",
            value="[string]Value"
        }
    },
    timeOffsetToken="[string]Time offset token",
})