Skip to content

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

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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions