jinaga-server 3.7.5 → 3.8.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/.claude/skills/degrees-of-freedom/.openskills.json +7 -0
- package/.claude/skills/degrees-of-freedom/SKILL.md +18 -0
- package/.claude/skills/degrees-of-freedom/degrees-of-freedom-constitution.md +142 -0
- package/.claude/skills/night-shift-setup/.openskills.json +7 -0
- package/.claude/skills/night-shift-setup/SKILL.md +95 -0
- package/.claude/skills/night-shift-setup/configuring-a-repository.md +111 -0
- package/.claude/skills/night-shift-setup/creating-the-log.md +59 -0
- package/.claude/skills/night-shift-setup/creating-the-routines.md +80 -0
- package/.claude/skills/night-shift-setup/log-model.factual +971 -0
- package/.claude/skills/night-shift-worker/.openskills.json +7 -0
- package/.claude/skills/night-shift-worker/SKILL.md +199 -0
- package/.claude/skills/night-shift-worker/night-shift-log.md +72 -0
- package/.claude/skills/night-shift-worker/scripts/register-stack.sh +173 -0
- package/.claude/skills/night-shift-worker/stacking.md +57 -0
- package/.claude/skills/night-shift-worker/verifying-a-merged-fix.md +16 -0
- package/.claude/skills/refining-issues/.openskills.json +7 -0
- package/.claude/skills/refining-issues/SKILL.md +327 -0
- package/.claude/skills/refining-issues/references/worked-example.md +234 -0
- package/.github/workflows/main.yml +2 -2
- package/.night-shift/config.md +84 -0
- package/.night-shift/skills.md +17 -0
- package/README.md +21 -0
- package/dist/http/router.js +19 -23
- package/dist/http/router.js.map +1 -1
- package/dist/http/stream.d.ts +0 -1
- package/dist/http/stream.js +6 -17
- package/dist/http/stream.js.map +1 -1
- package/dist/jinaga-server.js +74 -7
- package/dist/jinaga-server.js.map +1 -1
- package/integration-test/jinaga-test/package-lock.json +4 -4
- package/integration-test/jinaga-test/package.json +1 -1
- package/package.json +2 -2
- package/src/http/router.ts +31 -39
- package/src/http/stream.ts +21 -36
- package/src/jinaga-server.ts +92 -4
- package/test/http/streamLogVolumeSpec.ts +284 -0
- package/test/jinaga-server/poolConfigurationSpec.ts +147 -0
- package/test/jinaga-server/tracePoolSpec.ts +368 -0
package/src/http/router.ts
CHANGED
|
@@ -41,6 +41,13 @@ import { outputReadResultsStreaming } from "./output-formatters";
|
|
|
41
41
|
import { arrayToResultStream, ResultStream } from "./result-stream";
|
|
42
42
|
import { Stream } from "./stream";
|
|
43
43
|
|
|
44
|
+
// A streaming response reports itself twice: one line when the stream opens
|
|
45
|
+
// and one when it ends, carrying the messages sent, how long it lasted, and
|
|
46
|
+
// what ended it. Issue #192: the per-message and per-lifecycle-step lines this
|
|
47
|
+
// replaces made the log grow with traffic rather than with the number of events
|
|
48
|
+
// worth knowing about, and `STREAM DONE`'s message count already said what the
|
|
49
|
+
// per-message lines said one at a time. A plain GET says nothing at all, since
|
|
50
|
+
// Trace.dependency already records it. Failures still log.
|
|
44
51
|
function getOrStream<U>(
|
|
45
52
|
getMethod: ((req: RequestUser, params: { [key: string]: string }, query: qs.ParsedQs) => Promise<U | null>),
|
|
46
53
|
streamMethod: ((req: RequestUser, params: { [key: string]: string }, query: qs.ParsedQs) => Promise<Stream<U> | null>)
|
|
@@ -49,75 +56,68 @@ function getOrStream<U>(
|
|
|
49
56
|
const connectionId = Math.random().toString(36).substring(2, 10);
|
|
50
57
|
const user = <RequestUser>(req as any).user;
|
|
51
58
|
const accept = req.headers["accept"];
|
|
52
|
-
|
|
53
|
-
console.log(`[HttpConnection:${connectionId}] New request - User: ${user?.id || 'anonymous'}, Accept: ${accept}, URL: ${req.url}`);
|
|
54
|
-
|
|
59
|
+
|
|
55
60
|
if (accept && accept.indexOf("application/x-jinaga-feed-stream") >= 0) {
|
|
56
|
-
console.log(`[HttpConnection:${connectionId}] STREAMING request detected`);
|
|
57
|
-
|
|
58
61
|
streamMethod(user, req.params, req.query)
|
|
59
62
|
.then(response => {
|
|
60
63
|
if (!response) {
|
|
61
|
-
console.log(`[HttpConnection:${connectionId}] Stream method returned null - sending 404`);
|
|
62
64
|
sendRouteMiss(res);
|
|
63
65
|
next();
|
|
64
66
|
}
|
|
65
67
|
else {
|
|
66
|
-
console.log(`[HttpConnection:${connectionId}] Setting up streaming response`);
|
|
67
|
-
|
|
68
68
|
res.type("application/x-jinaga-feed-stream");
|
|
69
69
|
res.set("Connection", "keep-alive");
|
|
70
70
|
res.set("Cache-Control", "no-cache");
|
|
71
71
|
res.set("Access-Control-Allow-Origin", "*");
|
|
72
72
|
res.flushHeaders();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
|
|
74
|
+
const openedAt = Date.now();
|
|
75
|
+
console.log(`[HttpConnection:${connectionId}] Stream opened - User: ${user?.id || 'anonymous'}, URL: ${req.url}`);
|
|
76
|
+
|
|
76
77
|
let clientDisconnected = false;
|
|
77
|
-
let timeoutTriggered = false;
|
|
78
78
|
let streamClosed = false;
|
|
79
|
-
|
|
79
|
+
// What closed the stream from outside, where something
|
|
80
|
+
// other than the feed itself closed it. One value
|
|
81
|
+
// rather than a flag per cause, because a stream ends
|
|
82
|
+
// exactly once and only the first cause is the reason.
|
|
83
|
+
let closedBy: "client disconnect" | "timeout" | null = null;
|
|
84
|
+
|
|
80
85
|
// Client disconnect handler
|
|
81
86
|
req.on("close", () => {
|
|
82
|
-
console.log(`[HttpConnection:${connectionId}] CLIENT DISCONNECTED`);
|
|
83
87
|
clientDisconnected = true;
|
|
84
88
|
if (!streamClosed) {
|
|
85
|
-
|
|
86
|
-
response.close();
|
|
89
|
+
closedBy = "client disconnect";
|
|
87
90
|
streamClosed = true;
|
|
91
|
+
response.close();
|
|
88
92
|
}
|
|
89
93
|
});
|
|
90
|
-
|
|
94
|
+
|
|
91
95
|
// Timeout handler
|
|
92
96
|
const timeout = setTimeout(() => {
|
|
93
|
-
console.log(`[HttpConnection:${connectionId}] TIMEOUT TRIGGERED (5 minutes)`);
|
|
94
|
-
timeoutTriggered = true;
|
|
95
97
|
if (!streamClosed) {
|
|
96
|
-
|
|
97
|
-
response.close();
|
|
98
|
+
closedBy = "timeout";
|
|
98
99
|
streamClosed = true;
|
|
100
|
+
response.close();
|
|
99
101
|
}
|
|
100
102
|
}, 5 * 60 * 1000);
|
|
101
|
-
|
|
103
|
+
|
|
102
104
|
let messageCount = 0;
|
|
103
|
-
|
|
105
|
+
|
|
104
106
|
response
|
|
105
107
|
.next(data => {
|
|
106
108
|
messageCount++;
|
|
107
109
|
const writeStart = Date.now();
|
|
108
|
-
|
|
110
|
+
|
|
109
111
|
if (clientDisconnected) {
|
|
110
112
|
console.warn(`[HttpConnection:${connectionId}] Attempted to write to disconnected client - Message #${messageCount}`);
|
|
111
113
|
return;
|
|
112
114
|
}
|
|
113
|
-
|
|
115
|
+
|
|
114
116
|
try {
|
|
115
117
|
const jsonData = JSON.stringify(data) + "\n\n";
|
|
116
118
|
res.write(jsonData);
|
|
117
119
|
const writeDuration = Date.now() - writeStart;
|
|
118
|
-
|
|
119
|
-
console.log(`[HttpConnection:${connectionId}] Message #${messageCount} sent - Size: ${jsonData.length} bytes, Duration: ${writeDuration}ms`);
|
|
120
|
-
|
|
120
|
+
|
|
121
121
|
if (writeDuration > 50) {
|
|
122
122
|
console.warn(`[HttpConnection:${connectionId}] SLOW write operation - Duration: ${writeDuration}ms`);
|
|
123
123
|
}
|
|
@@ -126,24 +126,19 @@ function getOrStream<U>(
|
|
|
126
126
|
}
|
|
127
127
|
})
|
|
128
128
|
.done(() => {
|
|
129
|
-
console.log(`[HttpConnection:${connectionId}] STREAM DONE - Messages sent: ${messageCount}, Client disconnected: ${clientDisconnected}, Timeout triggered: ${timeoutTriggered}`);
|
|
130
|
-
|
|
131
129
|
clearTimeout(timeout);
|
|
132
130
|
streamClosed = true;
|
|
133
|
-
|
|
131
|
+
|
|
132
|
+
console.log(`[HttpConnection:${connectionId}] Stream ended - Messages sent: ${messageCount}, Duration: ${Date.now() - openedAt}ms, Reason: ${closedBy ?? "done"}`);
|
|
133
|
+
|
|
134
134
|
try {
|
|
135
135
|
if (!clientDisconnected) {
|
|
136
|
-
console.log(`[HttpConnection:${connectionId}] Ending socket connection`);
|
|
137
136
|
res.socket?.end();
|
|
138
|
-
} else {
|
|
139
|
-
console.log(`[HttpConnection:${connectionId}] Client already disconnected - skipping socket end`);
|
|
140
137
|
}
|
|
141
138
|
} catch (error) {
|
|
142
139
|
console.error(`[HttpConnection:${connectionId}] ERROR ending socket: ${error}`);
|
|
143
140
|
}
|
|
144
141
|
});
|
|
145
|
-
|
|
146
|
-
console.log(`[HttpConnection:${connectionId}] Stream handlers configured`);
|
|
147
142
|
}
|
|
148
143
|
})
|
|
149
144
|
.catch(error => {
|
|
@@ -152,16 +147,13 @@ function getOrStream<U>(
|
|
|
152
147
|
});
|
|
153
148
|
}
|
|
154
149
|
else {
|
|
155
|
-
console.log(`[HttpConnection:${connectionId}] Regular GET request`);
|
|
156
150
|
getMethod(user, req.params, req.query)
|
|
157
151
|
.then(response => {
|
|
158
152
|
if (!response) {
|
|
159
|
-
console.log(`[HttpConnection:${connectionId}] Get method returned null - sending 404`);
|
|
160
153
|
sendRouteMiss(res);
|
|
161
154
|
next();
|
|
162
155
|
}
|
|
163
156
|
else {
|
|
164
|
-
console.log(`[HttpConnection:${connectionId}] Sending JSON response`);
|
|
165
157
|
res.type("json");
|
|
166
158
|
res.send(JSON.stringify(response));
|
|
167
159
|
next();
|
package/src/http/stream.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
// This class logs nothing on its success path. A stream's lifetime is reported
|
|
2
|
+
// once by the request that opened it (see getOrStream in router.ts), and a line
|
|
3
|
+
// per feed, per replayed item, or per done handler says the same thing again
|
|
4
|
+
// once per unit of traffic. Issue #192: those per-item lines were 11.9% of a
|
|
5
|
+
// 2.8-million-line production log. Failures still log, because a failed handler
|
|
6
|
+
// is an event an operator cannot reconstruct from the summary.
|
|
1
7
|
export class Stream<T> {
|
|
2
8
|
private queue: T[] = [];
|
|
3
9
|
private handlers: ((data: T) => void)[] = [];
|
|
4
10
|
private doneHandlers: (() => void)[] = [];
|
|
5
11
|
private closed = false;
|
|
6
12
|
private readonly streamId: string;
|
|
7
|
-
private feedCount = 0;
|
|
8
13
|
|
|
9
14
|
constructor() {
|
|
10
15
|
this.streamId = Math.random().toString(36).substring(2, 10);
|
|
11
|
-
console.log(`[Stream:${this.streamId}] Created new stream`);
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
next(handler: (data: T) => void): Stream<T> {
|
|
@@ -16,21 +20,18 @@ export class Stream<T> {
|
|
|
16
20
|
console.warn(`[Stream:${this.streamId}] Attempted to add handler to closed stream`);
|
|
17
21
|
return this;
|
|
18
22
|
}
|
|
19
|
-
|
|
23
|
+
|
|
20
24
|
this.handlers.push(handler);
|
|
21
|
-
|
|
22
|
-
console.log(`[Stream:${this.streamId}] Added handler - Total handlers: ${this.handlers.length}, Queued items: ${queueLength}`);
|
|
23
|
-
|
|
25
|
+
|
|
24
26
|
// Replay queued data to new handler
|
|
25
27
|
this.queue.forEach((data, index) => {
|
|
26
28
|
try {
|
|
27
29
|
handler(data);
|
|
28
|
-
console.log(`[Stream:${this.streamId}] Replayed queued item ${index + 1}/${queueLength} to new handler`);
|
|
29
30
|
} catch (error) {
|
|
30
31
|
console.error(`[Stream:${this.streamId}] Error replaying queued item ${index + 1}: ${error}`);
|
|
31
32
|
}
|
|
32
33
|
});
|
|
33
|
-
|
|
34
|
+
|
|
34
35
|
return this;
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -44,9 +45,8 @@ export class Stream<T> {
|
|
|
44
45
|
}
|
|
45
46
|
return this;
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
+
|
|
48
49
|
this.doneHandlers.push(handler);
|
|
49
|
-
console.log(`[Stream:${this.streamId}] Added done handler - Total done handlers: ${this.doneHandlers.length}`);
|
|
50
50
|
return this;
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -55,70 +55,55 @@ export class Stream<T> {
|
|
|
55
55
|
console.warn(`[Stream:${this.streamId}] Attempted to close already closed stream`);
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
console.log(`[Stream:${this.streamId}] CLOSING stream - Handlers: ${this.handlers.length}, Done handlers: ${this.doneHandlers.length}, Queue: ${this.queue.length}, Feed count: ${this.feedCount}`);
|
|
60
|
-
|
|
58
|
+
|
|
61
59
|
this.closed = true;
|
|
62
|
-
|
|
60
|
+
|
|
63
61
|
// Execute done handlers
|
|
64
|
-
const doneHandlerCount = this.doneHandlers.length;
|
|
65
62
|
this.doneHandlers.forEach((handler, index) => {
|
|
66
63
|
try {
|
|
67
64
|
handler();
|
|
68
|
-
console.log(`[Stream:${this.streamId}] Executed done handler ${index + 1}/${doneHandlerCount}`);
|
|
69
65
|
} catch (error) {
|
|
70
66
|
console.error(`[Stream:${this.streamId}] Error in done handler ${index + 1}: ${error}`);
|
|
71
67
|
}
|
|
72
68
|
});
|
|
73
|
-
|
|
69
|
+
|
|
74
70
|
// Clear all references
|
|
75
71
|
this.queue = [];
|
|
76
72
|
this.handlers = [];
|
|
77
73
|
this.doneHandlers = [];
|
|
78
|
-
|
|
79
|
-
console.log(`[Stream:${this.streamId}] Stream closed and cleaned up`);
|
|
80
74
|
}
|
|
81
75
|
|
|
82
76
|
feed(data: any): void {
|
|
83
77
|
const feedStart = Date.now();
|
|
84
|
-
|
|
78
|
+
|
|
85
79
|
if (this.closed) {
|
|
86
80
|
console.warn(`[Stream:${this.streamId}] Attempted to feed data to closed stream - Data type: ${typeof data}`);
|
|
87
81
|
return;
|
|
88
82
|
}
|
|
89
|
-
|
|
90
|
-
this.feedCount++;
|
|
83
|
+
|
|
91
84
|
const handlerCount = this.handlers.length;
|
|
92
|
-
|
|
93
|
-
console.log(`[Stream:${this.streamId}] FEEDING data - Feed #${this.feedCount}, Handlers: ${handlerCount}, Queue size before: ${this.queue.length}`);
|
|
94
|
-
|
|
85
|
+
|
|
95
86
|
this.queue.push(data);
|
|
96
|
-
|
|
87
|
+
|
|
97
88
|
// Notify all handlers
|
|
98
|
-
let successCount = 0;
|
|
99
|
-
let errorCount = 0;
|
|
100
|
-
|
|
101
89
|
this.handlers.forEach((handler, index) => {
|
|
102
90
|
try {
|
|
103
91
|
const handlerStart = Date.now();
|
|
104
92
|
handler(data);
|
|
105
93
|
const handlerDuration = Date.now() - handlerStart;
|
|
106
|
-
|
|
107
|
-
|
|
94
|
+
|
|
108
95
|
if (handlerDuration > 50) {
|
|
109
96
|
console.warn(`[Stream:${this.streamId}] SLOW handler - Handler ${index + 1}/${handlerCount}, Duration: ${handlerDuration}ms`);
|
|
110
97
|
}
|
|
111
98
|
} catch (error) {
|
|
112
|
-
errorCount++;
|
|
113
99
|
console.error(`[Stream:${this.streamId}] ERROR in handler ${index + 1}/${handlerCount}: ${error}`);
|
|
114
100
|
}
|
|
115
101
|
});
|
|
116
|
-
|
|
102
|
+
|
|
117
103
|
const totalDuration = Date.now() - feedStart;
|
|
118
|
-
|
|
119
|
-
|
|
104
|
+
|
|
120
105
|
if (totalDuration > 100) {
|
|
121
106
|
console.warn(`[Stream:${this.streamId}] SLOW feed operation - Duration: ${totalDuration}ms`);
|
|
122
107
|
}
|
|
123
108
|
}
|
|
124
|
-
}
|
|
109
|
+
}
|
package/src/jinaga-server.ts
CHANGED
|
@@ -253,22 +253,82 @@ function getPool(uriOrPool: string | Pool, pools: { [uri: string]: Pool; }): Poo
|
|
|
253
253
|
}
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
type PoolSettingDeclaration = {
|
|
257
|
+
variable: string,
|
|
258
|
+
defaultValue: number,
|
|
259
|
+
minimum: number
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// The settings of a pool that this server builds for itself, each named by the
|
|
263
|
+
// environment variable that sets it. A pool the caller supplies is used as
|
|
264
|
+
// given, so none of these applies to it.
|
|
265
|
+
const poolSettingDeclarations = {
|
|
266
|
+
// A pool of zero connections can serve nothing, so one is the minimum.
|
|
267
|
+
max: { variable: "POSTGRES_POOL_MAX", defaultValue: 10, minimum: 1 },
|
|
268
|
+
idleTimeoutMillis: { variable: "POSTGRES_IDLE_TIMEOUT_MILLIS", defaultValue: 30000, minimum: 0 },
|
|
269
|
+
// Zero means wait forever for a connection to come free.
|
|
270
|
+
connectionTimeoutMillis: { variable: "POSTGRES_CONNECTION_TIMEOUT_MILLIS", defaultValue: 0, minimum: 0 }
|
|
271
|
+
} satisfies { [setting: string]: PoolSettingDeclaration };
|
|
272
|
+
|
|
273
|
+
type PoolSettings = {
|
|
274
|
+
[setting in keyof typeof poolSettingDeclarations]: number
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// The one rule by which every pool setting is read from the environment. An
|
|
278
|
+
// absent or empty variable takes the default; anything that is not an integer
|
|
279
|
+
// at or above the setting's minimum is refused rather than resolved, so the
|
|
280
|
+
// environment either names a pool that can be built or no pool at all.
|
|
281
|
+
function readPoolSetting({ variable, defaultValue, minimum }: PoolSettingDeclaration): number {
|
|
282
|
+
const value = process.env[variable];
|
|
283
|
+
if (value === undefined || value.trim() === '') {
|
|
284
|
+
return defaultValue;
|
|
285
|
+
}
|
|
286
|
+
const parsed = /^\d+$/.test(value.trim()) ? parseInt(value.trim(), 10) : NaN;
|
|
287
|
+
if (isNaN(parsed) || parsed < minimum) {
|
|
288
|
+
throw new Error(`The environment variable ${variable} is set to ${JSON.stringify(value)}, which is not an integer of at least ${minimum}.`);
|
|
289
|
+
}
|
|
290
|
+
return parsed;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function readPoolSettings(): PoolSettings {
|
|
294
|
+
return {
|
|
295
|
+
max: readPoolSetting(poolSettingDeclarations.max),
|
|
296
|
+
idleTimeoutMillis: readPoolSetting(poolSettingDeclarations.idleTimeoutMillis),
|
|
297
|
+
connectionTimeoutMillis: readPoolSetting(poolSettingDeclarations.connectionTimeoutMillis)
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
256
301
|
function createPool(postgresUri: string): Pool {
|
|
302
|
+
const settings = readPoolSettings();
|
|
257
303
|
const postgresPool = new Pool({
|
|
258
304
|
connectionString: postgresUri,
|
|
259
|
-
|
|
260
|
-
parseInt(process.env.POSTGRES_IDLE_TIMEOUT_MILLIS) :
|
|
261
|
-
30000,
|
|
305
|
+
...settings
|
|
262
306
|
});
|
|
263
307
|
|
|
308
|
+
Trace.info(`Postgres pool created with max ${settings.max}, idleTimeoutMillis ${settings.idleTimeoutMillis}, connectionTimeoutMillis ${settings.connectionTimeoutMillis}`);
|
|
309
|
+
|
|
264
310
|
tracePool(postgresPool);
|
|
265
311
|
|
|
266
312
|
return postgresPool;
|
|
267
313
|
}
|
|
268
314
|
|
|
315
|
+
const poolWindowMilliseconds = 10000;
|
|
316
|
+
|
|
269
317
|
export function tracePool(postgresPool: Pool) {
|
|
318
|
+
// Pool pressure is reported once per window of time rather than once per
|
|
319
|
+
// checkout, so the output rate no longer follows the acquire rate and no
|
|
320
|
+
// longer peaks when the pool is saturated. The window carries the largest
|
|
321
|
+
// waiting count it saw, so a spike that comes and goes inside one window
|
|
322
|
+
// still appears in the log.
|
|
323
|
+
let acquiredInWindow = false;
|
|
324
|
+
let peakWaiting = 0;
|
|
325
|
+
|
|
270
326
|
postgresPool.on('acquire', (client) => {
|
|
271
|
-
|
|
327
|
+
acquiredInWindow = true;
|
|
328
|
+
observeWaiting();
|
|
329
|
+
});
|
|
330
|
+
postgresPool.on('release', (err, client) => {
|
|
331
|
+
observeWaiting();
|
|
272
332
|
});
|
|
273
333
|
postgresPool.on('connect', (client) => {
|
|
274
334
|
Trace.metric('Postgres connected', poolMetrics());
|
|
@@ -280,6 +340,34 @@ export function tracePool(postgresPool: Pool) {
|
|
|
280
340
|
Trace.error(err);
|
|
281
341
|
});
|
|
282
342
|
|
|
343
|
+
// The window closes on a timer so that a storm which stops in the middle
|
|
344
|
+
// of a window still reports its peak. The timer is unreferenced, so it
|
|
345
|
+
// does not hold the process open, and it stops once the pool has ended.
|
|
346
|
+
const windowTimer = setInterval(closeWindow, poolWindowMilliseconds);
|
|
347
|
+
windowTimer.unref?.();
|
|
348
|
+
|
|
349
|
+
function observeWaiting() {
|
|
350
|
+
if (postgresPool.waitingCount > peakWaiting) {
|
|
351
|
+
peakWaiting = postgresPool.waitingCount;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function closeWindow() {
|
|
356
|
+
if (postgresPool.ended) {
|
|
357
|
+
clearInterval(windowTimer);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (acquiredInWindow) {
|
|
361
|
+
const metrics = poolMetrics();
|
|
362
|
+
Trace.metric('Postgres pool', { ...metrics, peakWaiting });
|
|
363
|
+
if (peakWaiting > 0) {
|
|
364
|
+
Trace.warn(`Postgres pool saturated: ${peakWaiting} waiting at peak, ${metrics.waiting} waiting of ${metrics.total} at window close.`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
acquiredInWindow = false;
|
|
368
|
+
peakWaiting = 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
283
371
|
function poolMetrics() {
|
|
284
372
|
return {
|
|
285
373
|
total: postgresPool.totalCount,
|