diff --git a/src/renderer/src/components/automations/automation-host-catalog-generation.test.ts b/src/renderer/src/components/automations/automation-host-catalog-generation.test.ts index f085311fda2..c95384d750b 100644 --- a/src/renderer/src/components/automations/automation-host-catalog-generation.test.ts +++ b/src/renderer/src/components/automations/automation-host-catalog-generation.test.ts @@ -67,6 +67,14 @@ describe('automation catalog generation', () => { expect(registry.get(ENV_B)).toBe(1) }) + it('bounds authority generations without reopening an evicted fence', () => { + const authorities = Array.from({ length: 600 }, (_, index) => runtime(`env-${index}`)) + registry.sync(buildAutomationHostCatalog(input({ runtimes: authorities }))) + + expect(registry.get({ kind: 'runtime', environmentId: 'env-0' })).toBeGreaterThan(1) + expect(registry.get({ kind: 'runtime', environmentId: 'env-599' })).toBe(1) + }) + it('advances only the authority whose target bucket hydrated', () => { registry.sync( buildAutomationHostCatalog( diff --git a/src/renderer/src/components/automations/automation-host-catalog-generation.ts b/src/renderer/src/components/automations/automation-host-catalog-generation.ts index 8db2c85c16c..3de17481787 100644 --- a/src/renderer/src/components/automations/automation-host-catalog-generation.ts +++ b/src/renderer/src/components/automations/automation-host-catalog-generation.ts @@ -38,6 +38,8 @@ export type AutomationCatalogGenerationRegistry = { reset: () => void } +const AUTOMATION_AUTHORITY_GENERATION_MAX_ENTRIES = 512 + // Health is deliberately absent: only membership and incarnation belong here. export function automationHostCatalogEntryFingerprint(entry: AutomationHostCatalogEntry): string { return `${entry.stableKey}|${entry.catalogState}|${entry.owner ? ownerKey(entry.owner) : '-'}` @@ -62,6 +64,29 @@ export function createAutomationCatalogGenerationRegistry(): AutomationCatalogGe const generationByAuthorityKey = new Map() const fingerprintByAuthorityKey = new Map() const ownerKeyByStableKey = new Map() + const evictedAuthorityKeys = new Set() + let generationFloor = 0 + + const trimAuthorityGenerations = (): void => { + while (generationByAuthorityKey.size > AUTOMATION_AUTHORITY_GENERATION_MAX_ENTRIES) { + const oldest = generationByAuthorityKey.keys().next() + if (oldest.done) { + break + } + const key = oldest.value + generationByAuthorityKey.delete(key) + fingerprintByAuthorityKey.delete(key) + evictedAuthorityKeys.delete(key) + evictedAuthorityKeys.add(key) + } + while (evictedAuthorityKeys.size > AUTOMATION_AUTHORITY_GENERATION_MAX_ENTRIES) { + const oldest = evictedAuthorityKeys.keys().next() + if (oldest.done) { + break + } + evictedAuthorityKeys.delete(oldest.value) + } + } /** Compared only where an owner exists: a disconnect can strip owner refs, and that is not a new incarnation. */ const reincarnations = (catalog: AutomationHostCatalog): string[] => { @@ -90,14 +115,28 @@ export function createAutomationCatalogGenerationRegistry(): AutomationCatalogGe const advance = (authorityKey: string, fingerprint: string): void => { fingerprintByAuthorityKey.set(authorityKey, fingerprint) - generationByAuthorityKey.set( - authorityKey, - (generationByAuthorityKey.get(authorityKey) ?? 0) + 1 - ) + const previous = generationByAuthorityKey.get(authorityKey) + const next = + previous !== undefined + ? previous + 1 + : evictedAuthorityKeys.has(authorityKey) + ? generationFloor + 1 + : 1 + generationFloor = next + evictedAuthorityKeys.delete(authorityKey) + generationByAuthorityKey.set(authorityKey, next) + trimAuthorityGenerations() } return { - get: (authority) => generationByAuthorityKey.get(automationAuthorityCatalogKey(authority)) ?? 0, + get: (authority) => { + const key = automationAuthorityCatalogKey(authority) + const generation = generationByAuthorityKey.get(key) + if (generation !== undefined) { + return generation + } + return evictedAuthorityKeys.has(key) ? generationFloor + 1 : 0 + }, sync: (catalog) => { const reincarnatedStableKeys = reincarnations(catalog) const next = fingerprintByAuthority(catalog) @@ -121,6 +160,8 @@ export function createAutomationCatalogGenerationRegistry(): AutomationCatalogGe generationByAuthorityKey.clear() fingerprintByAuthorityKey.clear() ownerKeyByStableKey.clear() + evictedAuthorityKeys.clear() + generationFloor = 0 } } }