Skip to content

IntervalActionWorker fires ~100x too fast on Linux (TimeSpan-tick vs Stopwatch-tick unit mismatch) #25876

@Reyalsorik

Description

@Reyalsorik

Version: 0.13.0.4 beta release-1024 (Linux / official strangeloopgames/eco-game-server image)

Summary:

Every periodic worker created via PeriodicWorkerFactory.CreateWithInterval fires roughly 100× too frequently on Linux. The same code is correct on Windows. The cause is a unit mismatch in IntervalActionWorker between TimeSpan ticks and Stopwatch ticks that only cancels out when Stopwatch.Frequency == TimeSpan.TicksPerSecond — true on Windows (10 MHz), false on .NET/Unix (1 GHz).

Replication:

  1. Run a dedicated server on Linux (the official Docker image).
  2. Start any periodic worker, e.g. PeriodicWorkerFactory.CreateWithInterval(TimeSpan.FromHours(1), doWork).
  3. Observe doWork runs every ~36 seconds instead of hourly.
  4. Run the identical code on Windows — it runs hourly as intended.

3600s / 36s = 100, which equals Stopwatch.Frequency (Linux 1e9) / TimeSpan.TicksPerSecond (1e7).

Root cause:

IntervalActionWorker.Interval's setter stores TimeSpan ticks:

https://github.com/StrangeLoopGames/Eco/blob/v0.13.0.4-beta/Server/Eco.Core/Utils/Threading/IntervalActionWorker.cs#L20

set => this.intervalTicks = value.Ticks;

…but intervalTicks is later consumed as Stopwatch ticks via MillisecondsFromTicks, which divides by Stopwatch.Frequency:

https://github.com/StrangeLoopGames/Eco/blob/v0.13.0.4-beta/Server/Eco.Core/Utils/Threading/IntervalActionWorker.cs#L29

var delayMs = (int)StopwatchUtils.MillisecondsFromTicks(this.intervalTicks - (Stopwatch.GetTimestamp() - start));

https://github.com/StrangeLoopGames/Eco/blob/v0.13.0.4-beta/Server/Eco.Shared/Utils/StopwatchExtensions.cs#L27
https://github.com/StrangeLoopGames/Eco/blob/v0.13.0.4-beta/Server/Eco.Shared/Utils/StopwatchExtensions.cs#L42

private static readonly long Frequency = Stopwatch.Frequency;          // ticks/sec, platform-dependent
public static double MillisecondsFromTicks(long ticks) => ticks / MillisecondFrequency;  // MillisecondFrequency = Frequency / 1000

So the interval is divided by Stopwatch.Frequency but was stored as TimeSpan.Ticks (= seconds * 10,000,000). These agree only when Stopwatch.Frequency == 10,000,000 (Windows). On Linux Stopwatch.Frequency == 1,000,000,000, so the effective interval is 100× too short.

Desired outcome:

The setter should convert the TimeSpan to Stopwatch ticks — the codebase already provides StopwatchUtils.TicksFromTimeSpan for exactly this:

https://github.com/StrangeLoopGames/Eco/blob/v0.13.0.4-beta/Server/Eco.Shared/Utils/StopwatchExtensions.cs#L40

// IntervalActionWorker.cs L20 — fix:
set => this.intervalTicks = StopwatchUtils.TicksFromTimeSpan(value);

The Interval getter then round-trips correctly via SecondsFromTicks, and interval timing is correct on both platforms.

@Kiro (232040014761033738) in Discord.
Kiro in SLG.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions