From 14478a5418b6c195c527ba28cf82e52aae8dd6d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Sun, 12 Mar 2023 00:48:22 +0100 Subject: [PATCH] feat(api): poll listens less often if user is inactive To save on Spotify API requests we have two different classes of polling intervals: - all users are polled at least every 10 minutes, this is a safe interval and no listens will be ever missed - if a user listened to a song within the last 60 minutes, we poll every minute to ensure that the UI shows new listens immediately --- src/listens/listens.service.ts | 10 ++++++++ src/sources/scheduler.service.ts | 32 ++++++++++++++++++++++---- src/sources/spotify/spotify.service.ts | 23 ++++++++++++++++-- 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/listens/listens.service.ts b/src/listens/listens.service.ts index ef93cec..d41923d 100644 --- a/src/listens/listens.service.ts +++ b/src/listens/listens.service.ts @@ -57,6 +57,16 @@ export class ListensService { }); } + async getMostRecentListenPerUser(): Promise { + return this.listenRepository + .createQueryBuilder("listen") + .leftJoinAndSelect("listen.user", "user") + .distinctOn(["user.id"]) + .orderBy({ "user.id": "ASC", "listen.playedAt": "DESC" }) + .limit(1) + .getMany(); + } + getScopedQueryBuilder(): ListenScopes { return this.listenRepository.scoped; } diff --git a/src/sources/scheduler.service.ts b/src/sources/scheduler.service.ts index 51ed6a8..b6b952b 100644 --- a/src/sources/scheduler.service.ts +++ b/src/sources/scheduler.service.ts @@ -10,6 +10,7 @@ import { UpdateSpotifyLibraryJob, } from "./jobs"; import { JobService } from "@apricote/nest-pg-boss"; +import { Span } from "nestjs-otel"; @Injectable() export class SchedulerService implements OnApplicationBootstrap { @@ -35,15 +36,38 @@ export class SchedulerService implements OnApplicationBootstrap { await this.superviseImportJobsJobService.schedule("*/1 * * * *", {}, {}); } + @Span() @CrawlerSupervisorJob.Handle() async superviseImportJobs(): Promise { this.logger.log("Starting crawler jobs"); - const users = await this.spotifyService.getCrawlableUserInfo(); + const userInfo = await this.spotifyService.getCrawlableUserInfo(); + + // To save on Spotify API requests we have two different classes of polling intervals: + // - all users are polled at least every 10 minutes, this is a safe interval + // and no listens will be ever missed + // - if a user listened to a song within the last 60 minutes, we poll every + // minute to ensure that the UI shows new listens immediately + const POLL_RATE_INACTIVE_SEC = 10 * 60; + const POLL_RATE_ACTIVE_SEC = 1 * 60; + + const INACTIVE_CUTOFF_MSEC = 60 * 60 * 1000; await Promise.all( - users.map((user) => - this.importSpotifyJobService.sendOnce({ userID: user.id }, {}, user.id) - ) + userInfo.map(({ user, lastListen }) => { + let pollRate = POLL_RATE_INACTIVE_SEC; + + const timeSinceLastListen = new Date().getTime() - lastListen.getTime(); + if (timeSinceLastListen < INACTIVE_CUTOFF_MSEC) { + pollRate = POLL_RATE_ACTIVE_SEC; + } + + this.importSpotifyJobService.sendThrottled( + { userID: user.id }, + {}, + pollRate, + user.id + ); + }) ); } diff --git a/src/sources/spotify/spotify.service.ts b/src/sources/spotify/spotify.service.ts index 0dbf44c..21dbb57 100644 --- a/src/sources/spotify/spotify.service.ts +++ b/src/sources/spotify/spotify.service.ts @@ -36,8 +36,27 @@ export class SpotifyService { ) {} @Span() - async getCrawlableUserInfo(): Promise { - return this.usersService.findAll(); + async getCrawlableUserInfo(): Promise<{ user: User; lastListen: Date }[]> { + // All of this is kinda inefficient, we do two db queries and join in code, + // i can't be bothered to do this properly in the db for now. + // Should be refactored if listory gets hundreds of users (lol). + + const [users, listens] = await Promise.all([ + this.usersService.findAll(), + this.listensService.getMostRecentListenPerUser(), + ]); + + return users.map((user) => { + const lastListen = listens.find((listen) => listen.user.id === user.id); + + return { + user, + // Return 1970 if no listen exists + lastListen: lastListen ? lastListen.playedAt : new Date(0), + }; + }); + + return; } @ImportSpotifyJob.Handle()