From 706a059f88678712e89e6ea79ba34cb21323cb08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Mon, 8 Sep 2025 15:58:14 +0200 Subject: [PATCH 1/5] wip --- src/utils/localstack-status.ts | 139 ++++++++++++++++++++++++++------- src/utils/manage.ts | 3 +- 2 files changed, 110 insertions(+), 32 deletions(-) diff --git a/src/utils/localstack-status.ts b/src/utils/localstack-status.ts index bfd63a2..5d280de 100644 --- a/src/utils/localstack-status.ts +++ b/src/utils/localstack-status.ts @@ -1,10 +1,12 @@ import type { Disposable, LogOutputChannel } from "vscode"; +import { de } from "zod/v4/locales"; import type { ContainerStatus, ContainerStatusTracker, } from "./container-status.ts"; import { createEmitter } from "./emitter.ts"; +import { fetchHealth } from "./manage.ts"; import type { TimeTracker } from "./time-tracker.ts"; export type LocalStackStatus = "starting" | "running" | "stopping" | "stopped"; @@ -18,16 +20,19 @@ export interface LocalStackStatusTracker extends Disposable { /** * Checks the status of the LocalStack instance in realtime. */ -export async function createLocalStackStatusTracker( +export function createLocalStackStatusTracker( containerStatusTracker: ContainerStatusTracker, outputChannel: LogOutputChannel, timeTracker: TimeTracker, -): Promise { +): LocalStackStatusTracker { let containerStatus: ContainerStatus | undefined; let status: LocalStackStatus | undefined; const emitter = createEmitter(outputChannel); - let healthCheck: boolean | undefined; + const healthCheckStatusTracker = createHealthStatusTracker( + outputChannel, + timeTracker, + ); const setStatus = (newStatus: LocalStackStatus) => { if (status !== newStatus) { @@ -37,7 +42,11 @@ export async function createLocalStackStatusTracker( }; const deriveStatus = () => { - const newStatus = getLocalStackStatus(containerStatus, healthCheck, status); + const newStatus = getLocalStackStatus( + containerStatus, + healthCheckStatusTracker.status(), + status, + ); setStatus(newStatus); }; @@ -48,15 +57,26 @@ export async function createLocalStackStatusTracker( } }); - let healthCheckTimeout: NodeJS.Timeout | undefined; - const startHealthCheck = async () => { - healthCheck = await fetchHealth(); - deriveStatus(); - healthCheckTimeout = setTimeout(() => void startHealthCheck(), 1_000); - }; + emitter.on((newStatus) => { + outputChannel.info(`localstack=${newStatus}`); - await timeTracker.run("localstack-status.healthCheck", async () => { - await startHealthCheck(); + if (newStatus === "running") { + healthCheckStatusTracker.stop(); + } + }); + + containerStatusTracker.onChange((newContainerStatus) => { + outputChannel.info( + `container=${newContainerStatus} (localstack=${status})`, + ); + + if (newContainerStatus === "running" && status !== "running") { + healthCheckStatusTracker.start(); + } + }); + + healthCheckStatusTracker.onChange(() => { + deriveStatus(); }); return { @@ -77,18 +97,18 @@ export async function createLocalStackStatusTracker( } }, dispose() { - clearTimeout(healthCheckTimeout); + healthCheckStatusTracker.dispose(); }, }; } function getLocalStackStatus( containerStatus: ContainerStatus | undefined, - healthCheck: boolean | undefined, + healthStatus: HealthStatus | undefined, previousStatus?: LocalStackStatus, ): LocalStackStatus { if (containerStatus === "running") { - if (healthCheck === true) { + if (healthStatus === "healthy") { return "running"; } else { // When the LS container is running, and the health check fails: @@ -106,20 +126,79 @@ function getLocalStackStatus( } } -async function fetchHealth(): Promise { - // Abort the fetch if it takes more than 500ms. - const controller = new AbortController(); - setTimeout(() => controller.abort(), 500); - - try { - // health is ok in the majority of use cases, however, determining status based on it can be flaky. - // for example, if localstack becomes unhealthy while running for reasons other that stop then reporting "stopping" may be misleading. - // though we don't know if it happens often. - const response = await fetch("http://localhost:4566/_localstack/health", { - signal: controller.signal, +type HealthStatus = "healthy" | "unhealthy"; + +interface HealthStatusTracker extends Disposable { + status(): HealthStatus | undefined; + start(): void; + stop(): void; + onChange(callback: (status: HealthStatus | undefined) => void): void; +} + +function createHealthStatusTracker( + outputChannel: LogOutputChannel, + timeTracker: TimeTracker, +): HealthStatusTracker { + let status: HealthStatus | undefined; + const emitter = createEmitter(outputChannel); + + let healthCheckTimeout: NodeJS.Timeout | undefined; + + const updateStatus = (newStatus: HealthStatus | undefined) => { + if (status !== newStatus) { + status = newStatus; + void emitter.emit(status); + } + }; + + const fetchAndUpdateStatus = async () => { + await timeTracker.run("localstack-status.health", async () => { + const newStatus = (await fetchHealth()) ? "healthy" : "unhealthy"; + updateStatus(newStatus); }); - return response.ok; - } catch (err) { - return false; - } + }; + + let enqueueAgain = false; + + const enqueueUpdateStatus = () => { + if (healthCheckTimeout) { + return; + } + + healthCheckTimeout = setTimeout(() => { + void fetchAndUpdateStatus().then(() => { + if (!enqueueAgain) { + return; + } + + healthCheckTimeout = undefined; + enqueueUpdateStatus(); + }); + }, 1_000); + }; + + return { + status() { + return status; + }, + start() { + enqueueAgain = true; + enqueueUpdateStatus(); + }, + stop() { + status = undefined; + enqueueAgain = false; + clearTimeout(healthCheckTimeout); + healthCheckTimeout = undefined; + }, + onChange(callback) { + emitter.on(callback); + if (status) { + callback(status); + } + }, + dispose() { + clearTimeout(healthCheckTimeout); + }, + }; } diff --git a/src/utils/manage.ts b/src/utils/manage.ts index c650bd4..2a9931e 100644 --- a/src/utils/manage.ts +++ b/src/utils/manage.ts @@ -5,14 +5,13 @@ import { commands, env, Uri, window } from "vscode"; import { spawnLocalStack } from "./cli.ts"; import { exec } from "./exec.ts"; import { checkIsLicenseValid } from "./license.ts"; -import { spawn } from "./spawn.ts"; import type { Telemetry } from "./telemetry.ts"; export type LocalstackStatus = "running" | "starting" | "stopping" | "stopped"; let previousStatus: LocalstackStatus | undefined; -async function fetchHealth(): Promise { +export async function fetchHealth(): Promise { // health is ok in the majority of use cases, however, determining status based on it can be flaky. // for example, if localstack becomes unhealthy while running for reasons other that stop then reporting "stopping" may be misleading. // though we don't know if it happens often. From d42ae29a6f1dc4d7eb1f932f94e1742d8b858af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Mon, 8 Sep 2025 16:00:44 +0200 Subject: [PATCH 2/5] wip --- src/utils/localstack-status.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/utils/localstack-status.ts b/src/utils/localstack-status.ts index 5d280de..9ad8d64 100644 --- a/src/utils/localstack-status.ts +++ b/src/utils/localstack-status.ts @@ -58,7 +58,7 @@ export function createLocalStackStatusTracker( }); emitter.on((newStatus) => { - outputChannel.info(`localstack=${newStatus}`); + outputChannel.trace(`[localstack-status] localstack=${newStatus}`); if (newStatus === "running") { healthCheckStatusTracker.stop(); @@ -66,8 +66,8 @@ export function createLocalStackStatusTracker( }); containerStatusTracker.onChange((newContainerStatus) => { - outputChannel.info( - `container=${newContainerStatus} (localstack=${status})`, + outputChannel.trace( + `[localstack-status] container=${newContainerStatus} (localstack=${status})`, ); if (newContainerStatus === "running" && status !== "running") { @@ -159,20 +159,16 @@ function createHealthStatusTracker( }; let enqueueAgain = false; - const enqueueUpdateStatus = () => { if (healthCheckTimeout) { return; } - healthCheckTimeout = setTimeout(() => { + healthCheckTimeout = setInterval(() => { void fetchAndUpdateStatus().then(() => { if (!enqueueAgain) { return; } - - healthCheckTimeout = undefined; - enqueueUpdateStatus(); }); }, 1_000); }; @@ -188,7 +184,7 @@ function createHealthStatusTracker( stop() { status = undefined; enqueueAgain = false; - clearTimeout(healthCheckTimeout); + clearInterval(healthCheckTimeout); healthCheckTimeout = undefined; }, onChange(callback) { From 41cb00bbccb8d12b0fc4aa8fc830487f03ed5d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Mon, 8 Sep 2025 16:03:54 +0200 Subject: [PATCH 3/5] use timeouts --- src/utils/localstack-status.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/utils/localstack-status.ts b/src/utils/localstack-status.ts index 9ad8d64..a3c0722 100644 --- a/src/utils/localstack-status.ts +++ b/src/utils/localstack-status.ts @@ -159,16 +159,20 @@ function createHealthStatusTracker( }; let enqueueAgain = false; + const enqueueUpdateStatus = () => { if (healthCheckTimeout) { return; } - healthCheckTimeout = setInterval(() => { + healthCheckTimeout = setTimeout(() => { void fetchAndUpdateStatus().then(() => { if (!enqueueAgain) { return; } + + healthCheckTimeout = undefined; + enqueueUpdateStatus(); }); }, 1_000); }; @@ -184,7 +188,7 @@ function createHealthStatusTracker( stop() { status = undefined; enqueueAgain = false; - clearInterval(healthCheckTimeout); + clearTimeout(healthCheckTimeout); healthCheckTimeout = undefined; }, onChange(callback) { From 574ac736abb27b90925c1eb7fd8e6f83f9622571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Mon, 8 Sep 2025 16:31:03 +0200 Subject: [PATCH 4/5] fix --- src/extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index 059251c..1eeac76 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -53,7 +53,7 @@ export async function activate(context: ExtensionContext) { ); context.subscriptions.push(containerStatusTracker); - const localStackStatusTracker = await createLocalStackStatusTracker( + const localStackStatusTracker = createLocalStackStatusTracker( containerStatusTracker, outputChannel, timeTracker, From 9faca994e1373226bf9705a05fe7ad06d06f2ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Tue, 9 Sep 2025 09:36:46 +0200 Subject: [PATCH 5/5] Update src/utils/localstack-status.ts Co-authored-by: Misha Tiurin <650819+tiurin@users.noreply.github.com> --- src/utils/localstack-status.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/localstack-status.ts b/src/utils/localstack-status.ts index a3c0722..8084fa6 100644 --- a/src/utils/localstack-status.ts +++ b/src/utils/localstack-status.ts @@ -1,5 +1,4 @@ import type { Disposable, LogOutputChannel } from "vscode"; -import { de } from "zod/v4/locales"; import type { ContainerStatus,