Table of Contents

Class GroupFinderQueueService

Namespace
FishMMO.Database.Npgsql.Services
Assembly
FishMMO-DB.dll

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

dbContextFactory INpgsqlDbContextFactory

DbContext 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

worldServerId long

World server to count on.

sceneType SceneType

Kind of instance.

sceneName string

Scene.

difficulty int

Difficulty or format index.

pulsedSinceUtc DateTime

Rows whose heartbeat is older than this are not counted.

cancellationToken CancellationToken

Cancellation 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

characterId long

Character to remove.

onlyIfWaiting bool

True 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.

cancellationToken CancellationToken

Cancellation 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

characterId long

Character to remove.

cancellationToken CancellationToken

Cancellation 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

pulsedBeforeUtc DateTime

Rows whose heartbeat is older than this are eligible.

maxRows int

Upper bound on rows removed in one call.

cancellationToken CancellationToken

Cancellation 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

worldServerId long

World server the character belongs to.

characterId long

Character queuing.

sceneType SceneType

Kind of instance: the shared value cast in — 2 (Group) for a dungeon, 3 (PvP) for an arena.

sceneName string

Dungeon or arena scene they want to play.

difficulty int

Index into the template's list: a dungeon's difficulty or an arena's format.

stalePulsedBeforeUtc DateTime

A 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.

cancellationToken CancellationToken

Cancellation token.

Returns

Task<DatabaseResult<long>>

The row's ID, or 0 when 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

worldServerId long

World server the group belongs to.

sceneType SceneType

Kind of instance being queued for.

sceneName string

Scene they want to play.

difficulty int

Format index into the template's list.

groupId long

Identity of the pre-made group; the party id.

characterIds IReadOnlyList<long>

Every member, including the one who pressed the button.

stalePulsedBeforeUtc DateTime

Stale threshold, as for EnqueueAsync(long, long, SceneType, string, int, DateTime, CancellationToken).

cancellationToken CancellationToken

Cancellation 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

characterIds IReadOnlyList<long>

Characters to look up; duplicates and non-positive ids are ignored.

cancellationToken CancellationToken

Cancellation 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

characterIds IReadOnlyList<long>

Characters to pulse; duplicates and non-positive ids are ignored.

cancellationToken CancellationToken

Cancellation 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

characterId long

Character to release.

instanceId long

Instance the claim named; a row matched to anything else is left alone.

cancellationToken CancellationToken

Cancellation 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

worldServerId long
sceneName string
format int
pulsedSinceUtc DateTime
cancellationToken CancellationToken

Returns

Task<DatabaseResult<ArenaBackfillData>>

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

characterId long

Waiting character.

partyId long

Party that owns the instance.

instanceId long

Instance being joined.

cancellationToken CancellationToken

Cancellation 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

worldServerId long

World server to form the match on.

sceneName string

Arena scene to open.

format int

Format index into the template's list, matched exactly.

templateId int

Arena template ID recorded on the match.

teamCount int

Teams to fill.

teamSize int

Seats per team.

pulsedSinceUtc DateTime

Waiters whose heartbeat is older than this are not eligible.

maxCandidates int

How many waiters to lock and consider. Clamped to a sane range.

ratingSource ArenaRatingSource
composeOptions ArenaComposeOptions
cancellationToken CancellationToken

Cancellation 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

worldServerId long
sceneName string
difficulty int
groupSize int
pulsedSinceUtc DateTime
sceneType SceneType
leaderRank byte
memberRank byte
cancellationToken CancellationToken

Returns

Task<DatabaseResult<GroupFinderMatchData>>