Avoid repeated cold info refreshes

This commit is contained in:
Ben
2026-06-08 00:31:56 -04:00
parent 8c795a3fa2
commit df35549f27
+51 -14
View File
@@ -18,6 +18,7 @@ import { RedisMessagingService } from './services/redis-messaging.service';
export class AppController { export class AppController {
private uptime = new Date(); private uptime = new Date();
private siteInfoRefreshPromise: Promise<SiteInfoResponse> | null = null;
constructor( constructor(
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache, @Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
@@ -37,27 +38,51 @@ export class AppController {
const CACHE_KEY = 'SITE_INFO'; const CACHE_KEY = 'SITE_INFO';
const STALE_CACHE_KEY = 'SITE_INFO_STALE';
const cachedResult = await this.getCached(CACHE_KEY, 5 * 60 * 1000); const cachedResult = await this.getCached(CACHE_KEY, 5 * 60 * 1000);
if (cachedResult != null) { if (cachedResult != null) {
return cachedResult; return cachedResult;
} }
let usedFallback = false; const staleResult = await this.getCached<SiteInfoResponse>(STALE_CACHE_KEY, 60 * 60 * 1000);
const withInfoTimeout = async <T>(label: string, promise: Promise<T>, fallback: T): Promise<T> => { if (staleResult != null) {
return await this.withTimeout(label, promise, fallback, () => { void this.refreshSiteInfo(staleResult);
usedFallback = true; return staleResult;
}
return this.refreshSiteInfo(null);
}
private async refreshSiteInfo(staleInfo: SiteInfoResponse | null): Promise<SiteInfoResponse> {
if (this.siteInfoRefreshPromise != null) {
return this.siteInfoRefreshPromise;
}
this.siteInfoRefreshPromise = this.loadSiteInfo(staleInfo)
.finally(() => {
this.siteInfoRefreshPromise = null;
}); });
return this.siteInfoRefreshPromise;
}
private async loadSiteInfo(staleInfo: SiteInfoResponse | null): Promise<SiteInfoResponse> {
const CACHE_KEY = 'SITE_INFO';
const STALE_CACHE_KEY = 'SITE_INFO_STALE';
const withInfoTimeout = async <T>(label: string, promise: Promise<T>, fallback: T): Promise<T> => {
return await this.withTimeout(label, promise, fallback);
}; };
const [blockData, highScores, poolAuthority, userAgentReport] = await Promise.all([ const [blockData, highScores, poolAuthority, userAgentReport] = await Promise.all([
withInfoTimeout('found blocks', this.blocksService.getFoundBlocks(), []), withInfoTimeout('found blocks', this.blocksService.getFoundBlocks(), staleInfo?.blockData ?? []),
withInfoTimeout('high scores', this.addressSettingsService.getHighScores(), []), withInfoTimeout('high scores', this.addressSettingsService.getHighScores(), staleInfo?.highScores ?? []),
withInfoTimeout('SV2 authority', this.stratumV2Service.getPoolAuthorityPublicKey(), { withInfoTimeout('SV2 authority', this.stratumV2Service.getPoolAuthorityPublicKey(), {
publicKey: '', publicKey: staleInfo?.sv2?.poolAuthorityPublicKey ?? '',
configured: false configured: staleInfo?.sv2?.authorityKeyConfigured ?? false
}), }),
withInfoTimeout<UserAgentReportView[]>('user agent report', this.userAgentReportService.getReport(), []), withInfoTimeout<UserAgentReportView[]>('user agent report', this.userAgentReportService.getReport(), staleInfo?.userAgents ?? []),
]); ]);
const other: { const other: {
@@ -87,7 +112,7 @@ export class AppController {
userAgents.push({ userAgent: 'Other', count: other.count.toString(), bestDifficulty: other.bestDifficulty, totalHashRate: other.totalHashRate.toString() }) userAgents.push({ userAgent: 'Other', count: other.count.toString(), bestDifficulty: other.bestDifficulty, totalHashRate: other.totalHashRate.toString() })
} }
const data = { const data: SiteInfoResponse = {
blockData, blockData,
userAgents, userAgents,
highScores, highScores,
@@ -98,11 +123,12 @@ export class AppController {
uptime: this.uptime uptime: this.uptime
}; };
// Match the pre-Timescale dashboard cache behavior; live accounting is exposed separately. // Cache a complete response even when one slow component falls back to stale data.
await this.setCached(CACHE_KEY, data, usedFallback ? 15 * 1000 : 5 * 60 * 1000); // A short retry loop here causes repeated DB work because timed-out TypeORM queries are not cancelled.
await this.setCached(CACHE_KEY, data, 5 * 60 * 1000);
await this.setCached(STALE_CACHE_KEY, data, 60 * 60 * 1000);
return data; return data;
} }
@Get('info/accounting') @Get('info/accounting')
@@ -211,7 +237,7 @@ export class AppController {
label: string, label: string,
promise: Promise<T>, promise: Promise<T>,
fallback: T, fallback: T,
onTimeout: () => void, onTimeout: () => void = () => undefined,
timeoutMs = 1500 timeoutMs = 1500
): Promise<T> { ): Promise<T> {
let timeout: NodeJS.Timeout; let timeout: NodeJS.Timeout;
@@ -246,3 +272,14 @@ export class AppController {
} }
} }
interface SiteInfoResponse {
blockData: unknown[];
userAgents: UserAgentReportView[];
highScores: unknown[];
sv2: {
poolAuthorityPublicKey: string;
authorityKeyConfigured: boolean;
};
uptime: Date;
}