← All articles

An interruptible world loop with persisted progress

Scheduling world updates is easier than preventing the same day from being applied twice. WorldLoop owns timing while IWorldStore owns durable progression.

t.Turgay
An interruptible world loop with persisted progress

Advancing a game world every ten seconds takes little timer code. The harder question is whether the same game day can be applied twice after a process fails during persistence. The world-step and end-of-day routines in my archive provide a useful context for separating scheduling from durable progress.

The loop owns when to call

WorldLoop uses PeriodicTimer to invoke the store. IWorldStore expresses the intended “apply the next day once” contract, but its method name is not an implementation of that guarantee. This hosting fragment still needs a real store and DI registration.

using Microsoft.Extensions.Hosting;
public interface IWorldStore
{
    Task ApplyNextDayOnceAsync(CancellationToken token);
}
public sealed class WorldLoop(IWorldStore store):BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken token)
    {
        using var timer=new PeriodicTimer(TimeSpan.FromSeconds(10));
        while(await timer.WaitForNextTickAsync(token))
            await store.ApplyNextDayOnceAsync(token);
    }
}

Awaiting each step prevents this one loop from starting its next operation before the current one returns. It does not prevent another process or worker from advancing the same world. In-loop serialization and data-level idempotency are different boundaries.

Game time is not simply timer callback count

The ten-second interval is an illustrative mapping to a game day. Slow steps and application downtime need an explicit policy. PeriodicTimer is not a durable queue of every missed day. A system that advances during downtime needs persisted knowledge of the last completed game period.

Should the world pause while offline or catch up from elapsed time? Replaying days one by one and skipping directly ahead can produce different resource and event outcomes. Those are game-design rules, not decisions a timer default should make accidentally.

Make the store protect one complete transition

A durable implementation can use world identity, current day and expected version in a conditional update. If two callers both try to move day seventeen to eighteen, only one should win. Resource production, player updates and events belonging to that transition may need one transaction. Incrementing the day number alone does not make all related side effects idempotent.

Retrying a partly completed step can also redraw random events and change the result. A repeatable seed, stored event decision or completed-step record can address that requirement. The appropriate design depends on replay and recovery needs; none of these are implemented by the interface alone.

Respect hosted-service dependency lifetimes

A hosted service is long-lived. A store relying on a scoped DbContext should be resolved within an appropriate step scope rather than captured for the process lifetime. The interface makes this decision visible, but does not automatically choose a safe DI lifetime.

Choose cancellation and failure behavior

The timer wait and store call both receive cancellation. Expected shutdown cancellation should not be reported as corrupted world state. Conversely, swallowing every store failure and proceeding to another day can create inconsistency. Decide which failures stop the host and which allow a safe retry of the same transition.

Test non-overlapping calls within one loop, two competing workers and a failure in the middle of a transition. A controlled clock and fake store can make these checks deterministic. The useful outcome is a correct next world state, not merely evidence that a timer produced ticks.

.NET worker services; PeriodicTimer behavior.

✳ You have reached the end of this orbit.
Explore more articles →
turgay.A little code, a few pixels, a lot of curiosity.© 2026 · Made on Earth ✳