Class SceneServerService
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 SceneServerService : BaseService<SceneServerEntity>, ISceneServerService, IFetchByKeyAction<long, SceneServerData>
- Inheritance
-
SceneServerService
- 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
SceneServerService(INpgsqlDbContextFactory)
Initializes a new instance of SceneServerService.
public SceneServerService(INpgsqlDbContextFactory dbContextFactory)
Parameters
dbContextFactoryINpgsqlDbContextFactoryDbContext factory for creating contexts.
Exceptions
- ArgumentNullException
Thrown when dbContextFactory is null.
Methods
DeleteAsync(long, CancellationToken)
Deletes a scene server registration.
public Task<DatabaseResult> DeleteAsync(long serverId, CancellationToken cancellationToken = default)
Parameters
serverIdlongServer ID to delete.
cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult>
A DatabaseResult indicating success or containing a DatabaseException on failure. Returns DatabaseEntityNotFoundException if server doesn't exist.
Remarks
Uses ExecuteSqlRawAsync with execution strategy wrapping to ensure transient database failures are automatically retried.
FetchAsync(long, CancellationToken)
Fetches an entity for the given key.
public Task<DatabaseResult<SceneServerData>> FetchAsync(long serverId, CancellationToken cancellationToken = default)
Parameters
serverIdlongcancellationTokenCancellationTokenToken to cancel the operation.
Returns
FetchSceneServersByIDsAsync(List<long>, int, CancellationToken)
Retrieves multiple scene servers by their IDs in batches.
public Task<DatabaseResult<IReadOnlyList<SceneServerData>>> FetchSceneServersByIDsAsync(List<long> serverIds, int maxBatchSize = 500, CancellationToken cancellationToken = default)
Parameters
serverIdsList<long>List of server IDs to query.
maxBatchSizeintMaximum number of IDs per database round-trip (500–1000).
cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult<IReadOnlyList<SceneServerData>>>
A list of SceneServerData for each found server.
PersistAsync(string, string, ushort, int, bool, CancellationToken)
Persists a scene server registration with atomic UPSERT.
public Task<DatabaseResult<(long ServerId, SceneServerData ServerData)>> PersistAsync(string name, string address, ushort port, int characterCount, bool locked, CancellationToken cancellationToken = default)
Parameters
namestringServer name (unique identifier).
addressstringServer address.
portushortServer port.
characterCountintCurrent character count.
lockedboolWhether server is locked.
cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult<(long ServerId, SceneServerData ServerData)>>
A DatabaseResult<T> containing a tuple with (ServerId, ServerData) on success, or a DatabaseException on failure.
Remarks
Uses FromSqlRaw with RETURNING clause and execution strategy wrapping to ensure transient database failures are automatically retried. Uses PostgreSQL ON CONFLICT for atomic UPSERT with full data return.
PulseAsync(long, int, CancellationToken)
Updates the last pulse timestamp, character count, and lock state for a scene server (heartbeat).
public Task<DatabaseResult<ServerControlState>> PulseAsync(long serverId, int characterCount, CancellationToken cancellationToken = default)
Parameters
serverIdlongServer ID.
characterCountintCurrent character count.
cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult<ServerControlState>>
A DatabaseResult indicating success or containing a DatabaseException on failure. Returns DatabaseEntityNotFoundException if server doesn't exist.
Remarks
Uses ExecuteSqlRawAsync with execution strategy wrapping to ensure transient database failures are automatically retried. Updates timestamp to current UTC time along with character count and lock state.
SetLockedAsync(long, bool, CancellationToken)
Opens or closes this scene server to new arrivals.
public Task<DatabaseResult> SetLockedAsync(long serverId, bool locked, CancellationToken cancellationToken = default)
Parameters
serverIdlongScene server row to update.
lockedboolTrue to close it.
cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult>
Success, or NotFound when the row is gone.
Remarks
A locked scene server is skipped by the world server's routing and stops dequeuing scene-load requests, so it drains as its players leave. Players already on it keep playing. The row is the authority; the server adopts it on its next pulse.
SetShutdownAsync(long, DateTime?, CancellationToken)
Schedules or cancels this scene server's shutdown.
public Task<DatabaseResult> SetShutdownAsync(long serverId, DateTime? shutdownAtUtc, CancellationToken cancellationToken = default)
Parameters
serverIdlongScene 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; cancelling does not unlock it.