Pace engine reads under the proxy's rate limit

The proxy allows 10 requests a second per address and answers the rest
503. Limiting how many reads run at once still sent 40-50 a second, since
each answers in about 100 ms. Reads now share one pace of 8 a second,
actions skip the queue, and a 503 pauses every read for 2 seconds.
Results and Schedule share each schedule's production status, and an
archived schedule is read once until an action.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Iqbal
2026-10-30 10:06:40 +07:00
parent 138be5ac96
commit 977a8fafc4
6 changed files with 121 additions and 7 deletions

View File

@@ -5,7 +5,7 @@
import { EngineError, request, sleep } from '../api/client'
import { t } from '../i18n'
import { kindLabel, type ContentKind, type Rule } from './parse'
import { expressAgents, MAX_ATTEMPTS, PLAN_ACTIVE, productionStatus, retryItem, runNow, type PlanItem, type Schedule } from './schedules'
import { expressAgents, forgetProductionReads, MAX_ATTEMPTS, PLAN_ACTIVE, productionStatus, retryItem, runNow, type PlanItem, type Schedule } from './schedules'
const AGENTS = '/organizations/current/agents'
@@ -206,13 +206,27 @@ const PER_SCHEDULE_LIMIT = 4
// The proxy's rate limit (503/429), a 5xx or no answer at all passes; a 4xx (e.g. a deleted agent) does not.
const passing = (e: unknown) => e instanceof EngineError && (e.status === 0 || e.status === 429 || e.status >= 500)
const settledReads: Set<number>[] = []
/** After any action: an archived schedule's results (a decision on one) and production plans
* (a started or retried piece) may have changed, so the next poll reads them again. */
export function forgetReads() {
for (const s of settledReads) s.clear()
forgetProductionReads()
}
/** Wraps a per-schedule read so a passing failure does not change what the page shows: a busy
* engine is asked once more after a pause, and if it still fails, the schedule's results from the
* last poll that read them are used. Dropping them instead made every count on the page dip for
* one poll and come back on the next. */
export function remembering<T>(fn: (s: Schedule) => Promise<T[]>, pause = 3000) {
const last = new Map<number, T[]>()
const settled = new Set<number>()
settledReads.push(settled)
return async (s: Schedule): Promise<T[]> => {
// An archived schedule makes nothing new, so after one read it costs nothing until an action.
const archived = s.agent.status === 'archived'
if (archived && settled.has(s.agent.id)) return last.get(s.agent.id)!
try {
const read = await fn(s).catch(async e => {
if (!(e instanceof EngineError && (e.status === 503 || e.status === 429))) throw e
@@ -220,6 +234,7 @@ export function remembering<T>(fn: (s: Schedule) => Promise<T[]>, pause = 3000)
return fn(s)
})
last.set(s.agent.id, read)
if (archived) settled.add(s.agent.id)
return read
} catch (e) {
const kept = last.get(s.agent.id)