Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/entities/game-stat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,17 +116,21 @@ export default class GameStat {
async recalculateGlobalValue({
em,
includeDevData,
devOnly,
}: {
em: EntityManager
includeDevData: boolean
devOnly?: boolean
}) {
const qb = em
.qb(PlayerGameStat, 'pgs')
.select(raw('SUM(pgs.value) as total'))
.where({ stat: this.id })

if (!includeDevData) {
qb.innerJoin('pgs.player', 'p').andWhere({ 'p.devBuild': false })
if (!includeDevData || devOnly) {
qb.innerJoin('pgs.player', 'p')
if (!includeDevData) qb.andWhere({ 'p.devBuild': false })
if (devOnly) qb.andWhere({ 'p.devBuild': true })
}

const result = await qb.execute<{ total: string | null }>('get')
Expand Down
11 changes: 10 additions & 1 deletion src/routes/protected/game-stat/reset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,13 @@ export const resetRoute = protectedRoute({
})

const deletedCount = await trx.repo(PlayerGameStat).nativeDelete(where)
await trx.repo(GameStat).nativeUpdate(stat.id, { globalValue: stat.defaultValue })

await stat.recalculateGlobalValue({
em: trx,
includeDevData: mode !== 'dev',
devOnly: mode === 'live',
})
await trx.repo(GameStat).nativeUpdate(stat.id, { globalValue: stat.globalValue })

createGameActivity(trx, {
user: ctx.state.user,
Expand Down Expand Up @@ -118,6 +124,9 @@ export const resetRoute = protectedRoute({
return deletedCount
})

await em.refresh(stat)
await ctx.redis.set(GameStat.getGlobalValueCacheKey(stat.id), stat.globalValue)

await Promise.allSettled([
deferClearResponseCache(GameStat.getIndexCacheKey(stat.game, true)),
deferClearResponseCache(PlayerGameStat.getCacheKeyForStat(stat, true)),
Expand Down
79 changes: 61 additions & 18 deletions tests/routes/protected/game-stat/reset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('Game stat - reset', () => {
.one()
}),
)
await em.persistAndFlush(playerStats)
await em.persist(playerStats).flush()

const res = await request(app)
.delete(`/games/${game.id}/game-stats/${stat.id}/player-stats`)
Expand All @@ -56,7 +56,7 @@ describe('Game stat - reset', () => {
expect(playerStats).toHaveLength(0)

await em.refresh(stat)
expect(stat.globalValue).toBe(stat.defaultValue)
expect(stat.globalValue).toBe(0)

assert(activity?.extra.display)
expect(activity.extra.statInternalName).toBe(stat.internalName)
Expand Down Expand Up @@ -89,7 +89,7 @@ describe('Game stat - reset', () => {
.one()
}),
)
await em.persistAndFlush(playerStats)
await em.persist(playerStats).flush()

const res = await request(app)
.delete(`/games/${game.id}/game-stats/${stat.id}/player-stats`)
Expand Down Expand Up @@ -134,7 +134,7 @@ describe('Game stat - reset', () => {
}),
)

await em.persistAndFlush([...devPlayerStats, ...livePlayerStats])
await em.persist([...devPlayerStats, ...livePlayerStats]).flush()

const res = await request(app)
.delete(`/games/${game.id}/game-stats/${stat.id}/player-stats`)
Expand All @@ -156,7 +156,7 @@ describe('Game stat - reset', () => {
expect(remainingPlayerStats.every((playerStat) => !playerStat.player.devBuild)).toBe(true)

await em.refresh(stat)
expect(stat.globalValue).toBe(stat.defaultValue)
expect(stat.globalValue).toBe(3 * 40) // sum of remaining live players
})

it('should reset only live player stats when mode is "live"', async () => {
Expand Down Expand Up @@ -187,7 +187,7 @@ describe('Game stat - reset', () => {
}),
)

await em.persistAndFlush([...devPlayerStats, ...livePlayerStats])
await em.persist([...devPlayerStats, ...livePlayerStats]).flush()

const res = await request(app)
.delete(`/games/${game.id}/game-stats/${stat.id}/player-stats`)
Expand All @@ -209,7 +209,7 @@ describe('Game stat - reset', () => {
expect(remainingPlayerStats.every((playerStat) => playerStat.player.devBuild)).toBe(true)

await em.refresh(stat)
expect(stat.globalValue).toBe(stat.defaultValue)
expect(stat.globalValue).toBe(2 * 35) // sum of remaining dev players
})

it('should return 0 deleted count when no player stats match the mode', async () => {
Expand All @@ -230,7 +230,7 @@ describe('Game stat - reset', () => {
}),
)

await em.persistAndFlush(devPlayerStats)
await em.persist(devPlayerStats).flush()

const res = await request(app)
.delete(`/games/${game.id}/game-stats/${stat.id}/player-stats`)
Expand All @@ -244,7 +244,7 @@ describe('Game stat - reset', () => {
expect(remainingPlayerStats).toHaveLength(2)

await em.refresh(stat)
expect(stat.globalValue).toBe(stat.defaultValue)
expect(stat.globalValue).toBe(2 * 20) // sum of remaining dev players
})

it('should create game activity with correct data', async () => {
Expand All @@ -259,7 +259,7 @@ describe('Game stat - reset', () => {
}),
)

await em.persistAndFlush(playerStats)
await em.persist(playerStats).flush()

await request(app)
.delete(`/games/${game.id}/game-stats/${stat.id}/player-stats`)
Expand Down Expand Up @@ -295,7 +295,7 @@ describe('Game stat - reset', () => {
}),
)

await em.persistAndFlush(playerStats)
await em.persist(playerStats).flush()

const res = await request(app)
.delete(`/games/${otherGame.id}/game-stats/${stat.id}/player-stats`)
Expand Down Expand Up @@ -325,7 +325,7 @@ describe('Game stat - reset', () => {
const [token] = await createUserAndToken({ type: UserType.ADMIN }, organisation)

const stat = await new GameStatFactory([game]).one()
await em.persistAndFlush(stat)
await em.persist(stat).flush()

const res = await request(app)
.delete(`/games/${game.id}/game-stats/${stat.id}/player-stats`)
Expand All @@ -352,7 +352,7 @@ describe('Game stat - reset', () => {
}),
)

await em.persistAndFlush(playerStats)
await em.persist(playerStats).flush()

await request(app)
.delete(`/games/${game.id}/game-stats/${stat.id}/player-stats`)
Expand All @@ -366,7 +366,7 @@ describe('Game stat - reset', () => {
expect(existingPlayers.length).toBeGreaterThanOrEqual(2)
})

it('should reset global stat value to default value for global stats', async () => {
it('should recalculate global stat value to 0 when all players are reset', async () => {
const [organisation, game] = await createOrganisationAndGame()
const [token] = await createUserAndToken({ type: UserType.ADMIN }, organisation)

Expand All @@ -388,15 +388,15 @@ describe('Game stat - reset', () => {
}),
)

await em.persistAndFlush(playerStats)
await em.persist(playerStats).flush()

await request(app)
.delete(`/games/${game.id}/game-stats/${stat.id}/player-stats`)
.auth(token, { type: 'bearer' })
.expect(200)

await em.refresh(stat)
expect(stat.globalValue).toBe(250)
expect(stat.globalValue).toBe(0) // no players remain
})

it('should handle resetting stats with no player stats gracefully', async () => {
Expand All @@ -411,7 +411,7 @@ describe('Game stat - reset', () => {
}))
.one()

await em.persistAndFlush(stat)
await em.persist(stat).flush()

const res = await request(app)
.delete(`/games/${game.id}/game-stats/${stat.id}/player-stats`)
Expand All @@ -430,6 +430,49 @@ describe('Game stat - reset', () => {
expect(activity.extra.display?.['Deleted count']).toBe(0)
})

it('should update the redis cache with the recalculated global value after reset', async () => {
const [organisation, game] = await createOrganisationAndGame()
const [token] = await createUserAndToken({ type: UserType.ADMIN }, organisation)

const stat = await new GameStatFactory([game])
.state(() => ({ global: true, globalValue: 0, defaultValue: 0 }))
.one()

const devPlayers = await new PlayerFactory([game]).devBuild().many(2)
const livePlayers = await new PlayerFactory([game]).many(3)

const devPlayerStats = await Promise.all(
devPlayers.map((player) =>
new PlayerGameStatFactory()
.construct(player, stat)
.state(() => ({ value: 10 }))
.one(),
),
)
const livePlayerStats = await Promise.all(
livePlayers.map((player) =>
new PlayerGameStatFactory()
.construct(player, stat)
.state(() => ({ value: 20 }))
.one(),
),
)

await em.persist([...devPlayerStats, ...livePlayerStats]).flush()

// prime the cache
await redis.set(GameStat.getGlobalValueCacheKey(stat.id), 999)

await request(app)
.delete(`/games/${game.id}/game-stats/${stat.id}/player-stats`)
.query({ mode: 'dev' })
.auth(token, { type: 'bearer' })
.expect(200)

const cached = await redis.get(GameStat.getGlobalValueCacheKey(stat.id))
expect(Number(cached)).toBe(3 * 20) // sum of remaining live players
})

it('should batch clickhouse deletions when resetting stats with over 100 player aliases', async () => {
const [organisation, game] = await createOrganisationAndGame()
const [token] = await createUserAndToken({ type: UserType.ADMIN }, organisation)
Expand All @@ -452,7 +495,7 @@ describe('Game stat - reset', () => {
}),
)

await em.persistAndFlush(playerStats)
await em.persist(playerStats).flush()

const handler = new FlushStatSnapshotsQueueHandler()
for (const playerStat of playerStats) {
Expand Down