anbaric-cloud-hosting 1.68.1 → 1.68.2
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anbaric-cloud-hosting",
|
|
3
|
-
"version": "1.68.
|
|
3
|
+
"version": "1.68.2",
|
|
4
4
|
"description": "Anbaric Cloud hosting service: Postgres-backed job persistence and queuing exposed over an HTTP API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "chris@anbaric.ai",
|
|
@@ -18,18 +18,18 @@
|
|
|
18
18
|
"@aws-sdk/client-s3": "^3.1110.0",
|
|
19
19
|
"@aws-sdk/client-secrets-manager": "^3.700.0",
|
|
20
20
|
"@aws-sdk/client-servicediscovery": "^3.1110.0",
|
|
21
|
-
"anbaric-data-store": "^1.68.
|
|
22
|
-
"anbaric-plugins": "^1.68.
|
|
23
|
-
"anbaric-tsapi": "^1.68.
|
|
24
|
-
"anbaric-web": "^1.68.
|
|
21
|
+
"anbaric-data-store": "^1.68.2",
|
|
22
|
+
"anbaric-plugins": "^1.68.2",
|
|
23
|
+
"anbaric-tsapi": "^1.68.2",
|
|
24
|
+
"anbaric-web": "^1.68.2",
|
|
25
25
|
"esbuild": "^0.28.2",
|
|
26
26
|
"pg": "^8.16.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^26.2.0",
|
|
30
30
|
"@types/pg": "^8.15.0",
|
|
31
|
-
"anbaric-impl-cloud": "^1.68.
|
|
32
|
-
"anbaric-state-machine": "^1.68.
|
|
31
|
+
"anbaric-impl-cloud": "^1.68.2",
|
|
32
|
+
"anbaric-state-machine": "^1.68.2",
|
|
33
33
|
"tsx": "^4.20.0",
|
|
34
34
|
"typescript": "^7.0.2"
|
|
35
35
|
},
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
type Exit = (code : number) => void;
|
|
2
|
+
type Log = (line : string) => void;
|
|
3
|
+
|
|
4
|
+
/* The platform's last words. An error nobody caught ends the process - Node
|
|
5
|
+
gives no choice there - but it should end it saying what happened, in one
|
|
6
|
+
line a log search can find, rather than a stack trace that only a person
|
|
7
|
+
reading the stream would notice. ECS restarts the task either way; this is
|
|
8
|
+
what makes the restart explicable afterwards. */
|
|
9
|
+
class FatalErrors {
|
|
10
|
+
|
|
11
|
+
constructor(private log : Log = line => console.error(line), private exit : Exit = code => process.exit(code)) {}
|
|
12
|
+
|
|
13
|
+
install(target : NodeJS.Process = process) : void {
|
|
14
|
+
target.on("uncaughtException", error => this.die("uncaughtException", error));
|
|
15
|
+
target.on("unhandledRejection", reason => this.die("unhandledRejection", reason));
|
|
16
|
+
target.on("SIGTERM", () => this.log(this.line("platform.stopping", { signal: "SIGTERM" })));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private die(kind : string, error : unknown) : void {
|
|
20
|
+
const detail = error instanceof Error
|
|
21
|
+
? { message: error.message, name: error.name, code: (error as { code? : string }).code, stack: error.stack }
|
|
22
|
+
: { message: String(error) };
|
|
23
|
+
this.log(this.line("platform.crash", { kind, ...detail }));
|
|
24
|
+
this.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private line(event : string, fields : Record<string, unknown>) : string {
|
|
28
|
+
return JSON.stringify({ level: "fatal", event, at: new Date().toISOString(), ...fields });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { FatalErrors };
|
package/src/hosting/Server.ts
CHANGED
|
@@ -17,14 +17,32 @@ class Server {
|
|
|
17
17
|
const message = error instanceof Error ? error.message : "Internal error";
|
|
18
18
|
const notFound = /^No .+ found/.test(message);
|
|
19
19
|
if (! notFound) console.error(`[server] ${incoming.method} ${incoming.url} failed (ray ${response.getHeader("x-anbaric-ray") ?? "-"}):`, error);
|
|
20
|
-
|
|
21
|
-
response.writeHead(notFound ? 404 : 500, { "content-type": "application/json" });
|
|
22
|
-
response.end(JSON.stringify({ error: notFound ? message : Server.internalErrorMessage(response) }));
|
|
23
|
-
}
|
|
20
|
+
Server.failResponse(response, notFound ? 404 : 500, notFound ? message : Server.internalErrorMessage(response));
|
|
24
21
|
});
|
|
25
22
|
});
|
|
26
23
|
}
|
|
27
24
|
|
|
25
|
+
/* The last word on a request that failed. A response that has not started
|
|
26
|
+
gets a status and a body; one that was already streaming when it failed
|
|
27
|
+
cannot be given either, so its connection is cut instead - the client
|
|
28
|
+
sees a truncated body, which is the truth. Writing headers twice would
|
|
29
|
+
throw inside the error handler itself, with nothing left to catch it,
|
|
30
|
+
and take the whole platform down with one dropped connection. */
|
|
31
|
+
private static failResponse(response : ServerResponse, status : number, error : string) : void {
|
|
32
|
+
if (response.writableEnded) return;
|
|
33
|
+
|
|
34
|
+
if (response.headersSent) {
|
|
35
|
+
response.destroy();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
response.writeHead(status, { "content-type": "application/json" });
|
|
40
|
+
response.end(JSON.stringify({ error }));
|
|
41
|
+
} catch {
|
|
42
|
+
response.destroy();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
28
46
|
/* What a failure inside the platform looks like from outside: never the
|
|
29
47
|
underlying error, which names infrastructure and credentials, only a
|
|
30
48
|
reference the logs can be searched for. */
|
|
@@ -54,7 +54,20 @@ class AppProxyHandler implements RequestHandler {
|
|
|
54
54
|
|
|
55
55
|
const body = request.method === "GET" || request.method === "HEAD" ? undefined : await request.rawBody();
|
|
56
56
|
|
|
57
|
+
/* Once the app's response has started, whatever ends it ends the
|
|
58
|
+
request: a browser that closed the tab mid-stream, or an app that
|
|
59
|
+
dropped the connection. Neither is a failure of the platform's,
|
|
60
|
+
and there is nothing left to say to the client, so the upstream is
|
|
61
|
+
let go and the request is over. Only a failure before the first
|
|
62
|
+
byte is an error worth answering. */
|
|
57
63
|
await new Promise<void>((resolve, reject) => {
|
|
64
|
+
const client = request.rawResponse;
|
|
65
|
+
const settle = (error : unknown) => {
|
|
66
|
+
if (! client.headersSent) return reject(error);
|
|
67
|
+
client.destroy();
|
|
68
|
+
resolve();
|
|
69
|
+
};
|
|
70
|
+
|
|
58
71
|
const upstream = httpRequest({
|
|
59
72
|
host: app.appHost,
|
|
60
73
|
port: app.appPort,
|
|
@@ -62,12 +75,18 @@ class AppProxyHandler implements RequestHandler {
|
|
|
62
75
|
path: `${appPath}${request.url.search}`,
|
|
63
76
|
headers: this.forwardHeaders(request, prefix),
|
|
64
77
|
}, response => {
|
|
65
|
-
|
|
66
|
-
response.pipe(
|
|
78
|
+
client.writeHead(response.statusCode ?? 502, this.passThrough(response.headers));
|
|
79
|
+
response.pipe(client);
|
|
67
80
|
response.on("end", resolve);
|
|
68
|
-
response.on("error",
|
|
81
|
+
response.on("error", settle);
|
|
82
|
+
response.on("close", () => { if (! response.complete) settle(new Error("The app closed the connection")); });
|
|
83
|
+
});
|
|
84
|
+
client.on("close", () => {
|
|
85
|
+
if (client.writableFinished) return;
|
|
86
|
+
upstream.destroy();
|
|
87
|
+
resolve();
|
|
69
88
|
});
|
|
70
|
-
upstream.on("error",
|
|
89
|
+
upstream.on("error", settle);
|
|
71
90
|
if (body && body.length > 0) upstream.write(body);
|
|
72
91
|
upstream.end();
|
|
73
92
|
});
|
package/src/main.ts
CHANGED
|
@@ -33,11 +33,14 @@ import {PostgresPromptManager} from "./data-store/PostgresPromptManager";
|
|
|
33
33
|
import {ConsumerRegistry} from "./queuing/ConsumerRegistry";
|
|
34
34
|
import {Dispatcher} from "./queuing/Dispatcher";
|
|
35
35
|
import {HostingServer} from "./hosting/HostingServer";
|
|
36
|
+
import {FatalErrors} from "./hosting/FatalErrors";
|
|
36
37
|
import {PluginLoader} from "./plugins/PluginLoader";
|
|
37
38
|
import {UsageReporter} from "./usage/UsageReporter";
|
|
38
39
|
import {HttpSubdomains} from "./subdomains/HttpSubdomains";
|
|
39
40
|
import {CentralBilling} from "./billing/CentralBilling";
|
|
40
41
|
|
|
42
|
+
new FatalErrors().install();
|
|
43
|
+
|
|
41
44
|
const pool = new Pool({ connectionString: process.env.ANBARIC_DATABASE_URL });
|
|
42
45
|
await ensureSchema(pool);
|
|
43
46
|
|