@naturalcycles/backend-lib 9.78.0 → 9.80.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/dist/deploy/deployHealthCheck.d.ts +1 -5
- package/dist/deploy/deployHealthCheck.js +3 -34
- package/dist/deploy/index.d.ts +2 -9
- package/dist/deploy/index.js +1 -6
- package/dist/express/createDefaultApp.js +2 -2
- package/dist/server/compressionMiddleware.d.ts +14 -1
- package/dist/server/compressionMiddleware.js +81 -27
- package/dist/server/serverStatusMiddleware.d.ts +2 -2
- package/dist/server/serverStatusMiddleware.js +5 -14
- package/dist/util.d.ts +0 -1
- package/dist/util.js +1 -4
- package/package.json +3 -13
- package/src/deploy/deployHealthCheck.ts +2 -48
- package/src/deploy/index.ts +2 -17
- package/src/express/createDefaultApp.ts +2 -2
- package/src/server/compressionMiddleware.ts +107 -28
- package/src/server/serverStatusMiddleware.ts +4 -20
- package/src/util.ts +1 -5
- package/bin/deploy-gae.js +0 -2
- package/bin/deploy-prepare.js +0 -2
- package/bin/undeploy-gae.js +0 -2
- package/dist/bin/deploy-gae.d.ts +0 -1
- package/dist/bin/deploy-gae.js +0 -12
- package/dist/bin/deploy-prepare.d.ts +0 -1
- package/dist/bin/deploy-prepare.js +0 -19
- package/dist/bin/undeploy-gae.d.ts +0 -1
- package/dist/bin/undeploy-gae.js +0 -13
- package/dist/deploy/backend.cfg.util.d.ts +0 -30
- package/dist/deploy/backend.cfg.util.js +0 -23
- package/dist/deploy/deploy.model.d.ts +0 -29
- package/dist/deploy/deploy.model.js +0 -1
- package/dist/deploy/deploy.util.d.ts +0 -7
- package/dist/deploy/deploy.util.js +0 -151
- package/dist/deploy/deployGae.d.ts +0 -10
- package/dist/deploy/deployGae.js +0 -90
- package/dist/deploy/deployInfo.util.d.ts +0 -1
- package/dist/deploy/deployInfo.util.js +0 -25
- package/dist/deploy/deployPrepare.d.ts +0 -13
- package/dist/deploy/deployPrepare.js +0 -85
- package/src/bin/deploy-gae.ts +0 -14
- package/src/bin/deploy-prepare.ts +0 -24
- package/src/bin/undeploy-gae.ts +0 -15
- package/src/deploy/backend.cfg.util.ts +0 -63
- package/src/deploy/deploy.model.ts +0 -32
- package/src/deploy/deploy.util.ts +0 -214
- package/src/deploy/deployGae.ts +0 -127
- package/src/deploy/deployInfo.util.ts +0 -27
- package/src/deploy/deployPrepare.ts +0 -113
- package/src/deploy/files-default/.gcloudignore +0 -7
|
@@ -4,11 +4,6 @@ export interface DeployHealthCheckOptions {
|
|
|
4
4
|
maxTries?: number;
|
|
5
5
|
timeoutSec?: number;
|
|
6
6
|
intervalSec?: number;
|
|
7
|
-
logOnFailure?: boolean;
|
|
8
|
-
logOnSuccess?: boolean;
|
|
9
|
-
gaeProject?: string;
|
|
10
|
-
gaeService?: string;
|
|
11
|
-
gaeVersion?: string;
|
|
12
7
|
authHeader?: string;
|
|
13
8
|
}
|
|
14
9
|
export declare const deployHealthCheckCliOptions: CliOptions;
|
|
@@ -16,5 +11,6 @@ export declare const deployHealthCheckCliOptions: CliOptions;
|
|
|
16
11
|
* Requires up to `healthyThreshold` consecutive OK responses to succeed.
|
|
17
12
|
* Fails on up to `unhealthyThreshold` consecutive FAIL responses.
|
|
18
13
|
* Fails after maxTries.
|
|
14
|
+
* Throws on failure.
|
|
19
15
|
*/
|
|
20
16
|
export declare function deployHealthCheck(url: string, opt?: DeployHealthCheckOptions): Promise<void>;
|
|
@@ -4,7 +4,6 @@ import { getFetcher } from '@naturalcycles/js-lib/http';
|
|
|
4
4
|
import { _filterFalsyValues, _filterUndefinedValues, } from '@naturalcycles/js-lib/object/object.util.js';
|
|
5
5
|
import { pDelay } from '@naturalcycles/js-lib/promise/pDelay.js';
|
|
6
6
|
import { dimGrey, red } from '@naturalcycles/nodejs-lib/colors';
|
|
7
|
-
import { exec2 } from '@naturalcycles/nodejs-lib/exec2';
|
|
8
7
|
import { coloredHttpCode } from '../server/request.log.util.js';
|
|
9
8
|
export const deployHealthCheckCliOptions = {
|
|
10
9
|
thresholdHealthy: {
|
|
@@ -27,25 +26,6 @@ export const deployHealthCheckCliOptions = {
|
|
|
27
26
|
type: 'number',
|
|
28
27
|
default: 1,
|
|
29
28
|
},
|
|
30
|
-
logOnFailure: {
|
|
31
|
-
type: 'boolean',
|
|
32
|
-
default: true,
|
|
33
|
-
desc: 'Show server logs on health check failure (requires gaeProject, gaeService, gaeVersion)',
|
|
34
|
-
},
|
|
35
|
-
logOnSuccess: {
|
|
36
|
-
type: 'boolean',
|
|
37
|
-
default: false,
|
|
38
|
-
desc: 'Show server logs on health check success (requires gaeProject, gaeService, gaeVersion)',
|
|
39
|
-
},
|
|
40
|
-
gaeProject: {
|
|
41
|
-
type: 'string',
|
|
42
|
-
},
|
|
43
|
-
gaeService: {
|
|
44
|
-
type: 'string',
|
|
45
|
-
},
|
|
46
|
-
gaeVersion: {
|
|
47
|
-
type: 'string',
|
|
48
|
-
},
|
|
49
29
|
authHeader: {
|
|
50
30
|
type: 'string',
|
|
51
31
|
},
|
|
@@ -58,9 +38,10 @@ const inspectOpt = {
|
|
|
58
38
|
* Requires up to `healthyThreshold` consecutive OK responses to succeed.
|
|
59
39
|
* Fails on up to `unhealthyThreshold` consecutive FAIL responses.
|
|
60
40
|
* Fails after maxTries.
|
|
41
|
+
* Throws on failure.
|
|
61
42
|
*/
|
|
62
43
|
export async function deployHealthCheck(url, opt = {}) {
|
|
63
|
-
const { thresholdHealthy = 5, thresholdUnhealthy = 8, maxTries = 30, timeoutSec = 30, intervalSec = 1,
|
|
44
|
+
const { thresholdHealthy = 5, thresholdUnhealthy = 8, maxTries = 30, timeoutSec = 30, intervalSec = 1, authHeader, } = opt;
|
|
64
45
|
let attempt = 0;
|
|
65
46
|
let countHealthy = 0;
|
|
66
47
|
let countUnhealthy = 0;
|
|
@@ -74,19 +55,7 @@ export async function deployHealthCheck(url, opt = {}) {
|
|
|
74
55
|
}
|
|
75
56
|
if (failed) {
|
|
76
57
|
console.log(red(`Health check failed!`));
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
exec2.spawn(`gcloud app logs read --project ${gaeProject} --service ${gaeService} --version ${gaeVersion}`);
|
|
80
|
-
}
|
|
81
|
-
catch { }
|
|
82
|
-
}
|
|
83
|
-
process.exit(1);
|
|
84
|
-
}
|
|
85
|
-
if (logOnSuccess) {
|
|
86
|
-
try {
|
|
87
|
-
exec2.spawn(`gcloud app logs read --project ${gaeProject} --service ${gaeService} --version ${gaeVersion}`);
|
|
88
|
-
}
|
|
89
|
-
catch { }
|
|
58
|
+
throw new Error(`Health check failed: ${doneReason}`);
|
|
90
59
|
}
|
|
91
60
|
async function makeAttempt() {
|
|
92
61
|
attempt++;
|
package/dist/deploy/index.d.ts
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import type { BackendCfg } from './backend.cfg.util.js';
|
|
2
|
-
import { getBackendCfg } from './backend.cfg.util.js';
|
|
3
|
-
import type { DeployInfo } from './deploy.model.js';
|
|
4
|
-
import { createAppYaml, createDeployInfo } from './deploy.util.js';
|
|
5
|
-
import { deployGae } from './deployGae.js';
|
|
6
1
|
import type { DeployHealthCheckOptions } from './deployHealthCheck.js';
|
|
7
2
|
import { deployHealthCheck } from './deployHealthCheck.js';
|
|
8
|
-
|
|
9
|
-
export
|
|
10
|
-
export { createAppYaml, createDeployInfo, deployGae, deployHealthCheck, deployPrepare, getBackendCfg, };
|
|
11
|
-
export * from './deployInfo.util.js';
|
|
3
|
+
export type { DeployHealthCheckOptions };
|
|
4
|
+
export { deployHealthCheck };
|
package/dist/deploy/index.js
CHANGED
|
@@ -1,7 +1,2 @@
|
|
|
1
|
-
import { getBackendCfg } from './backend.cfg.util.js';
|
|
2
|
-
import { createAppYaml, createDeployInfo } from './deploy.util.js';
|
|
3
|
-
import { deployGae } from './deployGae.js';
|
|
4
1
|
import { deployHealthCheck } from './deployHealthCheck.js';
|
|
5
|
-
|
|
6
|
-
export { createAppYaml, createDeployInfo, deployGae, deployHealthCheck, deployPrepare, getBackendCfg, };
|
|
7
|
-
export * from './deployInfo.util.js';
|
|
2
|
+
export { deployHealthCheck };
|
|
@@ -5,9 +5,9 @@ import { logMiddleware } from '../server/logMiddleware.js';
|
|
|
5
5
|
import { notFoundMiddleware } from '../server/notFoundMiddleware.js';
|
|
6
6
|
import { requestTimeoutMiddleware } from '../server/requestTimeoutMiddleware.js';
|
|
7
7
|
import { simpleRequestLoggerMiddleware } from '../server/simpleRequestLoggerMiddleware.js';
|
|
8
|
-
import { isGAE } from '../util.js';
|
|
9
8
|
const isTest = process.env['APP_ENV'] === 'test';
|
|
10
9
|
const isDev = process.env['APP_ENV'] === 'dev';
|
|
10
|
+
const isGAE = !!process.env['GAE_INSTANCE'];
|
|
11
11
|
export async function createDefaultApp(cfg) {
|
|
12
12
|
const { sentryService } = cfg;
|
|
13
13
|
const { default: express } = await import('express');
|
|
@@ -43,7 +43,7 @@ export async function createDefaultApp(cfg) {
|
|
|
43
43
|
maxAge: 86400,
|
|
44
44
|
...cfg.corsOptions,
|
|
45
45
|
}));
|
|
46
|
-
if (!isGAE
|
|
46
|
+
if (!isGAE && isTest) {
|
|
47
47
|
// compression is not used in AppEngine, because AppEngine provides it by default
|
|
48
48
|
app.use(compressionMiddleware());
|
|
49
49
|
}
|
|
@@ -10,6 +10,18 @@ export interface CompressionOptions {
|
|
|
10
10
|
* @default 1024
|
|
11
11
|
*/
|
|
12
12
|
threshold?: number | string;
|
|
13
|
+
/**
|
|
14
|
+
* Bodies sent in one `res.end(chunk)` call (which is what `res.json` and `res.send` do) up to this
|
|
15
|
+
* size are compressed synchronously on the event loop, instead of through a zlib stream.
|
|
16
|
+
* For typical JSON responses the per-response stream setup and threadpool round-trip cost more
|
|
17
|
+
* than the compression itself, and the sync path also keeps an exact `Content-Length`
|
|
18
|
+
* (streamed responses are chunked).
|
|
19
|
+
* Bodies above this size, and bodies written in several `res.write` calls, are streamed.
|
|
20
|
+
* Set to `0` to always stream.
|
|
21
|
+
*
|
|
22
|
+
* @default 128kb
|
|
23
|
+
*/
|
|
24
|
+
syncMaxBytes?: number | string;
|
|
13
25
|
/**
|
|
14
26
|
* Encoding to use when Accept-Encoding header is not present.
|
|
15
27
|
* @default 'identity'
|
|
@@ -39,7 +51,8 @@ export interface CompressionOptions {
|
|
|
39
51
|
/**
|
|
40
52
|
* Compression middleware with support for gzip, deflate, brotli, and zstd.
|
|
41
53
|
*
|
|
42
|
-
* This is a fork of the `compression` npm package with added zstd support
|
|
54
|
+
* This is a fork of the `compression` npm package with added zstd support,
|
|
55
|
+
* and a synchronous fast path for single-chunk bodies (see `syncMaxBytes`).
|
|
43
56
|
*
|
|
44
57
|
* Encoding preference order: zstd > br > gzip > deflate
|
|
45
58
|
*/
|
|
@@ -22,7 +22,8 @@ const cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/i;
|
|
|
22
22
|
/**
|
|
23
23
|
* Compression middleware with support for gzip, deflate, brotli, and zstd.
|
|
24
24
|
*
|
|
25
|
-
* This is a fork of the `compression` npm package with added zstd support
|
|
25
|
+
* This is a fork of the `compression` npm package with added zstd support,
|
|
26
|
+
* and a synchronous fast path for single-chunk bodies (see `syncMaxBytes`).
|
|
26
27
|
*
|
|
27
28
|
* Encoding preference order: zstd > br > gzip > deflate
|
|
28
29
|
*/
|
|
@@ -37,10 +38,14 @@ export function compressionMiddleware(options) {
|
|
|
37
38
|
},
|
|
38
39
|
};
|
|
39
40
|
// Zstd options
|
|
41
|
+
// Level 1 instead of zstd's own default of 3: for typical JSON responses the CPU cost and
|
|
42
|
+
// compression ratio are the same (measured 2026-10 on 22 KB and 115 KB payloads), but a level 3
|
|
43
|
+
// stream holds ~850 KB of native memory per in-flight response vs ~150 KB at level 1.
|
|
44
|
+
// That per-response allocation churn is what MALLOC_ARENA_MAX=2 works around on Cloud Run.
|
|
40
45
|
const optsZstd = {
|
|
41
46
|
...opts.zstd,
|
|
42
47
|
params: {
|
|
43
|
-
[zlib.constants.ZSTD_c_compressionLevel]:
|
|
48
|
+
[zlib.constants.ZSTD_c_compressionLevel]: 1,
|
|
44
49
|
...opts.zstd?.params,
|
|
45
50
|
},
|
|
46
51
|
};
|
|
@@ -56,11 +61,17 @@ export function compressionMiddleware(options) {
|
|
|
56
61
|
zlibOpts.windowBits = opts.windowBits;
|
|
57
62
|
if (opts.chunkSize !== undefined)
|
|
58
63
|
zlibOpts.chunkSize = opts.chunkSize;
|
|
64
|
+
const codecOpts = { zstd: optsZstd, brotli: optsBrotli, zlib: zlibOpts };
|
|
59
65
|
const filter = opts.filter || shouldCompress;
|
|
60
66
|
const threshold = parseBytes(opts.threshold) ?? 1024;
|
|
67
|
+
const syncMaxBytes = parseBytes(opts.syncMaxBytes) ?? 128 * 1024;
|
|
61
68
|
const enforceEncoding = opts.enforceEncoding || 'identity';
|
|
62
69
|
return function compression(req, res, next) {
|
|
63
70
|
let ended = false;
|
|
71
|
+
// Set by the first `res.write`: the body comes in several chunks, so only the stream path applies
|
|
72
|
+
let wrote = false;
|
|
73
|
+
// Set once the compress-or-not decision has been made for this response (by either path)
|
|
74
|
+
let decided = false;
|
|
64
75
|
let length;
|
|
65
76
|
let listeners = [];
|
|
66
77
|
let stream;
|
|
@@ -78,6 +89,7 @@ export function compressionMiddleware(options) {
|
|
|
78
89
|
if (ended) {
|
|
79
90
|
return false;
|
|
80
91
|
}
|
|
92
|
+
wrote = true;
|
|
81
93
|
if (!headersSent(res)) {
|
|
82
94
|
res.writeHead(res.statusCode);
|
|
83
95
|
}
|
|
@@ -96,6 +108,26 @@ export function compressionMiddleware(options) {
|
|
|
96
108
|
if (!res.getHeader('Content-Length')) {
|
|
97
109
|
length = chunkLength(chunk, encodingOrCallback);
|
|
98
110
|
}
|
|
111
|
+
// Single-chunk body (no prior `res.write`): the whole body is right here, so compress it
|
|
112
|
+
// synchronously instead of setting up a zlib stream for one chunk.
|
|
113
|
+
if (chunk && !wrote && !decided) {
|
|
114
|
+
const encoding = typeof encodingOrCallback === 'function' ? undefined : encodingOrCallback;
|
|
115
|
+
const body = toBuffer(chunk, encoding);
|
|
116
|
+
if (body.length <= syncMaxBytes) {
|
|
117
|
+
const method = decide(body.length);
|
|
118
|
+
if (method) {
|
|
119
|
+
const compressed = compressSync(method, body, codecOpts);
|
|
120
|
+
res.setHeader('Content-Encoding', method);
|
|
121
|
+
res.setHeader('Content-Length', compressed.length);
|
|
122
|
+
ended = true;
|
|
123
|
+
const cb = typeof encodingOrCallback === 'function' ? encodingOrCallback : callback;
|
|
124
|
+
// encoding is irrelevant for a Buffer chunk; it is passed to satisfy the `end` overload
|
|
125
|
+
return _end.call(res, compressed, 'utf8', cb);
|
|
126
|
+
}
|
|
127
|
+
// Not compressing: falls through to the plain `end`.
|
|
128
|
+
// The `onHeaders` listener below sees `decided` and does nothing.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
99
131
|
res.writeHead(res.statusCode);
|
|
100
132
|
}
|
|
101
133
|
if (!stream) {
|
|
@@ -127,46 +159,36 @@ export function compressionMiddleware(options) {
|
|
|
127
159
|
addListeners(res, _on, listeners);
|
|
128
160
|
listeners = null;
|
|
129
161
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
_on.call(res, 'close', function onResponseClose() {
|
|
138
|
-
closed = true;
|
|
139
|
-
stream?.destroy();
|
|
140
|
-
});
|
|
141
|
-
onHeaders(res, function onResponseHeaders() {
|
|
162
|
+
/**
|
|
163
|
+
* The compress-or-not decision, shared by the sync path (`res.end` holding the whole body)
|
|
164
|
+
* and the stream path (`onHeaders`).
|
|
165
|
+
* Returns the encoding to use, or undefined when the response is to be sent as-is.
|
|
166
|
+
*/
|
|
167
|
+
function decide(bodyLength) {
|
|
168
|
+
decided = true;
|
|
142
169
|
// determine if request is filtered
|
|
143
170
|
if (!filter(req, res)) {
|
|
144
|
-
nocompress('filtered');
|
|
145
|
-
return;
|
|
171
|
+
return nocompress('filtered');
|
|
146
172
|
}
|
|
147
173
|
// determine if the entity should be transformed
|
|
148
174
|
if (!shouldTransform(req, res)) {
|
|
149
|
-
nocompress('no transform');
|
|
150
|
-
return;
|
|
175
|
+
return nocompress('no transform');
|
|
151
176
|
}
|
|
152
177
|
// vary
|
|
153
178
|
vary(res, 'Accept-Encoding');
|
|
154
179
|
// content-length below threshold
|
|
155
180
|
if (Number(res.getHeader('Content-Length')) < threshold ||
|
|
156
|
-
(
|
|
157
|
-
nocompress('size below threshold');
|
|
158
|
-
return;
|
|
181
|
+
(bodyLength !== undefined && bodyLength < threshold)) {
|
|
182
|
+
return nocompress('size below threshold');
|
|
159
183
|
}
|
|
160
184
|
const encoding = res.getHeader('Content-Encoding') || 'identity';
|
|
161
185
|
// already encoded
|
|
162
186
|
if (encoding !== 'identity') {
|
|
163
|
-
nocompress('already encoded');
|
|
164
|
-
return;
|
|
187
|
+
return nocompress('already encoded');
|
|
165
188
|
}
|
|
166
189
|
// head
|
|
167
190
|
if (req.method === 'HEAD') {
|
|
168
|
-
nocompress('HEAD request');
|
|
169
|
-
return;
|
|
191
|
+
return nocompress('HEAD request');
|
|
170
192
|
}
|
|
171
193
|
// compression method
|
|
172
194
|
// Get all client-accepted encodings, then pick the first one from our preferred order
|
|
@@ -180,9 +202,28 @@ export function compressionMiddleware(options) {
|
|
|
180
202
|
}
|
|
181
203
|
// negotiation failed
|
|
182
204
|
if (!method || method === 'identity') {
|
|
183
|
-
nocompress('not acceptable');
|
|
184
|
-
return;
|
|
205
|
+
return nocompress('not acceptable');
|
|
185
206
|
}
|
|
207
|
+
return method;
|
|
208
|
+
}
|
|
209
|
+
// Release the compression stream when the response closes, freeing native
|
|
210
|
+
// zlib memory even if the client disconnected before the response finished.
|
|
211
|
+
// Registered before onHeaders so a close that happens before the stream
|
|
212
|
+
// exists is not missed (the `closed` flag below handles a stream created
|
|
213
|
+
// afterwards). Fixes CVE-2026-87776 / GHSA-vc2v-76pw-4v95.
|
|
214
|
+
// We use `stream.destroy()` (not the `destroy` npm package upstream added)
|
|
215
|
+
// because our Node target frees the zlib handle on destroy().
|
|
216
|
+
_on.call(res, 'close', function onResponseClose() {
|
|
217
|
+
closed = true;
|
|
218
|
+
stream?.destroy();
|
|
219
|
+
});
|
|
220
|
+
onHeaders(res, function onResponseHeaders() {
|
|
221
|
+
// The sync path has already handled this response (compressed it, or declined to)
|
|
222
|
+
if (decided)
|
|
223
|
+
return;
|
|
224
|
+
const method = decide(length);
|
|
225
|
+
if (!method)
|
|
226
|
+
return;
|
|
186
227
|
// compression stream
|
|
187
228
|
if (method === 'zstd') {
|
|
188
229
|
stream = zlib.createZstdCompress(optsZstd);
|
|
@@ -236,6 +277,19 @@ export function shouldCompress(_req, res) {
|
|
|
236
277
|
}
|
|
237
278
|
return true;
|
|
238
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* Compresses a whole body in one call, on the event loop.
|
|
282
|
+
* Same codec options as the streams, so the output is identical to the streamed one.
|
|
283
|
+
*/
|
|
284
|
+
function compressSync(method, body, opts) {
|
|
285
|
+
if (method === 'zstd')
|
|
286
|
+
return zlib.zstdCompressSync(body, opts.zstd);
|
|
287
|
+
if (method === 'br')
|
|
288
|
+
return zlib.brotliCompressSync(body, opts.brotli);
|
|
289
|
+
if (method === 'gzip')
|
|
290
|
+
return zlib.gzipSync(body, opts.zlib);
|
|
291
|
+
return zlib.deflateSync(body, opts.zlib);
|
|
292
|
+
}
|
|
239
293
|
function addListeners(stream, on, listeners) {
|
|
240
294
|
for (const [type, listener] of listeners) {
|
|
241
295
|
on.call(stream, type, listener);
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { BackendRequestHandler } from './server.model.js';
|
|
2
|
-
export declare function serverStatusMiddleware(
|
|
3
|
-
export declare function getServerStatusData(
|
|
2
|
+
export declare function serverStatusMiddleware(extra?: any): BackendRequestHandler;
|
|
3
|
+
export declare function getServerStatusData(extra?: any): Record<string, any>;
|
|
@@ -2,31 +2,22 @@ import os from 'node:os';
|
|
|
2
2
|
import { localTime } from '@naturalcycles/js-lib/datetime/localTime.js';
|
|
3
3
|
import { _filterNullishValues } from '@naturalcycles/js-lib/object/object.util.js';
|
|
4
4
|
import { memoryUsageFull, processSharedUtil } from '@naturalcycles/nodejs-lib';
|
|
5
|
-
import { getDeployInfo } from '../deploy/deployInfo.util.js';
|
|
6
5
|
const { versions, arch, platform } = process;
|
|
7
6
|
const availableParallelismAtStart = os.availableParallelism();
|
|
8
|
-
const {
|
|
9
|
-
export function serverStatusMiddleware(
|
|
7
|
+
const { GOOGLE_CLOUD_PROJECT, K_SERVICE, K_REVISION, APP_ENV, NODE_ENV, NODE_OPTIONS, UV_THREADPOOL_SIZE, MALLOC_ARENA_MAX, DEPLOY_BUILD_TIME, BUILD_VERSION, } = process.env;
|
|
8
|
+
export function serverStatusMiddleware(extra) {
|
|
10
9
|
return async (_req, res) => {
|
|
11
|
-
res.json(getServerStatusData(
|
|
10
|
+
res.json(getServerStatusData(extra));
|
|
12
11
|
};
|
|
13
12
|
}
|
|
14
|
-
export function getServerStatusData(
|
|
15
|
-
let deployBuildTime = DEPLOY_BUILD_TIME;
|
|
16
|
-
if (!deployBuildTime) {
|
|
17
|
-
const { ts } = getDeployInfo(projectDir);
|
|
18
|
-
deployBuildTime = localTime(ts).toPretty();
|
|
19
|
-
}
|
|
13
|
+
export function getServerStatusData(extra) {
|
|
20
14
|
const availableParallelism = os.availableParallelism();
|
|
21
15
|
return _filterNullishValues({
|
|
22
16
|
nodeProcessStarted: getStartedStr(),
|
|
23
|
-
|
|
17
|
+
DEPLOY_BUILD_TIME,
|
|
24
18
|
BUILD_VERSION,
|
|
25
19
|
APP_ENV,
|
|
26
20
|
GOOGLE_CLOUD_PROJECT,
|
|
27
|
-
GAE_APPLICATION,
|
|
28
|
-
GAE_SERVICE,
|
|
29
|
-
GAE_VERSION,
|
|
30
21
|
K_SERVICE,
|
|
31
22
|
K_REVISION,
|
|
32
23
|
processInfo: {
|
package/dist/util.d.ts
CHANGED
package/dist/util.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/backend-lib",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "9.
|
|
4
|
+
"version": "9.80.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@naturalcycles/db-lib": "^10",
|
|
7
7
|
"@naturalcycles/js-lib": "^15",
|
|
@@ -20,8 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"@sentry/node-core": "^10",
|
|
23
|
-
"firebase-admin": "^14"
|
|
24
|
-
"simple-git": "^4"
|
|
23
|
+
"firebase-admin": "^14"
|
|
25
24
|
},
|
|
26
25
|
"peerDependenciesMeta": {
|
|
27
26
|
"@sentry/node-core": {
|
|
@@ -29,9 +28,6 @@
|
|
|
29
28
|
},
|
|
30
29
|
"firebase-admin": {
|
|
31
30
|
"optional": true
|
|
32
|
-
},
|
|
33
|
-
"simple-git": {
|
|
34
|
-
"optional": true
|
|
35
31
|
}
|
|
36
32
|
},
|
|
37
33
|
"devDependencies": {
|
|
@@ -68,9 +64,6 @@
|
|
|
68
64
|
"!src/**/__exclude"
|
|
69
65
|
],
|
|
70
66
|
"bin": {
|
|
71
|
-
"deploy-gae": "bin/deploy-gae.js",
|
|
72
|
-
"undeploy-gae": "bin/undeploy-gae.js",
|
|
73
|
-
"deploy-prepare": "bin/deploy-prepare.js",
|
|
74
67
|
"deploy-health-check": "bin/deploy-health-check.js"
|
|
75
68
|
},
|
|
76
69
|
"types": "dist/index.d.ts",
|
|
@@ -85,7 +78,7 @@
|
|
|
85
78
|
"engines": {
|
|
86
79
|
"node": ">=24.10.0"
|
|
87
80
|
},
|
|
88
|
-
"description": "Standard library for making Express.js
|
|
81
|
+
"description": "Standard library for making Express.js based backend services",
|
|
89
82
|
"author": "Natural Cycles Team",
|
|
90
83
|
"license": "MIT",
|
|
91
84
|
"scripts": {
|
|
@@ -97,9 +90,6 @@
|
|
|
97
90
|
"check": "dev-lib check",
|
|
98
91
|
"typecheck": "dev-lib typecheck",
|
|
99
92
|
"dev": "APP_ENV=dev tsx watch src/test/server/server.ts",
|
|
100
|
-
"deploy-gae": "tsx src/bin/deploy-gae.ts",
|
|
101
|
-
"deploy-prepare": "tsx src/bin/deploy-prepare.ts",
|
|
102
|
-
"deploy-prepare-debug": "AA=AA1 BB=BB1 tsx src/bin/deploy-prepare.ts --projectDir ./src/test/project",
|
|
103
93
|
"deploy-health-check-debug1": "tsx src/bin/deploy-health-check.ts --url https://api-master.naturalcycles.com",
|
|
104
94
|
"deploy-health-check-debug2": "tsx src/bin/deploy-health-check.ts --url https://api-master2.naturalcycles.com --thresholdUnhealthy 5"
|
|
105
95
|
}
|
|
@@ -10,7 +10,6 @@ import { pDelay } from '@naturalcycles/js-lib/promise/pDelay.js'
|
|
|
10
10
|
import type { UnixTimestampMillis } from '@naturalcycles/js-lib/types'
|
|
11
11
|
import type { CliOptions } from '@naturalcycles/nodejs-lib/args'
|
|
12
12
|
import { dimGrey, red } from '@naturalcycles/nodejs-lib/colors'
|
|
13
|
-
import { exec2 } from '@naturalcycles/nodejs-lib/exec2'
|
|
14
13
|
import { coloredHttpCode } from '../server/request.log.util.js'
|
|
15
14
|
|
|
16
15
|
export interface DeployHealthCheckOptions {
|
|
@@ -19,11 +18,6 @@ export interface DeployHealthCheckOptions {
|
|
|
19
18
|
maxTries?: number
|
|
20
19
|
timeoutSec?: number
|
|
21
20
|
intervalSec?: number
|
|
22
|
-
logOnFailure?: boolean
|
|
23
|
-
logOnSuccess?: boolean
|
|
24
|
-
gaeProject?: string
|
|
25
|
-
gaeService?: string
|
|
26
|
-
gaeVersion?: string
|
|
27
21
|
authHeader?: string
|
|
28
22
|
}
|
|
29
23
|
|
|
@@ -48,25 +42,6 @@ export const deployHealthCheckCliOptions = {
|
|
|
48
42
|
type: 'number',
|
|
49
43
|
default: 1,
|
|
50
44
|
},
|
|
51
|
-
logOnFailure: {
|
|
52
|
-
type: 'boolean',
|
|
53
|
-
default: true,
|
|
54
|
-
desc: 'Show server logs on health check failure (requires gaeProject, gaeService, gaeVersion)',
|
|
55
|
-
},
|
|
56
|
-
logOnSuccess: {
|
|
57
|
-
type: 'boolean',
|
|
58
|
-
default: false,
|
|
59
|
-
desc: 'Show server logs on health check success (requires gaeProject, gaeService, gaeVersion)',
|
|
60
|
-
},
|
|
61
|
-
gaeProject: {
|
|
62
|
-
type: 'string',
|
|
63
|
-
},
|
|
64
|
-
gaeService: {
|
|
65
|
-
type: 'string',
|
|
66
|
-
},
|
|
67
|
-
gaeVersion: {
|
|
68
|
-
type: 'string',
|
|
69
|
-
},
|
|
70
45
|
authHeader: {
|
|
71
46
|
type: 'string',
|
|
72
47
|
},
|
|
@@ -81,6 +56,7 @@ const inspectOpt: InspectOptions = {
|
|
|
81
56
|
* Requires up to `healthyThreshold` consecutive OK responses to succeed.
|
|
82
57
|
* Fails on up to `unhealthyThreshold` consecutive FAIL responses.
|
|
83
58
|
* Fails after maxTries.
|
|
59
|
+
* Throws on failure.
|
|
84
60
|
*/
|
|
85
61
|
export async function deployHealthCheck(
|
|
86
62
|
url: string,
|
|
@@ -92,11 +68,6 @@ export async function deployHealthCheck(
|
|
|
92
68
|
maxTries = 30,
|
|
93
69
|
timeoutSec = 30,
|
|
94
70
|
intervalSec = 1,
|
|
95
|
-
logOnFailure,
|
|
96
|
-
logOnSuccess,
|
|
97
|
-
gaeProject,
|
|
98
|
-
gaeService,
|
|
99
|
-
gaeVersion,
|
|
100
71
|
authHeader,
|
|
101
72
|
} = opt
|
|
102
73
|
|
|
@@ -116,24 +87,7 @@ export async function deployHealthCheck(
|
|
|
116
87
|
|
|
117
88
|
if (failed) {
|
|
118
89
|
console.log(red(`Health check failed!`))
|
|
119
|
-
|
|
120
|
-
if (logOnFailure) {
|
|
121
|
-
try {
|
|
122
|
-
exec2.spawn(
|
|
123
|
-
`gcloud app logs read --project ${gaeProject} --service ${gaeService} --version ${gaeVersion}`,
|
|
124
|
-
)
|
|
125
|
-
} catch {}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
process.exit(1)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (logOnSuccess) {
|
|
132
|
-
try {
|
|
133
|
-
exec2.spawn(
|
|
134
|
-
`gcloud app logs read --project ${gaeProject} --service ${gaeService} --version ${gaeVersion}`,
|
|
135
|
-
)
|
|
136
|
-
} catch {}
|
|
90
|
+
throw new Error(`Health check failed: ${doneReason}`)
|
|
137
91
|
}
|
|
138
92
|
|
|
139
93
|
async function makeAttempt(): Promise<void> {
|
package/src/deploy/index.ts
CHANGED
|
@@ -1,21 +1,6 @@
|
|
|
1
|
-
import type { BackendCfg } from './backend.cfg.util.js'
|
|
2
|
-
import { getBackendCfg } from './backend.cfg.util.js'
|
|
3
|
-
import type { DeployInfo } from './deploy.model.js'
|
|
4
|
-
import { createAppYaml, createDeployInfo } from './deploy.util.js'
|
|
5
|
-
import { deployGae } from './deployGae.js'
|
|
6
1
|
import type { DeployHealthCheckOptions } from './deployHealthCheck.js'
|
|
7
2
|
import { deployHealthCheck } from './deployHealthCheck.js'
|
|
8
|
-
import { deployPrepare } from './deployPrepare.js'
|
|
9
3
|
|
|
10
|
-
export type {
|
|
4
|
+
export type { DeployHealthCheckOptions }
|
|
11
5
|
|
|
12
|
-
export {
|
|
13
|
-
createAppYaml,
|
|
14
|
-
createDeployInfo,
|
|
15
|
-
deployGae,
|
|
16
|
-
deployHealthCheck,
|
|
17
|
-
deployPrepare,
|
|
18
|
-
getBackendCfg,
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export * from './deployInfo.util.js'
|
|
6
|
+
export { deployHealthCheck }
|
|
@@ -14,10 +14,10 @@ import type {
|
|
|
14
14
|
BackendRequestHandler,
|
|
15
15
|
} from '../server/server.model.js'
|
|
16
16
|
import { simpleRequestLoggerMiddleware } from '../server/simpleRequestLoggerMiddleware.js'
|
|
17
|
-
import { isGAE } from '../util.js'
|
|
18
17
|
|
|
19
18
|
const isTest = process.env['APP_ENV'] === 'test'
|
|
20
19
|
const isDev = process.env['APP_ENV'] === 'dev'
|
|
20
|
+
const isGAE = !!process.env['GAE_INSTANCE']
|
|
21
21
|
|
|
22
22
|
export async function createDefaultApp(cfg: DefaultAppCfg): Promise<BackendApplication> {
|
|
23
23
|
const { sentryService } = cfg
|
|
@@ -69,7 +69,7 @@ export async function createDefaultApp(cfg: DefaultAppCfg): Promise<BackendAppli
|
|
|
69
69
|
}),
|
|
70
70
|
)
|
|
71
71
|
|
|
72
|
-
if (!isGAE
|
|
72
|
+
if (!isGAE && isTest) {
|
|
73
73
|
// compression is not used in AppEngine, because AppEngine provides it by default
|
|
74
74
|
app.use(compressionMiddleware())
|
|
75
75
|
}
|