perf(api): replace polling for in-memory caches with signals (#35)
This commit is contained in:
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user