fix(desktop): let the spawn coordinator follow the live pool max
Composition of #92581 on #100985: the hard cap is a constructor constant, so raising the pool max in Settings would have left new spawns queued behind the launch-time value. Add setLimit(); slot hand-off now goes through a single #drain that respects the current cap, which also fixes the original release path handing a slot to the next waiter even when the cap had just been lowered (test: lowering never revokes granted slots; new requests queue until under cap). main.ts constructs from poolLimits.maxBackends and pushes changes from setPoolLimits(); pinned by a wiring test.
This commit is contained in:
@@ -269,6 +269,56 @@ test('a failed or repeated cleanup releases exactly one slot', async () => {
|
||||
})
|
||||
|
||||
|
||||
test('raising the limit at runtime drains queued waiters into the new slots', async () => {
|
||||
const coordinator = new LocalBackendSpawnCoordinator(1)
|
||||
const first = await coordinator.acquire('a')
|
||||
const queuedB = coordinator.request('b')
|
||||
const queuedC = coordinator.request('c')
|
||||
await flush()
|
||||
assert.equal(coordinator.activeCount, 1)
|
||||
assert.equal(coordinator.queuedCount, 2)
|
||||
|
||||
coordinator.setLimit(2)
|
||||
const releaseB = await queuedB.acquired
|
||||
assert.equal(coordinator.activeCount, 2)
|
||||
assert.equal(coordinator.queuedCount, 1)
|
||||
|
||||
first()
|
||||
const releaseC = await queuedC.acquired
|
||||
assert.equal(coordinator.activeCount, 2)
|
||||
releaseB()
|
||||
releaseC()
|
||||
assert.equal(coordinator.activeCount, 0)
|
||||
})
|
||||
|
||||
test('lowering the limit never revokes granted slots; new requests queue until under cap', async () => {
|
||||
const coordinator = new LocalBackendSpawnCoordinator(3)
|
||||
const releases = await Promise.all(['a', 'b', 'c'].map(key => coordinator.acquire(key)))
|
||||
coordinator.setLimit(1)
|
||||
assert.equal(coordinator.activeCount, 3, 'granted slots stay granted')
|
||||
|
||||
const queued = coordinator.request('d')
|
||||
await flush()
|
||||
assert.equal(coordinator.queuedCount, 1)
|
||||
|
||||
releases[0]()
|
||||
releases[1]()
|
||||
await flush()
|
||||
assert.equal(coordinator.queuedCount, 1, 'still over the new cap of 1')
|
||||
|
||||
releases[2]()
|
||||
const releaseD = await queued.acquired
|
||||
assert.equal(coordinator.activeCount, 1)
|
||||
releaseD()
|
||||
})
|
||||
|
||||
test('setLimit rejects a non-positive or fractional cap', () => {
|
||||
const coordinator = new LocalBackendSpawnCoordinator(2)
|
||||
assert.throws(() => coordinator.setLimit(0), RangeError)
|
||||
assert.throws(() => coordinator.setLimit(1.5), RangeError)
|
||||
assert.equal(coordinator.limit, 2)
|
||||
})
|
||||
|
||||
// ── main.ts wiring ──────────────────────────────────────────────────────────
|
||||
// The coordinator is only as good as the timeout main.ts hands it. A queued
|
||||
// ticket that outlives the renderer's backend-boot budget holds the pool key
|
||||
@@ -296,4 +346,12 @@ test('a failed or repeated cleanup releases exactly one slot', async () => {
|
||||
assert.match(mainSource, /localBackendSpawnCoordinator\.request\(poolKey, \{ timeoutMs: POOL_SLOT_WAIT_MS \}\)/)
|
||||
assert.doesNotMatch(mainSource, /request\(poolKey, \{ timeoutMs: POOL_IDLE_MS \}\)/)
|
||||
})
|
||||
|
||||
test('main.ts pushes the live pool max into the coordinator when the preference changes', () => {
|
||||
// Pool sizing is a live device preference (#92581); the hard cap must
|
||||
// follow it, otherwise raising the max in Settings would leave spawns
|
||||
// queued behind the launch-time value.
|
||||
assert.match(mainSource, /new LocalBackendSpawnCoordinator\(poolLimits\.maxBackends\)/)
|
||||
assert.match(mainSource, /localBackendSpawnCoordinator\.setLimit\(poolLimits\.maxBackends\)/)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export async function releaseLocalBackendSlotAfterExit(
|
||||
* the child exits or the start fails. Remote descriptors never call request().
|
||||
*/
|
||||
export class LocalBackendSpawnCoordinator {
|
||||
readonly #limit: number
|
||||
#limit: number
|
||||
#active = 0
|
||||
#queue: Waiter[] = []
|
||||
|
||||
@@ -43,6 +43,25 @@ export class LocalBackendSpawnCoordinator {
|
||||
return this.#active
|
||||
}
|
||||
|
||||
get limit(): number {
|
||||
return this.#limit
|
||||
}
|
||||
|
||||
/**
|
||||
* Adopt a new cap at runtime (the pool size is a live device preference).
|
||||
* Raising it drains waiters into the newly freed slots immediately; lowering
|
||||
* it never revokes a granted slot — the running backends simply stay over
|
||||
* the cap until they exit, and LRU eviction (main.ts) converges the pool.
|
||||
*/
|
||||
setLimit(limit: number): void {
|
||||
if (!Number.isInteger(limit) || limit < 1) {
|
||||
throw new RangeError('Local backend spawn limit must be a positive integer.')
|
||||
}
|
||||
|
||||
this.#limit = limit
|
||||
this.#drain()
|
||||
}
|
||||
|
||||
get queuedCount(): number {
|
||||
return this.#queue.length
|
||||
}
|
||||
@@ -119,12 +138,16 @@ export class LocalBackendSpawnCoordinator {
|
||||
|
||||
released = true
|
||||
this.#active -= 1
|
||||
const next = this.#queue.shift()
|
||||
this.#drain()
|
||||
}
|
||||
}
|
||||
|
||||
if (next) {
|
||||
this.#clearTimer(next)
|
||||
next.resolve(this.#grant())
|
||||
}
|
||||
/** Hand free slots to queued waiters while under the (possibly lowered) cap. */
|
||||
#drain(): void {
|
||||
while (this.#active < this.#limit && this.#queue.length > 0) {
|
||||
const next = this.#queue.shift()!
|
||||
this.#clearTimer(next)
|
||||
next.resolve(this.#grant())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user