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, diff --git a/src/utils/localstack-status.ts b/src/utils/localstack-status.ts index bfd63a2..8084fa6 100644 --- a/src/utils/localstack-status.ts +++ b/src/utils/localstack-status.ts @@ -5,6 +5,7 @@ import type { 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 +19,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 +41,11 @@ export async function createLocalStackStatusTracker( }; const deriveStatus = () => { - const newStatus = getLocalStackStatus(containerStatus, healthCheck, status); + const newStatus = getLocalStackStatus( + containerStatus, + healthCheckStatusTracker.status(), + status, + ); setStatus(newStatus); }; @@ -48,15 +56,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.trace(`[localstack-status] localstack=${newStatus}`); - await timeTracker.run("localstack-status.healthCheck", async () => { - await startHealthCheck(); + if (newStatus === "running") { + healthCheckStatusTracker.stop(); + } + }); + + containerStatusTracker.onChange((newContainerStatus) => { + outputChannel.trace( + `[localstack-status] container=${newContainerStatus} (localstack=${status})`, + ); + + if (newContainerStatus === "running" && status !== "running") { + healthCheckStatusTracker.start(); + } + }); + + healthCheckStatusTracker.onChange(() => { + deriveStatus(); }); return { @@ -77,18 +96,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 +125,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.