Live Scripting - Pt4 - Background Jobs#

Background Jobs#

Welcome back to our five-part series on custom scripting with the Xsolla Backend Live Scripting System. So far, we’ve learned how to respond to web requests to transact and process data. While this works for many use cases, some scenarios require proactive, rather than reactive, behavior.

This article focuses on background services. A background service (or job) in Xsolla Backend is a special class executed on a regular schedule. It starts running at service startup and continues executing at defined intervals until the server shuts down. This is ideal for tasks like data processing or real-time game simulation.

Scripts and Samples#

Getting Started#

Let’s first examine an existing background service, MetricsCollector, which comes pre-installed with every project.

Step 1: Exploring the MetricsCollector Job#

  1. Open your test workspace and locate the MetricsCollector.ts file in the jobs folder.

[IMAGE: New file: jobs/MetricsCollector.ts]

A background service is simply a class that extends the BackgroundService base class. The base class requires the following abstract functions:

  • The schedule() function defines the execution interval using a crontab-like string. For help with crontab expressions, you can use an online tool.

  • The start() and stop() functions handle initialization and cleanup at service startup and shutdown, respectively.

  • The run() function executes at each interval, performing the bulk of the processing work.

In MetricsCollector, the schedule is set to */5 * * * * *, which means the job runs every 5 seconds. Let’s fill in the run function with an example to collect the total number of characters in the database.

Step 2: Modifying MetricsCollector#

To add this functionality, we first need access to the Character repository. Add the following variable:

@MongoRepository(Character)
private repo?: Repo<Character>;

Next, define a Gauge metric to track the total number of characters over time:

private totalCharacters: prom.Gauge = new prom.Gauge({
    name: "xbe_total_characters",
    help: "The total number of characters.",
});

Now implement the run() function to retrieve the total number of characters using the count() function and set the value of the totalCharacters metric:

public async run(): Promise<void> {
    const result: number = await this.repo.count();
    this.totalCharacters.set(result);
}

The MetricsCollector job now tracks the total number of characters in the database every 5 seconds. Save and publish the service and wait for the service to restart to start collecting metrics. The file should look like this:

import * as prom from "prom-client";
import { MongoRepository as Repo } from "typeorm";
import { BackgroundService, DatabaseDecorators } from "@composer-js/service-core";
import Character from "../models/Character";
const { MongoRepository } = DatabaseDecorators;

/**
 * The `MetricsCollector` provides a background service for collecting Prometheseus 
 * metrics for consumption by external clients and compatible servers using the built-in 
 * `MetricsRoute` route handler.
 */
export default class MetricsCollector extends BackgroundService {
    @MongoRepository(Character)
    private repo?: Repo<Character>;

    private totalCharacters: prom.Gauge = new prom.Gauge({
        name: "xbe_total_characters",
        help: "The total number of characters.",
    });

    constructor(config: any, logger: any) {
        super(config, logger);
    }

    public get schedule(): string | undefined {
        return "*/5 * * * * *";
    }

    public async run(): Promise<void> {
        const result: number = await this.repo.count();
        this.totalCharacters.set(result);
    }

    public async start(): Promise<void> {
        // TODO
    }

    public async stop(): Promise<void> {
        // TODO
    }
}

Step 3: Writing a New Background Service#

Let’s create a new background service from scratch to replenish a character’s health and mana at regular intervals.

  1. Create a new file called CharacterRegen.ts in the jobs folder.

  2. Add the following stub code:

import { MongoRepository as Repo } from "typeorm";
import { BackgroundService, ObjectDecorators, DatabaseDecorators } from "@composer-js/service-core";
import Character from "../models/Character";
const { MongoRepository } = DatabaseDecorators;
const { Config } = ObjectDecorators;

export default class CharacterRegen extends BackgroundService {
    @MongoRepository(Character)
    private repo?: Repo<Character>;

    constructor(config: any, logger: any) {
        super(config, logger);
    }

    public get schedule(): string | undefined {
        return "*/10 * * * * *";
    }

    public async run(): Promise<void> {
        // TODO
    }

    public async start(): Promise<void> {
        // TODO
    }

    public async stop(): Promise<void> {
        // TODO
    }
}

