Appearance
Database Scan Jobs
The Metaplay SDK provides a database scanning system to perform operations on server entities according to custom parameters.
Appearance
The Metaplay SDK provides a database scanning system to perform operations on server entities according to custom parameters.
A Database Scan Job is an operation that scans and processes each entity in an entity database table. A Scan Job happens over time and is not instantaneous. After being started, it runs to completion unless explicitly canceled. A running Scan Job can become paused due to higher-priority Jobs.
A Maintenance Job is a special type of Scan Job that requires less configuration and has to be run manually from the LiveOps Dashboard. It is meant to run during a game's regular operation, so it runs relatively slowly to avoid consuming too many resources.
Scan Jobs are useful when you need to operate on some or all entities of a specific kind. They are especially helpful when there is no simple way to pre-filter the entities down to the ones you want to target, but instead, need to evaluate each one.

Some examples of default Jobs the SDK provides are notification campaigns and player deletion.
Here are the basic components that make up the Scan Jobs system:

Besides regular Database Scan Jobs, you can create Maintenance Jobs. Maintenance Jobs are a subcategory of Scan Jobs that are initiated manually from the Scan Jobs page in the LiveOps Dashboard and are executed in a simple queued order. If a Maintenance Job is launched while another is active, the new one enters a queue. At most, one Maintenance Job can be active at a time.
Since maintenance Jobs are intended to be run during a game server's regular operation, they run relatively slowly to avoid consuming too many resources. For this reason, they have a lower priority than other types of Scan Jobs and are therefore paused if another Scan Job is running.
Maintenance Jobs are considerably simpler to implement when compared to regular Scan Jobs, so if they meet all of your requirements, we recommend you use them. Maintenance Jobs are appropriate if:
There are a few built-in Maintenance Jobs, like the schema migrator and the entity refresher.

