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
+39
View File
@@ -106,6 +106,45 @@ export async function isSelfStack(stackName: string, composeDir?: string): Promi
}
}
/**
* Identity sources for the self-stack check, resolved once and reused
* across every stack in a request (the /statuses handler resolves it once
* per request). The compose project name is a boot-cached property lookup;
* the container labels fallback (which lists every container on the node)
* is a single read shared by all stacks. Either source degrades
* independently to null, so a failure in one never discards the other.
*/
export interface SelfStackIdentity {
projectName: string | null;
labels: Record<string, string> | null;
}
/**
* Resolves both identity sources. Each failure degrades only its own
* source to null, matching the old per-stack behavior where a labels
* failure never discarded the resolved project name.
*/
export async function resolveSelfStackIdentity(): Promise<SelfStackIdentity> {
const projectName = await getSelfStackProjectName().catch((error) => {
console.error('Failed to resolve self-stack project name; self-stack check degraded:', error);
return null;
});
const labels = await getRunningContainerLabels().catch((error) => {
console.error('Failed to resolve self-stack container labels; self-stack check degraded:', error);
return null;
});
return { projectName, labels };
}
/** isSelf semantics identical to isSelfStack() when the identity resolves successfully. */
export function isSelfStackByIdentity(identity: SelfStackIdentity, stackName: string, composeDir?: string): boolean {
if (identity.projectName === stackName) return true;
const labels = identity.labels;
if (!labels) return false;
if (labels['com.docker.compose.project'] === stackName) return true;
return workingDirMatchesStack(labels['com.docker.compose.project.working_dir'], stackName, composeDir);
}
export interface SelfStackProtectedResult {
stackName: string;
ok: false;