From 50db94c47ddcaf814dcbc43d84be34f21f1ad3fe Mon Sep 17 00:00:00 2001 From: Siddharth VP Date: Sun, 24 Mar 2024 15:51:52 +0530 Subject: [PATCH] dyk-counts: redis caching for web endpoint --- eventstream-router/routes/dyk-counts.ts | 17 +++++++++++-- redis.ts | 4 ++-- webservice/routes/dyk.ts | 32 +++++++++++++++---------- 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/eventstream-router/routes/dyk-counts.ts b/eventstream-router/routes/dyk-counts.ts index c25dffa..a37e265 100644 --- a/eventstream-router/routes/dyk-counts.ts +++ b/eventstream-router/routes/dyk-counts.ts @@ -2,11 +2,14 @@ import { bot } from "../../botbase"; import { Route } from "../app"; import { createLocalSSHTunnel } from "../../utils"; import { ENWIKI_DB_HOST, enwikidb } from "../../db"; +import {Redis, getRedisInstance} from "../../redis"; +import {RecentChangeStreamEvent} from "../RecentChangeStreamEvent"; export default class DykCountsTask extends Route { name = 'dyk-counts'; db: enwikidb; + redis: Redis; counts: Record = {}; unflushedChanges: Record = {}; @@ -24,6 +27,7 @@ export default class DykCountsTask extends Route { await createLocalSSHTunnel(ENWIKI_DB_HOST); this.db = new enwikidb(); + this.redis = getRedisInstance(); bot.setOptions({ maxRetries: 0, defaultParams: { maxlag: undefined } }); await bot.getTokensAndSiteInfo(); @@ -50,6 +54,10 @@ export default class DykCountsTask extends Route { `); this.counts = Object.fromEntries(queryResult.map(e => [e.username, parseInt(e.noms as string)])); await this.saveCounts('Refreshing counts from database'); + + const keyValues = queryResult.flatMap(e => [e.username, e.noms]) as string[]; + await this.redis.del('dyk-counts').catch(e => this.redisError(e)); + await this.redis.hmset.apply(null, ['dyk-counts'].concat(keyValues)).catch(e => this.redisError(e)); } catch (e) { this.log(`[E] Error while running db refresh`, e); } @@ -81,15 +89,16 @@ export default class DykCountsTask extends Route { } } - filter(data): boolean { + filter(data: RecentChangeStreamEvent): boolean { return data.wiki === 'enwiki' && data.type === 'new' && data.title.startsWith('Template:Did you know nominations/'); } - async worker(data) { + async worker(data: RecentChangeStreamEvent) { let {user, title} = data; this.counts[user] = (this.counts[user] || 0) + 1; + this.redis.hincrby('dyk-counts', user, 1).catch(e => this.redisError(e)); this.unflushedChanges[user] = (this.unflushedChanges[user] || []).concat(title); this.log(`[i] Crediting "${user}" for [[${data.title}]]`); @@ -103,4 +112,8 @@ export default class DykCountsTask extends Route { } } } + + redisError(err: Error) { + this.log(`[E] Redis error: `, err) + } } diff --git a/redis.ts b/redis.ts index eb6f742..8d3266a 100644 --- a/redis.ts +++ b/redis.ts @@ -8,7 +8,7 @@ import { onToolforge, readFile } from "./utils"; type Omit = Pick>; type Omitted = Omit>; -interface Redis extends Omitted, redis.Commands> {} +export interface Redis extends Omitted, redis.Commands> {} export const REDIS_HOST = 'tools-redis'; @@ -47,7 +47,7 @@ export function getRedisConfig(config: redis.ClientOpts = {}): redis.ClientOpts /** * For typical usage with the default options. - * Note: this can trigger a network request even though it doesn't take a callback or return a + * Note: this triggers a network request even though it doesn't take a callback or return a * promise. */ export function getRedisInstance(): Redis { diff --git a/webservice/routes/dyk.ts b/webservice/routes/dyk.ts index f3c7a88..3954315 100644 --- a/webservice/routes/dyk.ts +++ b/webservice/routes/dyk.ts @@ -1,10 +1,11 @@ import * as express from "express"; import { enwikidb } from "../../../SDZeroBot/db"; +import {getRedisInstance} from "../../redis"; const router = express.Router(); const db = new enwikidb(); -// TODO: Fix up redis caching, last present in f541de61 +const redis = getRedisInstance() router.get('/credits/:user', async (req, res, next) => { const user = req.params.user.replace(/ /g, '_'); @@ -22,18 +23,23 @@ router.get('/credits/:user', async (req, res, next) => { router.get('/noms/:user', async (req, res, next) => { const user = req.params.user.replace(/ /g, '_'); - const result = await db.query(` - SELECT COUNT(*) AS count FROM revision_userindex - JOIN page ON rev_page = page_id - JOIN actor_revision ON rev_actor = actor_id - WHERE page_namespace = 10 - AND page_is_redirect = 0 - AND rev_parent_id = 0 - AND page_title LIKE 'Did_you_know_nominations/%' - AND actor_name = ? - `, [user]); - const count = result[0].count; - res.end(String(count)); + + let count = await redis.hget('dyk-counts', user) as unknown as string; + if (!count) { + const result = await db.query(` + SELECT COUNT(*) AS count FROM revision_userindex + JOIN page ON rev_page = page_id + JOIN actor_revision ON rev_actor = actor_id + WHERE page_namespace = 10 + AND page_is_redirect = 0 + AND rev_parent_id = 0 + AND page_title LIKE 'Did_you_know_nominations/%' + AND actor_name = ? + `, [user]); + count = String(result[0].count); + redis.hset('dyk-counts', user, count); + } + res.end(count); }); export default router;