Commit 68dde1f4 authored by nanahira's avatar nanahira

update tournament cache clean

parent 8426d009
......@@ -19,9 +19,6 @@ import { SrvproService } from './srvpro/srvpro.service';
import { SrvproController } from './srvpro/srvpro.controller';
import { HttpModule } from '@nestjs/axios';
import { AragamiModule } from 'nestjs-aragami';
import { TournamentSubscriberService } from './tournament-subscriber/tournament-subscriber.service';
import { ParticipantSubscriberService } from './participant-subscriber/participant-subscriber.service';
import { MatchSubscriberService } from './match-subscriber/match-subscriber.service';
@Module({
imports: [
......@@ -80,9 +77,6 @@ import { MatchSubscriberService } from './match-subscriber/match-subscriber.serv
ParticipantService,
ApiKeyService,
SrvproService,
TournamentSubscriberService,
ParticipantSubscriberService,
MatchSubscriberService,
],
controllers: [
TournamentController,
......
import { Test, TestingModule } from '@nestjs/testing';
import { MatchSubscriberService } from './match-subscriber.service';
describe('MatchSubscriberService', () => {
let service: MatchSubscriberService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [MatchSubscriberService],
}).compile();
service = module.get<MatchSubscriberService>(MatchSubscriberService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
import { Injectable, OnModuleInit } from '@nestjs/common';
import {
DataSource,
EntitySubscriberInterface,
InsertEvent,
RemoveEvent,
SoftRemoveEvent,
UpdateEvent,
} from 'typeorm';
import { InjectDataSource } from '@nestjs/typeorm';
import { Match } from '../match/entities/match.entity';
import { TournamentService } from '../tournament/tournament.service';
import { getFieldFromTypeormEvent } from '../utility/get-field-from-typeorm-event';
@Injectable()
export class MatchSubscriberService
implements EntitySubscriberInterface<Match>, OnModuleInit
{
constructor(
private tournamentService: TournamentService,
@InjectDataSource() private db: DataSource,
) {}
onModuleInit() {
this.db.subscribers.push(this);
}
listenTo() {
return Match;
}
async clearCacheOnEvent(
event:
| UpdateEvent<Match>
| RemoveEvent<Match>
| SoftRemoveEvent<Match>
| InsertEvent<Match>,
) {
let tournamentId = getFieldFromTypeormEvent(event, 'tournamentId');
if (!tournamentId) {
const id = getFieldFromTypeormEvent(event, 'id');
if (id) {
const match = await this.db
.getRepository(Match)
.findOne({ where: { id }, select: ['tournamentId'] });
tournamentId = match?.tournamentId;
}
}
if (tournamentId) {
await this.tournamentService.clearTournamentCache(tournamentId);
}
}
async afterInsert(event: InsertEvent<Match>) {
await this.clearCacheOnEvent(event);
}
async afterUpdate(event: UpdateEvent<Match>) {
await this.clearCacheOnEvent(event);
}
async afterRemove(event: RemoveEvent<Match>) {
await this.clearCacheOnEvent(event);
}
async afterSoftRemove(event: SoftRemoveEvent<Match>) {
await this.clearCacheOnEvent(event);
}
}
import { Test, TestingModule } from '@nestjs/testing';
import { ParticipantSubscriberService } from './participant-subscriber.service';
describe('ParticipantSubscriberService', () => {
let service: ParticipantSubscriberService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ParticipantSubscriberService],
}).compile();
service = module.get<ParticipantSubscriberService>(ParticipantSubscriberService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
import { Injectable, OnModuleInit } from '@nestjs/common';
import {
DataSource,
EntitySubscriberInterface,
InsertEvent,
RemoveEvent,
SoftRemoveEvent,
UpdateEvent,
} from 'typeorm';
import { InjectDataSource } from '@nestjs/typeorm';
import { Participant } from '../participant/entities/participant.entity';
import { TournamentService } from '../tournament/tournament.service';
import { getFieldFromTypeormEvent } from '../utility/get-field-from-typeorm-event';
@Injectable()
export class ParticipantSubscriberService
implements EntitySubscriberInterface<Participant>, OnModuleInit
{
constructor(
private tournamentService: TournamentService,
@InjectDataSource() private db: DataSource,
) {}
onModuleInit() {
this.db.subscribers.push(this);
}
listenTo() {
return Participant;
}
async clearCacheOnEvent(
event:
| UpdateEvent<Participant>
| RemoveEvent<Participant>
| SoftRemoveEvent<Participant>
| InsertEvent<Participant>,
) {
let tournamentId = getFieldFromTypeormEvent(event, 'tournamentId');
if (!tournamentId) {
const id = getFieldFromTypeormEvent(event, 'id');
if (id) {
const participant = await this.db
.getRepository(Participant)
.findOne({ where: { id }, select: ['tournamentId'] });
tournamentId = participant?.tournamentId;
}
}
if (tournamentId) {
await this.tournamentService.clearTournamentCache(tournamentId);
}
}
// 4 event handlers for different events
async afterUpdate(event: UpdateEvent<Participant>) {
await this.clearCacheOnEvent(event);
}
async afterSoftRemove(event: SoftRemoveEvent<Participant>) {
await this.clearCacheOnEvent(event);
}
async afterRemove(event: RemoveEvent<Participant>) {
await this.clearCacheOnEvent(event);
}
async afterInsert(event: InsertEvent<Participant>) {
await this.clearCacheOnEvent(event);
}
}
......@@ -9,6 +9,7 @@ import {
import { MycardUser } from 'nestjs-mycard';
import { TournamentService } from '../tournament/tournament.service';
import { normalSeq } from '../utility/normal-seq';
import _ from 'lodash';
@Injectable()
export class ParticipantService extends CrudService(Participant, {
......@@ -65,12 +66,28 @@ export class ParticipantService extends CrudService(Participant, {
user: MycardUser,
) {
await this.checkPermissionOfParticipant(id, user, dto.seq === undefined);
return this.update(id, dto);
const res = await this.update(id, dto);
const pt = await this.repo.manager.findOne(Participant, {
where: { id },
select: ['tournamentId'],
});
if (pt?.tournamentId) {
await this.tournamentService.clearTournamentCache(pt.tournamentId);
}
return res;
}
async deleteParticipant(id: number, user: MycardUser) {
await this.checkPermissionOfParticipant(id, user);
return this.delete(id);
const pt = await this.repo.manager.findOne(Participant, {
where: { id },
select: ['tournamentId'],
});
const res = await this.delete(id);
if (pt?.tournamentId) {
await this.tournamentService.clearTournamentCache(pt.tournamentId);
}
return res;
}
async checkPermissionOfParticipant(
......@@ -117,7 +134,7 @@ export class ParticipantService extends CrudService(Participant, {
user: MycardUser | number,
) {
normalSeq(participants);
return this.importEntities(participants, async (p) => {
const res = await this.importEntities(participants, async (p) => {
try {
await this.tournamentService.canModifyParticipants(
p.tournamentId,
......@@ -127,5 +144,15 @@ export class ParticipantService extends CrudService(Participant, {
return `玩家 ${p.name} 对应的比赛 ${p.tournamentId} 不存在或您没有权限。`;
}
});
const success = res.data
?.map((r) => r.entry?.tournamentId)
.filter((s) => s);
if (success && success.length > 0) {
await Promise.all(
_.uniq(success).map((tid) =>
this.tournamentService.clearTournamentCache(tid),
),
);
}
}
}
import { Test, TestingModule } from '@nestjs/testing';
import { TournamentSubscriberService } from './tournament-subscriber.service';
describe('TournamentSubscriberService', () => {
let service: TournamentSubscriberService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [TournamentSubscriberService],
}).compile();
service = module.get<TournamentSubscriberService>(TournamentSubscriberService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
import { Injectable, OnModuleInit } from '@nestjs/common';
import {
DataSource,
EntitySubscriberInterface,
RemoveEvent,
SoftRemoveEvent,
UpdateEvent,
} from 'typeorm';
import { Tournament } from '../tournament/entities/Tournament.entity';
import { TournamentService } from '../tournament/tournament.service';
import { InjectDataSource } from '@nestjs/typeorm';
import { getFieldFromTypeormEvent } from '../utility/get-field-from-typeorm-event';
@Injectable()
export class TournamentSubscriberService
implements EntitySubscriberInterface<Tournament>, OnModuleInit
{
constructor(
private tournamentService: TournamentService,
@InjectDataSource() private db: DataSource,
) {}
onModuleInit() {
this.db.subscribers.push(this);
}
listenTo() {
return Tournament;
}
async clearCacheOnEvent(
event:
| UpdateEvent<Tournament>
| SoftRemoveEvent<Tournament>
| RemoveEvent<Tournament>,
) {
await this.tournamentService.clearTournamentCache(
getFieldFromTypeormEvent(event, 'id'),
);
}
// three event handlers for different events
async afterUpdate(event: UpdateEvent<Tournament>) {
await this.clearCacheOnEvent(event);
}
async afterSoftRemove(event: SoftRemoveEvent<Tournament>) {
await this.clearCacheOnEvent(event);
}
async afterRemove(event: RemoveEvent<Tournament>) {
await this.clearCacheOnEvent(event);
}
}
......@@ -79,7 +79,6 @@ export class RuleSettings {
hasThirdPlaceMatch: boolean;
}
@CacheTTL(7200000)
@Entity()
export class Tournament extends DescBase {
@QueryEqual()
......
......@@ -29,7 +29,25 @@ import { dragRearrange } from '../utility/drag-rearrange';
import { normalSeq } from '../utility/normal-seq';
import { sortAfterSwiss } from '../utility/soft-after-swiss';
import { InjectAragami } from 'nestjs-aragami';
import { Aragami } from 'aragami';
import { Aragami, CacheKey, CacheTTL } from 'aragami';
import { Type } from 'class-transformer';
@CacheTTL(7200000)
class TournamentCache {
id: number;
noAnalytics: boolean;
@Type(() => Tournament)
tournament: Tournament;
@Type(() => Participant)
participants: Participant[];
@Type(() => Match)
matches: Match[];
@CacheKey()
cacheKey() {
return `tournament_cache:${this.id}:${this.noAnalytics ? 1 : 0}`;
}
}
@Injectable()
export class TournamentService extends CrudService(Tournament, {
......@@ -81,17 +99,26 @@ export class TournamentService extends CrudService(Tournament, {
qb.select(`${this.entityAliasName}.id`);
});
const key = `tournament_cache:${id}:${noAnalytics ? 1 : 0}`;
const res = await this.aragami.cache(Tournament, key, async () => {
const res = await this.aragami.cache(TournamentCache, key, async () => {
const result = await this.findOne(id);
if (result.data) {
if (result.data.status === TournamentStatus.Ready && !noAnalytics) {
result.data.analytics();
}
}
return result.data;
const cache = new TournamentCache();
cache.id = id;
cache.noAnalytics = noAnalytics;
cache.tournament = result.data;
cache.participants = result.data?.participants || [];
cache.matches = result.data?.matches || [];
return cache;
});
res.wipeDeckBuf(user);
return new GenericReturnMessageDto(200, 'success', res);
const tournament = res.tournament;
tournament.participants = res.participants;
tournament.matches = res.matches;
tournament.wipeDeckBuf(user);
return new GenericReturnMessageDto(200, 'success', tournament);
}
async getTournaments(dto: Partial<Tournament>, user: MycardUser | number) {
......@@ -128,12 +155,16 @@ export class TournamentService extends CrudService(Tournament, {
}
}
}
return this.update(id, dto);
const res = await this.update(id, dto);
await this.clearTournamentCache(id);
return res;
}
async deleteTournament(id: number, user: MycardUser | number) {
await this.checkPermissionOfTournament(id, user);
return this.delete(id);
const res = this.delete(id);
await this.clearTournamentCache(id);
return res;
}
async checkPermissionOfTournament(
......@@ -180,6 +211,7 @@ export class TournamentService extends CrudService(Tournament, {
await tdb.update(Tournament, id, { status: TournamentStatus.Ready });
await tdb.softDelete(Match, { tournamentId: id });
});
await this.clearTournamentCache(id);
return new BlankReturnMessageDto(200, 'success');
}
......@@ -206,6 +238,7 @@ export class TournamentService extends CrudService(Tournament, {
await tournament.getRuleHandler(edb.getRepository(Match)).initialize();
await edb.update(Tournament, id, { status: TournamentStatus.Running });
});
await this.clearTournamentCache(id);
return new BlankReturnMessageDto(200, 'success');
}
......@@ -234,6 +267,7 @@ export class TournamentService extends CrudService(Tournament, {
).toException();
}
await this.repo.update(id, { status: TournamentStatus.Finished });
await this.clearTournamentCache(id);
return new BlankReturnMessageDto(200, 'success');
}
......@@ -250,6 +284,7 @@ export class TournamentService extends CrudService(Tournament, {
await this.repo.manager.softDelete(Participant, {
tournamentId: id,
});
await this.clearTournamentCache(id);
return new BlankReturnMessageDto(200, 'success');
}
......@@ -280,6 +315,7 @@ export class TournamentService extends CrudService(Tournament, {
),
);
});
await this.clearTournamentCache(id);
}
}
......@@ -445,7 +481,6 @@ export class TournamentService extends CrudService(Tournament, {
});
}
}
return new GenericReturnMessageDto(
200,
'success',
......@@ -476,7 +511,10 @@ export class TournamentService extends CrudService(Tournament, {
// shuffle
shuffleArray(participants);
normalSeq(participants);
return this.updateParticipantSeq(participants);
const res = await this.updateParticipantSeq(participants);
await this.clearTournamentCache(id);
return res;
}
async dragParticipant(
......@@ -513,11 +551,15 @@ export class TournamentService extends CrudService(Tournament, {
: participants.findIndex((p) => p.id === dto.placeAfterParticipantId);
const { updates } = dragRearrange(participants, draggingIndex, insertAfter);
return this.updateParticipantSeq(updates);
const res = await this.updateParticipantSeq(updates);
await this.clearTournamentCache(id);
return res;
}
async clearTournamentCache(id: number) {
if (!id) return;
await this.aragami.clear(Tournament, `tournament_cache:${id}:`);
this.log.log(`Clearing cache for tournament ${id}`);
await this.aragami.clear(TournamentCache, `tournament_cache:${id}:`);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment