Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update matches scheduler #18

Merged
merged 4 commits into from
Dec 2, 2024
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
4 changes: 4 additions & 0 deletions backend/app/schedulers/appSchedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { CurrentRoundMatchesService, CurrentRoundMatchesServiceImpl } from './fo
import { TodayAndMorrowScheduler } from "./footballApi/matches.todayAndMorrow.scheduler";
import { SeasonNextRoundScheduler } from "./footballApi/season.nextRound.scheduler";
import { PredictionPointsScheduler } from './predictionPoints.scheduler';
import { LeaderboardScheduler } from './leaderboard.scheduler';
import { MakePredictionsScheduler } from "./makePredictions.scheduler";

export class AppSchedule {
private readonly currentRoundMatchesService: CurrentRoundMatchesService;
private readonly todayAndMorrowScheduler: TodayAndMorrowScheduler;
private readonly seasonNextRoundScheduler: SeasonNextRoundScheduler;
private readonly predictionPointsScheduler: PredictionPointsScheduler;
private readonly leaderboardScheduler: LeaderboardScheduler;
private readonly makePredictionsScheduler: MakePredictionsScheduler;

static getInstance() {
Expand All @@ -22,13 +24,15 @@ export class AppSchedule {
this.todayAndMorrowScheduler = TodayAndMorrowScheduler.getInstance();
this.seasonNextRoundScheduler = SeasonNextRoundScheduler.getInstance();
this.predictionPointsScheduler = PredictionPointsScheduler.getInstance();
this.leaderboardScheduler = LeaderboardScheduler.getInstance();
this.makePredictionsScheduler = MakePredictionsScheduler.getInstance();
}

async start() {
await this.currentRoundMatchesService.updateMatches();
await this.todayAndMorrowScheduler.startJob({ runImmediately: true }); // loop after min/max 90s/6H
await this.predictionPointsScheduler.startJob({ runImmediately: true, interval: '0 15 * * * *' }); // minute 15 every H
await this.leaderboardScheduler.startJob({ runImmediately: true }); // loop after 6H
await this.seasonNextRoundScheduler.startJob({ interval: '0 0 */2 * * *' }); // minute 0 every 2H
await this.makePredictionsScheduler.startJob(); // loop after 3H
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ const DEFAULT_INTERVAL_MILLISECONDS = 6 * 60 * 60 * 1000; // 6H
export class TodayAndMorrowScheduler extends BaseScheduler {
private scheduleDate?: Date;
private nextPoll?: moment.Moment; // ideally what is supposed to be the next poll
private hasLiveMatch = false;
private hasNewLiveMatch = false;
private liveMatchId = undefined;
private liveMatchHasFinished = false;

public static getInstance(
todayAndMorrowService = TodayAndMorrowServiceImpl.getInstance(),
Expand All @@ -30,12 +30,7 @@ export class TodayAndMorrowScheduler extends BaseScheduler {
this.scheduleDate = scheduleDate;
return;
}
const scheduleDateDiffInSeconds = moment.duration(moment(scheduleDate).diff(this.scheduleDate)).asSeconds()
if (scheduleDateDiffInSeconds >= 300) {
console.log(`${this.job.name} publish footballApiMatchUpdatesCompleted`);
this.eventMediator.publish('footballApiMatchUpdatesCompleted');
}
this.scheduleDate = scheduleDate;
console.log(`${this.job.name} onScheduled ${moment(scheduleDate).format()}`);
})
}

Expand All @@ -45,49 +40,72 @@ export class TodayAndMorrowScheduler extends BaseScheduler {
Math.round(moment.duration(this.nextPoll.diff(moment())).asHours())
if (nextPollInHours == undefined || nextPollInHours === 6) {
period = PERIOD.TODAY_AND_MORROW
} else if (this.hasLiveMatch && !this.hasNewLiveMatch) {
period = PERIOD.LIVE
} else if (this.liveMatchHasFinished) {
period = PERIOD.TODAY;
} else if (this.liveMatchId) {
period = PERIOD.LIVE;
}

const apiMatches = await this.todayAndMorrowService.syncMatches(period);
if (this.liveMatchId && this.liveMatchHasFinished) {
console.log(`${this.job.name} publish footballApiLiveMatchUpdatesCompleted`);
this.eventMediator.publish('footballApiLiveMatchUpdatesCompleted');
} else if (!this.liveMatchId && this.liveMatchHasFinished) {
console.log(`${this.job.name} publish footballApiMatchUpdatesCompleted`);
this.eventMediator.publish('footballApiMatchUpdatesCompleted');
}

if (this.liveMatchHasFinished) {
this.liveMatchHasFinished = false;
}
return apiMatches;
}

calculateNextInterval(result: any = []): number {
const apiMatches: any[] = result;
let liveMatch: any = apiMatches.find((match: any) => match.id === this.liveMatchId);

let hasLiveMatch = false;
let hasNewLiveMatch = false;
let nextPoll = moment().add(12, 'hours');
for (const match of apiMatches) {
const matchStatus = getMatchStatus(match.status);
const matchStart = moment(match.utcDate);

if (matchStatus === MatchStatus.LIVE) {
hasLiveMatch = true;
if (Math.abs(matchStart.diff(moment(), 'seconds')) <= 90) {
hasNewLiveMatch = true;
break;
}
} else if (matchStatus === MatchStatus.SCHEDULED) {
if (matchStart.isBefore(nextPoll)) {
nextPoll = matchStart.add(1, 'minutes');
if (!liveMatch) {
for (const match of apiMatches) {
const matchStatus = getMatchStatus(match.status);
const matchStart = moment(match.utcDate);

if (matchStatus === MatchStatus.LIVE) {
if (!liveMatch) {
liveMatch = match;
} else if (matchStart.isBefore(moment(liveMatch.utcDate))) {
liveMatch = match;
}
} else if (matchStatus === MatchStatus.SCHEDULED) {
if (matchStart.isBefore(nextPoll)) {
nextPoll = matchStart.add(1, 'minutes');
}
}
}
}

if (this.hasLiveMatch && !hasLiveMatch) {
this.hasLiveMatch = false;
this.nextPoll = moment().add(3, 'minutes');
} else if (hasLiveMatch) {
this.hasLiveMatch = true;
this.hasNewLiveMatch = hasNewLiveMatch;
this.nextPoll = moment().add(90, 'seconds');
} else {
// precautionary handle nextPoll being behind
const diff = nextPoll.diff(moment(), 'minutes');
this.nextPoll = diff <= 0 ? moment().add(3, 'minutes') : nextPoll;
const liveMatchId = liveMatch?.id;
if (liveMatchId && !this.liveMatchId) {
this.liveMatchId = liveMatchId;
} else if (liveMatchId && this.liveMatchId) {
if (this.liveMatchId !== liveMatchId) {
this.liveMatchId = liveMatchId;
this.liveMatchHasFinished = true;
}
} else if (!liveMatchId && this.liveMatchId) {
this.liveMatchId = undefined;
this.liveMatchHasFinished = true;
}

// precautionary handle nextPoll being behind
const diff = nextPoll.diff(moment(), 'minutes');
nextPoll = diff <= 0 ? moment().add(1, 'minutes') : nextPoll;

if (this.liveMatchId || this.liveMatchHasFinished) {
nextPoll = moment().add(1, 'minutes');
}
this.nextPoll = nextPoll;

const nextIntervalInMs = Math.min(this.getDefaultIntervalMs(), this.nextPoll.diff(moment()));
const nextIntervalInUTC = new Date(Date.now() + nextIntervalInMs).toUTCString();
Expand Down
39 changes: 39 additions & 0 deletions backend/app/schedulers/leaderboard.scheduler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { LeaderboardService, LeaderboardServiceImpl } from './leaderboard.service';
import { PredictionService, PredictionServiceImpl } from './prediction.service';
import { BaseScheduler } from './baseScheduler';
import { EventMediator, EventMediatorImpl } from '../../common/eventMediator';

const DEFAULT_INTERVAL_MILLISECONDS = 6 * 60 * 60 * 1000; // 6H

export class LeaderboardScheduler extends BaseScheduler {
public static getInstance(
leaderboardService = LeaderboardServiceImpl.getInstance(),
predictionService = PredictionServiceImpl.getInstance(),
eventMediator = EventMediatorImpl.getInstance(),
) {
return new LeaderboardScheduler(leaderboardService, predictionService, eventMediator);
}

constructor(
private leaderboardService: LeaderboardService,
private predictionService: PredictionService,
private eventMediator: EventMediator
) {
super('LeaderboardJob');
this.eventMediator.addListener(
'footballApiMatchUpdatesCompleted', async () => {
console.log(`${this.job.name} handle footballApiMatchUpdatesCompleted`);
await this.runJob();
}
);
}

async task() {
await this.predictionService.calculatePredictionPoints();
await this.leaderboardService.updateGlobalLeaderboards();
}

protected getDefaultIntervalMs(): number {
return DEFAULT_INTERVAL_MILLISECONDS;
}
}
10 changes: 3 additions & 7 deletions backend/app/schedulers/predictionPoints.scheduler.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,29 @@
import { PredictionService, PredictionServiceImpl } from './prediction.service';
import { BaseScheduler } from './baseScheduler';
import { EventMediator, EventMediatorImpl } from '../../common/eventMediator';
import { LeaderboardService, LeaderboardServiceImpl } from './leaderboard.service';

export class PredictionPointsScheduler extends BaseScheduler {
public static getInstance(
predictionsService = PredictionServiceImpl.getInstance(),
leaderboardService = LeaderboardServiceImpl.getInstance(),
eventMediator = EventMediatorImpl.getInstance(),
) {
return new PredictionPointsScheduler(predictionsService, leaderboardService, eventMediator);
return new PredictionPointsScheduler(predictionsService, eventMediator);
}

constructor(
private predictionService: PredictionService,
private leaderboardService: LeaderboardService,
private eventMediator: EventMediator
) {
super('PredictionPointsJob');
this.eventMediator.addListener(
'footballApiMatchUpdatesCompleted', async () => {
console.log(`${this.job.name} handle footballApiMatchUpdatesCompleted`);
'footballApiLiveMatchUpdatesCompleted', async () => {
console.log(`${this.job.name} handle footballApiLiveMatchUpdatesCompleted`);
await this.runJob();
}
);
}

async task() {
await this.predictionService.calculatePredictionPoints();
await this.leaderboardService.updateGlobalLeaderboards();
}
}