mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-17 05:58:37 +00:00
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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user