Note that schedule() is set to "*/10 * * * * *", so every 10 seconds we’ll regenerate a bit of health and mana for each character.

  1. Define an interface for the CharacterStatSettings that will hold the min, max, and rate of regen for our character stats:

export interface CharacterStatSettings {
    min: number;
    max: number;
    regen: number;
}
  1. Declare default CharacterStateSettings values for health and mana:

export const STAT_DEFAULTS_HEALTH: CharacterStatSettings = {
    min: 0,
    max: 200,
    regen: 5,
};

export const STAT_DEFAULTS_MANA: CharacterStatSettings = {
    min: 0,
    max: 500,
    regen: 10,
};
  1. Add fields to the job class for health and mana settings. Use the @Config decorator to automatically inject overrides from the config.ts file or environment variables:

@Config("character.regen.health", STAT_DEFAULTS_HEALTH)
private healthSettings: CharacterStatSettings;

@Config("character.regen.mana", STAT_DEFAULTS_MANA)
private manaSettings: CharacterStatSettings;
  • The healthSettings and manaSettings fields will be populated with the values from:

    • …the config at the path character.regen.health and charactger.regen.mana if found, or…

    • …if not found on the config they will default to the STAT_DEFAULTS_... values passed.

Step 4: Implementing Character Regeneration#

In the run() function, regenerate health and mana for each character, ensuring not to exceed the maximum configured values:

public async run(): Promise<void> {
    const chars: Character[] = await this.repo.find();
    for (const char of chars) {
        char.health = Math.min(char.health + this.healthSettings.regen, this.healthSettings.max);
        char.mana = Math.min(char.mana + this.manaSettings.regen, this.manaSettings.max);
        char.dateModified = new Date();
        char.version += 1;
        await this.repo.save(char);
    }
}

This code updates each character’s health and mana and persists the changes in the database. However, it’s inefficient to process characters already at maximum health or mana.

Step 5: Optimizing the Regeneration Job#

To optimize, add search criteria to only retrieve characters whose health or mana are below their maximum values:

const chars: Character[] = await this.repo.find({
    $or: [
        { health: { $lt: this.healthSettings.max } },
        { mana: { $lt: this.manaSettings.max } }
    ],
});

for (const char of chars) {
    char.health = Math.min(char.health + this.healthSettings.regen, this.healthSettings.max);
    char.mana = Math.min(char.mana + this.manaSettings.regen, this.manaSettings.max);
    char.dateModified = new Date();
    char.version += 1;
    await this.repo.save(char);
}

Now, the job only processes characters who need regeneration. Save and publish the service. It will start executing once the service restarts.

The final script should look like this:

import { MongoRepository as Repo, Document } from "typeorm";
import { Event } from "@composer-js/core";
import { ObjectDecorators, DatabaseDecorators } from "@composer-js/service-core";
import { AXRObjectDecorators } from "@acceleratxr/service-core";
import Character from "../models/Character";
import { STAT_DEFAULTS_HEALTH, STAT_DEFAULTS_MANA } from "../jobs/CharacterRegen";
const { Config } = ObjectDecorators;
const { EventListener, OnEvent } = AXRObjectDecorators;
const { MongoRepository } = DatabaseDecorators;

@EventListener()
export default class CharacterEvents {
    @MongoRepository(Character)
    private repo?: Repo<Character>;

    @Config("characterStats:health:max", STAT_DEFAULTS_HEALTH.max)
    private maxHealth: number;

    @Config("characterStats:mana:max", STAT_DEFAULTS_MANA.max)
    private maxMana: number;

    @OnEvent(["UserLogin", "QuestComplete"])
    private async regenHealthAndMana(evt: Event): Promise<void> {
        const character: Document | undefined = await this.repo.updateOne(
            { userUid: evt.userId },
            {
                $set: {
                    health: this.maxHealth,
                    mana: this.maxMana,
                    dateModified: new Date(),
                },
                $inc: {
                    version: 1,
                },
            });
    }
}

Conclusion#

In this part, we explored how to create background jobs in the Xsolla Backend Live Scripting System. You now know how to:

  • Create and configure background services.

  • Track and report metrics using Prometheus.

  • Optimize background tasks for efficiency.

In the next and final part of the series, we’ll explore how to use events to automatically reset character stats upon user login, quest completion, or level-up.