Skip to content

Commit 4528033

Browse files
skyrpextiurin
andauthored
fix: too many health checks (#40)
The toolkit now uses health checks every second only when the LocalStack instance is starting. When the LocalStack is up and running, the health checks stop. --------- Co-authored-by: Misha Tiurin <[email protected]>
1 parent 8eb64b0 commit 4528033

File tree

3 files changed

+110
-33
lines changed

3 files changed

+110
-33
lines changed

src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export async function activate(context: ExtensionContext) {
5353
);
5454
context.subscriptions.push(containerStatusTracker);
5555

56-
const localStackStatusTracker = await createLocalStackStatusTracker(
56+
const localStackStatusTracker = createLocalStackStatusTracker(
5757
containerStatusTracker,
5858
outputChannel,
5959
timeTracker,

src/utils/localstack-status.ts

Lines changed: 108 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
ContainerStatusTracker,
66
} from "./container-status.ts";
77
import { createEmitter } from "./emitter.ts";
8+
import { fetchHealth } from "./manage.ts";
89
import type { TimeTracker } from "./time-tracker.ts";
910

1011
export type LocalStackStatus = "starting" | "running" | "stopping" | "stopped";
@@ -18,16 +19,19 @@ export interface LocalStackStatusTracker extends Disposable {
1819
/**
1920
* Checks the status of the LocalStack instance in realtime.
2021
*/
21-
export async function createLocalStackStatusTracker(
22+
export function createLocalStackStatusTracker(
2223
containerStatusTracker: ContainerStatusTracker,
2324
outputChannel: LogOutputChannel,
2425
timeTracker: TimeTracker,
25-
): Promise<LocalStackStatusTracker> {
26+
): LocalStackStatusTracker {
2627
let containerStatus: ContainerStatus | undefined;
2728
let status: LocalStackStatus | undefined;
2829
const emitter = createEmitter<LocalStackStatus>(outputChannel);
2930

30-
let healthCheck: boolean | undefined;
31+
const healthCheckStatusTracker = createHealthStatusTracker(
32+
outputChannel,
33+
timeTracker,
34+
);
3135

3236
const setStatus = (newStatus: LocalStackStatus) => {
3337
if (status !== newStatus) {
@@ -37,7 +41,11 @@ export async function createLocalStackStatusTracker(
3741
};
3842

3943
const deriveStatus = () => {
40-
const newStatus = getLocalStackStatus(containerStatus, healthCheck, status);
44+
const newStatus = getLocalStackStatus(
45+
containerStatus,
46+
healthCheckStatusTracker.status(),
47+
status,
48+
);
4149
setStatus(newStatus);
4250
};
4351

@@ -48,15 +56,26 @@ export async function createLocalStackStatusTracker(
4856
}
4957
});
5058

51-
let healthCheckTimeout: NodeJS.Timeout | undefined;
52-
const startHealthCheck = async () => {
53-
healthCheck = await fetchHealth();
54-
deriveStatus();
55-
healthCheckTimeout = setTimeout(() => void startHealthCheck(), 1_000);
56-
};
59+
emitter.on((newStatus) => {
60+
outputChannel.trace(`[localstack-status] localstack=${newStatus}`);
5761

58-
await timeTracker.run("localstack-status.healthCheck", async () => {
59-
await startHealthCheck();
62+
if (newStatus === "running") {
63+
healthCheckStatusTracker.stop();
64+
}
65+
});
66+
67+
containerStatusTracker.onChange((newContainerStatus) => {
68+
outputChannel.trace(
69+
`[localstack-status] container=${newContainerStatus} (localstack=${status})`,
70+
);
71+
72+
if (newContainerStatus === "running" && status !== "running") {
73+
healthCheckStatusTracker.start();
74+
}
75+
});
76+
77+
healthCheckStatusTracker.onChange(() => {
78+
deriveStatus();
6079
});
6180

6281
return {
@@ -77,18 +96,18 @@ export async function createLocalStackStatusTracker(
7796
}
7897
},
7998
dispose() {
80-
clearTimeout(healthCheckTimeout);
99+
healthCheckStatusTracker.dispose();
81100
},
82101
};
83102
}
84103

85104
function getLocalStackStatus(
86105
containerStatus: ContainerStatus | undefined,
87-
healthCheck: boolean | undefined,
106+
healthStatus: HealthStatus | undefined,
88107
previousStatus?: LocalStackStatus,
89108
): LocalStackStatus {
90109
if (containerStatus === "running") {
91-
if (healthCheck === true) {
110+
if (healthStatus === "healthy") {
92111
return "running";
93112
} else {
94113
// When the LS container is running, and the health check fails:
@@ -106,20 +125,79 @@ function getLocalStackStatus(
106125
}
107126
}
108127

109-
async function fetchHealth(): Promise<boolean> {
110-
// Abort the fetch if it takes more than 500ms.
111-
const controller = new AbortController();
112-
setTimeout(() => controller.abort(), 500);
113-
114-
try {
115-
// health is ok in the majority of use cases, however, determining status based on it can be flaky.
116-
// for example, if localstack becomes unhealthy while running for reasons other that stop then reporting "stopping" may be misleading.
117-
// though we don't know if it happens often.
118-
const response = await fetch("http://localhost:4566/_localstack/health", {
119-
signal: controller.signal,
128+
type HealthStatus = "healthy" | "unhealthy";
129+
130+
interface HealthStatusTracker extends Disposable {
131+
status(): HealthStatus | undefined;
132+
start(): void;
133+
stop(): void;
134+
onChange(callback: (status: HealthStatus | undefined) => void): void;
135+
}
136+
137+
function createHealthStatusTracker(
138+
outputChannel: LogOutputChannel,
139+
timeTracker: TimeTracker,
140+
): HealthStatusTracker {
141+
let status: HealthStatus | undefined;
142+
const emitter = createEmitter<HealthStatus | undefined>(outputChannel);
143+
144+
let healthCheckTimeout: NodeJS.Timeout | undefined;
145+
146+
const updateStatus = (newStatus: HealthStatus | undefined) => {
147+
if (status !== newStatus) {
148+
status = newStatus;
149+
void emitter.emit(status);
150+
}
151+
};
152+
153+
const fetchAndUpdateStatus = async () => {
154+
await timeTracker.run("localstack-status.health", async () => {
155+
const newStatus = (await fetchHealth()) ? "healthy" : "unhealthy";
156+
updateStatus(newStatus);
120157
});
121-
return response.ok;
122-
} catch (err) {
123-
return false;
124-
}
158+
};
159+
160+
let enqueueAgain = false;
161+
162+
const enqueueUpdateStatus = () => {
163+
if (healthCheckTimeout) {
164+
return;
165+
}
166+
167+
healthCheckTimeout = setTimeout(() => {
168+
void fetchAndUpdateStatus().then(() => {
169+
if (!enqueueAgain) {
170+
return;
171+
}
172+
173+
healthCheckTimeout = undefined;
174+
enqueueUpdateStatus();
175+
});
176+
}, 1_000);
177+
};
178+
179+
return {
180+
status() {
181+
return status;
182+
},
183+
start() {
184+
enqueueAgain = true;
185+
enqueueUpdateStatus();
186+
},
187+
stop() {
188+
status = undefined;
189+
enqueueAgain = false;
190+
clearTimeout(healthCheckTimeout);
191+
healthCheckTimeout = undefined;
192+
},
193+
onChange(callback) {
194+
emitter.on(callback);
195+
if (status) {
196+
callback(status);
197+
}
198+
},
199+
dispose() {
200+
clearTimeout(healthCheckTimeout);
201+
},
202+
};
125203
}

src/utils/manage.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@ import { commands, env, Uri, window } from "vscode";
55
import { spawnLocalStack } from "./cli.ts";
66
import { exec } from "./exec.ts";
77
import { checkIsLicenseValid } from "./license.ts";
8-
import { spawn } from "./spawn.ts";
98
import type { Telemetry } from "./telemetry.ts";
109

1110
export type LocalstackStatus = "running" | "starting" | "stopping" | "stopped";
1211

1312
let previousStatus: LocalstackStatus | undefined;
1413

15-
async function fetchHealth(): Promise<boolean> {
14+
export async function fetchHealth(): Promise<boolean> {
1615
// health is ok in the majority of use cases, however, determining status based on it can be flaky.
1716
// for example, if localstack becomes unhealthy while running for reasons other that stop then reporting "stopping" may be misleading.
1817
// though we don't know if it happens often.

0 commit comments

Comments
 (0)