mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 14:14:47 +00:00
fix(bar): avoid blocking on lower-priority probes (#1548)
Bar server probing no longer blocks on lower-priority probes.
This commit is contained in:
1 parent
639334b40b
commit
8bd289bfae
2 files changed
+78
-6
No files matched your search
@@ -34,9 +34,9 @@ export function resolveBarPort(ccsDir: string): number | null {
|
|||||||
*
|
*
|
||||||
* Both IPv4 (127.0.0.1) and IPv6 (::1) loopback addresses are probed for each
|
* Both IPv4 (127.0.0.1) and IPv6 (::1) loopback addresses are probed for each
|
||||||
* port. All probes are fired concurrently so worst-case latency is ~1.5 s
|
* port. All probes are fired concurrently so worst-case latency is ~1.5 s
|
||||||
* (one timeout) rather than N × 1.5 s sequentially. Priority selection is
|
* (one timeout) rather than N × 1.5 s sequentially. Results are awaited in
|
||||||
* applied after all results are in: the bar.json port is preferred over the
|
* priority order so a lower-priority slow or streaming response cannot block
|
||||||
* defaults, and within a port 127.0.0.1 is preferred over [::1].
|
* returning an already-known higher-priority hit.
|
||||||
*/
|
*/
|
||||||
export async function defaultFindRunningServer(ccsDir: string): Promise<DashboardInfo | null> {
|
export async function defaultFindRunningServer(ccsDir: string): Promise<DashboardInfo | null> {
|
||||||
const { request } = await import('undici');
|
const { request } = await import('undici');
|
||||||
@@ -48,7 +48,22 @@ export async function defaultFindRunningServer(ccsDir: string): Promise<Dashboar
|
|||||||
headersTimeout: 1500,
|
headersTimeout: 1500,
|
||||||
bodyTimeout: 1500,
|
bodyTimeout: 1500,
|
||||||
});
|
});
|
||||||
await body.text();
|
|
||||||
|
// The summary endpoint only needs the status code for liveness. Do not
|
||||||
|
// buffer the response body: a non-CCS loopback service can stream forever
|
||||||
|
// and keep discovery from returning a higher-priority hit. Release the
|
||||||
|
// response as soon as headers arrive.
|
||||||
|
try {
|
||||||
|
if (typeof body.resume === 'function') {
|
||||||
|
body.resume();
|
||||||
|
} else if (typeof body.destroy === 'function') {
|
||||||
|
body.destroy();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Closing the response is a best-effort cleanup; liveness depends only
|
||||||
|
// on the status code already received.
|
||||||
|
}
|
||||||
|
|
||||||
return { ok: statusCode === 200 };
|
return { ok: statusCode === 200 };
|
||||||
} catch {
|
} catch {
|
||||||
return { ok: false };
|
return { ok: false };
|
||||||
@@ -65,10 +80,10 @@ export async function defaultFindRunningServer(ccsDir: string): Promise<Dashboar
|
|||||||
{ port, baseUrl: `http://[::1]:${port}`, url: `http://[::1]:${port}/api/bar/summary` },
|
{ port, baseUrl: `http://[::1]:${port}`, url: `http://[::1]:${port}/api/bar/summary` },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const results = await Promise.all(probeTargets.map((t) => probe(t.url)));
|
const probes = probeTargets.map((t) => probe(t.url));
|
||||||
|
|
||||||
for (let i = 0; i < probeTargets.length; i++) {
|
for (let i = 0; i < probeTargets.length; i++) {
|
||||||
if (results[i].ok) {
|
if ((await probes[i]).ok) {
|
||||||
const { port, baseUrl } = probeTargets[i];
|
const { port, baseUrl } = probeTargets[i];
|
||||||
return { port, baseUrl };
|
return { port, baseUrl };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2889,3 +2889,60 @@ describe('bar install: silent-decline fix — hint on user decline (review findi
|
|||||||
expect(allOutput).not.toMatch(/Run `ccs bar` to launch later/i);
|
expect(allOutput).not.toMatch(/Run `ccs bar` to launch later/i);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('defaultFindRunningServer: streaming lower-priority probes', () => {
|
||||||
|
it('returns a higher-priority hit without waiting for lower-priority response bodies', async () => {
|
||||||
|
const ccsDir = path.join(tempHome, '.ccs');
|
||||||
|
fs.mkdirSync(ccsDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(ccsDir, 'bar.json'),
|
||||||
|
JSON.stringify({ port: 41235, baseUrl: 'http://127.0.0.1:41235', authMode: 'loopback' })
|
||||||
|
);
|
||||||
|
|
||||||
|
let highPriorityBodyDestroyed = false;
|
||||||
|
let lowerPriorityProbeStarted = false;
|
||||||
|
|
||||||
|
mock.module('undici', () => ({
|
||||||
|
request: (url: string) => {
|
||||||
|
if (url === 'http://127.0.0.1:41235/api/bar/summary') {
|
||||||
|
return Promise.resolve({
|
||||||
|
statusCode: 200,
|
||||||
|
body: {
|
||||||
|
destroy: () => {
|
||||||
|
highPriorityBodyDestroyed = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url === 'http://127.0.0.1:3000/api/bar/summary') {
|
||||||
|
lowerPriorityProbeStarted = true;
|
||||||
|
return new Promise(() => {
|
||||||
|
// Simulate a lower-priority service that never finishes responding.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve({
|
||||||
|
statusCode: 404,
|
||||||
|
body: { destroy: () => {} },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
moduleSeq++;
|
||||||
|
const { defaultFindRunningServer } = (await import(
|
||||||
|
`../../../src/commands/bar/bar-server-probe?test=${Date.now()}-${moduleSeq}`
|
||||||
|
)) as {
|
||||||
|
defaultFindRunningServer: (ccsDir: string) => Promise<{ port: number; baseUrl: string } | null>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await Promise.race([
|
||||||
|
defaultFindRunningServer(ccsDir),
|
||||||
|
new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), 250)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result).toEqual({ port: 41235, baseUrl: 'http://127.0.0.1:41235' });
|
||||||
|
expect(highPriorityBodyDestroyed).toBe(true);
|
||||||
|
expect(lowerPriorityProbeStarted).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user