perf(stacks): remove repeated self-identity discovery from statuses hot path (#1811)

* perf(stacks): hoist self-stack identity resolution out of statuses enrichment

GET /api/stacks/statuses resolved the self-stack identity once per stack on
every request, including cache hits. The fallback path in isSelfStack lists
every container on the node, so a cache hit still paid N container-list
calls. Measured on a Docker-shaped local container with 6 stacks: cache hits
took 161-168ms with dockerMs=null; the enrichment alone was ~160ms.

Resolve the identity once per request (boot-cached compose project name plus
a single container-labels read) and compare stack names against it in the
response loop. isSelf semantics are unchanged: project-name match, then
labels project match, then working-dir basename match, with the same
fail-safe false on identity failure. Also expose an enrichmentMs subspan in
the developer-mode timing line so cache-hit cost stays decomposable.

Measured after the change in the same environment: cache hits 20-34ms
(enrichment ~20-33ms, a single listContainers call), computed 44-59ms.
Cache-hit latency improved roughly 5-8x; per-stack Docker identity discovery
is eliminated (N calls down to 1).

* test(stacks): cover the empty-stacks statuses path

The statuses handler skips self-stack identity resolution entirely when a
node has no stacks. Pin that branch: an empty response still returns 200
with {} and never resolves identity, so a future refactor cannot silently
reintroduce a node-wide container-list call on empty-node polls.
This commit is contained in:
Anso
2026-08-09 17:32:45 -04:00
committed by GitHub
parent 4119be6e86
commit 600367e66c
5 changed files with 208 additions and 3 deletions
@@ -20,6 +20,7 @@ import { setupTestDb, cleanupTestDb, TEST_USERNAME, TEST_PASSWORD } from './help
import { installArcstatsFsMock, arcstatsBody, DEFAULT_ARC_PATH, type ArcstatsFsMock } from './helpers/arcstatsFsMock';
import { GitSourceService } from '../services/GitSourceService';
import type { PublicGitSource } from '../services/GitSourceService';
import * as selfStackGuard from '../helpers/selfStackGuard';
// ── Hoisted mocks (must come before importing the app) ─────────────────
@@ -284,6 +285,63 @@ describe('GET /api/stacks/statuses caching', () => {
listSpy.mockRestore();
});
it('resolves self-stack identity once per request, cache hits included, and labels each stack from it', async () => {
mockGetStacks.mockResolvedValue(['sencho.yml', 'web.yml']);
mockGetBulkStackStatuses.mockResolvedValue({
sencho: { status: 'running' },
web: { status: 'running' },
});
const identitySpy = vi
.spyOn(selfStackGuard, 'resolveSelfStackIdentity')
.mockResolvedValue({ projectName: 'sencho', labels: null });
const first = await request(app).get('/api/stacks/statuses').set('Cookie', authCookie);
const second = await request(app).get('/api/stacks/statuses').set('Cookie', authCookie);
// Hoisted: one resolution per request (including the cache hit) serves
// every stack in the response.
expect(identitySpy).toHaveBeenCalledTimes(2);
for (const res of [first, second]) {
expect(res.body['sencho.yml'].isSelf).toBe(true);
expect(res.body['web.yml'].isSelf).toBe(false);
}
identitySpy.mockRestore();
});
it('skips identity resolution entirely when the node has no stacks', async () => {
mockGetStacks.mockResolvedValue([]);
mockGetBulkStackStatuses.mockResolvedValue({});
const identitySpy = vi.spyOn(selfStackGuard, 'resolveSelfStackIdentity');
const res = await request(app).get('/api/stacks/statuses').set('Cookie', authCookie);
expect(res.status).toBe(200);
expect(res.body).toEqual({});
expect(identitySpy).not.toHaveBeenCalled();
identitySpy.mockRestore();
});
it('serves 200 with isSelf false everywhere when identity resolution degrades', async () => {
mockGetStacks.mockResolvedValue(['web.yml', 'db.yml']);
mockGetBulkStackStatuses.mockResolvedValue({
web: { status: 'running' },
db: { status: 'running' },
});
const identitySpy = vi
.spyOn(selfStackGuard, 'resolveSelfStackIdentity')
.mockResolvedValue({ projectName: null, labels: null });
const res = await request(app).get('/api/stacks/statuses').set('Cookie', authCookie);
expect(res.status).toBe(200);
expect(res.body['web.yml'].isSelf).toBe(false);
expect(res.body['db.yml'].isSelf).toBe(false);
identitySpy.mockRestore();
});
it('falls back to local labels (200, not 500) when the git-source lookup throws', async () => {
mockGetStacks.mockResolvedValue(['web.yml']);
mockGetBulkStackStatuses.mockResolvedValue({ web: { status: 'running' } });
@@ -103,6 +103,10 @@ describe('[Stacks:debug] GET /api/stacks/statuses', () => {
expect(second).toMatch(/cacheOutcome=hit/);
// No docker call on a cache hit, so the subspan is null rather than 0.
expect(second).toMatch(/dockerMs=null/);
// Enrichment runs on every request, cache hits included, so its subspan
// is a number on both legs.
expect(first).toMatch(/enrichmentMs=\d+/);
expect(second).toMatch(/enrichmentMs=\d+/);
// The compute ran the fetcher exactly once across both requests.
expect(dockerCalls).toBe(1);
});
@@ -3,7 +3,9 @@ import SelfIdentityService from '../services/SelfIdentityService';
import DockerController from '../services/DockerController';
import {
isSelfStack,
isSelfStackByIdentity,
getSelfStackProjectName,
resolveSelfStackIdentity,
SELF_STACK_PROTECTED_CODE,
SELF_STACK_PROTECTED_MESSAGE,
selfStackProtectedBulkResult,
@@ -93,6 +95,96 @@ describe('getSelfStackProjectName', () => {
});
});
describe('resolveSelfStackIdentity + isSelfStackByIdentity', () => {
it('matches the stack whose name equals the resolved project name', async () => {
stubComposeProject('sencho');
const identity = await resolveSelfStackIdentity();
expect(isSelfStackByIdentity(identity, 'sencho')).toBe(true);
expect(isSelfStackByIdentity(identity, 'web')).toBe(false);
});
it('matches by the container compose project label when the project name is unknown', async () => {
const runtimeId = 'c'.repeat(64);
process.env.HOSTNAME = runtimeId.slice(0, 12);
stubComposeProject(null);
vi.spyOn(DockerController, 'getInstance').mockReturnValue({
getDocker: () => ({
listContainers: vi.fn().mockResolvedValue([
{
Id: runtimeId,
Labels: { 'com.docker.compose.project': 'renamed-project' },
},
]),
}),
} as unknown as ReturnType<typeof DockerController.getInstance>);
const identity = await resolveSelfStackIdentity();
expect(isSelfStackByIdentity(identity, 'renamed-project')).toBe(true);
expect(isSelfStackByIdentity(identity, 'web')).toBe(false);
});
it('matches by the working directory basename inside the compose dir', async () => {
const runtimeId = 'd'.repeat(64);
process.env.HOSTNAME = runtimeId.slice(0, 12);
stubComposeProject(null);
vi.spyOn(DockerController, 'getInstance').mockReturnValue({
getDocker: () => ({
listContainers: vi.fn().mockResolvedValue([
{
Id: runtimeId,
Labels: { 'com.docker.compose.project.working_dir': '/app/compose/sencho' },
},
]),
}),
} as unknown as ReturnType<typeof DockerController.getInstance>);
const identity = await resolveSelfStackIdentity();
expect(isSelfStackByIdentity(identity, 'sencho', '/app/compose')).toBe(true);
});
it('does not match a working directory outside the compose dir', async () => {
const runtimeId = 'g'.repeat(64);
process.env.HOSTNAME = runtimeId.slice(0, 12);
stubComposeProject(null);
vi.spyOn(DockerController, 'getInstance').mockReturnValue({
getDocker: () => ({
listContainers: vi.fn().mockResolvedValue([
{
Id: runtimeId,
Labels: { 'com.docker.compose.project.working_dir': '/srv/other/sencho' },
},
]),
}),
} as unknown as ReturnType<typeof DockerController.getInstance>);
const identity = await resolveSelfStackIdentity();
expect(isSelfStackByIdentity(identity, 'sencho', '/app/compose')).toBe(false);
});
it('is false when no identity source is available', async () => {
stubComposeProject(null);
const identity = await resolveSelfStackIdentity();
expect(isSelfStackByIdentity(identity, 'sencho')).toBe(false);
});
it('resolves identity with at most one container list read, shared by all stacks', async () => {
const runtimeId = 'e'.repeat(64);
process.env.HOSTNAME = runtimeId.slice(0, 12);
stubComposeProject('sencho');
const listContainers = vi.fn().mockResolvedValue([]);
vi.spyOn(DockerController, 'getInstance').mockReturnValue({
getDocker: () => ({ listContainers }),
} as unknown as ReturnType<typeof DockerController.getInstance>);
const identity = await resolveSelfStackIdentity();
// Reuse the resolved identity across every stack in the request.
for (const name of ['sencho', 'web', 'db', 'cache']) {
isSelfStackByIdentity(identity, name, '/app/compose');
}
expect(listContainers).toHaveBeenCalledTimes(1);
});
});
describe('selfStackProtectedBulkResult', () => {
it('returns a per-stack bulk failure with the protected code', () => {
const result = selfStackProtectedBulkResult('sencho');