diff --git a/package-lock.json b/package-lock.json index 3590590e..cb513310 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "game-services", - "version": "0.80.5", + "version": "0.80.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "game-services", - "version": "0.80.5", + "version": "0.80.6", "license": "MIT", "dependencies": { "@clickhouse/client": "^1.11.0", diff --git a/package.json b/package.json index cad7646d..8e3d5e3a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "game-services", - "version": "0.80.5", + "version": "0.80.6", "description": "", "main": "src/index.ts", "scripts": { diff --git a/src/entities/subscribers/player-group.subscriber.ts b/src/entities/subscribers/player-group.subscriber.ts index b1cd0e2d..a4a975e8 100644 --- a/src/entities/subscribers/player-group.subscriber.ts +++ b/src/entities/subscribers/player-group.subscriber.ts @@ -4,6 +4,7 @@ import Player from '../player' import LeaderboardEntry from '../leaderboard-entry' import PlayerGameStat from '../player-game-stat' import { createRedisConnection } from '../../config/redis.config' +import { captureException } from '@sentry/node' const enableLogging = process.env.NODE_ENV !== 'test' @@ -57,7 +58,8 @@ export default class PlayerGroupSubscriber implements EventSubscriber { console.info(`Group memberships lock created for ${player.id}`) } - if (await checkGroupMemberships(em, player)) { + const shouldRefresh = await checkGroupMemberships(em, player) + if (shouldRefresh) { const label = `Refreshing memberships for ${player.id}` /* v8 ignore next 3 */ @@ -73,6 +75,9 @@ export default class PlayerGroupSubscriber implements EventSubscriber { } } } + } catch (err) { + console.error(`Failed checking memberships: ${(err as Error).message}`) + captureException(err) } finally { if (lockCreated) { /* v8 ignore next 3 */ diff --git a/src/lib/groups/checkGroupMemberships.ts b/src/lib/groups/checkGroupMemberships.ts index 78fc7622..d6e67d0c 100644 --- a/src/lib/groups/checkGroupMemberships.ts +++ b/src/lib/groups/checkGroupMemberships.ts @@ -25,7 +25,7 @@ class PlayerGroupMember { export default async function checkGroupMemberships(em: EntityManager, player: Player): Promise { const groups = await em.repo(PlayerGroup).find({ game: player.game - }, getResultCacheOptions(`groups-for-memberships-${player.game}`)) + }, getResultCacheOptions(`groups-for-memberships-${player.game}`, 1000)) if (groups.length === 0) { return false @@ -38,7 +38,7 @@ export default async function checkGroupMemberships(em: EntityManager, player: P console.time(label) } - let shouldFlush = false + let shouldRefresh = false for (const group of groups) { await group.members.init({ ref: true }) @@ -49,7 +49,7 @@ export default async function checkGroupMemberships(em: EntityManager, player: P const notEligibleButInGroup = !playerIsEligible && playerCurrentlyInGroup if (eligibleButNotInGroup || notEligibleButInGroup) { - shouldFlush = true + shouldRefresh = true } const groupMember = new PlayerGroupMember(player, group) @@ -74,5 +74,5 @@ export default async function checkGroupMemberships(em: EntityManager, player: P console.timeEnd(label) } - return shouldFlush + return shouldRefresh } diff --git a/src/tasks/cleanupOnlinePlayers.ts b/src/tasks/cleanupOnlinePlayers.ts index 007fb67a..2b4a482d 100644 --- a/src/tasks/cleanupOnlinePlayers.ts +++ b/src/tasks/cleanupOnlinePlayers.ts @@ -18,6 +18,7 @@ type CleanupStats = { newSessionDurationMinutes: number[] newSessionDurationLessThanOneMinute: number presenceUpdated: number + presenceDeleted: number } let cleanupStats: Record @@ -31,7 +32,8 @@ function getOrCreateGameCleanupStats(gameId: number): CleanupStats { hadSessionSinceOriginal: 0, newSessionDurationMinutes: [], newSessionDurationLessThanOneMinute: 0, - presenceUpdated: 0 + presenceUpdated: 0, + presenceDeleted: 0 } } return cleanupStats[gameId] @@ -134,6 +136,12 @@ async function cleanupPresence(em: EntityManager, clickhouse: ClickHouseClient, } } +async function removeDisconnectedPresence(em: EntityManager, presence: PlayerPresence) { + const gameStats = getOrCreateGameCleanupStats(presence.playerAlias.player.game.id) + gameStats.presenceDeleted++ + await em.removeAndFlush(presence) +} + export default async function cleanupOnlinePlayers() { const orm = await MikroORM.init(ormConfig) const em = orm.em.fork() @@ -157,6 +165,17 @@ export default async function cleanupOnlinePlayers() { await cleanupSession(em, clickhouse, session) } + // todo, find out how this is happening + const disconnectedPresence = await em.repo(PlayerPresence).find({ + player: null + }) + + console.info(`Found ${disconnectedPresence.length} disconnected presence`) + + for (const presence of disconnectedPresence) { + await removeDisconnectedPresence(em, presence) + } + const onlinePresence = await em.repo(PlayerPresence).find({ online: true, updatedAt: { @@ -167,6 +186,8 @@ export default async function cleanupOnlinePlayers() { populate: ['player'] }) + console.info(`Found ${onlinePresence.length} online presence`) + for (const presence of onlinePresence) { await cleanupPresence(em, clickhouse, presence) } diff --git a/tests/tasks/cleanupOnlinePlayers.test.ts b/tests/tasks/cleanupOnlinePlayers.test.ts index 8515e847..c05b6870 100644 --- a/tests/tasks/cleanupOnlinePlayers.test.ts +++ b/tests/tasks/cleanupOnlinePlayers.test.ts @@ -7,6 +7,7 @@ import { formatDateForClickHouse } from '../../src/lib/clickhouse/formatDateTime import PlayerPresenceFactory from '../fixtures/PlayerPresenceFactory' import assert from 'node:assert' import PlayerPresence from '../../src/entities/player-presence' +import Player from '../../src/entities/player' describe('cleanupOnlinePlayers', () => { beforeEach(() => { @@ -387,4 +388,32 @@ describe('cleanupOnlinePlayers', () => { expect(updatedPresence.online).toBe(true) expect(updatedPresence.updatedAt).toEqual(originalUpdatedAt) }) + + it('should delete presence that no longer has a player', async () => { + vi.useRealTimers() + + const [, game] = await createOrganisationAndGame() + const player = await new PlayerFactory([game]) + .state(async (player) => ({ + presence: await new PlayerPresenceFactory(player.game) + .online() + .state(() => ({ updatedAt: subDays(new Date(), 2) })) + .one() + })) + .one() + await em.persistAndFlush(player) + + assert(player.presence) + const originalPresenceId = player.presence.id + + await em.nativeDelete(Player, player.id) + + const originalPresence = await em.repo(PlayerPresence).findOne(originalPresenceId) + assert(originalPresence) + + await cleanupOnlinePlayers() + + const updatedPresence = await em.refresh(originalPresence) + expect(updatedPresence).toBeNull() + }) })