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.Runthere 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.Messageget "One or more errors occurred." This waits without throwing and letsGetAwaiter().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
Methods
BeginShutdownBudget(int)
Caps the total time shutdown may block the main thread across all call sites.
public static void BeginShutdownBudget(int totalMilliseconds)
Parameters
totalMillisecondsintBudget 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
timeoutMillisecondsint
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
operationFunc<CancellationToken, Task>Async work to run. The supplied token is cancelled on timeout.
timeoutMillisecondsintMaximum wait. 30 seconds if omitted.
Exceptions
- ArgumentNullException
operationis 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
operationFunc<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.timeoutMillisecondsintMaximum wait. 30 seconds if omitted.
Returns
- T
The operation result.
Type Parameters
TResult type of the async operation.
Exceptions
- ArgumentNullException
operationis 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
operationFunc<CancellationToken, Task>Async work to run. The supplied token is cancelled on timeout.
timeoutMillisecondsintMaximum wait. 30 seconds if omitted.
Returns
- bool
trueif the operation completed within the timeout.
Exceptions
- ArgumentNullException
operationis 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
operationFunc<CancellationToken, Task<T>>Async work to run. The supplied token is cancelled on timeout.
resultTThe operation result, or
defaulton timeout.timeoutMillisecondsintMaximum wait. 30 seconds if omitted.
Returns
- bool
trueif the operation completed within the timeout.
Type Parameters
TResult 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
operationis null.