Commit 8426d009 authored by nanahira's avatar nanahira

add tournament cache

parent 64cd40bb
Pipeline #39681 passed with stages
in 4 minutes and 26 seconds
This diff is collapsed.
......@@ -18,6 +18,10 @@ import { ApiKeyController } from './api-key/api-key.controller';
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: [
......@@ -54,6 +58,20 @@ import { HttpModule } from '@nestjs/axios';
}),
}),
TypeOrmModule.forFeature([Tournament, Match, Participant, ApiKey]),
AragamiModule.registerAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => {
const redisUrl = config.get<string>('REDIS_URL');
if (redisUrl) {
return {
redis: {
uri: redisUrl,
},
};
}
return {};
},
}),
HttpModule,
],
providers: [
......@@ -62,6 +80,9 @@ import { HttpModule } from '@nestjs/axios';
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);
}
}
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);
}
}
......@@ -43,6 +43,7 @@ import { TournamentRules } from '../../tournament-rules/rule-map';
import _ from 'lodash';
import { RenameClass } from 'nicot/dist/src/utility/rename-class';
import { QueryIn } from '../../utility/decorators/query-in';
import { CacheTTL } from 'aragami';
export enum TournamentRule {
SingleElimination = 'SingleElimination',
......@@ -78,6 +79,7 @@ export class RuleSettings {
hasThirdPlaceMatch: boolean;
}
@CacheTTL(7200000)
@Entity()
export class Tournament extends DescBase {
@QueryEqual()
......@@ -242,6 +244,16 @@ export class Tournament extends DescBase {
return this.checkPermissionWithUserId(user.id);
}
wipeDeckBuf(user: MycardUser | number) {
try {
this.checkPermission(user);
} catch (e) {
this.participants?.forEach((p) => {
p.deckbuf = undefined;
});
}
}
calculateScore() {
const rule = this.getRuleHandler();
this.participants.forEach((p) => {
......
......@@ -28,6 +28,8 @@ import { DragParticipantDto } from './dto/drag-participant.dto';
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';
@Injectable()
export class TournamentService extends CrudService(Tournament, {
......@@ -56,6 +58,7 @@ export class TournamentService extends CrudService(Tournament, {
private readonly participantService: ParticipantService,
private config: ConfigService,
private http: HttpService,
@InjectAragami() private aragami: Aragami,
) {
super(repo);
}
......@@ -72,18 +75,23 @@ export class TournamentService extends CrudService(Tournament, {
user: MycardUser | number,
noAnalytics = false,
) {
const result = await this.findOne(id, (qb) =>
Tournament.extraQueryForUser(user, qb, this.entityAliasName),
);
if (result.data) {
result.data.participants?.forEach((p) =>
this.participantService.wipeDeckbuf(user, p, result.data),
);
if (result.data.status === TournamentStatus.Ready && !noAnalytics) {
result.data.analytics();
// we check for permission first
await this.findOne(id, (qb) => {
Tournament.extraQueryForUser(user, qb, this.entityAliasName);
qb.select(`${this.entityAliasName}.id`);
});
const key = `tournament_cache:${id}:${noAnalytics ? 1 : 0}`;
const res = await this.aragami.cache(Tournament, key, async () => {
const result = await this.findOne(id);
if (result.data) {
if (result.data.status === TournamentStatus.Ready && !noAnalytics) {
result.data.analytics();
}
}
}
return result;
return result.data;
});
res.wipeDeckBuf(user);
return new GenericReturnMessageDto(200, 'success', res);
}
async getTournaments(dto: Partial<Tournament>, user: MycardUser | number) {
......@@ -91,11 +99,7 @@ export class TournamentService extends CrudService(Tournament, {
const res = await new tmpCls(this.repo).findAll(dto, (qb) =>
Tournament.extraQueryForUser(user, qb, this.entityAliasName),
);
res.data?.forEach((t) =>
t.participants?.forEach((p) =>
this.participantService.wipeDeckbuf(user, p, t),
),
);
res.data?.forEach((t) => t.wipeDeckBuf(user));
return res;
}
......@@ -511,4 +515,9 @@ export class TournamentService extends CrudService(Tournament, {
const { updates } = dragRearrange(participants, draggingIndex, insertAfter);
return this.updateParticipantSeq(updates);
}
async clearTournamentCache(id: number) {
if (!id) return;
await this.aragami.clear(Tournament, `tournament_cache:${id}:`);
}
}
import {
InsertEvent,
RemoveEvent,
SoftRemoveEvent,
UpdateEvent,
} from 'typeorm';
export const getFieldFromTypeormEvent = <T, K extends keyof T>(
evt: InsertEvent<T> | UpdateEvent<T> | SoftRemoveEvent<T> | RemoveEvent<T>,
field: K,
) => {
return (evt.entity as T)?.[field] ?? (evt['databaseEntity'] as T)?.[field];
};
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