From 19cff347ad7864784a1a3cb042f4afd3682b11e7 Mon Sep 17 00:00:00 2001 From: Mabolla <133767935+Mabolla@users.noreply.github.com> Date: Sat, 5 Sep 2026 02:49:42 +0300 Subject: [PATCH] fix(desktop): wait for an evicted backend to release its slot Await LRU teardown in each pooled backend creation path so replacement wakes do not race an exiting child for the hard spawn slot. --- apps/desktop/electron/main.ts | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index b769e522b5..6a19766ca0 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -290,7 +290,7 @@ import { localRouteFallbackProfiles, undialedSshRouteSeeds } from './plugin-profile-routes' -import { selectPoolEvictions } from './pool-eviction' +import { evictPoolEntries } from './pool-eviction' import { clampPoolLimits, parsePoolLimits, POOL_LIMITS_DEFAULTS } from './pool-limits' import { isBackgroundSlotWaitTimeout, @@ -1623,7 +1623,7 @@ function setPoolLimits(raw) { poolLimits = clampPoolLimits(raw) persistPoolLimits(poolLimits) localBackendSpawnCoordinator.setLimit(poolLimits.maxBackends) - evictLruPoolBackends(poolMaxBackends()) + void evictLruPoolBackends(poolMaxBackends()) startPoolIdleReaper() return { ...poolLimits } @@ -11430,7 +11430,10 @@ async function ensureBackend(profile, opts: { passive?: boolean; spawnPriority?: } assertNotPassiveSpawn(passive, key) - evictLruPoolBackends(poolMaxBackends() - 1) + // The hard slot is released only after the evicted child exits. Wait for + // that teardown before entering the spawn queue; otherwise a successful + // LRU choice still leaves this wake racing the old child for 30 seconds. + await evictLruPoolBackends(poolMaxBackends() - 1) const entry = { process: null, @@ -11609,7 +11612,7 @@ async function ensureRegistryBackend( } assertNotPassiveSpawn(passive, localRoute.poolKey) - evictLruPoolBackends(poolMaxBackends() - 1) + await evictLruPoolBackends(poolMaxBackends() - 1) const localEntry = { process: null, @@ -11683,7 +11686,7 @@ async function ensureRegistryBackend( } assertNotPassiveSpawn(passive, key) - evictLruPoolBackends(poolMaxBackends() - 1) + await evictLruPoolBackends(poolMaxBackends() - 1) const entry = { process: null, @@ -12334,13 +12337,17 @@ function touchPoolBackend(profile) { // across N registered remote connections LRU-evict a REAL local backend that // was merely idle past the keepalive window. Descriptors are still reclaimed // by the idle reaper. -function evictLruPoolBackends(keep) { - const evictions = selectPoolEvictions(backendPool.entries(), Math.max(0, keep), Date.now(), POOL_KEEPALIVE_FRESH_MS) - - for (const profile of evictions) { - rememberLog(`Evicting idle profile backend "${profile}" (LRU cap ${poolMaxBackends()})`) - stopPoolBackend(profile) - } +async function evictLruPoolBackends(keep) { + return evictPoolEntries( + backendPool.entries(), + Math.max(0, keep), + Date.now(), + POOL_KEEPALIVE_FRESH_MS, + async profile => { + rememberLog(`Evicting idle profile backend "${profile}" (LRU cap ${poolMaxBackends()})`) + await stopPoolBackend(profile) + } + ) } function startPoolIdleReaper() {