Dependent Jobs #

Dependent jobs are a Pragma Engine structure that enable concurrent processing across services at moments when there are dependencies between processed data.

Dependent jobs can be used to manage the following conditions:

  • processing that can occur concurrently
  • processing that has a dependency on other processing
  • the need to collate data from multiple systems to be sent to the game client

Using dependent jobs #

There are three primary cases for dependent jobs:

  • Match End Plugin
  • Login Data Plugin
  • Custom service plugins

Match End Plugin #

On match end, the Match End Plugin allows the authorship of dependent jobs that will be run after each match. The DefaultPragmaMatchEndPlugin employs the ​​InventoryMatchEndDependentJob to process player inventory updates based on match end data and sends this information to the game client.

Login Data Plugin #

Another place where data must be collected from multiple sources and sent to the client is on player login. The Login Data Plugin defines jobs that should be executed whenever a player logs in, and by default collects a player’s inventory data. Authoring additional dependent jobs can handle new capabilities, such as granting players rewards on login.

Custom Service Plugins #

Dependent jobs can be authored to pull custom data from a custom service. For more information, visit the custom services page.

Authoring dependent jobs #

A dependent job is composed of two main pieces:

  • dependenciesMet: a definition of dependencies that gate the execution of the job
  • makeRequest the job’s work

These are reflected in the method signature of the abstract class DependentJob:

protected abstract fun dependenciesMet(context: DependentJobContext): Boolean
protected abstract suspend fun makeRequest(context: DependentJobContext, mutable: ThreadsafeMutable<T>)

Dependencies #

When the method dependenciesMet returns true, then the dependent job is no longer gated and makeRequest will be called on the DependentJob.

To ascertain if dependencies are met, the dependent job uses DependentJobContext, which is passed into both methods. This is a cache of data types that have been processed.

Example

Let’s look at how we can set data into the context so another dependent job can depend on it.

Here is a sample of the makeRequest of a dependent job that collects a player’s inventory:

    override suspend fun makeRequest(context: DependentJobContext, mutable: ThreadsafeMutable<GameDataRpc.GetLoginDataV1Response.Builder>) {
        val result = service.requestRpc(
            InventoryRpc.GetLoginDataV1Request.newBuilder().setPlayerId(playerId.toFixed128()).build(),
            InventoryRpc.GetLoginDataV1Response::class
        )

        when (result) {
            is PragmaResultResponse -> {
                val response = result.response
                mutable.write {
                    it.loginDataBuilder.apply(LoginDataWrapper(response))
                }
                context.setResult(result.response)
            }
            is PragmaResultError -> {
                service.logger.error("Error getLoginDataV1 playerId: {} for {}", playerId, result.error)
            }
        }
    }

Note that the response type of the RPC request is added to the DependentJobContext via setResult. The presence of this type in the context object can be checked for in another job’s dependenciesMet method. Such as:


    override fun dependenciesMet(context: DependentJobContext): Boolean {
        return context.hasResult(InventoryRpc.GetLoginDataV1Response::class)
    }

In this case, the work of the dependent job only executes once GetLoginDataV1Response has been added to the DependentJobContext.

Request #

Once a dependent job has determined that its dependencies are met, it can execute its work. The intent is to enable service requests to gather and collate data.

For example, during match end, the default behavior is to send match rewards to be processed by the Inventory service. Results from this processing are subsequently sent to the game client.

To capture results and add them to the notification sent to the player, updates are added to the ThreadsafeMutable. The ThreadsafeMutable class is a container for the context object collating the player response data, ensuring thread-safe access.

Example

Here is an example of updating a player’s inventory with match end data and sending the match ID and inventory summary to the player:

override suspend fun makeRequest(context: DependentJobContext, mutable: ThreadsafeMutable<GameDataRpc.MatchProcessedV3Notification.Builder>) {
        val result = service.requestRpc(
            InventoryRpc.MatchEndV4Request.newBuilder()
                .setPlayerId(playerId.toFixed128())
                .setMatchEnd(matchEnd.toProtoV4())
                .build(),
            InventoryRpc.MatchEndV4Response::class
        )

        when (result) {
            is PragmaResultResponse -> {
                val response = result.response
                mutable.write {
                    it.matchId = matchEnd.matchId.toFixed128()
                    it.pragmaMatchProcessedBuilder.addInventorySummaries(response.updateSummary.player)
                }
                context.setResult(result.response)
            }
            is PragmaResultError -> {
                service.logger.error(
                    errorMessage(playerId, matchEnd.matchId, "Inventory"), ServiceErrorFacade(result.error).error()
                )
            }
        }
    }