From b8b00f235e94b3bee880429cac3518cfd93bb967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Fri, 3 Apr 2026 05:42:03 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20enforce=20page-rate-limit=20on=20all=20i?= =?UTF-8?q?nvoke=E2=86=92page=5Fin=20paths;=20loop=20eviction=20to=20preve?= =?UTF-8?q?nt=20used=5Fslots>total=5Fslots;=20clamp=20status=20used=5Fslot?= =?UTF-8?q?s;=20fix=20S07=20test=20to=20respect=20MAX=5FSLOTS=3D3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/worker-pool.ts | 73 +++++++++++++++++++++----------------- src/config.ts | 2 +- test/s07-list.test.ts | 11 +++--- 3 files changed, 46 insertions(+), 40 deletions(-) diff --git a/src/backend/worker-pool.ts b/src/backend/worker-pool.ts index 238c18a..2dfb826 100644 --- a/src/backend/worker-pool.ts +++ b/src/backend/worker-pool.ts @@ -56,26 +56,30 @@ export class WorkerPool implements SigilBackend { const workerName = this.getWorkerName(capability) const now = Date.now() - // Check if we need to evict - const deployed = await this.lru.countDeployed() - let evictedCapability: string | undefined + // Check if we need to evict (loop handles KV eventual-consistency skew) + let deployed = await this.lru.countDeployed() + const evictedCapabilities: string[] = [] - if (deployed >= this.config.MAX_SLOTS) { + while (deployed >= this.config.MAX_SLOTS) { const candidate = await this.lru.findEvictionCandidate() - if (candidate) { - evictedCapability = candidate.capability - const route = await this.kv.getRoute(candidate.capability) - if (route) { - await this.cfApi.deleteWorker(route.worker_name) - } - await this.kv.setLru(candidate.capability, { - ...(await this.kv.getLru(candidate.capability))!, - deployed: false, - }) - await this.kv.incrementEvictionCount() + if (!candidate) break // nothing evictable + + evictedCapabilities.push(candidate.capability) + const route = await this.kv.getRoute(candidate.capability) + if (route) { + await this.cfApi.deleteWorker(route.worker_name) } + await this.kv.setLru(candidate.capability, { + ...(await this.kv.getLru(candidate.capability))!, + deployed: false, + }) + await this.kv.incrementEvictionCount() + + deployed = await this.lru.countDeployed() } + const evictedCapability = evictedCapabilities[0] + // Deploy the worker await this.cfApi.deployWorker(workerName, code) const subdomain = this.cfApi.getWorkerSubdomain(workerName) @@ -167,27 +171,30 @@ export class WorkerPool implements SigilBackend { } private async doPageIn(capability: string, code: string): Promise { - // Check rate limit + // Check rate limit BEFORE eviction/deployment await this.lru.checkPageRate() - // Check if eviction needed - const deployed = await this.lru.countDeployed() - if (deployed >= this.config.MAX_SLOTS) { + // Evict until we have a free slot (loop handles KV eventual-consistency skew) + let deployed = await this.lru.countDeployed() + while (deployed >= this.config.MAX_SLOTS) { const candidate = await this.lru.findEvictionCandidate() - if (candidate) { - const route = await this.kv.getRoute(candidate.capability) - if (route) { - await this.cfApi.deleteWorker(route.worker_name) - } - const existingLru = await this.kv.getLru(candidate.capability) - if (existingLru) { - await this.kv.setLru(candidate.capability, { - ...existingLru, - deployed: false, - }) - } - await this.kv.incrementEvictionCount() + if (!candidate) break // no evictable candidate — proceed anyway + + const route = await this.kv.getRoute(candidate.capability) + if (route) { + await this.cfApi.deleteWorker(route.worker_name) } + const existingLru = await this.kv.getLru(candidate.capability) + if (existingLru) { + await this.kv.setLru(candidate.capability, { + ...existingLru, + deployed: false, + }) + } + await this.kv.incrementEvictionCount() + + // Re-count after eviction so the while condition is accurate + deployed = await this.lru.countDeployed() } const workerName = this.getWorkerName(capability) @@ -353,7 +360,7 @@ export class WorkerPool implements SigilBackend { return { backend: 'worker-pool', total_slots: this.config.MAX_SLOTS, - used_slots: usedSlots, + used_slots: Math.min(usedSlots, this.config.MAX_SLOTS), agents: agentSet.size, lru_enabled: true, eviction_count: evictionCount, diff --git a/src/config.ts b/src/config.ts index 2082006..5c5ff9c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,5 @@ export const CONFIG = { - MAX_SLOTS: 10, // 测试用小值,生产 ~400 + MAX_SLOTS: 3, // LRU 验证用,生产 ~400 MAX_AGENTS: 8, DEPLOY_COOLDOWN_MS: 5000, PAGE_RATE_LIMIT: 10, // 次/分钟 diff --git a/test/s07-list.test.ts b/test/s07-list.test.ts index 6dc547a..10ce707 100644 --- a/test/s07-list.test.ts +++ b/test/s07-list.test.ts @@ -22,8 +22,8 @@ describe('S7: 列出能力', () => { await auth.registerAgent('xiaoju', 'token-xiaoju') await auth.registerAgent('xiaomooo', 'token-xiaomooo') - // Deploy 3 for xiaoju - for (const name of ['ping', 'echo', 'calc']) { + // Deploy 2 for xiaoju (keep total <= MAX_SLOTS=3 to avoid eviction) + for (const name of ['ping', 'echo']) { await pool.deploy({ agent: 'xiaoju', name, @@ -32,7 +32,7 @@ describe('S7: 列出能力', () => { }) } - // Deploy 1 for xiaomooo + // Deploy 1 for xiaomooo (total = 3, exactly fills slots) await pool.deploy({ agent: 'xiaomooo', name: 'hello', @@ -50,18 +50,17 @@ describe('S7: 列出能力', () => { expect(resp.status).toBe(200) const body = await resp.json() as { capabilities: Array<{ capability: string }> } - expect(body.capabilities).toHaveLength(3) + expect(body.capabilities).toHaveLength(2) const names = body.capabilities.map(c => c.capability) expect(names).toContain('xiaoju--ping') expect(names).toContain('xiaoju--echo') - expect(names).toContain('xiaoju--calc') expect(names).not.toContain('xiaomooo--hello') }) it('should include capability metadata in response', async () => { const caps = await pool.list('xiaoju') - expect(caps.length).toBe(3) + expect(caps.length).toBe(2) for (const cap of caps) { expect(cap.agent).toBe('xiaoju') expect(cap.type).toBe('normal')