mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-19 23:06:49 +00:00
perf(statuses): align stack-status cache TTL and invalidation with polling (#1814)
The 3s stack-statuses cache TTL never survived the 10s dashboard poll, so every ordinary poll recomputed. Raise the TTL to 15s and move the git-source label and self-identity enrichment inside the cached payload so cache hits serve fully decorated statuses with zero per-request work. Invalidation closes the gaps the longer TTL would otherwise widen: - DockerEventService drops stack-statuses:<nodeId> on container state events so the UI's state-invalidate refetch recomputes instead of hitting a stale entry. The narrow key only: container events do not reshape stack identity or file roots, and the stats key self-refreshes on its own 2s TTL. - git-source link and unlink invalidate node caches before responding, so the source label stays fresh without waiting for the TTL. - a payload whose enrichment degraded (identity probe failure or git-source scan failure) is never cached, so a mislabeled not-self or 'local' badge cannot persist for a full TTL window. Running outside Docker is not degradation, so host installs cache normally.
This commit is contained in:
@@ -44,4 +44,8 @@ export const MFA_REPLAY_PURGE_INTERVAL_MS = 60 * 1000;
|
||||
// Keys are per-node: "stats:<nodeId>", "system-stats:<nodeId>", "stack-statuses:<nodeId>".
|
||||
export const STATS_CACHE_TTL_MS = 2_000;
|
||||
export const SYSTEM_STATS_CACHE_TTL_MS = 3_000;
|
||||
export const STACK_STATUSES_CACHE_TTL_MS = 3_000;
|
||||
// Stack statuses are cached past the frontend dashboard poll cadence (10s),
|
||||
// so ordinary polls hit instead of recomputing. Container events and
|
||||
// lifecycle mutations invalidate the key; 15s bounds worst-case staleness for
|
||||
// any missed invalidation path.
|
||||
export const STACK_STATUSES_CACHE_TTL_MS = 15_000;
|
||||
|
||||
@@ -50,19 +50,18 @@ function workingDirMatchesStack(workingDir: string | undefined, stackName: strin
|
||||
}
|
||||
|
||||
async function getRunningContainerLabels(): Promise<Record<string, string> | null> {
|
||||
try {
|
||||
const runtimeIds = await getRuntimeContainerIdCandidates();
|
||||
if (runtimeIds.length === 0) return null;
|
||||
const runtimeIds = await getRuntimeContainerIdCandidates();
|
||||
if (runtimeIds.length === 0) return null;
|
||||
|
||||
const containers = await DockerController.getInstance().getDocker().listContainers({ all: true }) as ListedContainer[];
|
||||
const selfContainer = containers.find((container) => {
|
||||
const containerId = container.Id;
|
||||
return typeof containerId === 'string' && runtimeIds.some(id => matchesContainerId(containerId, id));
|
||||
});
|
||||
return selfContainer?.Labels ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
// A listContainers failure (Docker socket unreachable) propagates so
|
||||
// resolveSelfStackIdentity can mark the resolution degraded. Callers that
|
||||
// need null-on-failure wrap this call themselves.
|
||||
const containers = await DockerController.getInstance().getDocker().listContainers({ all: true }) as ListedContainer[];
|
||||
const selfContainer = containers.find((container) => {
|
||||
const containerId = container.Id;
|
||||
return typeof containerId === 'string' && runtimeIds.some(id => matchesContainerId(containerId, id));
|
||||
});
|
||||
return selfContainer?.Labels ?? null;
|
||||
}
|
||||
|
||||
async function runningContainerMatchesStack(stackName: string, composeDir?: string): Promise<boolean> {
|
||||
@@ -89,10 +88,14 @@ export async function getSelfStackProjectName(): Promise<string | null> {
|
||||
|
||||
/** Directory name of the running Sencho compose project, when it is under COMPOSE_DIR. */
|
||||
export async function getSelfStackDirectoryName(composeDir?: string): Promise<string | null> {
|
||||
const labels = await getRunningContainerLabels();
|
||||
const workingDirStack = stackNameFromWorkingDir(labels?.['com.docker.compose.project.working_dir'], composeDir);
|
||||
if (workingDirStack) return workingDirStack;
|
||||
return getSelfStackProjectName();
|
||||
try {
|
||||
const labels = await getRunningContainerLabels();
|
||||
const workingDirStack = stackNameFromWorkingDir(labels?.['com.docker.compose.project.working_dir'], composeDir);
|
||||
if (workingDirStack) return workingDirStack;
|
||||
return getSelfStackProjectName();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** True when the stack appears to be the running Sencho compose project. */
|
||||
@@ -117,25 +120,46 @@ export async function isSelfStack(stackName: string, composeDir?: string): Promi
|
||||
export interface SelfStackIdentity {
|
||||
projectName: string | null;
|
||||
labels: Record<string, string> | null;
|
||||
/**
|
||||
* True when the container-labels probe failed (Docker socket unreachable).
|
||||
* A degraded identity cannot be trusted to classify every stack correctly,
|
||||
* so it must not be cached: the next request should re-resolve. Running
|
||||
* outside Docker is NOT degraded: both sources legitimately resolve to
|
||||
* null there and the identity is correct as-is.
|
||||
*/
|
||||
degraded: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Resolves both identity sources. A labels-probe failure degrades only that
|
||||
* source to null and marks the resolution degraded, matching the old
|
||||
* per-stack behavior where a labels failure never discarded the resolved
|
||||
* project name. getSelfStackProjectName swallows its own failures and
|
||||
* returns null, so it cannot mark the identity degraded.
|
||||
*/
|
||||
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) => {
|
||||
const projectName = await getSelfStackProjectName();
|
||||
let labels: Record<string, string> | null = null;
|
||||
let degraded = false;
|
||||
try {
|
||||
labels = await getRunningContainerLabels();
|
||||
} catch (error) {
|
||||
console.error('Failed to resolve self-stack container labels; self-stack check degraded:', error);
|
||||
return null;
|
||||
});
|
||||
return { projectName, labels };
|
||||
degraded = true;
|
||||
}
|
||||
return { projectName, labels, degraded };
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity used when no resolution is attempted (empty fleet). Not degraded:
|
||||
* with no stacks there is nothing to mislabel, so the payload caches as-is.
|
||||
*/
|
||||
export const UNRESOLVED_SELF_STACK_IDENTITY: SelfStackIdentity = {
|
||||
projectName: null,
|
||||
labels: null,
|
||||
degraded: false,
|
||||
};
|
||||
|
||||
/** 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;
|
||||
|
||||
Reference in New Issue
Block a user