@intuned/runtime-dev 0.1.0-test.26 → 0.1.0-test.28
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/InterfaceTemplate/index.js +1 -0
- package/WebTemplate/accessKeyHelpers.js +20 -0
- package/WebTemplate/api.js +48 -0
- package/WebTemplate/app.js +13 -0
- package/WebTemplate/controllers/async.js +78 -0
- package/WebTemplate/controllers/authSessions/check.js +44 -0
- package/WebTemplate/controllers/authSessions/create.js +69 -0
- package/WebTemplate/controllers/authSessions/create.ts +6 -2
- package/WebTemplate/controllers/authSessions/index.js +25 -0
- package/WebTemplate/controllers/authSessions/killOperation.js +26 -0
- package/WebTemplate/controllers/authSessions/resumeOperation.js +42 -0
- package/WebTemplate/controllers/authSessions/resumeOperation.ts +1 -1
- package/WebTemplate/controllers/authSessions/store.js +1 -0
- package/WebTemplate/controllers/controllers.js +30 -0
- package/WebTemplate/controllers/runApi/helpers.js +90 -0
- package/WebTemplate/controllers/runApi/index.js +45 -0
- package/WebTemplate/controllers/runApi/types.js +1 -0
- package/WebTemplate/controllers/traces.js +108 -0
- package/WebTemplate/features.js +6 -0
- package/WebTemplate/headers.js +6 -0
- package/WebTemplate/index.playwright.js +37 -0
- package/WebTemplate/index.vanilla.js +19 -0
- package/WebTemplate/jobs.js +259 -0
- package/WebTemplate/shutdown.js +51 -0
- package/WebTemplate/utils.js +189 -0
- package/api/authed.js +6 -0
- package/api/test.js +3 -0
- package/api/test2.js +17 -0
- package/auth-sessions/check.js +6 -0
- package/auth-sessions/create.js +20 -0
- package/dist/commands/common/utils/settings.js +0 -1
- package/package.json +1 -1
- package/playwright.config.js +43 -0
- package/vite.config.js +16 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { getTraceFilePath } from "../utils";
|
|
2
|
+
import { pathExists, remove, createReadStream, stat } from "fs-extra";
|
|
3
|
+
import fetch from "node-fetch";
|
|
4
|
+
import { ok, err } from "neverthrow";
|
|
5
|
+
export async function uploadTrace({ runId, attemptNumber, signedUrl, }) {
|
|
6
|
+
const traceFilePath = getTraceFilePath(runId, attemptNumber);
|
|
7
|
+
if (!(await pathExists(traceFilePath))) {
|
|
8
|
+
return err({
|
|
9
|
+
message: "Trace file not found",
|
|
10
|
+
error: "Not found",
|
|
11
|
+
errorCode: "trace_file_not_found",
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
const fileSize = (await stat(traceFilePath)).size;
|
|
15
|
+
const file = createReadStream(traceFilePath);
|
|
16
|
+
const response = await fetch(signedUrl, {
|
|
17
|
+
method: "PUT",
|
|
18
|
+
headers: {
|
|
19
|
+
"Content-Length": fileSize.toString(),
|
|
20
|
+
},
|
|
21
|
+
body: file,
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
return err({
|
|
25
|
+
message: `Upload trace failed with status code ${response.status}`,
|
|
26
|
+
error: "Upload trace failed",
|
|
27
|
+
errorCode: "upload_trace_failed",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return ok({
|
|
31
|
+
traceName: `${runId}.zip`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export async function deleteTrace({ runId, attemptNumber, }) {
|
|
35
|
+
const traceFilePath = getTraceFilePath(runId, attemptNumber);
|
|
36
|
+
if (!(await pathExists(traceFilePath))) {
|
|
37
|
+
return err({
|
|
38
|
+
message: "Trace file not found",
|
|
39
|
+
error: "Not found",
|
|
40
|
+
errorCode: "trace_file_not_found",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
await remove(traceFilePath);
|
|
45
|
+
return ok(undefined);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return err({
|
|
49
|
+
message: `Delete trace file failed ${runId}`,
|
|
50
|
+
error: "Delete trace failed",
|
|
51
|
+
errorCode: "delete_trace_failed",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export const uploadTraceController = async (req, res) => {
|
|
56
|
+
const { runId } = req.params;
|
|
57
|
+
const { attemptNumber } = req.query;
|
|
58
|
+
const signedUrl = req.body.signedUrl;
|
|
59
|
+
if (!runId) {
|
|
60
|
+
return res.status(400).json({
|
|
61
|
+
error: "runId not provided",
|
|
62
|
+
message: "Please add provide run id",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (!signedUrl) {
|
|
66
|
+
return res.status(400).json({
|
|
67
|
+
error: "signedUrl not provided",
|
|
68
|
+
message: "Please add provide signedUrl",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const uploadTraceResult = await uploadTrace({
|
|
72
|
+
runId,
|
|
73
|
+
attemptNumber: attemptNumber,
|
|
74
|
+
signedUrl,
|
|
75
|
+
});
|
|
76
|
+
if (uploadTraceResult.isOk()) {
|
|
77
|
+
return res.status(200).json({
|
|
78
|
+
result: uploadTraceResult.value,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const error = uploadTraceResult.error;
|
|
82
|
+
if (error.errorCode === "trace_file_not_found") {
|
|
83
|
+
return res.status(404).json(error);
|
|
84
|
+
}
|
|
85
|
+
return res.status(500).json(error);
|
|
86
|
+
};
|
|
87
|
+
export const deleteTraceController = async (req, res) => {
|
|
88
|
+
const { runId } = req.params;
|
|
89
|
+
const { attemptNumber } = req.query;
|
|
90
|
+
if (!runId) {
|
|
91
|
+
return res.status(400).json({
|
|
92
|
+
error: "runId not provided",
|
|
93
|
+
message: "Please add provide run id",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const deleteTraceResult = await deleteTrace({
|
|
97
|
+
runId,
|
|
98
|
+
attemptNumber: attemptNumber,
|
|
99
|
+
});
|
|
100
|
+
if (deleteTraceResult.isOk()) {
|
|
101
|
+
return res.status(204).send("");
|
|
102
|
+
}
|
|
103
|
+
const error = deleteTraceResult.error;
|
|
104
|
+
if (error.errorCode === "trace_file_not_found") {
|
|
105
|
+
return res.status(404).json(error);
|
|
106
|
+
}
|
|
107
|
+
return res.status(500).json(error);
|
|
108
|
+
};
|
|
@@ -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,37 @@
|
|
|
1
|
+
import { initializeAppInsights, getTelemetryClient, } from "@intuned/runtime/dist/common/telemetry";
|
|
2
|
+
import { getExecutionContext } from "@intuned/runtime";
|
|
3
|
+
import { app } from "./app";
|
|
4
|
+
import { RUN_ID_HEADER } from "./headers";
|
|
5
|
+
import { ShutdownController } from "./shutdown";
|
|
6
|
+
const port = process.env.PORT ? parseInt(process.env.PORT) : 4000;
|
|
7
|
+
initializeAppInsights();
|
|
8
|
+
app.use((req, res, next) => {
|
|
9
|
+
// Cleanup after the response is sent
|
|
10
|
+
res.on("finish", () => {
|
|
11
|
+
console.log("finished", req.headers[RUN_ID_HEADER]);
|
|
12
|
+
void ShutdownController.instance.checkForShutdown();
|
|
13
|
+
});
|
|
14
|
+
next();
|
|
15
|
+
});
|
|
16
|
+
export * from "./api";
|
|
17
|
+
export * from "./jobs";
|
|
18
|
+
const server = app.listen(port, () => {
|
|
19
|
+
// when deployed on flyio, the server will be turned on and
|
|
20
|
+
// will shutdown after the specified time in TIME_TO_SHUTDOWN env variable
|
|
21
|
+
ShutdownController.initialize(server);
|
|
22
|
+
void ShutdownController.instance.checkForShutdown();
|
|
23
|
+
console.log(`Server is running on port ${port}`);
|
|
24
|
+
});
|
|
25
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
26
|
+
const telemetryClient = getTelemetryClient();
|
|
27
|
+
console.error("Unhandled Rejection at:", promise, "reason:", reason);
|
|
28
|
+
const context = getExecutionContext();
|
|
29
|
+
telemetryClient?.trackEvent({
|
|
30
|
+
name: "UNHANDLED_REJECTION",
|
|
31
|
+
properties: {
|
|
32
|
+
runId: context?.runId,
|
|
33
|
+
promise,
|
|
34
|
+
reason,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { App } from "@tinyhttp/app";
|
|
2
|
+
import { json as parser } from "milliparsec";
|
|
3
|
+
import { accessKeyValidatorMiddleware } from "./accessKeyHelpers";
|
|
4
|
+
const port = process.env.PORT ? parseInt(process.env.PORT) : 4000;
|
|
5
|
+
const app = new App();
|
|
6
|
+
app.use("/", parser());
|
|
7
|
+
app.post("/api/run/*", async (req, res) => {
|
|
8
|
+
return res.status(502).json({ message: "Not implemented" });
|
|
9
|
+
});
|
|
10
|
+
app.get("/api/health", async (req, res) => {
|
|
11
|
+
res.status(200).json({ status: "ok" });
|
|
12
|
+
});
|
|
13
|
+
app.use(accessKeyValidatorMiddleware);
|
|
14
|
+
app.get("/protected/health", async (req, res) => {
|
|
15
|
+
res.status(200).json({ status: "ok" });
|
|
16
|
+
});
|
|
17
|
+
app.listen(port, () => {
|
|
18
|
+
console.log(`Server is running on port ${port}`);
|
|
19
|
+
});
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { runApi } from "./controllers/runApi";
|
|
2
|
+
import { ATTEMPT_NUMBER_HEADER, JOB_ID_HEADER, JOB_RUN_ID_HEADER, RUN_ID_HEADER, SHOULD_SHUTDOWN_HEADER, } from "./headers";
|
|
3
|
+
import { getErrorResponse, isHeadless, isJobRunMachine, proxyToUrl, } from "./utils";
|
|
4
|
+
import { callBackendFunctionWithToken, backendFunctionsTokenManager, } from "@intuned/runtime/dist/common/jwtTokenManager";
|
|
5
|
+
import retry from "async-retry";
|
|
6
|
+
import { runWithContext } from "@intuned/runtime";
|
|
7
|
+
import { setTimeout } from "timers/promises";
|
|
8
|
+
import { deleteTrace, uploadTrace } from "./controllers/traces";
|
|
9
|
+
if (isJobRunMachine()) {
|
|
10
|
+
console.log("Running in job v3 mode");
|
|
11
|
+
void jobsV3();
|
|
12
|
+
}
|
|
13
|
+
async function runApiInContext(jobPayload) {
|
|
14
|
+
const runId = jobPayload.headers[RUN_ID_HEADER];
|
|
15
|
+
const jobId = process.env.JOB_ID;
|
|
16
|
+
const jobRunId = process.env.JOB_RUN_ID;
|
|
17
|
+
const proxy = jobPayload.payload.proxy
|
|
18
|
+
? proxyToUrl(jobPayload.payload.proxy)
|
|
19
|
+
: undefined;
|
|
20
|
+
const contextData = {
|
|
21
|
+
runId: runId ?? "",
|
|
22
|
+
jobId,
|
|
23
|
+
jobRunId,
|
|
24
|
+
queueId: undefined,
|
|
25
|
+
proxy,
|
|
26
|
+
timeoutInfo: {
|
|
27
|
+
extendTimeoutCallback: async () => {
|
|
28
|
+
try {
|
|
29
|
+
await extendTimeout(jobPayload);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
console.error("Error extending timeout", error);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
...(jobPayload.payload.executionContext ?? {}),
|
|
37
|
+
};
|
|
38
|
+
const backendFunctionsToken = jobPayload.payload.functionsToken;
|
|
39
|
+
if (backendFunctionsToken) {
|
|
40
|
+
backendFunctionsTokenManager.token = backendFunctionsToken;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
return await runWithContext(contextData, runApi, {
|
|
44
|
+
functionName: jobPayload.functionName,
|
|
45
|
+
...jobPayload.payload,
|
|
46
|
+
runId: jobPayload.headers[RUN_ID_HEADER],
|
|
47
|
+
attemptNumber: jobPayload.headers[ATTEMPT_NUMBER_HEADER],
|
|
48
|
+
shouldSaveTrace: true,
|
|
49
|
+
headless: isHeadless(),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
return getErrorResponse(error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function jobsV3() {
|
|
57
|
+
await reportReady();
|
|
58
|
+
const initialDelay = 1000;
|
|
59
|
+
let delay = initialDelay;
|
|
60
|
+
while (true) {
|
|
61
|
+
await setTimeout(delay);
|
|
62
|
+
try {
|
|
63
|
+
const { payload, shouldShutdown } = await requestPayload();
|
|
64
|
+
if (!payload) {
|
|
65
|
+
if (shouldShutdown) {
|
|
66
|
+
console.log("Received shutdown signal from job");
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
delay = Math.min(delay * 2, 60000);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
delay = initialDelay;
|
|
73
|
+
console.log("Running payload", payload.functionName);
|
|
74
|
+
const { body, status } = await runApiInContext(payload);
|
|
75
|
+
await reportResultsAndUploadTraces(payload, status, body);
|
|
76
|
+
if (shouldShutdown) {
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
console.error("Error in payload", error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function reportReady() {
|
|
86
|
+
console.log("Ready");
|
|
87
|
+
}
|
|
88
|
+
async function requestPayload() {
|
|
89
|
+
console.log("Requesting payload");
|
|
90
|
+
const result = await callBackendFunctionWithToken("jobs/requestPayload", {
|
|
91
|
+
headers: {
|
|
92
|
+
"fly-instance-id": process.env.FLY_ALLOC_ID ?? "",
|
|
93
|
+
[JOB_ID_HEADER]: process.env.JOB_ID ?? "",
|
|
94
|
+
[JOB_RUN_ID_HEADER]: process.env.JOB_RUN_ID ?? "",
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
const shouldShutdown = result.headers.get(SHOULD_SHUTDOWN_HEADER) === "true";
|
|
98
|
+
if (result.status === 200) {
|
|
99
|
+
const payload = (await result.json());
|
|
100
|
+
console.log("Received payload", payload);
|
|
101
|
+
return {
|
|
102
|
+
payload: {
|
|
103
|
+
...payload,
|
|
104
|
+
headers: {
|
|
105
|
+
...payload.headers,
|
|
106
|
+
[JOB_ID_HEADER]: process.env.JOB_ID ?? "",
|
|
107
|
+
[JOB_RUN_ID_HEADER]: process.env.JOB_RUN_ID ?? "",
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
shouldShutdown,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (result.status === 204) {
|
|
114
|
+
console.log("No payload available");
|
|
115
|
+
return {
|
|
116
|
+
payload: null,
|
|
117
|
+
shouldShutdown,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
console.log("Failed to obtain payload", result.status, await result.text());
|
|
121
|
+
return {
|
|
122
|
+
payload: null,
|
|
123
|
+
shouldShutdown,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
async function reportResultsAndUploadTraces(payload, statusCode, body) {
|
|
127
|
+
await reportJobRunResult({
|
|
128
|
+
runId: payload.headers[RUN_ID_HEADER],
|
|
129
|
+
result: {
|
|
130
|
+
statusCode,
|
|
131
|
+
body,
|
|
132
|
+
receiptHandle: payload.receiptHandle,
|
|
133
|
+
},
|
|
134
|
+
originalPayload: payload.originalPayload,
|
|
135
|
+
startTime: payload.startTime,
|
|
136
|
+
functionsToken: backendFunctionsTokenManager.token,
|
|
137
|
+
});
|
|
138
|
+
await uploadJobRunTrace(payload);
|
|
139
|
+
}
|
|
140
|
+
async function reportJobRunResult({ runId, result, originalPayload, startTime, functionsToken, }) {
|
|
141
|
+
console.log("Reporting payload result");
|
|
142
|
+
const reqResult = await retry(async (bail) => {
|
|
143
|
+
const response = await callBackendFunctionWithToken("jobs/reportResult", {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: {
|
|
146
|
+
"Content-Type": "application/json",
|
|
147
|
+
"fly-instance-id": process.env.FLY_ALLOC_ID ?? "",
|
|
148
|
+
[JOB_ID_HEADER]: process.env.JOB_ID ?? "",
|
|
149
|
+
[JOB_RUN_ID_HEADER]: process.env.JOB_RUN_ID ?? "",
|
|
150
|
+
},
|
|
151
|
+
body: JSON.stringify({
|
|
152
|
+
runId,
|
|
153
|
+
result,
|
|
154
|
+
originalPayload,
|
|
155
|
+
startTime,
|
|
156
|
+
}),
|
|
157
|
+
});
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
if ([401, 403, 404, 413].includes(response.status)) {
|
|
160
|
+
bail(new Error(`Reporting result failed (non-retryable), status ${response.status}`));
|
|
161
|
+
}
|
|
162
|
+
throw new Error(`Reporting result failed, status ${response.status}`);
|
|
163
|
+
}
|
|
164
|
+
return response;
|
|
165
|
+
}, {
|
|
166
|
+
retries: 5,
|
|
167
|
+
factor: 2,
|
|
168
|
+
maxTimeout: 1000 * 60, // 1 minute
|
|
169
|
+
minTimeout: 1000 * 5, // 5 seconds
|
|
170
|
+
});
|
|
171
|
+
console.log("Reported payload result", reqResult.ok, await reqResult.text());
|
|
172
|
+
}
|
|
173
|
+
async function uploadJobRunTrace(payload) {
|
|
174
|
+
if (!payload.traceSignedUrl) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const runId = payload.headers[RUN_ID_HEADER];
|
|
178
|
+
const attemptNumber = payload.headers[ATTEMPT_NUMBER_HEADER];
|
|
179
|
+
console.log("Uploading trace", runId, attemptNumber);
|
|
180
|
+
const uploadTraceResult = await uploadTrace({
|
|
181
|
+
runId,
|
|
182
|
+
attemptNumber,
|
|
183
|
+
signedUrl: payload.traceSignedUrl,
|
|
184
|
+
});
|
|
185
|
+
if (uploadTraceResult.isErr()) {
|
|
186
|
+
console.error("Error uploading trace", uploadTraceResult.error, {
|
|
187
|
+
runId,
|
|
188
|
+
attemptNumber,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
console.log("Trace uploaded successfully, reporting trace", runId, attemptNumber);
|
|
193
|
+
await retry(async (bail) => {
|
|
194
|
+
const response = await callBackendFunctionWithToken("jobs/reportTrace", {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: {
|
|
197
|
+
"Content-Type": "application/json",
|
|
198
|
+
"fly-instance-id": process.env.FLY_ALLOC_ID ?? "",
|
|
199
|
+
[JOB_ID_HEADER]: process.env.JOB_ID ?? "",
|
|
200
|
+
[JOB_RUN_ID_HEADER]: process.env.JOB_RUN_ID ?? "",
|
|
201
|
+
},
|
|
202
|
+
body: JSON.stringify({
|
|
203
|
+
runId,
|
|
204
|
+
attemptNumber,
|
|
205
|
+
traceSignedUrl: payload.traceSignedUrl,
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
if (!response.ok) {
|
|
209
|
+
if ([401, 403, 404].includes(response.status)) {
|
|
210
|
+
bail(new Error(`Reporting trace failed (non-retryable), status ${response.status}`));
|
|
211
|
+
}
|
|
212
|
+
throw new Error(`Reporting trace failed, status ${response.status}`);
|
|
213
|
+
}
|
|
214
|
+
console.log("Trace reported successfully", runId, attemptNumber);
|
|
215
|
+
return response;
|
|
216
|
+
}, {
|
|
217
|
+
retries: 2,
|
|
218
|
+
minTimeout: 1000 * 5,
|
|
219
|
+
}).catch(() => undefined);
|
|
220
|
+
}
|
|
221
|
+
console.log("Deleting trace", runId, attemptNumber);
|
|
222
|
+
const deleteTraceResult = await deleteTrace({
|
|
223
|
+
runId,
|
|
224
|
+
attemptNumber,
|
|
225
|
+
});
|
|
226
|
+
if (deleteTraceResult.isErr()) {
|
|
227
|
+
console.error("Error deleting trace", deleteTraceResult.error, {
|
|
228
|
+
runId,
|
|
229
|
+
attemptNumber,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async function extendTimeout(payload) {
|
|
234
|
+
console.log("Requesting timeout extension for", payload.functionName);
|
|
235
|
+
const extendTimeoutRes = await callBackendFunctionWithToken("jobs/extendTimeout", {
|
|
236
|
+
method: "POST",
|
|
237
|
+
headers: {
|
|
238
|
+
"Content-Type": "application/json",
|
|
239
|
+
"fly-instance-id": process.env.FLY_ALLOC_ID ?? "",
|
|
240
|
+
[JOB_ID_HEADER]: process.env.JOB_ID ?? "",
|
|
241
|
+
[JOB_RUN_ID_HEADER]: process.env.JOB_RUN_ID ?? "",
|
|
242
|
+
},
|
|
243
|
+
body: JSON.stringify({
|
|
244
|
+
receiptHandle: payload.receiptHandle,
|
|
245
|
+
}),
|
|
246
|
+
});
|
|
247
|
+
if (!extendTimeoutRes.ok) {
|
|
248
|
+
console.error("Requesting timeout extension failed", extendTimeoutRes.status, await extendTimeoutRes.text());
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
console.log("Request timeout extension success", extendTimeoutRes.ok);
|
|
252
|
+
const response = await extendTimeoutRes.json();
|
|
253
|
+
const newFunctionsToken = response.functionsToken;
|
|
254
|
+
if (newFunctionsToken) {
|
|
255
|
+
backendFunctionsTokenManager.token = newFunctionsToken;
|
|
256
|
+
console.log("Backend functions token renewed");
|
|
257
|
+
}
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { isJobRunMachine } from "./utils";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import { AsyncRunEndpointController } from "./utils";
|
|
4
|
+
export class ShutdownController {
|
|
5
|
+
static initialize(server) {
|
|
6
|
+
if (this._instance) {
|
|
7
|
+
throw new Error("ShutdownController is already initialized");
|
|
8
|
+
}
|
|
9
|
+
this._instance = new ShutdownController(server);
|
|
10
|
+
}
|
|
11
|
+
static get instance() {
|
|
12
|
+
if (!this._instance) {
|
|
13
|
+
throw new Error("ShutdownController is not initialized");
|
|
14
|
+
}
|
|
15
|
+
return this._instance;
|
|
16
|
+
}
|
|
17
|
+
constructor(server) {
|
|
18
|
+
this.timer = null;
|
|
19
|
+
this.server = server;
|
|
20
|
+
}
|
|
21
|
+
async checkForShutdown() {
|
|
22
|
+
if (await this.serverShouldShutdown()) {
|
|
23
|
+
if (this.timer)
|
|
24
|
+
clearTimeout(this.timer);
|
|
25
|
+
this.timer = setTimeout(async () => {
|
|
26
|
+
if (await this.serverShouldShutdown()) {
|
|
27
|
+
console.log("No active requests, shutting down the server.");
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
// TIME_TO_SHUTDOWN in seconds to match tired-proxy
|
|
31
|
+
}, this.calculateShutdownDelay());
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async getActiveRequests() {
|
|
35
|
+
const activeConnections = await promisify((...params) => this.server.getConnections(...params))();
|
|
36
|
+
return activeConnections + AsyncRunEndpointController.activeRequestsCount;
|
|
37
|
+
}
|
|
38
|
+
async serverShouldShutdown() {
|
|
39
|
+
if (isJobRunMachine()) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return (await this.getActiveRequests()) === 0;
|
|
43
|
+
}
|
|
44
|
+
calculateShutdownDelay() {
|
|
45
|
+
const shutdownTime = +process.env.TIME_TO_SHUTDOWN;
|
|
46
|
+
if (isNaN(shutdownTime) || shutdownTime <= 0) {
|
|
47
|
+
return 60 * 1000;
|
|
48
|
+
}
|
|
49
|
+
return shutdownTime * 1000;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import * as playwright from "@intuned/playwright-core";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { getExecutionContext } from "@intuned/runtime";
|
|
4
|
+
import { setTimeout } from "timers/promises";
|
|
5
|
+
export class FunctionNotFoundError extends Error {
|
|
6
|
+
constructor(functionName, path) {
|
|
7
|
+
const message = `function ${functionName} not found in ${path}`;
|
|
8
|
+
super(message);
|
|
9
|
+
this.functionName = functionName;
|
|
10
|
+
this.path = path;
|
|
11
|
+
this.name = "FunctionNotFound";
|
|
12
|
+
Object.setPrototypeOf(this, FunctionNotFoundError.prototype);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function getIsRetryableError(error) {
|
|
16
|
+
if (error?.message) {
|
|
17
|
+
return error.message.includes("ERR_NETWORK_CHANGED");
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
export function getErrorResponse(error) {
|
|
22
|
+
if (error instanceof FunctionNotFoundError) {
|
|
23
|
+
return {
|
|
24
|
+
status: 404,
|
|
25
|
+
body: {
|
|
26
|
+
message: error.message,
|
|
27
|
+
error: error.name,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (error instanceof playwright.errors.TimeoutError) {
|
|
32
|
+
return {
|
|
33
|
+
status: 500,
|
|
34
|
+
body: { message: error.message, error: error.name },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* here we use error.constructor.name instead of error instanceof RunError
|
|
39
|
+
* this is because the error is thrown by importing the runner from the api code on intuned app
|
|
40
|
+
* the definition of class RunError which is imported from here is different than the class RunError
|
|
41
|
+
* imported from the user.
|
|
42
|
+
*/
|
|
43
|
+
if (error.constructor.name === "RunError") {
|
|
44
|
+
return {
|
|
45
|
+
status: 200,
|
|
46
|
+
body: {
|
|
47
|
+
status: error.options.status_code ?? 500,
|
|
48
|
+
message: error.message,
|
|
49
|
+
error: error.options.error_code ?? error.name,
|
|
50
|
+
intunedOptions: error.options,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
status: 500,
|
|
56
|
+
body: { error: error?.name ?? error, message: error?.message },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function handlePlaywrightExecutionError(error, res) {
|
|
60
|
+
const { status, body } = getErrorResponse(error);
|
|
61
|
+
return res.status(status).json(body);
|
|
62
|
+
}
|
|
63
|
+
export function errorRetryMiddleware(handler) {
|
|
64
|
+
return async (...args) => {
|
|
65
|
+
let attempts = 1;
|
|
66
|
+
const [req, res, next] = args;
|
|
67
|
+
// eslint-disable-next-line no-constant-condition
|
|
68
|
+
while (true) {
|
|
69
|
+
try {
|
|
70
|
+
await handler(req, res, next);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
console.log(error?.name, error?.message);
|
|
75
|
+
if (!getIsRetryableError(error) || attempts >= 3) {
|
|
76
|
+
return handlePlaywrightExecutionError(error, res);
|
|
77
|
+
}
|
|
78
|
+
attempts++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function getTraceFilePath(runId, attemptNumber) {
|
|
84
|
+
const fileName = `${runId}${attemptNumber ? `_${attemptNumber}` : ""}`;
|
|
85
|
+
return path.join(process.env.TRACES_DIRECTORY ?? "", `${fileName}.zip`);
|
|
86
|
+
}
|
|
87
|
+
export function proxyToUrl(proxy) {
|
|
88
|
+
const url = new URL(proxy.server);
|
|
89
|
+
url.username = proxy.username;
|
|
90
|
+
url.password = proxy.password;
|
|
91
|
+
return url.toString();
|
|
92
|
+
}
|
|
93
|
+
export function isJobRunMachine() {
|
|
94
|
+
return process.env.JOB_ID && process.env.JOB_RUN_ID;
|
|
95
|
+
}
|
|
96
|
+
export function isHeadless() {
|
|
97
|
+
return process.env.INTUNED_PLAYWRIGHT_HEADLESS !== "0";
|
|
98
|
+
}
|
|
99
|
+
export class AsyncRunEndpointController {
|
|
100
|
+
static isRunning(taskToken) {
|
|
101
|
+
return this.activeRequests.has(taskToken);
|
|
102
|
+
}
|
|
103
|
+
static addRequest(taskToken) {
|
|
104
|
+
this.activeRequests.add(taskToken);
|
|
105
|
+
}
|
|
106
|
+
static removeRequest(taskToken) {
|
|
107
|
+
this.activeRequests.delete(taskToken);
|
|
108
|
+
}
|
|
109
|
+
static get activeRequestsCount() {
|
|
110
|
+
return this.activeRequests.size;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
AsyncRunEndpointController.activeRequests = new Set();
|
|
114
|
+
export async function waitWithExtendableTimeout({ initialTimeout, promise, abortController, }) {
|
|
115
|
+
const context = getExecutionContext();
|
|
116
|
+
if (context.timeoutInfo) {
|
|
117
|
+
context.timeoutInfo.timeoutTimestamp = Date.now() + initialTimeout;
|
|
118
|
+
}
|
|
119
|
+
const timerSymbol = Symbol("timer");
|
|
120
|
+
if (!context) {
|
|
121
|
+
const result = await Promise.race([
|
|
122
|
+
promise,
|
|
123
|
+
setTimeout(initialTimeout, timerSymbol),
|
|
124
|
+
]);
|
|
125
|
+
if (result === timerSymbol) {
|
|
126
|
+
abortController?.abort("Timed out");
|
|
127
|
+
return { timedOut: true };
|
|
128
|
+
}
|
|
129
|
+
return { timedOut: false, result };
|
|
130
|
+
}
|
|
131
|
+
let taskTimeout = initialTimeout;
|
|
132
|
+
while (true) {
|
|
133
|
+
const result = await Promise.race([
|
|
134
|
+
promise,
|
|
135
|
+
setTimeout(taskTimeout, timerSymbol),
|
|
136
|
+
]);
|
|
137
|
+
if (result !== timerSymbol) {
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
const remainingTime = (context.timeoutInfo?.timeoutTimestamp ?? 0) - Date.now();
|
|
141
|
+
if (remainingTime < 0) {
|
|
142
|
+
abortController?.abort("Timed out");
|
|
143
|
+
return { timedOut: true };
|
|
144
|
+
}
|
|
145
|
+
taskTimeout = remainingTime;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
timedOut: false,
|
|
149
|
+
result: await promise,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
export async function importModule(path) {
|
|
153
|
+
// cleanup environment variables before running the user code
|
|
154
|
+
// cleanEnvironmentVariables();
|
|
155
|
+
const [folderName, ...functionNameParts] = path.split("/");
|
|
156
|
+
const functionNameDepth = functionNameParts.length;
|
|
157
|
+
// string literals should be inline
|
|
158
|
+
// currently we support only 5 levels of depth
|
|
159
|
+
// rollup dynamic import does not support multiple levels of dynamic imports so we need to specify the possible paths explicitly
|
|
160
|
+
try {
|
|
161
|
+
let imported = undefined;
|
|
162
|
+
switch (functionNameDepth) {
|
|
163
|
+
case 1:
|
|
164
|
+
imported = await import(`./${folderName}/${functionNameParts[0]}.ts`);
|
|
165
|
+
break;
|
|
166
|
+
case 2:
|
|
167
|
+
imported = await import(`./${folderName}/${functionNameParts[0]}/${functionNameParts[1]}.ts`);
|
|
168
|
+
break;
|
|
169
|
+
case 3:
|
|
170
|
+
imported = await import(`./${folderName}/${functionNameParts[0]}/${functionNameParts[1]}/${functionNameParts[2]}.ts`);
|
|
171
|
+
break;
|
|
172
|
+
case 4:
|
|
173
|
+
imported = await import(`./${folderName}/${functionNameParts[0]}/${functionNameParts[1]}/${functionNameParts[2]}/${functionNameParts[3]}.ts`);
|
|
174
|
+
break;
|
|
175
|
+
case 5:
|
|
176
|
+
imported = await import(`./${folderName}/${functionNameParts[0]}/${functionNameParts[1]}/${functionNameParts[2]}/${functionNameParts[3]}/${functionNameParts[4]}.ts`);
|
|
177
|
+
break;
|
|
178
|
+
default:
|
|
179
|
+
throw new Error("intuned supports maximum 5 levels of depth in the api folder");
|
|
180
|
+
}
|
|
181
|
+
return imported;
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (error.message.includes("Unknown variable dynamic import")) {
|
|
185
|
+
throw new FunctionNotFoundError("", path);
|
|
186
|
+
}
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
package/api/authed.js
ADDED
package/api/test.js
ADDED