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';
const { h64ToString } = await xxhash();
const QuerySchema = z.object({
requestPath: z.string().openapi({
param: {
@@ -62,7 +64,6 @@ const handler: RouteHandler<typeof route> = async (ctx) => {
}
const { requestPath } = ctx.req.valid('query');
const { h64ToString } = await xxhash();
const key = 'rsshub:koa-redis-cache:' + h64ToString(requestPath + ':rss');
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';
const bypassList = new Set(['/', '/robots.txt', '/logo.png', '/favicon.ico']);
const { h64ToString } = await xxhash();
// only give cache string, as the `!` condition tricky
// XXH64 is used to shrink key size
// plz, write these tips in comments!
@@ -19,13 +21,18 @@ const middleware: MiddlewareHandler = async (ctx, next) => {
const requestPath = ctx.req.path;
const format = `:${ctx.req.query('format') || 'rss'}`;
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 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 bypass = false;
while (retryTimes > 0) {
@@ -41,10 +48,9 @@ const middleware: MiddlewareHandler = async (ctx, next) => {
if (!bypass) {
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) {
ctx.status(200);
ctx.header('RSSHub-Cache-Status', 'HIT');
@@ -53,8 +59,10 @@ const middleware: MiddlewareHandler = async (ctx, next) => {
return;
}
// Doesn't hit the cache? We need to let others know!
await cacheModule.globalCache.set(controlKey, '1', config.cache.requestTimeout);
if (!claimed) {
// 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
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 { h64ToString } = await xxhash();
export const route: Route = {
path: '/espresso',
categories: ['traditional-media'],
@@ -37,7 +39,6 @@ export const route: Route = {
};
async function handler() {
const { h64ToString } = await xxhash();
const nextData = await cache.tryGet(
link,
async () => {
+35
View File
@@ -11,10 +11,16 @@ const globalCache: {
get: (key: string) => Promise<string | null | undefined> | string | null | undefined;
has: (key: string) => Promise<boolean> | boolean;
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,
has: () => false,
set: () => null,
claim: () => true,
};
const noopCacheModule: CacheModule = {
@@ -54,6 +60,13 @@ if (isWorker) {
return false;
};
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;
}
case 'http':
@@ -75,6 +88,17 @@ if (isWorker) {
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;
case 'memory': {
cacheModule = memory;
@@ -102,6 +126,17 @@ if (isWorker) {
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;
}
default: