-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathlocalstack-status.ts
More file actions
125 lines (111 loc) · 3.52 KB
/
localstack-status.ts
File metadata and controls
125 lines (111 loc) · 3.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import type { Disposable, LogOutputChannel } from "vscode";
import type {
ContainerStatus,
ContainerStatusTracker,
} from "./container-status.ts";
import { createEmitter } from "./emitter.ts";
import type { TimeTracker } from "./time-tracker.ts";
export type LocalStackStatus = "starting" | "running" | "stopping" | "stopped";
export interface LocalStackStatusTracker extends Disposable {
status(): LocalStackStatus;
forceContainerStatus(status: ContainerStatus): void;
onChange(callback: (status: LocalStackStatus) => void): void;
}
/**
* Checks the status of the LocalStack instance in realtime.
*/
export async function createLocalStackStatusTracker(
containerStatusTracker: ContainerStatusTracker,
outputChannel: LogOutputChannel,
timeTracker: TimeTracker,
): Promise<LocalStackStatusTracker> {
let containerStatus: ContainerStatus | undefined;
let status: LocalStackStatus | undefined;
const emitter = createEmitter<LocalStackStatus>(outputChannel);
let healthCheck: boolean | undefined;
const setStatus = (newStatus: LocalStackStatus) => {
if (status !== newStatus) {
status = newStatus;
void emitter.emit(status);
}
};
const deriveStatus = () => {
const newStatus = getLocalStackStatus(containerStatus, healthCheck, status);
setStatus(newStatus);
};
containerStatusTracker.onChange((newContainerStatus) => {
if (containerStatus !== newContainerStatus) {
containerStatus = newContainerStatus;
deriveStatus();
}
});
let healthCheckTimeout: NodeJS.Timeout | undefined;
const startHealthCheck = async () => {
healthCheck = await fetchHealth();
deriveStatus();
healthCheckTimeout = setTimeout(() => void startHealthCheck(), 1_000);
};
await timeTracker.run("localstack-status.healthCheck", async () => {
await startHealthCheck();
});
return {
status() {
// biome-ignore lint/style/noNonNullAssertion: false positive
return status!;
},
forceContainerStatus(newContainerStatus) {
if (containerStatus !== newContainerStatus) {
containerStatus = newContainerStatus;
deriveStatus();
}
},
onChange(callback) {
emitter.on(callback);
if (status) {
callback(status);
}
},
dispose() {
clearTimeout(healthCheckTimeout);
},
};
}
function getLocalStackStatus(
containerStatus: ContainerStatus | undefined,
healthCheck: boolean | undefined,
previousStatus?: LocalStackStatus,
): LocalStackStatus {
if (containerStatus === "running") {
if (healthCheck === true) {
return "running";
} else {
// When the LS container is running, and the health check fails:
// - If the previous status was "running", we are likely stopping LS
// - If the previous status was "stopping", we are still stopping LS
if (previousStatus === "running" || previousStatus === "stopping") {
return "stopping";
}
return "starting";
}
} else if (containerStatus === "stopping") {
return "stopping";
} else {
return "stopped";
}
}
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,
});
return response.ok;
} catch (err) {
return false;
}
}