@intuned/runtime-dev 0.0.1-split.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.babelrc +21 -0
- package/.eslintignore +10 -0
- package/.eslintrc.js +39 -0
- package/.vite/deps_temp_01af7156/package.json +3 -0
- package/.vscode/extensions.json +3 -0
- package/.vscode/launch.json +102 -0
- package/.vscode/settings.json +12 -0
- package/WebTemplate/accessKeyHelpers.ts +28 -0
- package/WebTemplate/api.ts +139 -0
- package/WebTemplate/app.ts +18 -0
- package/WebTemplate/controllers/async.ts +138 -0
- package/WebTemplate/controllers/authSessions/check.ts +68 -0
- package/WebTemplate/controllers/authSessions/create.ts +128 -0
- package/WebTemplate/controllers/authSessions/index.ts +41 -0
- package/WebTemplate/controllers/authSessions/killOperation.ts +35 -0
- package/WebTemplate/controllers/authSessions/resumeOperation.ts +80 -0
- package/WebTemplate/controllers/authSessions/store.ts +14 -0
- package/WebTemplate/controllers/controllers.ts +73 -0
- package/WebTemplate/controllers/runApi/helpers.ts +220 -0
- package/WebTemplate/controllers/runApi/index.ts +68 -0
- package/WebTemplate/controllers/runApi/types.ts +13 -0
- package/WebTemplate/controllers/traces.ts +151 -0
- package/WebTemplate/features.ts +8 -0
- package/WebTemplate/headers.ts +6 -0
- package/WebTemplate/index.playwright.ts +47 -0
- package/WebTemplate/index.vanilla.ts +44 -0
- package/WebTemplate/jobs.ts +356 -0
- package/WebTemplate/shutdown.ts +64 -0
- package/WebTemplate/utils.ts +294 -0
- package/bin/intuned-api-run +2 -0
- package/bin/intuned-auth-session-check +2 -0
- package/bin/intuned-auth-session-create +2 -0
- package/bin/intuned-auth-session-load +2 -0
- package/bin/intuned-auth-session-refresh +2 -0
- package/bin/intuned-browser-save-state +2 -0
- package/bin/intuned-browser-start +2 -0
- package/bin/intuned-build +2 -0
- package/bin/intuned-ts-check +2 -0
- package/package.json +133 -0
- package/playwright.config.ts +48 -0
- package/src/commands/api/run.ts +225 -0
- package/src/commands/auth-sessions/load.ts +42 -0
- package/src/commands/auth-sessions/run-check.ts +70 -0
- package/src/commands/auth-sessions/run-create.ts +124 -0
- package/src/commands/browser/save-state.ts +22 -0
- package/src/commands/browser/start-browser.ts +17 -0
- package/src/commands/build.ts +125 -0
- package/src/commands/common/browserUtils.ts +80 -0
- package/src/commands/common/getDefaultExportFromFile.ts +13 -0
- package/src/commands/common/getFirstLineNumber.test.ts +274 -0
- package/src/commands/common/getFirstLineNumber.ts +146 -0
- package/src/commands/common/sendMessageToClient.ts +8 -0
- package/src/commands/common/utils/fileUtils.ts +25 -0
- package/src/commands/common/utils/settings.ts +23 -0
- package/src/commands/common/utils/webTemplate.ts +46 -0
- package/src/commands/testing/saveVisibleHtml.ts +29 -0
- package/src/commands/ts-check.ts +88 -0
- package/src/common/Logger/Logger/index.ts +64 -0
- package/src/common/Logger/Logger/types.ts +9 -0
- package/src/common/Logger/index.ts +64 -0
- package/src/common/Logger/types.ts +9 -0
- package/src/common/assets/browser_scripts.js +2214 -0
- package/src/common/asyncLocalStorage/index.ts +29 -0
- package/src/common/cleanEnvironmentVariables.ts +13 -0
- package/src/common/constants.ts +1 -0
- package/src/common/contextStorageStateHelpers.ts +71 -0
- package/src/common/getPlaywrightConstructs.ts +283 -0
- package/src/common/jwtTokenManager.ts +111 -0
- package/src/common/settingsSchema.ts +16 -0
- package/src/common/telemetry.ts +49 -0
- package/src/index.ts +14 -0
- package/src/runtime/RunError.ts +16 -0
- package/src/runtime/downloadDirectory.ts +14 -0
- package/src/runtime/enums.d.ts +11 -0
- package/src/runtime/enums.ts +11 -0
- package/src/runtime/executionHelpers.test.ts +70 -0
- package/src/runtime/export.d.ts +202 -0
- package/src/runtime/extendPayload.ts +22 -0
- package/src/runtime/extendTimeout.ts +32 -0
- package/src/runtime/index.ts +8 -0
- package/src/runtime/requestMoreInfo.ts +40 -0
- package/src/runtime/runInfo.ts +19 -0
- package/template.tsconfig.json +14 -0
- package/tsconfig.eslint.json +5 -0
- package/tsconfig.json +24 -0
- package/typedoc.json +49 -0
- package/vite.config.ts +17 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { getTraceFilePath } from "../utils";
|
|
2
|
+
import { pathExists, remove, createReadStream, stat } from "fs-extra";
|
|
3
|
+
import fetch from "node-fetch";
|
|
4
|
+
import { Handler } from "@tinyhttp/app";
|
|
5
|
+
import { ok, err } from "neverthrow";
|
|
6
|
+
|
|
7
|
+
export async function uploadTrace({
|
|
8
|
+
runId,
|
|
9
|
+
attemptNumber,
|
|
10
|
+
signedUrl,
|
|
11
|
+
}: {
|
|
12
|
+
runId: string;
|
|
13
|
+
attemptNumber?: string;
|
|
14
|
+
signedUrl: string;
|
|
15
|
+
}) {
|
|
16
|
+
const traceFilePath = getTraceFilePath(
|
|
17
|
+
runId,
|
|
18
|
+
attemptNumber as string | undefined
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
if (!(await pathExists(traceFilePath))) {
|
|
22
|
+
return err({
|
|
23
|
+
message: "Trace file not found",
|
|
24
|
+
error: "Not found",
|
|
25
|
+
errorCode: "trace_file_not_found" as const,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const fileSize = (await stat(traceFilePath)).size;
|
|
30
|
+
const file = createReadStream(traceFilePath);
|
|
31
|
+
|
|
32
|
+
const response = await fetch(signedUrl, {
|
|
33
|
+
method: "PUT",
|
|
34
|
+
headers: {
|
|
35
|
+
"Content-Length": fileSize.toString(),
|
|
36
|
+
},
|
|
37
|
+
body: file,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
return err({
|
|
42
|
+
message: `Upload trace failed with status code ${response.status}`,
|
|
43
|
+
error: "Upload trace failed",
|
|
44
|
+
errorCode: "upload_trace_failed" as const,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return ok({
|
|
49
|
+
traceName: `${runId}.zip`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function deleteTrace({
|
|
54
|
+
runId,
|
|
55
|
+
attemptNumber,
|
|
56
|
+
}: {
|
|
57
|
+
runId: string;
|
|
58
|
+
attemptNumber?: string;
|
|
59
|
+
}) {
|
|
60
|
+
const traceFilePath = getTraceFilePath(
|
|
61
|
+
runId,
|
|
62
|
+
attemptNumber as string | undefined
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (!(await pathExists(traceFilePath))) {
|
|
66
|
+
return err({
|
|
67
|
+
message: "Trace file not found",
|
|
68
|
+
error: "Not found",
|
|
69
|
+
errorCode: "trace_file_not_found" as const,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
await remove(traceFilePath);
|
|
75
|
+
return ok(undefined);
|
|
76
|
+
} catch (error: any) {
|
|
77
|
+
return err({
|
|
78
|
+
message: `Delete trace file failed ${runId}`,
|
|
79
|
+
error: "Delete trace failed",
|
|
80
|
+
errorCode: "delete_trace_failed" as const,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const uploadTraceController: Handler = async (req, res) => {
|
|
86
|
+
const { runId } = req.params;
|
|
87
|
+
const { attemptNumber } = req.query;
|
|
88
|
+
|
|
89
|
+
const signedUrl = req.body.signedUrl as string;
|
|
90
|
+
if (!runId) {
|
|
91
|
+
return res.status(400).json({
|
|
92
|
+
error: "runId not provided",
|
|
93
|
+
message: "Please add provide run id",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!signedUrl) {
|
|
98
|
+
return res.status(400).json({
|
|
99
|
+
error: "signedUrl not provided",
|
|
100
|
+
message: "Please add provide signedUrl",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const uploadTraceResult = await uploadTrace({
|
|
105
|
+
runId,
|
|
106
|
+
attemptNumber: attemptNumber as string | undefined,
|
|
107
|
+
signedUrl,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (uploadTraceResult.isOk()) {
|
|
111
|
+
return res.status(200).json({
|
|
112
|
+
result: uploadTraceResult.value,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const error = uploadTraceResult.error;
|
|
117
|
+
if (error.errorCode === "trace_file_not_found") {
|
|
118
|
+
return res.status(404).json(error);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return res.status(500).json(error);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const deleteTraceController: Handler = async (req, res) => {
|
|
125
|
+
const { runId } = req.params;
|
|
126
|
+
const { attemptNumber } = req.query;
|
|
127
|
+
|
|
128
|
+
if (!runId) {
|
|
129
|
+
return res.status(400).json({
|
|
130
|
+
error: "runId not provided",
|
|
131
|
+
message: "Please add provide run id",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const deleteTraceResult = await deleteTrace({
|
|
136
|
+
runId,
|
|
137
|
+
attemptNumber: attemptNumber as string | undefined,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (deleteTraceResult.isOk()) {
|
|
141
|
+
return res.status(204).send("");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const error = deleteTraceResult.error;
|
|
145
|
+
|
|
146
|
+
if (error.errorCode === "trace_file_not_found") {
|
|
147
|
+
return res.status(404).json(error);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return res.status(500).json(error);
|
|
151
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const RUN_ID_HEADER = "x-run-id";
|
|
2
|
+
export const JOB_ID_HEADER = "x-job-id";
|
|
3
|
+
export const JOB_RUN_ID_HEADER = "x-job-run-id";
|
|
4
|
+
export const QUEUE_ID_HEADER = "x-queue-id";
|
|
5
|
+
export const ATTEMPT_NUMBER_HEADER = "x-attempt-number";
|
|
6
|
+
export const SHOULD_SHUTDOWN_HEADER = "x-should-shutdown";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
initializeAppInsights,
|
|
3
|
+
getTelemetryClient,
|
|
4
|
+
} from "@intuned/runtime/dist/common/telemetry";
|
|
5
|
+
import { getExecutionContext } from "@intuned/runtime";
|
|
6
|
+
import { app } from "./app";
|
|
7
|
+
import { RUN_ID_HEADER } from "./headers";
|
|
8
|
+
import { ShutdownController } from "./shutdown";
|
|
9
|
+
|
|
10
|
+
const port = process.env.PORT ? parseInt(process.env.PORT) : 4000;
|
|
11
|
+
|
|
12
|
+
initializeAppInsights();
|
|
13
|
+
|
|
14
|
+
app.use((req, res, next) => {
|
|
15
|
+
// Cleanup after the response is sent
|
|
16
|
+
res.on("finish", () => {
|
|
17
|
+
console.log("finished", req.headers[RUN_ID_HEADER]);
|
|
18
|
+
void ShutdownController.instance.checkForShutdown();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
next();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export * from "./api";
|
|
25
|
+
export * from "./jobs";
|
|
26
|
+
|
|
27
|
+
const server = app.listen(port, () => {
|
|
28
|
+
// when deployed on flyio, the server will be turned on and
|
|
29
|
+
// will shutdown after the specified time in TIME_TO_SHUTDOWN env variable
|
|
30
|
+
ShutdownController.initialize(server);
|
|
31
|
+
void ShutdownController.instance.checkForShutdown();
|
|
32
|
+
console.log(`Server is running on port ${port}`);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
36
|
+
const telemetryClient = getTelemetryClient();
|
|
37
|
+
console.error("Unhandled Rejection at:", promise, "reason:", reason);
|
|
38
|
+
const context = getExecutionContext();
|
|
39
|
+
telemetryClient?.trackEvent({
|
|
40
|
+
name: "UNHANDLED_REJECTION",
|
|
41
|
+
properties: {
|
|
42
|
+
runId: context?.runId,
|
|
43
|
+
promise,
|
|
44
|
+
reason,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { App } from "@tinyhttp/app";
|
|
2
|
+
import { json as parser } from "milliparsec";
|
|
3
|
+
import { accessKeyValidatorMiddleware } from "./accessKeyHelpers";
|
|
4
|
+
import { callFunction } from "./utils";
|
|
5
|
+
|
|
6
|
+
const port = process.env.PORT ? parseInt(process.env.PORT) : 4000;
|
|
7
|
+
const app = new App();
|
|
8
|
+
|
|
9
|
+
app.use("/", parser());
|
|
10
|
+
|
|
11
|
+
app.post("/api/run/*", async (req, res, next) => {
|
|
12
|
+
// filter(Boolean) should remove empty strings in case of a trailing slash
|
|
13
|
+
const functionName = req.path.split("/").filter(Boolean).pop();
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const body = req.body;
|
|
17
|
+
const result = await callFunction("api", functionName as string, [body]);
|
|
18
|
+
|
|
19
|
+
res.status(200).json(result);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (
|
|
22
|
+
error.message ===
|
|
23
|
+
`Unknown variable dynamic import: ./api/${functionName}.ts`
|
|
24
|
+
) {
|
|
25
|
+
return res.status(404).json({});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
next(error);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
app.get("/api/health", async (req, res) => {
|
|
33
|
+
res.status(200).json({ status: "ok" });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
app.use(accessKeyValidatorMiddleware);
|
|
37
|
+
|
|
38
|
+
app.get("/protected/health", async (req, res) => {
|
|
39
|
+
res.status(200).json({ status: "ok" });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
app.listen(port, () => {
|
|
43
|
+
console.log(`Server is running on port ${port}`);
|
|
44
|
+
});
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { runApi, RunBody } from "./controllers/runApi";
|
|
2
|
+
import {
|
|
3
|
+
ATTEMPT_NUMBER_HEADER,
|
|
4
|
+
JOB_ID_HEADER,
|
|
5
|
+
JOB_RUN_ID_HEADER,
|
|
6
|
+
RUN_ID_HEADER,
|
|
7
|
+
SHOULD_SHUTDOWN_HEADER,
|
|
8
|
+
} from "./headers";
|
|
9
|
+
import {
|
|
10
|
+
getErrorResponse,
|
|
11
|
+
isHeadless,
|
|
12
|
+
isJobRunMachine,
|
|
13
|
+
ProxyConfig,
|
|
14
|
+
proxyToUrl,
|
|
15
|
+
} from "./utils";
|
|
16
|
+
import {
|
|
17
|
+
callBackendFunctionWithToken,
|
|
18
|
+
backendFunctionsTokenManager,
|
|
19
|
+
} from "@intuned/runtime/dist/common/jwtTokenManager";
|
|
20
|
+
import retry from "async-retry";
|
|
21
|
+
import { runWithContext } from "@intuned/runtime";
|
|
22
|
+
import { setTimeout } from "timers/promises";
|
|
23
|
+
import { deleteTrace, uploadTrace } from "./controllers/traces";
|
|
24
|
+
|
|
25
|
+
type JobPayload = {
|
|
26
|
+
headers: Record<string, string>;
|
|
27
|
+
originalPayload: any;
|
|
28
|
+
payload: RunBody;
|
|
29
|
+
startTime: number;
|
|
30
|
+
functionName: string;
|
|
31
|
+
receiptHandle: string;
|
|
32
|
+
traceSignedUrl?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (isJobRunMachine()) {
|
|
36
|
+
console.log("Running in job v3 mode");
|
|
37
|
+
void jobsV3();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function runApiInContext(
|
|
41
|
+
jobPayload: JobPayload
|
|
42
|
+
): Promise<Awaited<ReturnType<typeof runApi>>> {
|
|
43
|
+
const runId = jobPayload.headers[RUN_ID_HEADER] as string;
|
|
44
|
+
const jobId = process.env.JOB_ID as string;
|
|
45
|
+
const jobRunId = process.env.JOB_RUN_ID as string;
|
|
46
|
+
const proxy = jobPayload.payload.proxy
|
|
47
|
+
? (proxyToUrl(jobPayload.payload.proxy as ProxyConfig) as string)
|
|
48
|
+
: undefined;
|
|
49
|
+
const contextData = {
|
|
50
|
+
runId: runId ?? "",
|
|
51
|
+
jobId,
|
|
52
|
+
jobRunId,
|
|
53
|
+
queueId: undefined,
|
|
54
|
+
proxy,
|
|
55
|
+
timeoutInfo: {
|
|
56
|
+
extendTimeoutCallback: async () => {
|
|
57
|
+
try {
|
|
58
|
+
await extendTimeout(jobPayload);
|
|
59
|
+
} catch (error: any) {
|
|
60
|
+
console.error("Error extending timeout", error);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
...(jobPayload.payload.executionContext ?? {}),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const backendFunctionsToken = jobPayload.payload.functionsToken;
|
|
68
|
+
if (backendFunctionsToken) {
|
|
69
|
+
backendFunctionsTokenManager.token = backendFunctionsToken;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
return await runWithContext(contextData, runApi, {
|
|
74
|
+
functionName: jobPayload.functionName,
|
|
75
|
+
...jobPayload.payload,
|
|
76
|
+
runId: jobPayload.headers[RUN_ID_HEADER],
|
|
77
|
+
attemptNumber: jobPayload.headers[ATTEMPT_NUMBER_HEADER],
|
|
78
|
+
shouldSaveTrace: true,
|
|
79
|
+
headless: isHeadless(),
|
|
80
|
+
});
|
|
81
|
+
} catch (error: any) {
|
|
82
|
+
return getErrorResponse(error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function jobsV3() {
|
|
87
|
+
await reportReady();
|
|
88
|
+
const initialDelay = 1000;
|
|
89
|
+
let delay = initialDelay;
|
|
90
|
+
while (true as boolean) {
|
|
91
|
+
await setTimeout(delay);
|
|
92
|
+
try {
|
|
93
|
+
const { payload, shouldShutdown } = await requestPayload();
|
|
94
|
+
if (!payload) {
|
|
95
|
+
if (shouldShutdown) {
|
|
96
|
+
console.log("Received shutdown signal from job");
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
delay = Math.min(delay * 2, 60000);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
delay = initialDelay;
|
|
103
|
+
console.log("Running payload", payload.functionName);
|
|
104
|
+
const { body, status } = await runApiInContext(payload);
|
|
105
|
+
|
|
106
|
+
await reportResultsAndUploadTraces(payload, status, body);
|
|
107
|
+
|
|
108
|
+
if (shouldShutdown) {
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
} catch (error: any) {
|
|
112
|
+
console.error("Error in payload", error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function reportReady() {
|
|
118
|
+
console.log("Ready");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function requestPayload(): Promise<{
|
|
122
|
+
payload: JobPayload | null;
|
|
123
|
+
shouldShutdown: boolean;
|
|
124
|
+
}> {
|
|
125
|
+
console.log("Requesting payload");
|
|
126
|
+
const result = await callBackendFunctionWithToken("jobs/requestPayload", {
|
|
127
|
+
headers: {
|
|
128
|
+
"fly-instance-id": process.env.FLY_ALLOC_ID ?? "",
|
|
129
|
+
[JOB_ID_HEADER]: process.env.JOB_ID ?? "",
|
|
130
|
+
[JOB_RUN_ID_HEADER]: process.env.JOB_RUN_ID ?? "",
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
const shouldShutdown = result.headers.get(SHOULD_SHUTDOWN_HEADER) === "true";
|
|
134
|
+
if (result.status === 200) {
|
|
135
|
+
const payload = (await result.json()) as JobPayload;
|
|
136
|
+
console.log("Received payload", payload);
|
|
137
|
+
return {
|
|
138
|
+
payload: {
|
|
139
|
+
...payload,
|
|
140
|
+
headers: {
|
|
141
|
+
...payload.headers,
|
|
142
|
+
[JOB_ID_HEADER]: process.env.JOB_ID ?? "",
|
|
143
|
+
[JOB_RUN_ID_HEADER]: process.env.JOB_RUN_ID ?? "",
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
shouldShutdown,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (result.status === 204) {
|
|
150
|
+
console.log("No payload available");
|
|
151
|
+
return {
|
|
152
|
+
payload: null,
|
|
153
|
+
shouldShutdown,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
console.log("Failed to obtain payload", result.status, await result.text());
|
|
157
|
+
return {
|
|
158
|
+
payload: null,
|
|
159
|
+
shouldShutdown,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function reportResultsAndUploadTraces(
|
|
164
|
+
payload: JobPayload,
|
|
165
|
+
statusCode: number,
|
|
166
|
+
body: any
|
|
167
|
+
) {
|
|
168
|
+
await reportJobRunResult({
|
|
169
|
+
runId: payload.headers[RUN_ID_HEADER] as string,
|
|
170
|
+
result: {
|
|
171
|
+
statusCode,
|
|
172
|
+
body,
|
|
173
|
+
receiptHandle: payload.receiptHandle,
|
|
174
|
+
},
|
|
175
|
+
originalPayload: payload.originalPayload,
|
|
176
|
+
startTime: payload.startTime,
|
|
177
|
+
functionsToken: backendFunctionsTokenManager.token,
|
|
178
|
+
});
|
|
179
|
+
await uploadJobRunTrace(payload);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function reportJobRunResult({
|
|
183
|
+
runId,
|
|
184
|
+
result,
|
|
185
|
+
originalPayload,
|
|
186
|
+
startTime,
|
|
187
|
+
functionsToken,
|
|
188
|
+
}: {
|
|
189
|
+
runId: string;
|
|
190
|
+
result: {
|
|
191
|
+
statusCode: number;
|
|
192
|
+
body: any;
|
|
193
|
+
receiptHandle: string;
|
|
194
|
+
};
|
|
195
|
+
originalPayload: any;
|
|
196
|
+
startTime: number;
|
|
197
|
+
functionsToken?: string;
|
|
198
|
+
}) {
|
|
199
|
+
console.log("Reporting payload result");
|
|
200
|
+
const reqResult = await retry(
|
|
201
|
+
async (bail) => {
|
|
202
|
+
const response = await callBackendFunctionWithToken("jobs/reportResult", {
|
|
203
|
+
method: "POST",
|
|
204
|
+
headers: {
|
|
205
|
+
"Content-Type": "application/json",
|
|
206
|
+
"fly-instance-id": process.env.FLY_ALLOC_ID ?? "",
|
|
207
|
+
[JOB_ID_HEADER]: process.env.JOB_ID ?? "",
|
|
208
|
+
[JOB_RUN_ID_HEADER]: process.env.JOB_RUN_ID ?? "",
|
|
209
|
+
},
|
|
210
|
+
body: JSON.stringify({
|
|
211
|
+
runId,
|
|
212
|
+
result,
|
|
213
|
+
originalPayload,
|
|
214
|
+
startTime,
|
|
215
|
+
}),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
if ([401, 403, 404, 413].includes(response.status)) {
|
|
220
|
+
bail(
|
|
221
|
+
new Error(
|
|
222
|
+
`Reporting result failed (non-retryable), status ${response.status}`
|
|
223
|
+
)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
throw new Error(`Reporting result failed, status ${response.status}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return response;
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
retries: 5,
|
|
233
|
+
factor: 2,
|
|
234
|
+
maxTimeout: 1000 * 60, // 1 minute
|
|
235
|
+
minTimeout: 1000 * 5, // 5 seconds
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
console.log("Reported payload result", reqResult.ok, await reqResult.text());
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function uploadJobRunTrace(
|
|
242
|
+
payload: NonNullable<
|
|
243
|
+
NonNullable<Awaited<ReturnType<typeof requestPayload>>>["payload"]
|
|
244
|
+
>
|
|
245
|
+
) {
|
|
246
|
+
if (!payload.traceSignedUrl) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const runId = payload.headers[RUN_ID_HEADER] as string;
|
|
250
|
+
const attemptNumber = payload.headers[ATTEMPT_NUMBER_HEADER] as
|
|
251
|
+
| string
|
|
252
|
+
| undefined;
|
|
253
|
+
console.log("Uploading trace", runId, attemptNumber);
|
|
254
|
+
const uploadTraceResult = await uploadTrace({
|
|
255
|
+
runId,
|
|
256
|
+
attemptNumber,
|
|
257
|
+
signedUrl: payload.traceSignedUrl,
|
|
258
|
+
});
|
|
259
|
+
if (uploadTraceResult.isErr()) {
|
|
260
|
+
console.error("Error uploading trace", uploadTraceResult.error, {
|
|
261
|
+
runId,
|
|
262
|
+
attemptNumber,
|
|
263
|
+
});
|
|
264
|
+
} else {
|
|
265
|
+
console.log(
|
|
266
|
+
"Trace uploaded successfully, reporting trace",
|
|
267
|
+
runId,
|
|
268
|
+
attemptNumber
|
|
269
|
+
);
|
|
270
|
+
await retry(
|
|
271
|
+
async (bail) => {
|
|
272
|
+
const response = await callBackendFunctionWithToken(
|
|
273
|
+
"jobs/reportTrace",
|
|
274
|
+
{
|
|
275
|
+
method: "POST",
|
|
276
|
+
headers: {
|
|
277
|
+
"Content-Type": "application/json",
|
|
278
|
+
"fly-instance-id": process.env.FLY_ALLOC_ID ?? "",
|
|
279
|
+
[JOB_ID_HEADER]: process.env.JOB_ID ?? "",
|
|
280
|
+
[JOB_RUN_ID_HEADER]: process.env.JOB_RUN_ID ?? "",
|
|
281
|
+
},
|
|
282
|
+
body: JSON.stringify({
|
|
283
|
+
runId,
|
|
284
|
+
attemptNumber,
|
|
285
|
+
traceSignedUrl: payload.traceSignedUrl,
|
|
286
|
+
}),
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
if (!response.ok) {
|
|
291
|
+
if ([401, 403, 404].includes(response.status)) {
|
|
292
|
+
bail(
|
|
293
|
+
new Error(
|
|
294
|
+
`Reporting trace failed (non-retryable), status ${response.status}`
|
|
295
|
+
)
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
throw new Error(`Reporting trace failed, status ${response.status}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
console.log("Trace reported successfully", runId, attemptNumber);
|
|
302
|
+
return response;
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
retries: 2,
|
|
306
|
+
minTimeout: 1000 * 5,
|
|
307
|
+
}
|
|
308
|
+
).catch(() => undefined);
|
|
309
|
+
}
|
|
310
|
+
console.log("Deleting trace", runId, attemptNumber);
|
|
311
|
+
const deleteTraceResult = await deleteTrace({
|
|
312
|
+
runId,
|
|
313
|
+
attemptNumber,
|
|
314
|
+
});
|
|
315
|
+
if (deleteTraceResult.isErr()) {
|
|
316
|
+
console.error("Error deleting trace", deleteTraceResult.error, {
|
|
317
|
+
runId,
|
|
318
|
+
attemptNumber,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function extendTimeout(payload: JobPayload) {
|
|
324
|
+
console.log("Requesting timeout extension for", payload.functionName);
|
|
325
|
+
const extendTimeoutRes = await callBackendFunctionWithToken(
|
|
326
|
+
"jobs/extendTimeout",
|
|
327
|
+
{
|
|
328
|
+
method: "POST",
|
|
329
|
+
headers: {
|
|
330
|
+
"Content-Type": "application/json",
|
|
331
|
+
"fly-instance-id": process.env.FLY_ALLOC_ID ?? "",
|
|
332
|
+
[JOB_ID_HEADER]: process.env.JOB_ID ?? "",
|
|
333
|
+
[JOB_RUN_ID_HEADER]: process.env.JOB_RUN_ID ?? "",
|
|
334
|
+
},
|
|
335
|
+
body: JSON.stringify({
|
|
336
|
+
receiptHandle: payload.receiptHandle,
|
|
337
|
+
}),
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
if (!extendTimeoutRes.ok) {
|
|
341
|
+
console.error(
|
|
342
|
+
"Requesting timeout extension failed",
|
|
343
|
+
extendTimeoutRes.status,
|
|
344
|
+
await extendTimeoutRes.text()
|
|
345
|
+
);
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
console.log("Request timeout extension success", extendTimeoutRes.ok);
|
|
349
|
+
const response = await extendTimeoutRes.json();
|
|
350
|
+
const newFunctionsToken = response.functionsToken;
|
|
351
|
+
if (newFunctionsToken) {
|
|
352
|
+
backendFunctionsTokenManager.token = newFunctionsToken;
|
|
353
|
+
console.log("Backend functions token renewed");
|
|
354
|
+
}
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { isJobRunMachine } from "./utils";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { AsyncRunEndpointController } from "./utils";
|
|
4
|
+
import { Server } from "http";
|
|
5
|
+
|
|
6
|
+
export class ShutdownController {
|
|
7
|
+
private static _instance?: ShutdownController;
|
|
8
|
+
|
|
9
|
+
static initialize(server: Server) {
|
|
10
|
+
if (this._instance) {
|
|
11
|
+
throw new Error("ShutdownController is already initialized");
|
|
12
|
+
}
|
|
13
|
+
this._instance = new ShutdownController(server);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public static get instance() {
|
|
17
|
+
if (!this._instance) {
|
|
18
|
+
throw new Error("ShutdownController is not initialized");
|
|
19
|
+
}
|
|
20
|
+
return this._instance;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private server: Server;
|
|
24
|
+
private timer: NodeJS.Timeout | null = null;
|
|
25
|
+
private constructor(server: Server) {
|
|
26
|
+
this.server = server;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async checkForShutdown() {
|
|
30
|
+
if (await this.serverShouldShutdown()) {
|
|
31
|
+
if (this.timer) clearTimeout(this.timer);
|
|
32
|
+
this.timer = setTimeout(async () => {
|
|
33
|
+
if (await this.serverShouldShutdown()) {
|
|
34
|
+
console.log("No active requests, shutting down the server.");
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
// TIME_TO_SHUTDOWN in seconds to match tired-proxy
|
|
38
|
+
}, this.calculateShutdownDelay());
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private async getActiveRequests() {
|
|
43
|
+
const activeConnections = await promisify(
|
|
44
|
+
(...params: Parameters<typeof this.server.getConnections>) =>
|
|
45
|
+
this.server.getConnections(...params)
|
|
46
|
+
)();
|
|
47
|
+
return activeConnections + AsyncRunEndpointController.activeRequestsCount;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private async serverShouldShutdown() {
|
|
51
|
+
if (isJobRunMachine()) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return (await this.getActiveRequests()) === 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private calculateShutdownDelay(): number | undefined {
|
|
58
|
+
const shutdownTime = +process.env.TIME_TO_SHUTDOWN;
|
|
59
|
+
if (isNaN(shutdownTime) || shutdownTime <= 0) {
|
|
60
|
+
return 60 * 1000;
|
|
61
|
+
}
|
|
62
|
+
return shutdownTime * 1000;
|
|
63
|
+
}
|
|
64
|
+
}
|