Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export async function activate(context: ExtensionContext) {
);
context.subscriptions.push(containerStatusTracker);

const localStackStatusTracker = await createLocalStackStatusTracker(
const localStackStatusTracker = createLocalStackStatusTracker(
containerStatusTracker,
outputChannel,
timeTracker,
Expand Down
138 changes: 108 additions & 30 deletions src/utils/localstack-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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> {
): LocalStackStatusTracker {
let containerStatus: ContainerStatus | undefined;
let status: LocalStackStatus | undefined;
const emitter = createEmitter<LocalStackStatus>(outputChannel);

let healthCheck: boolean | undefined;
const healthCheckStatusTracker = createHealthStatusTracker(
outputChannel,
timeTracker,
);

const setStatus = (newStatus: LocalStackStatus) => {
if (status !== newStatus) {
Expand All @@ -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);
};

Expand All @@ -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 {
Expand All @@ -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:
Expand All @@ -106,20 +125,79 @@ function getLocalStackStatus(
}
}

async function fetchHealth(): Promise<boolean> {
// 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<HealthStatus | undefined>(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);
},
};
}
3 changes: 1 addition & 2 deletions src/utils/manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> {
export async function fetchHealth(): Promise<boolean> {
// 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.
Expand Down
Loading