fix: bound relay hermes run count cache (#4278)
This commit is contained in:
parent
c3125803a3
commit
a7e8471bf2
|
|
@ -195,4 +195,46 @@ describe('ExternalAutomationsHandler', () => {
|
|||
).resolves.toEqual({ total: 2, runs: [] })
|
||||
expect(readHermesRunRefs).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('evicts oldest remote Hermes count cache entries when many job ids are observed', async () => {
|
||||
const { handler, requestHandlers } = createHandlerHarness()
|
||||
const readHermesRunRefs = vi.fn(async (jobId: string) =>
|
||||
jobId === 'job-0'
|
||||
? [{ id: 'job-0:2026-05-15_09-00-00.md', run_at: '2026-05-15T09:00:00' }]
|
||||
: []
|
||||
)
|
||||
const handlerInternals = handler as unknown as {
|
||||
readHermesRunRefs: typeof readHermesRunRefs
|
||||
}
|
||||
handlerInternals.readHermesRunRefs = readHermesRunRefs
|
||||
|
||||
await expect(
|
||||
requestHandlers.get('externalAutomations.runs')?.({
|
||||
provider: 'hermes',
|
||||
jobId: 'job-0',
|
||||
page: 1,
|
||||
pageSize: 0
|
||||
})
|
||||
).resolves.toEqual({ total: 1, runs: [] })
|
||||
|
||||
for (let i = 1; i <= 200; i += 1) {
|
||||
await requestHandlers.get('externalAutomations.runs')?.({
|
||||
provider: 'hermes',
|
||||
jobId: `job-${i}`,
|
||||
page: 1,
|
||||
pageSize: 0
|
||||
})
|
||||
}
|
||||
|
||||
await expect(
|
||||
requestHandlers.get('externalAutomations.runs')?.({
|
||||
provider: 'hermes',
|
||||
jobId: 'job-0',
|
||||
page: 1,
|
||||
pageSize: 0
|
||||
})
|
||||
).resolves.toEqual({ total: 1, runs: [] })
|
||||
|
||||
expect(readHermesRunRefs).toHaveBeenCalledTimes(202)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const HERMES_OUTPUT_FILE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d
|
|||
const HERMES_RUN_KEY_PATTERN = /^(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})$/
|
||||
const MAX_SESSION_OUTPUT_GAP_MS = 24 * 60 * 60 * 1000
|
||||
const MAX_REFERENCED_LOG_BYTES = 5 * 1024 * 1024
|
||||
const HERMES_RUN_COUNT_CACHE_MAX_ENTRIES = 200
|
||||
const FULL_SESSION_LOG_HEADING = '## Full session log'
|
||||
const REFERENCED_LOG_HEADING = '## Latest log file'
|
||||
const LATEST_LOG_PATH_PATTERN =
|
||||
|
|
@ -572,6 +573,12 @@ export class ExternalAutomationsHandler {
|
|||
if (cached && cached.expiresAt > now) {
|
||||
return cached.promise
|
||||
}
|
||||
if (cached) {
|
||||
this.hermesRunCountCache.delete(jobId)
|
||||
}
|
||||
// Why: remote Hermes jobs can churn independently of Orca; relay
|
||||
// processes are long-lived, so stale job ids need both TTL and a hard cap.
|
||||
this.pruneHermesRunCountCache(now)
|
||||
const entry: HermesRunCountCacheEntry = {
|
||||
promise: this.readHermesRunRefs(jobId).then((refs) => refs.length),
|
||||
expiresAt: Number.POSITIVE_INFINITY
|
||||
|
|
@ -589,6 +596,21 @@ export class ExternalAutomationsHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private pruneHermesRunCountCache(now: number): void {
|
||||
for (const [jobId, entry] of this.hermesRunCountCache) {
|
||||
if (entry.expiresAt <= now) {
|
||||
this.hermesRunCountCache.delete(jobId)
|
||||
}
|
||||
}
|
||||
while (this.hermesRunCountCache.size >= HERMES_RUN_COUNT_CACHE_MAX_ENTRIES) {
|
||||
const oldestJobId = this.hermesRunCountCache.keys().next().value
|
||||
if (oldestJobId === undefined) {
|
||||
return
|
||||
}
|
||||
this.hermesRunCountCache.delete(oldestJobId)
|
||||
}
|
||||
}
|
||||
|
||||
private clearHermesRunCountCache(jobId?: string): void {
|
||||
if (jobId) {
|
||||
this.hermesRunCountCache.delete(jobId)
|
||||
|
|
|
|||
Loading…
Reference in New Issue