Defining custom Maintenance Jobs entails creating subclasses of DatabaseScanProcessor and MaintenanceJobSpec, that implement the scan processor and Job specification, respectively.
The DatabaseScanProcessor subclass implements the StartProcessItemBatchAsync method that processes items for the Job. Additionally, a DatabaseScanProcessor specifies some values that control the speed and batch size of the scanning. A DatabaseScanProcessor can contain a state, but it may not be required for all kinds of Jobs. We recommend you derive your class from DatabaseScanProcessor<TScannedItem>, which automatically implements some boilerplate parts of the code based on the item being scanned. Here's some sample code of a processor for a custom Scan Job that just goes over all players and counts them:
[MetaSerializableDerived(202)]
public class CustomMaintenanceJobProcessor : DatabaseScanProcessor<PersistedPlayerBase>
{
[MetaMember(1)] CustomMaintenanceJobProcessingStatistics _statistics;
// Control speed and batch size of scanning
public override int DesiredScanBatchSize => 100;
public override TimeSpan ScanInterval => TimeSpan.FromSeconds(0.1);
public override TimeSpan PersistInterval => TimeSpan.FromMilliseconds(1000);
public override TimeSpan TickInterval => TimeSpan.FromSeconds(1.0);
public override bool CanCurrentlyProcessMoreItems => true;
public override bool HasCompletedAllWorkSoFar => true;
public override DatabaseScanProcessingStatistics Stats => _statistics;
public CustomMaintenanceJobProcessor() { }
public CustomMaintenanceJobProcessor(CustomMaintenanceJobProcessingStatistics initialStatisticsMaybe)
{
_statistics = initialStatisticsMaybe ?? new CustomMaintenanceJobProcessingStatistics();
}
public override Task StartProcessItemBatchAsync(IContext context, IEnumerable<PersistedPlayerBase> persistedPlayers)
{
foreach (PersistedPlayerBase persistedPlayer in persistedPlayers)
{
// Do something.
_statistics.PlayersScanned++;
}
return Task.CompletedTask;
}
public override Task TickAsync(IContext context)
{
return Task.CompletedTask;
}
public override void Cancel(IContext context)
{
}
}The CustomMaintenanceJobProcessingStatistics inherits from DatabaseScanProcessingStatistics and is used to keep track of whatever statistics you wish. In this case, we're using it to count the number of players scanned.
[MetaSerializableDerived(202)]
public class CustomMaintenanceJobProcessingStatistics : DatabaseScanProcessingStatistics
{
[MetaMember(1)] public int PlayersScanned = 0;
public static CustomMaintenanceJobProcessingStatistics ComputeAggregate(IEnumerable<CustomMaintenanceJobProcessingStatistics> parts)
{
CustomMaintenanceJobProcessingStatistics aggregate = new CustomMaintenanceJobProcessingStatistics();
foreach (CustomMaintenanceJobProcessingStatistics part in parts)
{
aggregate.PlayersScanned += part.PlayersScanned;
}
return aggregate;
}
}Next, we implement the MaintenanceJobSpec subclass for our Job specification:
[MetaSerializableDerived(202)]
public class CustomMaintenanceJobSpec : MaintenanceJobSpec
{
public override string JobTitle => "Custom Maintenance Job";
public override string JobDescription => "Counts the number of players scanned.";
public override string MetricsTag => "CustomMaintenanceJob";
public override int Priority => DatabaseScanJobPriorities.ScheduledPlayerDeletion;
public override EntityKind EntityKind => EntityKindCore.Player;
public override object JobKindDiscriminator => null;
CustomMaintenanceJobSpec() { }
public static CustomMaintenanceJobSpec CreateDefault()
{
return new CustomMaintenanceJobSpec();
}
public override DatabaseScanProcessor CreateProcessor(DatabaseScanProcessingStatistics initialStatisticsMaybe)
{
return new CustomMaintenanceJobProcessor((CustomMaintenanceJobProcessingStatistics)initialStatisticsMaybe);
}
public override MetaDictionary<string, object> CreateSummary(DatabaseScanProcessingStatistics statsParam)
{
CustomMaintenanceJobProcessingStatistics stats = (CustomMaintenanceJobProcessingStatistics)statsParam;
MetaDictionary<string, object> summary = new()
{
{ "Players Scanned", stats.PlayersScanned },
};
return summary;
}
public override DatabaseScanProcessingStatistics ComputeAggregateStatistics(IEnumerable<DatabaseScanProcessingStatistics> parts)
{
return CustomMaintenanceJobProcessingStatistics.ComputeAggregate(parts.Cast<CustomMaintenanceJobProcessingStatistics>());
}
}Each Job has a priority, given by the Priority property of DatabaseScanJobSpec. When a Job is already active, the Scan Coordinator will only start a new Job if the priority of the new Job is higher than the existing Job. It will then pause the execution of the lower-priority Job until the new Job completes. In the above snippet, we set the priority to the same value as used by the SDK's player deletion Job, but you can set it to any appropriate value to define its priority relative to other types of Jobs.
You'll also need to register the maintenance Job. Mark the affected PersistedEntityConfigs with the EntityMaintenanceJob attribute:
[EntityConfig]
[EntityMaintenanceJob("Custom", typeof(CustomMaintenanceJobSpec))]
public class PlayerConfig : PlayerConfigBase
{
public override Type EntityActorType => typeof(PlayerActor);
}:::
This mechanism supports long-running background Jobs while still allowing more urgent Jobs to start without delay. For example, a running player deletion job can be paused when a new notification campaign starts. Once the notification campaign ends, the deletion job will resume.
To define a full-blown Database Scan Job as opposed to a Maintenance Job, you need to create a subclass of DatabaseScanJobManager. In particular, the subclass will implement the TryGetNextDueJob method, which will return a Job Specification for a Job that is due and whether it is valid to start or null if no Job is available. Additionally, the subclass will implement communication methods with the rest of the game backend and the Scan Coordinator.
Take a look at NotificationCampaignJob.cs and ScheduledPlayerDeletionJob.cs for examples of how their Job managers are implemented. It's also worthwhile to study DatabaseScanUser.cs since this is where all of the base classes you will derive your custom classes from are defined.
The SDK detects all defined manager classes and automatically creates one instance of each. The manager's serializable state will be automatically persisted in the database.
Following this article, it might be a good idea to take a look at the built-in Scan Jobs the Metaplay SDK offers:
Here are some more pages on server programming that relate to entities and can be of interest following Scan Jobs: