perf: use only one WASM alloc for xxhash

This commit is contained in:
TonyRL
2026-07-08 23:41:01 +08:00
parent ee7c8549ab
commit 8dd9c9a2ba
4 changed files with 54 additions and 9 deletions
+2 -1
View File
@@ -4,6 +4,8 @@ import xxhash from 'xxhash-wasm';
import cacheModule from '@/utils/cache/index'; import cacheModule from '@/utils/cache/index';
const { h64ToString } = await xxhash();
const QuerySchema = z.object({ const QuerySchema = z.object({
requestPath: z.string().openapi({ requestPath: z.string().openapi({
param: { param: {
@@ -62,7 +64,6 @@ const handler: RouteHandler<typeof route> = async (ctx) => {
} }
const { requestPath } = ctx.req.valid('query'); const { requestPath } = ctx.req.valid('query');
const { h64ToString } = await xxhash();
const key = 'rsshub:koa-redis-cache:' + h64ToString(requestPath + ':rss'); const key = 'rsshub:koa-redis-cache:' + h64ToString(requestPath + ':rss');
const cached = await cacheModule.globalCache.has(key); const cached = await cacheModule.globalCache.has(key);
+15 -7
View File
@@ -7,6 +7,8 @@ import type { Data } from '@/types';
import cacheModule from '@/utils/cache/index'; import cacheModule from '@/utils/cache/index';
const bypassList = new Set(['/', '/robots.txt', '/logo.png', '/favicon.ico']); const bypassList = new Set(['/', '/robots.txt', '/logo.png', '/favicon.ico']);
const { h64ToString } = await xxhash();
// only give cache string, as the `!` condition tricky // only give cache string, as the `!` condition tricky
// XXH64 is used to shrink key size // XXH64 is used to shrink key size
// plz, write these tips in comments! // plz, write these tips in comments!
@@ -19,13 +21,18 @@ const middleware: MiddlewareHandler = async (ctx, next) => {
const requestPath = ctx.req.path; const requestPath = ctx.req.path;
const format = `:${ctx.req.query('format') || 'rss'}`; const format = `:${ctx.req.query('format') || 'rss'}`;
const limit = ctx.req.query('limit') ? `:${ctx.req.query('limit')}` : ''; const limit = ctx.req.query('limit') ? `:${ctx.req.query('limit')}` : '';
const { h64ToString } = await xxhash();
const key = 'rsshub:koa-redis-cache:' + h64ToString(requestPath + format + limit); const key = 'rsshub:koa-redis-cache:' + h64ToString(requestPath + format + limit);
const controlKey = 'rsshub:path-requested:' + h64ToString(requestPath + format + limit); const controlKey = 'rsshub:path-requested:' + h64ToString(requestPath + format + limit);
const isRequesting = await cacheModule.globalCache.get(controlKey); let value = await cacheModule.globalCache.get(key);
if (isRequesting === '1') { // Doesn't hit the cache? Try to become the fetcher and let others know!
let claimed = false;
if (!value) {
claimed = await cacheModule.globalCache.claim(controlKey, config.cache.requestTimeout);
}
if (!value && !claimed) {
let retryTimes = process.env.NODE_ENV === 'test' ? 1 : 10; let retryTimes = process.env.NODE_ENV === 'test' ? 1 : 10;
let bypass = false; let bypass = false;
while (retryTimes > 0) { while (retryTimes > 0) {
@@ -41,10 +48,9 @@ const middleware: MiddlewareHandler = async (ctx, next) => {
if (!bypass) { if (!bypass) {
throw new RequestInProgressError('This path is currently fetching, please come back later!'); throw new RequestInProgressError('This path is currently fetching, please come back later!');
} }
value = await cacheModule.globalCache.get(key);
} }
const value = await cacheModule.globalCache.get(key);
if (value) { if (value) {
ctx.status(200); ctx.status(200);
ctx.header('RSSHub-Cache-Status', 'HIT'); ctx.header('RSSHub-Cache-Status', 'HIT');
@@ -53,8 +59,10 @@ const middleware: MiddlewareHandler = async (ctx, next) => {
return; return;
} }
// Doesn't hit the cache? We need to let others know! if (!claimed) {
await cacheModule.globalCache.set(controlKey, '1', config.cache.requestTimeout); // waited out a stale claim without finding a cache entry, take over the fetch
await cacheModule.globalCache.set(controlKey, '1', config.cache.requestTimeout);
}
// let routers control cache // let routers control cache
ctx.set('cacheKey', key); ctx.set('cacheKey', key);
+2 -1
View File
@@ -11,6 +11,8 @@ import { parseDate } from '@/utils/parse-date';
const link = 'https://www.economist.com/the-world-in-brief'; const link = 'https://www.economist.com/the-world-in-brief';
const { h64ToString } = await xxhash();
export const route: Route = { export const route: Route = {
path: '/espresso', path: '/espresso',
categories: ['traditional-media'], categories: ['traditional-media'],
@@ -37,7 +39,6 @@ export const route: Route = {
}; };
async function handler() { async function handler() {
const { h64ToString } = await xxhash();
const nextData = await cache.tryGet( const nextData = await cache.tryGet(
link, link,
async () => { async () => {
+35
View File
@@ -11,10 +11,16 @@ const globalCache: {
get: (key: string) => Promise<string | null | undefined> | string | null | undefined; get: (key: string) => Promise<string | null | undefined> | string | null | undefined;
has: (key: string) => Promise<boolean> | boolean; has: (key: string) => Promise<boolean> | boolean;
set: (key: string, value?: string | Record<string, any>, maxAge?: number) => any; set: (key: string, value?: string | Record<string, any>, maxAge?: number) => any;
/**
* Atomically set `key` to '1' and return true, unless it is already '1' (return false).
* A get-then-set in the caller races: two same-tick requests would both read "not '1'".
*/
claim: (key: string, maxAge: number) => Promise<boolean> | boolean;
} = { } = {
get: () => null, get: () => null,
has: () => false, has: () => false,
set: () => null, set: () => null,
claim: () => true,
}; };
const noopCacheModule: CacheModule = { const noopCacheModule: CacheModule = {
@@ -54,6 +60,13 @@ if (isWorker) {
return false; return false;
}; };
globalCache.set = cacheModule.set; globalCache.set = cacheModule.set;
globalCache.claim = async (key, maxAge) => {
if (!key || !cacheModule.status.available || !redisClient) {
return true;
}
const result = await redisClient.eval("if redis.call('GET', KEYS[1]) == '1' then return 0 end redis.call('SET', KEYS[1], '1', 'EX', ARGV[1]) return 1", 1, key, maxAge);
return result === 1;
};
break; break;
} }
case 'http': case 'http':
@@ -75,6 +88,17 @@ if (isWorker) {
return cacheModule.set(key, value, maxAge); return cacheModule.set(key, value, maxAge);
} }
}; };
globalCache.claim = async (key, maxAge) => {
if (!key || !cacheModule.status.available) {
return true;
}
// best effort: the HTTP cache protocol has no atomic operation
if ((await cacheModule.get(key, false)) === '1') {
return false;
}
await cacheModule.set(key, '1', maxAge);
return true;
};
break; break;
case 'memory': { case 'memory': {
cacheModule = memory; cacheModule = memory;
@@ -102,6 +126,17 @@ if (isWorker) {
return memoryCache.set(key, value, { ttl: maxAge * 1000 }); return memoryCache.set(key, value, { ttl: maxAge * 1000 });
} }
}; };
// fully synchronous, so nothing can interleave between the read and the write
globalCache.claim = (key, maxAge) => {
if (!key || !cacheModule.status.available || !memoryCache) {
return true;
}
if (memoryCache.get(key, { updateAgeOnGet: false }) === '1') {
return false;
}
memoryCache.set(key, '1', { ttl: maxAge * 1000 });
return true;
};
break; break;
} }
default: default: