Table of Contents

Class UnitySyncOverAsync

Namespace
FishMMO.Server.Implementation
Assembly
FishMMO.Server.dll

Blocks the calling thread on an async operation without risking a SynchronizationContext deadlock.

Unity installs a UnitySynchronizationContext on the main thread; continuations posted to it only run when the player loop drains them. Any await in the callee that captures that context — i.e. any await without ConfigureAwait(false), anywhere in the call chain — can therefore never resume while the main thread sits in GetResult()/.Result/.Wait(). The server then stays alive but never finishes InitializeOnce, so the transport never binds its port.

Shutdown paths only. OnDestroy/OnApplicationQuit cannot yield and the process exits immediately afterwards, so there is no continuation to hand work to — a bounded block is the only way to flush pending state before exit. Startup has no such constraint and must not use this: behaviours initialize through ServerBehaviour.InitializeOnceAsync, driven by Server's initialization coroutine, which leaves the main thread free to drain continuations.

Where a bounded block genuinely is required, route it through here rather than hand-rolling Task.Run(...).Wait(...): call sites should not have to audit an entire EF/Npgsql call chain to know whether blocking is safe.

public static class UnitySyncOverAsync
Inheritance
UnitySyncOverAsync
Inherited Members

Remarks

Behaviour that hand-rolled Task.Run(...).Wait(timeout) gets wrong:

  • No pointless thread hop. When the caller already has no synchronization context (a worker thread), the operation starts inline. Task.Run there would block one pool thread while requiring the pool to hand out another to complete the very work being waited on — self-inflicted starvation under load.
  • Original exceptions. Wait(int) throws AggregateException, so callers logging ex.Message get "One or more errors occurred." This waits without throwing and lets GetAwaiter().GetResult() rethrow the original exception.
  • Timeouts cancel. The operation receives a token that is cancelled when the timeout expires, so the database work stops instead of running on unobserved with its result discarded. The abandoned task's exception is observed so it can never surface as an unobserved-task exception.

Fields

DefaultTimeoutMilliseconds

Default wait for server startup/shutdown database calls. Matches the registration timeout used by LoginServerSystem.

public const int DefaultTimeoutMilliseconds = 30000

Field Value

int

Methods

BeginShutdownBudget(int)

Caps the total time shutdown may block the main thread across all call sites.

public static void BeginShutdownBudget(int totalMilliseconds)

Parameters

totalMilliseconds int

Budget for the whole teardown.

Remarks

Individual timeouts are each reasonable but unbounded in aggregate: a scene server can serialize a 5s database cleanup, a 10s chat flush and a 30s character save. On a wedged database that is ~45s, which exceeds a Kubernetes 30s grace period and a Docker 10s stop timeout — the process gets SIGKILLed mid-flush having accomplished nothing, which is strictly worse than flushing what fits and exiting cleanly. Clamping every call to the remaining budget keeps teardown inside the supervisor's window.

ClampToShutdownBudget(int)

Clamps a requested timeout to whatever remains of the shutdown budget.

public static int ClampToShutdownBudget(int timeoutMilliseconds)

Parameters

timeoutMilliseconds int

Returns

int

The effective timeout, or 0 when the budget is spent — callers then fail immediately rather than blocking teardown further.

Remarks

Public because the budget has to cover every blocking wait on the teardown path, not only the ones that go through TryRun<T>(Func<CancellationToken, Task<T>>, out T, int). AsyncWorkerData drains its in-flight work with a bounded sleep rather than an async wait — it is waiting on a count, not on a task — and an unaccounted three seconds there is exactly what this budget exists to prevent: the total is sized to fit inside a supervisor's stop timeout, and overrunning it means being SIGKILLed mid-flush having accomplished nothing.

ClearShutdownBudget()

Clears any active shutdown budget. Used when a teardown is aborted (Editor domain reload) so a later run is not clamped by a stale deadline.

public static void ClearShutdownBudget()

Run(Func<CancellationToken, Task>, int)

Runs operation off Unity's synchronization context and blocks until it completes or timeoutMilliseconds elapses.

public static void Run(Func<CancellationToken, Task> operation, int timeoutMilliseconds = 30000)

Parameters

operation Func<CancellationToken, Task>

Async work to run. The supplied token is cancelled on timeout.

timeoutMilliseconds int

Maximum wait. 30 seconds if omitted.

Exceptions

ArgumentNullException

operation is null.

TimeoutException

The wait exceeded timeoutMilliseconds.

Run<T>(Func<CancellationToken, Task<T>>, int)

Runs operation off Unity's synchronization context and blocks until it completes or timeoutMilliseconds elapses.

public static T Run<T>(Func<CancellationToken, Task<T>> operation, int timeoutMilliseconds = 30000)

Parameters

operation Func<CancellationToken, Task<T>>

Async work to run. The supplied token is cancelled on timeout — forward it to the database call. Use _ => only when the work genuinely cannot be cancelled.

timeoutMilliseconds int

Maximum wait. 30 seconds if omitted.

Returns

T

The operation result.

Type Parameters

T

Result type of the async operation.

Exceptions

ArgumentNullException

operation is null.

TimeoutException

The wait exceeded timeoutMilliseconds.

TryRun(Func<CancellationToken, Task>, int)

Runs operation off Unity's synchronization context and blocks until it completes or timeoutMilliseconds elapses. Returns false on timeout instead of throwing.

public static bool TryRun(Func<CancellationToken, Task> operation, int timeoutMilliseconds = 30000)

Parameters

operation Func<CancellationToken, Task>

Async work to run. The supplied token is cancelled on timeout.

timeoutMilliseconds int

Maximum wait. 30 seconds if omitted.

Returns

bool

true if the operation completed within the timeout.

Exceptions

ArgumentNullException

operation is null.

TryRun<T>(Func<CancellationToken, Task<T>>, out T, int)

Runs operation off Unity's synchronization context and blocks until it completes or timeoutMilliseconds elapses. Returns false on timeout instead of throwing, for call sites that degrade gracefully.

public static bool TryRun<T>(Func<CancellationToken, Task<T>> operation, out T result, int timeoutMilliseconds = 30000)

Parameters

operation Func<CancellationToken, Task<T>>

Async work to run. The supplied token is cancelled on timeout.

result T

The operation result, or default on timeout.

timeoutMilliseconds int

Maximum wait. 30 seconds if omitted.

Returns

bool

true if the operation completed within the timeout.

Type Parameters

T

Result type of the async operation.

Remarks

Only a timeout returns false. Exceptions thrown by the operation propagate unwrapped, exactly as a direct await would surface them.

Exceptions

ArgumentNullException

operation is null.