perf(api): replace polling for in-memory caches with signals (#35)

This commit is contained in:
hampus-fluxer
2026-01-06 00:22:30 +01:00
committed by GitHub
parent 9c665413ac
commit 8f9daac8b0
9 changed files with 106 additions and 38 deletions

View File

@@ -18,8 +18,10 @@
*/
import {createMiddleware} from 'hono/factory';
import type {Redis} from 'ioredis';
import type {HonoEnv} from '~/App';
import {AdminRepository} from '~/admin/AdminRepository';
import {IP_BAN_REFRESH_CHANNEL} from '~/constants/IpBan';
import {IpBannedError} from '~/Errors';
import {Logger} from '~/Logger';
import {type IpFamily, parseIpBanEntry, tryParseSingleIp} from '~/utils/IpRangeUtils';
@@ -42,38 +44,57 @@ class IpBanCache {
private singleIpBans: FamilyMap<SingleCacheEntry>;
private rangeIpBans: FamilyMap<RangeCacheEntry>;
private isInitialized = false;
private refreshIntervalMs = 30 * 1000;
private adminRepository = new AdminRepository();
private consecutiveFailures = 0;
private maxConsecutiveFailures = 5;
private redisSubscriber: Redis | null = null;
private subscriberInitialized = false;
constructor() {
this.singleIpBans = this.createFamilyMaps();
this.rangeIpBans = this.createFamilyMaps();
}
setRefreshSubscriber(subscriber: Redis | null): void {
this.redisSubscriber = subscriber;
}
async initialize(): Promise<void> {
if (this.isInitialized) return;
await this.refresh();
this.isInitialized = true;
this.setupSubscriber();
}
setInterval(() => {
this.refresh().catch((err) => {
this.consecutiveFailures++;
private setupSubscriber(): void {
if (this.subscriberInitialized || !this.redisSubscriber) {
return;
}
if (this.consecutiveFailures >= this.maxConsecutiveFailures) {
console.error(
`Failed to refresh IP ban cache ${this.consecutiveFailures} times in a row. ` +
`Last error: ${err.message}. Cache may be stale.`,
);
} else {
console.warn(
`Failed to refresh IP ban cache (${this.consecutiveFailures}/${this.maxConsecutiveFailures}): ${err.message}`,
);
}
const subscriber = this.redisSubscriber;
subscriber
.subscribe(IP_BAN_REFRESH_CHANNEL)
.then(() => {
subscriber.on('message', (channel) => {
if (channel === IP_BAN_REFRESH_CHANNEL) {
this.refresh().catch((err) => {
this.consecutiveFailures++;
const message = err instanceof Error ? err.message : String(err);
if (this.consecutiveFailures >= this.maxConsecutiveFailures) {
Logger.error({error: message}, 'Failed to refresh IP ban cache after notification');
} else {
Logger.warn({error: message}, 'Failed to refresh IP ban cache after notification');
}
});
}
});
})
.catch((error) => {
Logger.error({error}, 'Failed to subscribe to IP ban refresh channel');
});
}, this.refreshIntervalMs);
this.subscriberInitialized = true;
}
async refresh(): Promise<void> {

View File

@@ -145,7 +145,8 @@ const cloudflarePurgeQueue: ICloudflarePurgeQueue = Config.cloudflare.purgeEnabl
const assetDeletionQueue: IAssetDeletionQueue = new AssetDeletionQueue(redis);
const featureFlagRepository = new FeatureFlagRepository();
const featureFlagService = new FeatureFlagService(featureFlagRepository, cacheService);
const featureFlagSubscriber = new Redis(Config.redis.url);
const featureFlagService = new FeatureFlagService(featureFlagRepository, cacheService, featureFlagSubscriber);
let featureFlagServiceInitialized = false;
const snowflakeReservationRepository = new SnowflakeReservationRepository();
const snowflakeReservationSubscriber = new Redis(Config.redis.url);