Skip to content

Commit 706a059

Browse files
committed
wip
1 parent 1d06af7 commit 706a059

File tree

2 files changed

+110
-32
lines changed

2 files changed

+110
-32
lines changed

src/utils/localstack-status.ts

Lines changed: 109 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import type { Disposable, LogOutputChannel } from "vscode";
2+
import { de } from "zod/v4/locales";
23

34
import type {
45
ContainerStatus,
56
ContainerStatusTracker,
67
} from "./container-status.ts";
78
import { createEmitter } from "./emitter.ts";
9+
import { fetchHealth } from "./manage.ts";
810
import type { TimeTracker } from "./time-tracker.ts";
911

1012
export type LocalStackStatus = "starting" | "running" | "stopping" | "stopped";
@@ -18,16 +20,19 @@ export interface LocalStackStatusTracker extends Disposable {
1820
/**
1921
* Checks the status of the LocalStack instance in realtime.
2022
*/
21-
export async function createLocalStackStatusTracker(
23+
export function createLocalStackStatusTracker(
2224
containerStatusTracker: ContainerStatusTracker,
2325
outputChannel: LogOutputChannel,
2426
timeTracker: TimeTracker,
25-
): Promise<LocalStackStatusTracker> {
27+
): LocalStackStatusTracker {
2628
let containerStatus: ContainerStatus | undefined;
2729
let status: LocalStackStatus | undefined;
2830
const emitter = createEmitter<LocalStackStatus>(outputChannel);
2931

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

3237
const setStatus = (newStatus: LocalStackStatus) => {
3338
if (status !== newStatus) {
@@ -37,7 +42,11 @@ export async function createLocalStackStatusTracker(
3742
};
3843

3944
const deriveStatus = () => {
40-
const newStatus = getLocalStackStatus(containerStatus, healthCheck, status);
45+
const newStatus = getLocalStackStatus(
46+
containerStatus,
47+
healthCheckStatusTracker.status(),
48+
status,
49+
);
4150
setStatus(newStatus);
4251
};
4352

@@ -48,15 +57,26 @@ export async function createLocalStackStatusTracker(
4857
}
4958
});
5059

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

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

6282
return {
@@ -77,18 +97,18 @@ export async function createLocalStackStatusTracker(
7797
}
7898
},
7999
dispose() {
80-
clearTimeout(healthCheckTimeout);
100+
healthCheckStatusTracker.dispose();
81101
},
82102
};
83103
}
84104

85105
function getLocalStackStatus(
86106
containerStatus: ContainerStatus | undefined,
87-
healthCheck: boolean | undefined,
107+
healthStatus: HealthStatus | undefined,
88108
previousStatus?: LocalStackStatus,
89109
): LocalStackStatus {
90110
if (containerStatus === "running") {
91-
if (healthCheck === true) {
111+
if (healthStatus === "healthy") {
92112
return "running";
93113
} else {
94114
// When the LS container is running, and the health check fails:
@@ -106,20 +126,79 @@ function getLocalStackStatus(
106126
}
107127
}
108128

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,
129+
type HealthStatus = "healthy" | "unhealthy";
130+
131+
interface HealthStatusTracker extends Disposable {
132+
status(): HealthStatus | undefined;
133+
start(): void;
134+
stop(): void;
135+
onChange(callback: (status: HealthStatus | undefined) => void): void;
136+
}
137+
138+
function createHealthStatusTracker(
139+
outputChannel: LogOutputChannel,
140+
timeTracker: TimeTracker,
141+
): HealthStatusTracker {
142+
let status: HealthStatus | undefined;
143+
const emitter = createEmitter<HealthStatus | undefined>(outputChannel);
144+
145+
let healthCheckTimeout: NodeJS.Timeout | undefined;
146+
147+
const updateStatus = (newStatus: HealthStatus | undefined) => {
148+
if (status !== newStatus) {
149+
status = newStatus;
150+
void emitter.emit(status);
151+
}
152+
};
153+
154+
const fetchAndUpdateStatus = async () => {
155+
await timeTracker.run("localstack-status.health", async () => {
156+
const newStatus = (await fetchHealth()) ? "healthy" : "unhealthy";
157+
updateStatus(newStatus);
120158
});
121-
return response.ok;
122-
} catch (err) {
123-
return false;
124-
}
159+
};
160+
161+
let enqueueAgain = false;
162+
163+
const enqueueUpdateStatus = () => {
164+
if (healthCheckTimeout) {
165+
return;
166+
}
167+
168+
healthCheckTimeout = setTimeout(() => {
169+
void fetchAndUpdateStatus().then(() => {
170+
if (!enqueueAgain) {
171+
return;
172+
}
173+
174+
healthCheckTimeout = undefined;
175+
enqueueUpdateStatus();
176+
});
177+
}, 1_000);
178+
};
179+
180+
return {
181+
status() {
182+
return status;
183+
},
184+
start() {
185+
enqueueAgain = true;
186+
enqueueUpdateStatus();
187+
},
188+
stop() {
189+
status = undefined;
190+
enqueueAgain = false;
191+
clearTimeout(healthCheckTimeout);
192+
healthCheckTimeout = undefined;
193+
},
194+
onChange(callback) {
195+
emitter.on(callback);
196+
if (status) {
197+
callback(status);
198+
}
199+
},
200+
dispose() {
201+
clearTimeout(healthCheckTimeout);
202+
},
203+
};
125204
}

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)