perf(backend): индексы, кэш чартов, пропуск upsert, фикс N+1 обогащения

- track_plays(played_at,track_id,station_id) покрывающий + tracks(first_seen_at) WHERE
  pending частичный (применены CONCURRENTLY на проде + миграция idempotent)
- ChartsService.getTopTracks: in-memory TTL-кэш 90с по (period,genre,limit) → детальная
  страница и параллельные запросы не пересчитывают тяжёлые агрегации
- NowPlayingService.ingest: не пишем now_playing и не шлём сокет, если трек не изменился
  (было ~20k бесполезных upsert/час)
- enrichNowPlaying: вместо N+1 (300 upsert/мин) — один batched findMany по normKey

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nk
2026-06-06 16:20:25 +03:00
parent 924a4a0ab1
commit a3434ed894
5 changed files with 72 additions and 13 deletions

View File

@@ -92,19 +92,39 @@ export class EnrichmentService {
const rows = await this.prisma.nowPlaying.findMany({
select: { artist: true, song: true },
});
const todo: { id: string; artist: string; song: string; normKey: string }[] = [];
// Треки уже созданы в ChartsService.recordPlay — не upsert'им построчно (был
// N+1 на ~300 строк/мин), а читаем пачкой по normKey. Мусор/исключённые
// станции трек не создавали → их и не обогащаем (это правильно).
const normKeys = new Set<string>();
for (const r of rows) {
const artist = (r.artist ?? '').trim();
const song = (r.song ?? '').trim();
if (!artist || !song) continue;
const normKey = this.buildNormKey(artist, song);
const track = await this.prisma.track.upsert({
where: { normKey },
create: { normKey, artist, song },
update: {},
select: { id: true, coverUrl: true, enrichStatus: true },
});
if (!track.coverUrl) todo.push({ id: track.id, artist, song, normKey });
if (artist && song) normKeys.add(this.buildNormKey(artist, song));
}
if (normKeys.size === 0) return;
const tracks = await this.prisma.track.findMany({
where: { normKey: { in: [...normKeys] } },
select: {
id: true,
artist: true,
song: true,
normKey: true,
coverUrl: true,
enrichStatus: true,
},
});
const todo: { id: string; artist: string; song: string; normKey: string }[] = [];
for (const track of tracks) {
if (!track.coverUrl) {
todo.push({
id: track.id,
artist: track.artist,
song: track.song,
normKey: track.normKey,
});
}
// полное обогащение (жанр) — в общую очередь, если ещё не сделано
if (track.enrichStatus !== 'done') this.enqueue(track.id);
}