Files
openchamber/packages/web/server/lib/event-stream/upstream-reader.test.js
T
Shyamalan KannanandBohdan Triapitsyn a9ee499ae4 fix: add concurrency controls for multiple sessions using the same provider (#1069)
* fix: add concurrency controls for multiple sessions using the same provider

Adds OS-inspired scheduling primitives (from HiveMind/AIMD research) to prevent
concurrent sessions from the same provider from experiencing slowdowns, random
stops, and cascading failures.

Server-side:
- Health check skips OpenCode restart when sessions are actively busy — a busy
  server under concurrent load can fail the health check timeout without being
  dead. Staleness guard forces restart if unhealthy+busy persists >2 minutes.
- Upstream SSE stall timeout scaled from 20s to 60s to avoid unnecessary
  reconnections when multiple sessions are waiting for LLM responses.

Client-side (HiveMind primitives, arXiv:2604.17111):
- Transparent retry with exponential backoff (1s→2s→4s, max 32s) for
  429/502/503/504 errors — the #1 most effective primitive from the paper.
- Circuit breaker: opens after 3 consecutive retryable errors, cooldown
  doubles each trip (30s→60s→120s, capped 128s), matching TCP AIMD.
- Per-provider session tracking with TTL eviction (1h idle sweep).
- Fetch-level retry gated on AbortError/TypeError only (not DNS failures).

Refs github-code-review skill findings (all 8 issues resolved).

* fix: use definite assignment assertion for response variable

Fixes TS2454: Variable 'response' is used before being assigned
in strict mode. The for-loop body always assigns it on every path
that reaches the post-loop code, but TS can't prove that.

* fix: add cleanupSession to error paths and remove unreachable code

P1 fixes (Greptile review):
- cleanupSession called on fetch error throw path
- cleanupSession called on non-retryable HTTP error throw path
- Removed unreachable post-loop code (loop always terminates via return or throw)

Adds explicit post-loop throw to satisfy TypeScript strict return check.

* fix: address Greptile review feedback on concurrent session controls

Removes client-side session tracking that leaked on normal completion paths.

The session tracking was redundant — the server-side health check already reads from

sessionRuntime.getSessionActivitySnapshot() for busy-session detection.

Changes:

- Remove activeSessions Set and all session-tracking functions from provider-tracker

- Remove trackSessionStarted/cleanupSession calls from client.ts

- Remove unreachable (response as Response) block after retry loop

- Make upstreamStallTimeoutMs conditional: 60s when >1 sessions, 20s otherwise

Refs #1069

* fix: enforce dynamic concurrency safeguards

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-05-01 14:57:31 +03:00

205 lines
5.3 KiB
JavaScript

import { describe, expect, it } from 'vitest';
import { createUpstreamSseReader } from './upstream-reader.js';
function createSseResponse({ blocks = [], signal, holdOpen = false }) {
const encoder = new TextEncoder();
let index = 0;
return {
ok: true,
status: 200,
body: {
getReader() {
return {
async read() {
if (index < blocks.length) {
return { value: encoder.encode(blocks[index++]), done: false };
}
if (!holdOpen) {
return { value: undefined, done: true };
}
return new Promise((_resolve, reject) => {
const onAbort = () => {
signal.removeEventListener('abort', onAbort);
const error = new Error('Aborted');
error.name = 'AbortError';
reject(error);
};
signal.addEventListener('abort', onAbort, { once: true });
});
},
};
},
},
};
}
describe('createUpstreamSseReader', () => {
it('emits parsed events and tracks the latest event id', async () => {
const events = [];
let reader;
reader = createUpstreamSseReader({
buildUrl: () => 'http://127.0.0.1:4096/global/event',
reconnectDelayMs: 0,
fetchImpl: async (_url, options) => createSseResponse({
signal: options.signal,
blocks: [
'id: evt-1\r\ndata: {"type":"server.connected","properties":{"directory":"/tmp/project"}}\r\n\r\n',
],
}),
onEvent(event) {
events.push(event);
reader.stop();
},
});
await reader.start();
expect(events).toHaveLength(1);
expect(events[0].eventId).toBe('evt-1');
expect(events[0].directory).toBe('/tmp/project');
expect(events[0].payload).toEqual({
type: 'server.connected',
properties: {
directory: '/tmp/project',
},
});
expect(reader.getLastEventId()).toBe('evt-1');
});
it('reconnects a stalled stream with Last-Event-ID', async () => {
const fetchLastEventIds = [];
const events = [];
let attempt = 0;
let reader;
reader = createUpstreamSseReader({
buildUrl: () => 'http://127.0.0.1:4096/global/event',
stallTimeoutMs: 10,
reconnectDelayMs: 0,
fetchImpl: async (_url, options) => {
fetchLastEventIds.push(options.headers['Last-Event-ID'] ?? null);
attempt += 1;
if (attempt === 1) {
return createSseResponse({
signal: options.signal,
holdOpen: true,
blocks: [
'id: evt-1\ndata: {"type":"server.connected","properties":{}}\n\n',
],
});
}
return createSseResponse({
signal: options.signal,
blocks: [
'id: evt-2\ndata: {"type":"session.updated","properties":{}}\n\n',
],
});
},
onEvent(event) {
events.push(event.eventId);
if (event.eventId === 'evt-2') {
reader.stop();
}
},
});
await reader.start();
expect(events).toEqual(['evt-1', 'evt-2']);
expect(fetchLastEventIds.slice(0, 2)).toEqual([null, 'evt-1']);
expect(reader.getLastEventId()).toBe('evt-2');
});
it('resolves the stall timeout for each upstream read window', async () => {
const events = [];
let attempt = 0;
let currentTimeout = 10;
let reader;
reader = createUpstreamSseReader({
buildUrl: () => 'http://127.0.0.1:4096/global/event',
stallTimeoutMs: () => currentTimeout,
reconnectDelayMs: 0,
fetchImpl: async (_url, options) => {
attempt += 1;
if (attempt === 1) {
currentTimeout = 60;
return createSseResponse({
signal: options.signal,
holdOpen: true,
blocks: [
'id: evt-1\ndata: {"type":"server.connected","properties":{}}\n\n',
],
});
}
return createSseResponse({
signal: options.signal,
blocks: [
'id: evt-2\ndata: {"type":"session.updated","properties":{}}\n\n',
],
});
},
onEvent(event) {
events.push(event.eventId);
if (event.eventId === 'evt-2') {
reader.stop();
}
},
});
await reader.start();
expect(events).toEqual(['evt-1', 'evt-2']);
expect(attempt).toBe(2);
});
it('reports unavailable upstream responses and continues reconnecting until stopped', async () => {
const errors = [];
let attempt = 0;
let reader;
reader = createUpstreamSseReader({
buildUrl: () => 'http://127.0.0.1:4096/global/event',
reconnectDelayMs: 0,
fetchImpl: async (_url, options) => {
attempt += 1;
if (attempt === 1) {
return { ok: false, status: 503, body: null };
}
return createSseResponse({
signal: options.signal,
blocks: [
'id: evt-1\ndata: {"type":"server.connected","properties":{}}\n\n',
],
});
},
onError(error) {
errors.push(error);
},
onEvent() {
reader.stop();
},
});
await reader.start();
expect(errors).toEqual([
expect.objectContaining({
type: 'upstream_unavailable',
status: 503,
}),
]);
expect(attempt).toBe(2);
});
});