Background Jobs #

A common engineering practice is to run complex or lengthy tasks as scheduled jobs in the background. This is often used in a game server context to perform data maintenance or run tasks such as handing out player daily rewards across the entire player base at a specific time. These tasks are often quick and non-problematic when your player base is small; however, as your player base begins to grow, so too does the length of time it takes to perform these tasks. This can create spikes of high CPU and database load on your server at specific times of the day, which can lead to degraded performance and player experience during those times.

A solution to this is to use a Just-In-Time (JIT) approach instead. Let’s look at an example whereby each player receives 100 gold coins per day, regardless of whether or not they have logged into the game that day.

The Background Job approach #

Using a background job approach you would schedule a task at a time that is the least impactful to your player base, say Midnight, and run a task to loop through all players and give them 100 gold coins.

You could do some clever filtering here to only loop through players that have been active within a certain period of time, but in this case we want to give gold to every player regardless of whether or not they have recently played.

In a best case scenario, this method is going to be in the O(n) order of time complexity; whereby as your player base grows, so too does the time it takes to perform this task.

As you can imagine, over time this task will produce an ever increasing burden on your server’s CPU and Database and eventually lead to network performance impacts on your players during this time.

The Just-In-Time approach #

Instead of performing a bulk handout of gold coins each day at Midnight, a Just-In-Time approach would hand out the appropriate amount of gold coins to players whenever they next login to the game.

To implement this you would take the following steps when a player logs in:

  • Check to see when the player last logged in; either via a property on their user metadata or a value in the storage engine
  • If there was no last login, give the player 100 gold
  • If there was a last login, give the player the amount of gold owed based on how long it has been since they last logged in
  • Store their last login timestamp as the current time

Regardless of which user logs in and at what time of day, this task will always be in the O(1) order of time complexity.

The benefit of this approach is threefold:

  1. The demand on the CPU/Database for this task is distributed throughout the day as and when it is needed in small chunks
  2. Players won’t experience a spike in degraded network performance as it would if the task was performed against a large player base
  3. A player who never logs back into the game never costs any CPU/Database processing time

How to implement a JIT approach #

Using the same example of handing out 100 gold to each player every day at Midnight, let’s implement it using the JIT approach outlined above.

Since we want to trigger this whenever a user logs in to the game, we will setup an After Hook for whenever they authenticate using their device ID. We will register this inside the InitModule function.

1
2
3
let InitModule: nkruntime.InitModule = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, initializer: nkruntime.Initializer) {
  initializer.registerAfterAuthenticateDevice(afterAuthenticateDevice)
}

Then define the function to run:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
let afterAuthenticateDevice : AfterHookFunction<Session, AuthenticateDeviceRequest> = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, session: nkruntime.Session, request: nkruntime.AuthenticateDeviceRequest) {
  // Define a constant for how many milliseconds are in a day
  const msInADay = 1000 * 60 * 60 * 24;

  // Get previous midnight
  const previousMidnight = new Date().setHours(0, 0, 0, 0);

  // Get the user's account so that we can check their metadata for a lastLoginTimestamp
  const account = nk.accountGetId(ctx.userId);
  const lastLoginTimestamp : number = account.user.metadata.lastLoginTimestamp;

  let rewardCoins = 0;

  if (!lastLoginTimestamp) {
    // If the user has never logged in, give them 100 coins
    rewardCoins = 100;
    logger.debug("Player has never logged in before, giving them 100 coins.");
  } else if(lastLoginTimestamp < previousMidnight) {
    // If the user's last login was before midnight, we're going to give them at least 100 coins
    rewardCoins = 100;

    // Calculate how many full days have passed since midnight and the time they last logged in
    const timeBetweenMidnightAndLastLogin = previousMidnight - account.user.metadata.lastLoginTimestamp;
    const totalFullDaysSinceLastLogin = Math.floor(timeBetweenMidnightAndLastLogin / msInADay);

    // Increase the reward by 100 for each full day since last login
    rewardCoins += totalFullDaysSinceLastLogin * 100;

    logger.debug(`Player has logged in for the first time today and it has been a further ${totalFullDaysSinceLastLogin} days; giving them ${rewardCoins} coins.`);
  } else {
    logger.debug("Player has already logged in today, no coins are being rewarded.");
  }

  // Update the user's lastLoginTimestamp
  const metadata = account.user.metadata;
  metadata.lastLoginTimestamp = Date.now();
  nk.accountUpdateId(ctx.userId, null, null, null, null, null, null, metadata);

  // Give the player their coins
  nk.walletUpdate(ctx.userId, { coins: rewardCoins });
};

As you can see in the example above, when a user logs in, the afterAuthenticateDevice function runs as a hook. This function checks to see how long has passed since the user last logged in (if they ever have at all) and rewards 100 coins for their first login of the day, plus 100 more coins per extra full day since their last login.

This example uses a hook that runs after a user authenticates, however you are not limited to this. You could perform your desired logic at various different points in the user’s session. For example, before a user links an alternative authentication method, or after they add a friend. You may also wish to have the user call a custom RPC function instead. For a full list of available hooks see the Message names section of the documentation.