Class WorldServerService
Base class for database services that execute EF Core operations with consistent execution behavior (transactional and read-only), retry behavior for transient failures, and standardized error mapping into DatabaseResult.
public sealed class WorldServerService : BaseService<WorldServerEntity>, IWorldServerService, IFetchByKeyAction<long, WorldServerData>
- Inheritance
-
WorldServerService
- Implements
- Inherited Members
Remarks
This base type provides three primary execution paths:
- ExecuteTransactionAsync(Func<Task>, CancellationToken) and ExecuteTransactionAsync<TResult>(Func<Task<TResult>>,string,CancellationToken) create a fresh NpgsqlDbContext, begin an explicit transaction, execute the delegate, then call SaveChangesAsync(CancellationToken) and commit.
- ExecuteWriteAsync(Func<Task>,string,CancellationToken) and ExecuteWriteAsync<TResult>(Func<Task<TResult>>,string,CancellationToken) create a fresh NpgsqlDbContext, execute the delegate, then call SaveChangesAsync(CancellationToken) without starting an explicit transaction.
- ExecuteReadAsync(Func<Task>,string,CancellationToken) and ExecuteReadAsync<TResult>(Func<Task<TResult>>,string,CancellationToken) create a fresh NpgsqlDbContext but do not start an explicit transaction and do not call SaveChanges.
Standalone Execution (no ambient scope): A new context is created per attempt to avoid EF change-tracker state leaking across retries. Transient database failures are retried with exponential backoff. Optimistic concurrency conflicts (Version-based authority) and StaleStateException are never retried; they are returned as non-transient failures so the caller can re-read and decide how to proceed.
Ambient Scope Execution (inside an existing Unit of Work): When an operation detects an active FishMMO.Database.Npgsql.Services.DatabaseExecutionScope, it reuses the ambient NpgsqlDbContext and does NOT retry on transient failures. This is by design for the following reasons:
- Transaction State Corruption: PostgreSQL aborts the entire transaction on most transient failures. The connection and transaction become unusable, making retry with the same context impossible.
- Context State Pollution: The DbContext's change tracker accumulates state. Retrying with a polluted change tracker can cause duplicate key violations or incorrect updates.
- Semantic Correctness: If operation A succeeded and operation B failed transiently, retrying B alone without re-evaluating A's preconditions could violate business invariants.
When a transient failure occurs inside an ambient scope, the failure is returned immediately so the caller can restart the entire unit of work with fresh state. Savepoints are used for nested atomicity but cannot recover from connection-level failures.
Constructors
WorldServerService(INpgsqlDbContextFactory)
Initializes a new instance of WorldServerService.
public WorldServerService(INpgsqlDbContextFactory dbContextFactory)
Parameters
dbContextFactoryINpgsqlDbContextFactoryDbContext factory for creating contexts.
Exceptions
- ArgumentNullException
Thrown when dbContextFactory is null.
Methods
DeleteAsync(long, CancellationToken)
public Task<DatabaseResult> DeleteAsync(long serverId, CancellationToken cancellationToken = default)
Parameters
serverIdlongcancellationTokenCancellationToken
Returns
FetchActiveAsync(float, CancellationToken)
Fetches active world servers that have pulsed within the timeout window. Filters servers by last_pulse timestamp to return only servers that are currently online.
public Task<DatabaseResult<List<WorldServerData>>> FetchActiveAsync(float idleTimeoutSeconds = 60, CancellationToken cancellationToken = default)
Parameters
idleTimeoutSecondsfloatIdle timeout in seconds before server considered inactive (default 60).
cancellationTokenCancellationTokenCancellation token for async operation.
Returns
- Task<DatabaseResult<List<WorldServerData>>>
DatabaseResult containing List of active WorldServerData ordered by name; empty list if no active servers.
Remarks
Operation: LINQ query filtering by last_pulse >= (UtcNow - timeout), ordered by name.
Execution Strategy: BaseService handles retries and centralized exception mapping; explicit transactions are used only when a write requires multiple database statements.
FetchAsync(long, CancellationToken)
Fetches an entity for the given key.
public Task<DatabaseResult<WorldServerData>> FetchAsync(long serverId, CancellationToken cancellationToken = default)
Parameters
serverIdlongcancellationTokenCancellationTokenToken to cancel the operation.
Returns
FetchControlStateAsync(long, CancellationToken)
Reads a world server's lock and shutdown state without writing a pulse.
public Task<DatabaseResult<ServerControlState>> FetchControlStateAsync(long serverId, CancellationToken cancellationToken = default)
Parameters
serverIdlongWorld server row to read.
cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult<ServerControlState>>
The control state, or NotFound when the row is gone.
Remarks
For scene servers, which host scenes on behalf of a world but do not pulse its row. A world-wide shutdown has to reach them so they can warn their players and clear the world's characters out on the same deadline.
PersistAsync(string, string, ushort, int, bool, CancellationToken)
Persists a world server registration (insert or update). Uses an insert-first approach and falls back to update on unique constraint conflicts.
public Task<DatabaseResult<(long ServerId, WorldServerData ServerData)>> PersistAsync(string name, string address, ushort port, int characterCount, bool locked, CancellationToken cancellationToken = default)
Parameters
namestringServer name (unique identifier for conflict resolution).
addressstringServer IP address or hostname.
portushortServer port number.
characterCountintCurrent character count on server.
lockedboolWhether server is locked from accepting new connections.
cancellationTokenCancellationTokenCancellation token for async operation.
Returns
- Task<DatabaseResult<(long ServerId, WorldServerData ServerData)>>
DatabaseResult containing tuple (ServerId, ServerData) if successful.
Remarks
Operation: Attempts INSERT; on unique violation, loads the existing row and updates it.
Returns: The returned ServerId is populated after SaveChanges completes inside the BaseService execution wrapper.
Returns: Failure if name/address empty or operation fails; Success with (ServerId, ServerData) on success.
PulseAsync(long, int, CancellationToken)
public Task<DatabaseResult<ServerControlState>> PulseAsync(long serverId, int characterCount, CancellationToken cancellationToken = default)
Parameters
serverIdlongcharacterCountintcancellationTokenCancellationToken
Returns
SetLockedAsync(long, bool, CancellationToken)
Opens or closes this world server to new connections.
public Task<DatabaseResult> SetLockedAsync(long serverId, bool locked, CancellationToken cancellationToken = default)
Parameters
serverIdlongWorld server row to update.
lockedboolTrue to close it to new arrivals.
cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult>
Success, or NotFound when the row is gone.
Remarks
The row is the authority; the server adopts it on its next pulse. Locking drains rather than evicts — see Locked.
SetShutdownAsync(long, DateTime?, CancellationToken)
Schedules or cancels this world server's shutdown.
public Task<DatabaseResult> SetShutdownAsync(long serverId, DateTime? shutdownAtUtc, CancellationToken cancellationToken = default)
Parameters
serverIdlongWorld server row to update.
shutdownAtUtcDateTime?Absolute UTC stop time, or
nullto cancel.cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult>
Success, or NotFound when the row is gone.
Remarks
Scheduling also locks the server, in the same statement. Cancelling does not unlock it: halting a shutdown and reopening to players are separate decisions.