Class GroupFinderQueueService
Service for the dungeon group finder's queue. See IGroupFinderQueueService.
public sealed class GroupFinderQueueService : BaseService<GroupFinderQueueEntity>, IGroupFinderQueueService
- Inheritance
-
GroupFinderQueueService
- Implements
- Inherited Members
Remarks
Every state change here is a single statement, or a single transaction, whose
WHERE re-asserts the state it expects. The queue is worked by every scene server on
the world at once, and that is what makes two of them touching the same row produce one
winner and one no-op.
Constructors
GroupFinderQueueService(INpgsqlDbContextFactory)
Initializes a new instance of GroupFinderQueueService.
public GroupFinderQueueService(INpgsqlDbContextFactory dbContextFactory)
Parameters
dbContextFactoryINpgsqlDbContextFactoryDbContext factory for creating contexts.
Methods
CountWaitingAsync(long, SceneType, string, int, DateTime, CancellationToken)
Counts the live waiters for one dungeon at one difficulty.
public Task<DatabaseResult<int>> CountWaitingAsync(long worldServerId, SceneType sceneType, string sceneName, int difficulty, DateTime pulsedSinceUtc, CancellationToken cancellationToken = default)
Parameters
worldServerIdlongWorld server to count on.
sceneTypeSceneTypeKind of instance.
sceneNamestringScene.
difficultyintDifficulty or format index.
pulsedSinceUtcDateTimeRows whose heartbeat is older than this are not counted.
cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult<int>>
How many are waiting.
DeleteAsync(long, bool, CancellationToken)
Removes a character's queue row.
public Task<DatabaseResult<bool>> DeleteAsync(long characterId, bool onlyIfWaiting, CancellationToken cancellationToken = default)
Parameters
characterIdlongCharacter to remove.
onlyIfWaitingboolTrue to leave a Matched row alone. A player who presses Leave in the instant after their group formed has already been placed in a party, and quietly deleting the row would leave them in that party with no transfer coming; the caller must refuse instead. False when the row's job is done — the transfer has been dispatched — or when the character is gone.
cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult<bool>>
True when a row was removed.
DeleteReturningAsync(long, CancellationToken)
Removes a character's queue row whatever its state, and reports what was removed.
public Task<DatabaseResult<GroupFinderQueueData?>> DeleteReturningAsync(long characterId, CancellationToken cancellationToken = default)
Parameters
characterIdlongCharacter to remove.
cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult<GroupFinderQueueData?>>
The removed row, or null when the character had none.
Remarks
For the disconnect path. Whether the row was still waiting or already matched decides
what else has to happen — a matched character who logged out before being moved holds a
seat in a party that will never see them, and must be taken out of it — and reading the
row first would race the matcher on another scene server. DELETE ... RETURNING
answers atomically: if the matcher's transaction holds the row, this waits for it to
commit and then removes, and reports, the matched row.
DeleteStaleAsync(DateTime, int, CancellationToken)
Removes rows whose heartbeat stopped before pulsedBeforeUtc.
public Task<DatabaseResult<int>> DeleteStaleAsync(DateTime pulsedBeforeUtc, int maxRows = 256, CancellationToken cancellationToken = default)
Parameters
pulsedBeforeUtcDateTimeRows whose heartbeat is older than this are eligible.
maxRowsintUpper bound on rows removed in one call.
cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult<int>>
Rows deleted.
Remarks
Bounded per call so a large backlog drains across several sweeps rather than in one long statement. Matched rows are reaped too: a matched character whose server died was placed in a party it will never be transferred into, and the party system's own absent- member handling takes it from there.
EnqueueAsync(long, long, SceneType, string, int, DateTime, CancellationToken)
Puts a character in the queue for one dungeon at one difficulty, or moves their existing entry to it.
public Task<DatabaseResult<long>> EnqueueAsync(long worldServerId, long characterId, SceneType sceneType, string sceneName, int difficulty, DateTime stalePulsedBeforeUtc, CancellationToken cancellationToken = default)
Parameters
worldServerIdlongWorld server the character belongs to.
characterIdlongCharacter queuing.
sceneTypeSceneTypeKind of instance: the shared value cast in — 2 (Group) for a dungeon, 3 (PvP) for an arena.
sceneNamestringDungeon or arena scene they want to play.
difficultyintIndex into the template's list: a dungeon's difficulty or an arena's format.
stalePulsedBeforeUtcDateTimeA matched row whose heartbeat is older than this is treated as abandoned and re-pointed like a waiting one. Without this a character whose scene server died between the match and the transfer could never queue again until the stale sweep happened to reach them.
cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult<long>>
The row's ID, or
0when the character is already matched and nothing changed.
Remarks
A character already waiting for something else is re-pointed rather than refused — pressing Find Group at a second entrance means "this one instead", and their place in line restarts. A character whose row is already Matched cannot be moved: the group they were placed in is real, and their scene server is about to transfer them into it.
EnqueueGroupAsync(long, SceneType, string, int, long, IReadOnlyList<long>, DateTime, CancellationToken)
Queues a pre-made group together, all or none.
public Task<DatabaseResult<int>> EnqueueGroupAsync(long worldServerId, SceneType sceneType, string sceneName, int difficulty, long groupId, IReadOnlyList<long> characterIds, DateTime stalePulsedBeforeUtc, CancellationToken cancellationToken = default)
Parameters
worldServerIdlongWorld server the group belongs to.
sceneTypeSceneTypeKind of instance being queued for.
sceneNamestringScene they want to play.
difficultyintFormat index into the template's list.
groupIdlongIdentity of the pre-made group; the party id.
characterIdsIReadOnlyList<long>Every member, including the one who pressed the button.
stalePulsedBeforeUtcDateTimeStale threshold, as for EnqueueAsync(long, long, SceneType, string, int, DateTime, CancellationToken).
cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult<int>>
Rows written, equal to the member count on success. Zero when a member could not be queued and nothing was written.
Remarks
Every member gets a row carrying the same groupId, in one transaction:
a member whose row is live-matched already refuses the whole group, so a party cannot
end up half in one queue and half in another. Members already waiting for something else
are re-pointed exactly as EnqueueAsync(long, long, SceneType, string, int, DateTime, CancellationToken) re-points a single character.
FetchByCharactersAsync(IReadOnlyList<long>, CancellationToken)
Reads the queue rows of the given characters.
public Task<DatabaseResult<IReadOnlyList<GroupFinderQueueData>>> FetchByCharactersAsync(IReadOnlyList<long> characterIds, CancellationToken cancellationToken = default)
Parameters
characterIdsIReadOnlyList<long>Characters to look up; duplicates and non-positive ids are ignored.
cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult<IReadOnlyList<GroupFinderQueueData>>>
The rows that exist. Characters with no row are simply absent.
Remarks
The scene server's pump reads its own characters' rows once per interval to learn which of them have been matched — by itself or by any other server — and which have been removed underneath it.
PulseAsync(IReadOnlyList<long>, CancellationToken)
Refreshes the heartbeat on the rows of characters connected to the calling scene server.
public Task<DatabaseResult<int>> PulseAsync(IReadOnlyList<long> characterIds, CancellationToken cancellationToken = default)
Parameters
characterIdsIReadOnlyList<long>Characters to pulse; duplicates and non-positive ids are ignored.
cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult<int>>
Rows updated.
Remarks
A row whose heartbeat stops is excluded from matching by TryFormGroupAsync(long, string, int, int, DateTime, SceneType, byte, byte, CancellationToken) and CountWaitingAsync(long, SceneType, string, int, DateTime, CancellationToken) and eventually removed by DeleteStaleAsync(DateTime, int, CancellationToken). That is the only defence against a scene server dying with waiters on it: nothing else would ever remove them, and a group formed around a character nobody can reach is a party of four with an empty slot that never fills.
ReleaseClaimAsync(long, long, CancellationToken)
Returns a character claimed by TryClaimForInstanceAsync(long, long, long, CancellationToken) to the waiting state, when the join it was claimed for could not be completed.
public Task<DatabaseResult<bool>> ReleaseClaimAsync(long characterId, long instanceId, CancellationToken cancellationToken = default)
Parameters
characterIdlongCharacter to release.
instanceIdlongInstance the claim named; a row matched to anything else is left alone.
cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult<bool>>
True when a row was released.
TryBackfillArenaSeatAsync(long, string, int, DateTime, CancellationToken)
Seats the longest-waiting eligible waiter for an arena in a live match of that arena and format that has a vacated seat and an open backfill window.
public Task<DatabaseResult<ArenaBackfillData>> TryBackfillArenaSeatAsync(long worldServerId, string sceneName, int format, DateTime pulsedSinceUtc, CancellationToken cancellationToken = default)
Parameters
worldServerIdlongsceneNamestringformatintpulsedSinceUtcDateTimecancellationTokenCancellationToken
Returns
Remarks
One transaction: the match row is locked, the vacancy re-counted under the lock, the oldest waiter locked and bound to the instance, and a new seated member row inserted. Ranked matches take waiters from the same (ranked) format only, because the format is what the queue row records.
TryClaimForInstanceAsync(long, long, long, CancellationToken)
Marks one waiting character as matched into an existing instance, for the late-join path that fills empty slots in runs already open.
public Task<DatabaseResult<bool>> TryClaimForInstanceAsync(long characterId, long partyId, long instanceId, CancellationToken cancellationToken = default)
Parameters
characterIdlongWaiting character.
partyIdlongParty that owns the instance.
instanceIdlongInstance being joined.
cancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult<bool>>
True when the row was waiting and is now matched.
Remarks
The status is re-asserted in the WHERE: a row that another server has already
matched is not touched, so a character cannot be handed to two runs at once. The caller
adds the character to the instance's party after this succeeds, and calls
ReleaseClaimAsync(long, long, CancellationToken) if that is refused.
TryFormArenaMatchAsync(long, string, int, int, int, int, DateTime, int, ArenaRatingSource, ArenaComposeOptions, CancellationToken)
Forms an arena match from the longest-waiting eligible players, keeping pre-made groups together, opening the instance and writing the match and its seats, all in one transaction.
public Task<DatabaseResult<ArenaMatchFormedData>> TryFormArenaMatchAsync(long worldServerId, string sceneName, int format, int templateId, int teamCount, int teamSize, DateTime pulsedSinceUtc, int maxCandidates = 128, ArenaRatingSource ratingSource = default, ArenaComposeOptions composeOptions = default, CancellationToken cancellationToken = default)
Parameters
worldServerIdlongWorld server to form the match on.
sceneNamestringArena scene to open.
formatintFormat index into the template's list, matched exactly.
templateIdintArena template ID recorded on the match.
teamCountintTeams to fill.
teamSizeintSeats per team.
pulsedSinceUtcDateTimeWaiters whose heartbeat is older than this are not eligible.
maxCandidatesintHow many waiters to lock and consider. Clamped to a sane range.
ratingSourceArenaRatingSourcecomposeOptionsArenaComposeOptionscancellationTokenCancellationTokenCancellation token.
Returns
- Task<DatabaseResult<ArenaMatchFormedData>>
The match, or a result whose Formed is false when no full match could be composed.
Remarks
The arena counterpart of TryFormGroupAsync(long, string, int, int, DateTime, SceneType, byte, byte, CancellationToken), with the same guarantees: the
candidates are row-locked with plain FOR UPDATE, eligibility is re-asserted inside
the transaction, and either everything is written or nothing is. Composition — which
waiter sits on which team — is decided by ArenaMatchComposer on the locked
candidates, so the transaction has nothing to decide and the rule is testable on its own.
Eligibility here is the arena's: a seat in a match that has not ended, or a usable dungeon or arena instance held by the character, excludes them. Party membership does not, because arenas are the place a party queues together.
The instance row is written private with no owning party; the match row is what says who is in it. It is still inserted under the one-instance guard, against every seat.
TryFormGroupAsync(long, string, int, int, DateTime, SceneType, byte, byte, CancellationToken)
public Task<DatabaseResult<GroupFinderMatchData>> TryFormGroupAsync(long worldServerId, string sceneName, int difficulty, int groupSize, DateTime pulsedSinceUtc, SceneType sceneType, byte leaderRank, byte memberRank, CancellationToken cancellationToken = default)
Parameters
worldServerIdlongsceneNamestringdifficultyintgroupSizeintpulsedSinceUtcDateTimesceneTypeSceneTypeleaderRankbytememberRankbytecancellationTokenCancellationToken