@mark1russell7/service 0.5.12 → 0.5.13
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/README.md +93 -1
- package/build/{core-C2wnF4mg.d.ts → core-BPLRDcUJ.d.ts} +45 -2
- package/build/dial-DA8vJfh2.d.ts +16 -0
- package/build/grpc.d.ts +307 -0
- package/build/grpc.js +859 -0
- package/build/grpc.js.map +1 -0
- package/build/http-client.d.ts +248 -0
- package/build/http-client.js +612 -0
- package/build/http-client.js.map +1 -0
- package/build/index.d.ts +3 -147
- package/build/{machine-Czfb6fV4.js → machine-BhTRu3u9.js} +182 -2
- package/build/machine-BhTRu3u9.js.map +1 -0
- package/build/mongodb.d.ts +307 -0
- package/build/mongodb.js +688 -0
- package/build/mongodb.js.map +1 -0
- package/build/pg.d.ts +1 -1
- package/build/pg.js +30 -33
- package/build/pg.js.map +1 -1
- package/build/queue-snRHK3Jm.d.ts +150 -0
- package/build/rabbitmq.d.ts +294 -0
- package/build/rabbitmq.js +748 -0
- package/build/rabbitmq.js.map +1 -0
- package/build/redis.d.ts +3 -9
- package/build/redis.js +6 -38
- package/build/redis.js.map +1 -1
- package/build/websocket.d.ts +275 -0
- package/build/websocket.js +780 -0
- package/build/websocket.js.map +1 -0
- package/package.json +38 -1
- package/build/machine-Czfb6fV4.js.map +0 -1
package/build/mongodb.js
ADDED
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
import { realClock } from "./clock.js";
|
|
2
|
+
import { B as zeroProblems, C as notSent, E as poolProblems, F as timeProblems, G as immediate, J as keepaliveSilent, K as keepaliveAnswered, L as wholeProblems, M as sendWithReissue, P as stepChecks, R as withinBound, S as ms, T as orZero, U as WHAT, V as MANIFESTATIONS, W as firstDetector, X as unwatched, Y as kernelGiveUp, _ as inFlight, a as helloDial, b as message, c as ageProblems, d as clientProblems, f as clientRecorder, h as finiteMs, j as roundTripCheck, l as attachClient, m as dialCheck, n as closeTimer, p as deadlineProblem, q as keepaliveReset, r as transition, s as STARTING_POLL, t as clientKnob, u as clientBounds, v as keepaliveOnProblems, w as operationTable, y as localStateCheck } from "./machine-BhTRu3u9.js";
|
|
3
|
+
import { quantity } from "@mark1russell7/quantity";
|
|
4
|
+
//#region src/mongodb/connection.ts
|
|
5
|
+
/**
|
|
6
|
+
* This factory makes a `MongoClient`. It loads the driver only when a client connects, thus a
|
|
7
|
+
* service that does not use MongoDB does not need it. The specifier is a value, thus the package
|
|
8
|
+
* needs no types of the driver. The driver gives `MongoClient` as a named export.
|
|
9
|
+
*/
|
|
10
|
+
const nodeMongo = async ({ url, options }) => {
|
|
11
|
+
return new (await (import("mongodb"))).MongoClient(url, options);
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/mongodb/meets.ts
|
|
15
|
+
/**
|
|
16
|
+
* This function gives the manifestations that a MongoDB client can meet, and the detectors of each,
|
|
17
|
+
* from its knobs. The cells of S1 measured them on the driver 7.7.0 against mongod 9.0.4:
|
|
18
|
+
* - The monitor writes off a silent server at `connectTimeoutMS + heartbeatFrequencyMS` after its last
|
|
19
|
+
* answer: 40.002 s at the defaults, and 7 s for 5 s + 2 s. The driver then clears the pool, and
|
|
20
|
+
* fails each command in flight. With `connectTimeoutMS` 0, nothing found a freeze in 60 s.
|
|
21
|
+
* - At `tcp_retries2 = 5`, the kernel gave up at 12.936 s.
|
|
22
|
+
* - A `keepAliveInitialDelay` of 27 s ended an idle stream at 37.255 s.
|
|
23
|
+
*
|
|
24
|
+
* The monitor tests the server, not each connection of the pool. Thus a write-off finds a failure of
|
|
25
|
+
* the server in flight and idle, and a failure of one connection waits for its own detector.
|
|
26
|
+
*/
|
|
27
|
+
const mongoMeetings = (name, knobs) => {
|
|
28
|
+
const writeoffTime = quantity((ms(knobs.connect_bound) + ms(knobs.heartbeat_interval)) / 1e3, "s");
|
|
29
|
+
const writeoff = (situation) => ({
|
|
30
|
+
by: "driver_writeoff_time",
|
|
31
|
+
within: writeoffTime,
|
|
32
|
+
situation,
|
|
33
|
+
effect: situation === "in flight" ? `the monitor writes the server off, and the driver fails the command as indeterminate. ${name} goes to failing` : `the monitor writes the server off, and the driver closes each idle connection. ${name} goes to failing`
|
|
34
|
+
});
|
|
35
|
+
const read = {
|
|
36
|
+
by: "request_timeout",
|
|
37
|
+
within: knobs.request_timeout,
|
|
38
|
+
situation: "in flight",
|
|
39
|
+
effect: "the command fails with a timeout of the socket, as indeterminate, and the driver closes the connection"
|
|
40
|
+
};
|
|
41
|
+
const idleExpiry = {
|
|
42
|
+
by: "pool_idle_expiry",
|
|
43
|
+
within: knobs.pool_idle_expiry,
|
|
44
|
+
situation: "idle",
|
|
45
|
+
effect: "the next checkout ends the idle connection, thus no command gets the dead connection"
|
|
46
|
+
};
|
|
47
|
+
const giveUp = kernelGiveUp(knobs.retrans_giveup_time, "the command fails as indeterminate");
|
|
48
|
+
const silent = (manifestation) => ({
|
|
49
|
+
manifestation,
|
|
50
|
+
what: WHAT[manifestation],
|
|
51
|
+
detectors: [
|
|
52
|
+
writeoff("in flight"),
|
|
53
|
+
read,
|
|
54
|
+
giveUp,
|
|
55
|
+
writeoff("idle"),
|
|
56
|
+
idleExpiry,
|
|
57
|
+
keepaliveSilent(knobs.keepalive_idle)
|
|
58
|
+
]
|
|
59
|
+
});
|
|
60
|
+
return [
|
|
61
|
+
{
|
|
62
|
+
manifestation: "rst",
|
|
63
|
+
what: WHAT.rst,
|
|
64
|
+
detectors: [
|
|
65
|
+
immediate("in flight", "the command fails with ECONNRESET, and the driver clears the pool"),
|
|
66
|
+
{
|
|
67
|
+
by: "heartbeat_interval",
|
|
68
|
+
within: knobs.heartbeat_interval,
|
|
69
|
+
situation: "idle",
|
|
70
|
+
effect: "the next answer of the monitor gets a reset, thus the driver writes the server off and clears the pool"
|
|
71
|
+
},
|
|
72
|
+
idleExpiry,
|
|
73
|
+
keepaliveReset(knobs.keepalive_idle)
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
manifestation: "fin",
|
|
78
|
+
what: WHAT.fin,
|
|
79
|
+
detectors: [immediate("in flight", "the command fails: the connection closed"), immediate("idle", "the stream of the monitor closes, thus the driver writes the server off and clears the pool")]
|
|
80
|
+
},
|
|
81
|
+
silent("blackhole"),
|
|
82
|
+
silent("freeze-host"),
|
|
83
|
+
{
|
|
84
|
+
manifestation: "freeze-process",
|
|
85
|
+
what: WHAT["freeze-process"],
|
|
86
|
+
detectors: [
|
|
87
|
+
writeoff("in flight"),
|
|
88
|
+
read,
|
|
89
|
+
writeoff("idle"),
|
|
90
|
+
idleExpiry,
|
|
91
|
+
keepaliveAnswered()
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
manifestation: "app-stall",
|
|
96
|
+
what: "the server stops its replies to commands for a time, for example behind a lock, and its monitor still answers",
|
|
97
|
+
detectors: [
|
|
98
|
+
{
|
|
99
|
+
by: "statement_timeout",
|
|
100
|
+
within: knobs.statement_timeout,
|
|
101
|
+
situation: "in flight",
|
|
102
|
+
effect: "the server ends the command with error 50 (MaxTimeMSExpired), a certain outcome, and the connection stays"
|
|
103
|
+
},
|
|
104
|
+
read,
|
|
105
|
+
idleExpiry,
|
|
106
|
+
keepaliveAnswered()
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
];
|
|
110
|
+
};
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/mongodb/client.ts
|
|
113
|
+
/** The driver takes a period of the monitor of 500 ms at least. */
|
|
114
|
+
const LEAST_HEARTBEAT_MS = 500;
|
|
115
|
+
/** This function gives the problems of a spec: the knobs that the driver cannot use. */
|
|
116
|
+
const specProblems = (name, spec) => {
|
|
117
|
+
const { knobs } = spec;
|
|
118
|
+
const problems = [
|
|
119
|
+
...timeProblems(name, knobs),
|
|
120
|
+
...zeroProblems(name, "the driver", knobs, {
|
|
121
|
+
bounded: [
|
|
122
|
+
"connect_bound",
|
|
123
|
+
"request_timeout",
|
|
124
|
+
"heartbeat_interval",
|
|
125
|
+
"client_retry_window",
|
|
126
|
+
"close_bound"
|
|
127
|
+
],
|
|
128
|
+
open: [
|
|
129
|
+
"statement_timeout",
|
|
130
|
+
"checkout_timeout",
|
|
131
|
+
"pool_idle_expiry"
|
|
132
|
+
]
|
|
133
|
+
}),
|
|
134
|
+
...wholeProblems(name, knobs, {
|
|
135
|
+
millis: [
|
|
136
|
+
"connect_bound",
|
|
137
|
+
"request_timeout",
|
|
138
|
+
"statement_timeout",
|
|
139
|
+
"heartbeat_interval",
|
|
140
|
+
"client_retry_window",
|
|
141
|
+
"checkout_timeout",
|
|
142
|
+
"pool_idle_expiry"
|
|
143
|
+
],
|
|
144
|
+
seconds: ["keepalive_idle"]
|
|
145
|
+
}),
|
|
146
|
+
...poolProblems(name, spec.pool)
|
|
147
|
+
];
|
|
148
|
+
if (ms(knobs.heartbeat_interval) < LEAST_HEARTBEAT_MS) problems.push(`${name}'s heartbeat_interval is below 500 ms, the least that the driver takes`);
|
|
149
|
+
problems.push(...keepaliveOnProblems(name, knobs, "the driver"), ...ageProblems(name, knobs, "the driver has no age for a connection"));
|
|
150
|
+
return problems;
|
|
151
|
+
};
|
|
152
|
+
/** This function gives the arguments of `new MongoClient` from a spec. Each timer of the driver comes from the declaration. */
|
|
153
|
+
const mongoOptions = (name, spec) => {
|
|
154
|
+
const { knobs } = spec;
|
|
155
|
+
return {
|
|
156
|
+
url: `mongodb://${[{
|
|
157
|
+
host: spec.host,
|
|
158
|
+
port: spec.port
|
|
159
|
+
}, ...spec.seeds ?? []].map((seed) => `${seed.host}:${seed.port}`).join(",")}/`,
|
|
160
|
+
options: {
|
|
161
|
+
appName: name,
|
|
162
|
+
...spec.replicaSet === void 0 ? {} : { replicaSet: spec.replicaSet },
|
|
163
|
+
...spec.username === void 0 ? {} : { auth: {
|
|
164
|
+
username: spec.username,
|
|
165
|
+
password: spec.password ?? ""
|
|
166
|
+
} },
|
|
167
|
+
...spec.authSource === void 0 ? {} : { authSource: spec.authSource },
|
|
168
|
+
maxPoolSize: spec.pool.max,
|
|
169
|
+
minPoolSize: spec.pool.min,
|
|
170
|
+
connectTimeoutMS: ms(knobs.connect_bound),
|
|
171
|
+
socketTimeoutMS: ms(knobs.request_timeout),
|
|
172
|
+
serverSelectionTimeoutMS: ms(knobs.client_retry_window),
|
|
173
|
+
heartbeatFrequencyMS: ms(knobs.heartbeat_interval),
|
|
174
|
+
waitQueueTimeoutMS: orZero(knobs.checkout_timeout),
|
|
175
|
+
maxIdleTimeMS: orZero(knobs.pool_idle_expiry),
|
|
176
|
+
keepAliveInitialDelay: ms(knobs.keepalive_idle),
|
|
177
|
+
serverMonitoringMode: "stream",
|
|
178
|
+
retryReads: false,
|
|
179
|
+
retryWrites: false,
|
|
180
|
+
monitorCommands: true
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* The bytes of an OP_MSG with the command `{ hello: 1, $db: "admin" }`. They are a header of 16
|
|
186
|
+
* bytes with the op code 2013, the flags, a section of kind 0, and the BSON document. The server answers a
|
|
187
|
+
* hello before the authentication, thus the dial needs no password.
|
|
188
|
+
*/
|
|
189
|
+
const HELLO = Uint8Array.of(52, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 221, 7, 0, 0, 0, 0, 0, 0, 0, 31, 0, 0, 0, 16, 104, 101, 108, 108, 111, 0, 1, 0, 0, 0, 2, 36, 100, 98, 0, 6, 0, 0, 0, 97, 100, 109, 105, 110, 0, 0);
|
|
190
|
+
/** The sizes of the values of the BSON types that a hello reply holds, by type. A null size has a length prefix. */
|
|
191
|
+
const FIXED = {
|
|
192
|
+
1: 8,
|
|
193
|
+
7: 12,
|
|
194
|
+
8: 1,
|
|
195
|
+
9: 8,
|
|
196
|
+
10: 0,
|
|
197
|
+
16: 4,
|
|
198
|
+
17: 8,
|
|
199
|
+
18: 8,
|
|
200
|
+
19: 16
|
|
201
|
+
};
|
|
202
|
+
/** This function gives the value of `ok` at the top of a BSON document, or an error. */
|
|
203
|
+
const okOf = (doc) => {
|
|
204
|
+
let at = 4;
|
|
205
|
+
while (at < doc.length - 1) {
|
|
206
|
+
const type = doc[at];
|
|
207
|
+
const end = doc.indexOf(0, at + 1);
|
|
208
|
+
if (end < 0) return /* @__PURE__ */ new Error("the hello reply has a field with no end");
|
|
209
|
+
const key = doc.toString("latin1", at + 1, end);
|
|
210
|
+
at = end + 1;
|
|
211
|
+
if (key === "ok") {
|
|
212
|
+
if (type === 1) return doc.readDoubleLE(at);
|
|
213
|
+
if (type === 16) return doc.readInt32LE(at);
|
|
214
|
+
return /* @__PURE__ */ new Error(`the ok of the hello reply has the BSON type ${type}`);
|
|
215
|
+
}
|
|
216
|
+
const fixed = FIXED[type];
|
|
217
|
+
if (fixed !== void 0) at += fixed;
|
|
218
|
+
else if (type === 2) at += 4 + doc.readInt32LE(at);
|
|
219
|
+
else if (type === 3 || type === 4) at += doc.readInt32LE(at);
|
|
220
|
+
else if (type === 5) at += 5 + doc.readInt32LE(at);
|
|
221
|
+
else return /* @__PURE__ */ new Error(`the hello reply has the BSON type ${type}, which the dial does not read`);
|
|
222
|
+
}
|
|
223
|
+
return /* @__PURE__ */ new Error("the hello reply has no ok");
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* This judge reads the answer to the hello. It waits for the whole message. The message must be an
|
|
227
|
+
* OP_MSG with a section of kind 0, and its document must have `ok` 1.
|
|
228
|
+
*/
|
|
229
|
+
const helloJudge = (received) => {
|
|
230
|
+
if (received.length < 4) return "more";
|
|
231
|
+
const length = received.readInt32LE(0);
|
|
232
|
+
if (length < 26 || length > 50331648) return /* @__PURE__ */ new Error(`the answer to the hello has a length of ${length}`);
|
|
233
|
+
if (received.length < length) return "more";
|
|
234
|
+
const opCode = received.readInt32LE(12);
|
|
235
|
+
if (opCode !== 2013) return /* @__PURE__ */ new Error(`the answer to the hello has the op code ${opCode}, not 2013`);
|
|
236
|
+
if (received[20] !== 0) return /* @__PURE__ */ new Error("the answer to the hello has no section of kind 0");
|
|
237
|
+
const ok = okOf(received.subarray(21, length));
|
|
238
|
+
if (ok instanceof Error) return ok;
|
|
239
|
+
return ok === 1 ? null : /* @__PURE__ */ new Error(`the hello reply has ok ${ok}`);
|
|
240
|
+
};
|
|
241
|
+
/** This dial function opens a TCP connection, sends a hello, and waits for its reply. The server opens no session for it. */
|
|
242
|
+
const helloCommandDial = helloDial(HELLO, helloJudge);
|
|
243
|
+
/** These errors tell that the command did not leave the client: no server, a full pool, a cleared pool, or a closed client. */
|
|
244
|
+
const NOT_SENT = /* @__PURE__ */ new Set([
|
|
245
|
+
"MongoServerSelectionError",
|
|
246
|
+
"MongoWaitQueueTimeoutError",
|
|
247
|
+
"MongoPoolClearedError",
|
|
248
|
+
"MongoPoolClosedError",
|
|
249
|
+
"MongoServerClosedError",
|
|
250
|
+
"MongoTopologyClosedError",
|
|
251
|
+
"MongoNotConnectedError",
|
|
252
|
+
"MongoClientClosedError"
|
|
253
|
+
]);
|
|
254
|
+
/**
|
|
255
|
+
* These codes of the server tell that the server interrupted the command. The causes are its
|
|
256
|
+
* shutdown, a step down, or a change of the state of the replica set. A part of the command can have applied.
|
|
257
|
+
*/
|
|
258
|
+
const INTERRUPTED = /* @__PURE__ */ new Set([
|
|
259
|
+
91,
|
|
260
|
+
189,
|
|
261
|
+
11600,
|
|
262
|
+
11602
|
|
263
|
+
]);
|
|
264
|
+
/** The writable servers: a primary, a standalone, a mongos, and a load balancer. */
|
|
265
|
+
const WRITABLE = /* @__PURE__ */ new Set([
|
|
266
|
+
"RSPrimary",
|
|
267
|
+
"Standalone",
|
|
268
|
+
"Mongos",
|
|
269
|
+
"LoadBalancer"
|
|
270
|
+
]);
|
|
271
|
+
/** This function gives the outcome of a reply. A write concern error tells that the write can roll back, thus it is not certain. */
|
|
272
|
+
const replyOutcome = (reply) => {
|
|
273
|
+
const concern = reply["writeConcernError"];
|
|
274
|
+
if (concern === void 0 || concern === null) return {
|
|
275
|
+
outcome: "ok",
|
|
276
|
+
value: reply
|
|
277
|
+
};
|
|
278
|
+
return {
|
|
279
|
+
outcome: "indeterminate",
|
|
280
|
+
error: `write concern ${String(concern.code)} ${String(concern.errmsg)}`
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
/**
|
|
284
|
+
* This function gives the outcome of a failure of the driver. The driver names each error class,
|
|
285
|
+
* thus the wrapper reads the name, and depends on no class of the driver:
|
|
286
|
+
* - A failure before the command left the client is certain: it did not apply.
|
|
287
|
+
* - An error of the server is certain, except an interruption, which can leave a part applied.
|
|
288
|
+
* - A write concern error, a network error after the handshake, and a timeout of the socket are not certain.
|
|
289
|
+
*/
|
|
290
|
+
const failureOutcome = (error) => {
|
|
291
|
+
const text = message(error);
|
|
292
|
+
const { name, code, beforeHandshake } = typeof error === "object" && error !== null ? error : {};
|
|
293
|
+
if (typeof name === "string" && NOT_SENT.has(name)) return notSent(text);
|
|
294
|
+
if (name === "MongoNetworkError" && beforeHandshake === true) return notSent(text);
|
|
295
|
+
if (name === "MongoServerError" && typeof code === "number") {
|
|
296
|
+
const said = `${code} ${text}`;
|
|
297
|
+
return INTERRUPTED.has(code) ? {
|
|
298
|
+
outcome: "indeterminate",
|
|
299
|
+
error: said
|
|
300
|
+
} : {
|
|
301
|
+
outcome: "error",
|
|
302
|
+
error: said,
|
|
303
|
+
reply: true
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
outcome: "indeterminate",
|
|
308
|
+
error: text
|
|
309
|
+
};
|
|
310
|
+
};
|
|
311
|
+
/** This function gives the host and the port of an address `host:port`. */
|
|
312
|
+
const endsOf = (address) => {
|
|
313
|
+
const at = address.lastIndexOf(":");
|
|
314
|
+
const port = Number(address.slice(at + 1));
|
|
315
|
+
return at < 0 || !Number.isInteger(port) ? {
|
|
316
|
+
host: address,
|
|
317
|
+
port: null
|
|
318
|
+
} : {
|
|
319
|
+
host: address.slice(0, at),
|
|
320
|
+
port
|
|
321
|
+
};
|
|
322
|
+
};
|
|
323
|
+
/**
|
|
324
|
+
* This function makes a MongoDB client, a dependency of a service over a `MongoClient`.
|
|
325
|
+
* - It sets each timer of the driver from the declaration, and it turns off the retries of the driver.
|
|
326
|
+
* The client sends again only an idempotent operation of its table.
|
|
327
|
+
* - It tells the outcome of a command in three values, from the name of each error of the driver.
|
|
328
|
+
* - It writes the deadline of the server in each command (`maxTimeMS`), thus the server ends a slow command first (O07).
|
|
329
|
+
* - It gives the state from the view of the topology: ready while a server is writable.
|
|
330
|
+
* - It listens to each error of the driver. In Node, an error with no listener throws, and library code does not end the process (A5).
|
|
331
|
+
* - It records the connection id of the server for each connection of the pool (D1).
|
|
332
|
+
*
|
|
333
|
+
* The outcomes, the second try, the commands in flight, the bounded close and the checks come from
|
|
334
|
+
* the core of the clients. This entry gives what the driver does in its own way.
|
|
335
|
+
*/
|
|
336
|
+
const mongoClient = (name, spec, options = {}) => {
|
|
337
|
+
const problems = specProblems(name, spec);
|
|
338
|
+
if (problems.length > 0) throw new Error(problems.join("; "));
|
|
339
|
+
const { knobs } = spec;
|
|
340
|
+
const clock = options.clock ?? realClock;
|
|
341
|
+
const create = options.create ?? nodeMongo;
|
|
342
|
+
const dial = options.dial ?? helloCommandDial;
|
|
343
|
+
const operations = operationTable({}, spec.operations);
|
|
344
|
+
const deadline = finiteMs(knobs.statement_timeout);
|
|
345
|
+
const callBound = ms(knobs.client_retry_window) + ms(knobs.request_timeout);
|
|
346
|
+
let driver = null;
|
|
347
|
+
let status = "none";
|
|
348
|
+
const joins = [];
|
|
349
|
+
const joined = /* @__PURE__ */ new Map();
|
|
350
|
+
const flight = inFlight();
|
|
351
|
+
const emit = clientRecorder(options.record, clock, name);
|
|
352
|
+
const record = (on, to = status, detail) => {
|
|
353
|
+
const from = status;
|
|
354
|
+
status = to;
|
|
355
|
+
emit(from, to, on, detail);
|
|
356
|
+
};
|
|
357
|
+
const closing = () => status === "closing" || status === "end";
|
|
358
|
+
const onTopology = (event) => {
|
|
359
|
+
const writable = [...event.newDescription.servers.values()].some((server) => WRITABLE.has(server.type));
|
|
360
|
+
if (writable && (status === "connecting" || status === "failing")) record("topology.writable", "ready");
|
|
361
|
+
else if (!writable && status === "ready") record("topology.unwritable", "failing");
|
|
362
|
+
};
|
|
363
|
+
const onReady = (event) => {
|
|
364
|
+
const generation = joins.length + 1;
|
|
365
|
+
const { host, port } = endsOf(event.address);
|
|
366
|
+
joined.set(`${event.address}#${String(event.connectionId)}`, joins.length);
|
|
367
|
+
joins.push({
|
|
368
|
+
generation,
|
|
369
|
+
at: clock.now(),
|
|
370
|
+
address: event.address,
|
|
371
|
+
serverConnectionId: null,
|
|
372
|
+
localAddress: null,
|
|
373
|
+
localPort: null,
|
|
374
|
+
remoteAddress: host,
|
|
375
|
+
remotePort: port
|
|
376
|
+
});
|
|
377
|
+
record("join", status, `generation ${generation}, ${event.address}`);
|
|
378
|
+
};
|
|
379
|
+
const onStarted = (event) => {
|
|
380
|
+
const index = joined.get(`${event.address}#${String(event.connectionId)}`);
|
|
381
|
+
const id = event.serverConnectionId;
|
|
382
|
+
if (index === void 0 || id === null || id === void 0) return;
|
|
383
|
+
joined.delete(`${event.address}#${String(event.connectionId)}`);
|
|
384
|
+
joins[index] = {
|
|
385
|
+
...joins[index],
|
|
386
|
+
serverConnectionId: Number(id)
|
|
387
|
+
};
|
|
388
|
+
};
|
|
389
|
+
const onCleared = (event) => record("pool.cleared", status, `${event.address}${event.interruptInUseConnections === true ? ", with the commands in flight" : ""}`);
|
|
390
|
+
const onHeartbeat = (event) => record("heartbeat.failed", status, `${event.connectionId}: ${message(event.failure)}`);
|
|
391
|
+
const onError = (error) => {
|
|
392
|
+
if (status !== "end") record("client.error", status, message(error));
|
|
393
|
+
};
|
|
394
|
+
const listeners = [
|
|
395
|
+
["topologyDescriptionChanged", onTopology],
|
|
396
|
+
["connectionReady", onReady],
|
|
397
|
+
["commandStarted", onStarted],
|
|
398
|
+
["connectionPoolCleared", onCleared],
|
|
399
|
+
["serverHeartbeatFailed", onHeartbeat]
|
|
400
|
+
];
|
|
401
|
+
/** This function makes one try of a command. The driver selects a server, checks out a connection, and sends the command. */
|
|
402
|
+
const attempt = (document, signal) => {
|
|
403
|
+
const c = driver;
|
|
404
|
+
if (c === null) return Promise.resolve(notSent(`${name} has not connected`));
|
|
405
|
+
if (closing()) return Promise.resolve(notSent(`${name} is ${status}: the command did not leave the client`));
|
|
406
|
+
if (signal?.aborted) return Promise.resolve(notSent("the caller aborted the command before it left the client"));
|
|
407
|
+
const sent = deadline === void 0 || "maxTimeMS" in document ? document : {
|
|
408
|
+
...document,
|
|
409
|
+
maxTimeMS: deadline
|
|
410
|
+
};
|
|
411
|
+
return flight.track(signal, "the caller abandoned the command after it went to the driver", (settle) => {
|
|
412
|
+
c.db(spec.database).command(sent, signal === void 0 ? {} : { signal }).then((reply) => settle(replyOutcome(reply)), (error) => settle(failureOutcome(error)));
|
|
413
|
+
});
|
|
414
|
+
};
|
|
415
|
+
const command = (operation, document, sendOptions = {}) => sendWithReissue({
|
|
416
|
+
clock,
|
|
417
|
+
bound: callBound,
|
|
418
|
+
operation: operations[operation],
|
|
419
|
+
options: sendOptions,
|
|
420
|
+
attempt: (signal) => attempt(document, signal),
|
|
421
|
+
onReissue: () => record("command.reissue", status, operation)
|
|
422
|
+
});
|
|
423
|
+
const handle = {
|
|
424
|
+
async connect(signal) {
|
|
425
|
+
record("status.connecting", "connecting");
|
|
426
|
+
const c = await create(mongoOptions(name, spec));
|
|
427
|
+
driver = c;
|
|
428
|
+
for (const [event, listener] of listeners) c.on(event, listener);
|
|
429
|
+
c.on("error", onError);
|
|
430
|
+
if (signal.aborted) throw new Error(`${name}: the service stopped before the connect`);
|
|
431
|
+
const connecting = c.connect().then(() => void 0, (error) => {
|
|
432
|
+
if (!closing() && status !== "ready") record("connect.failed", "failing", message(error));
|
|
433
|
+
});
|
|
434
|
+
await withinBound(clock, ms(knobs.connect_bound), connecting);
|
|
435
|
+
if (status === "connecting") record("connect.failed", "failing", "no writable server in connect_bound");
|
|
436
|
+
},
|
|
437
|
+
async close() {
|
|
438
|
+
const c = driver;
|
|
439
|
+
if (c === null || closing()) return;
|
|
440
|
+
record("close.request", "closing");
|
|
441
|
+
if (!await withinBound(clock, ms(knobs.close_bound), c.close())) record("close.bound", status, "the close of the driver goes on");
|
|
442
|
+
flight.settleAll("the client closed with the command in flight");
|
|
443
|
+
for (const [event, listener] of listeners) c.off(event, listener);
|
|
444
|
+
record("status.end", "end");
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
const declaration = {
|
|
448
|
+
system: spec.system,
|
|
449
|
+
knobs: {
|
|
450
|
+
connect_bound: knobs.connect_bound,
|
|
451
|
+
request_timeout: knobs.request_timeout,
|
|
452
|
+
statement_timeout: knobs.statement_timeout,
|
|
453
|
+
heartbeat_interval: knobs.heartbeat_interval,
|
|
454
|
+
client_retry_window: knobs.client_retry_window,
|
|
455
|
+
driver_writeoff_time: {
|
|
456
|
+
value: (ms(knobs.connect_bound) + ms(knobs.heartbeat_interval)) / 1e3,
|
|
457
|
+
unit: "s"
|
|
458
|
+
},
|
|
459
|
+
checkout_timeout: knobs.checkout_timeout,
|
|
460
|
+
pool_idle_expiry: knobs.pool_idle_expiry,
|
|
461
|
+
keepalive_idle: knobs.keepalive_idle,
|
|
462
|
+
idle_refresh_interval: knobs.keepalive_idle,
|
|
463
|
+
max_connection_age: knobs.max_connection_age,
|
|
464
|
+
close_bound: knobs.close_bound,
|
|
465
|
+
retrans_giveup_time: knobs.retrans_giveup_time
|
|
466
|
+
},
|
|
467
|
+
operations
|
|
468
|
+
};
|
|
469
|
+
const checks = (checkOptions) => stepChecks(name, checkOptions, {
|
|
470
|
+
"local-state": localStateCheck(() => status === "ready"),
|
|
471
|
+
"round-trip": roundTripCheck((sendOptions) => command("check", { ping: 1 }, sendOptions)),
|
|
472
|
+
dial: dialCheck(dial, spec.host, spec.port)
|
|
473
|
+
});
|
|
474
|
+
return {
|
|
475
|
+
name,
|
|
476
|
+
declaration,
|
|
477
|
+
handle,
|
|
478
|
+
meets: mongoMeetings(name, knobs),
|
|
479
|
+
get status() {
|
|
480
|
+
return status;
|
|
481
|
+
},
|
|
482
|
+
joins,
|
|
483
|
+
command,
|
|
484
|
+
checks,
|
|
485
|
+
attach: (svc, checkOptions) => attachClient(svc, name, declaration, handle, checks(checkOptions)),
|
|
486
|
+
problems(description) {
|
|
487
|
+
const late = deadlineProblem(name, knobs.request_timeout, knobs.statement_timeout, "command");
|
|
488
|
+
return [...clientProblems(name, description, clientBounds(knobs)), ...late === null ? [] : [late]];
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
};
|
|
492
|
+
//#endregion
|
|
493
|
+
//#region src/mongodb/machine.ts
|
|
494
|
+
/** The states where the driver exists and serves. */
|
|
495
|
+
const SERVING = [
|
|
496
|
+
"connecting",
|
|
497
|
+
"ready",
|
|
498
|
+
"failing"
|
|
499
|
+
];
|
|
500
|
+
const OPEN = [...SERVING, "closing"];
|
|
501
|
+
/**
|
|
502
|
+
* The MongoDB client of the service as a `mark1russell7.machine/1` document. It is the topology as
|
|
503
|
+
* the wrapper sees it, not one connection: the document `mongodb-node` of `@mark1russell7/machines`
|
|
504
|
+
* gives the monitor and each connection. The state is the view of the topology:
|
|
505
|
+
* - `ready`: a server is writable: a primary, a standalone, a mongos, or a load balancer.
|
|
506
|
+
* - `failing`: no server is writable, or the startup found none in `connect_bound`. The monitor
|
|
507
|
+
* goes on, and a command waits for a writable server in `client_retry_window`.
|
|
508
|
+
*
|
|
509
|
+
* A connection that closes and a pool that the driver clears do not change the state by themselves.
|
|
510
|
+
* The write-off of a server changes the view of the topology, and that changes the state. A test
|
|
511
|
+
* records the transitions of the client and replays them against this document.
|
|
512
|
+
*/
|
|
513
|
+
const MONGO_CLIENT_MACHINE = {
|
|
514
|
+
format: "mark1russell7.machine/1",
|
|
515
|
+
id: "service.machine.mongodb-client",
|
|
516
|
+
title: "The MongoDB client of the service, over a MongoClient of the driver 7",
|
|
517
|
+
layer: "driver",
|
|
518
|
+
version: "0.1.0",
|
|
519
|
+
sources: [{
|
|
520
|
+
ref: "packages/service/src/mongodb (the client implements this document)",
|
|
521
|
+
rung: "implemented"
|
|
522
|
+
}],
|
|
523
|
+
states: {
|
|
524
|
+
none: {
|
|
525
|
+
initial: true,
|
|
526
|
+
note: "the client exists, and its MongoClient does not"
|
|
527
|
+
},
|
|
528
|
+
connecting: { note: "the monitor looks for the servers of the topology" },
|
|
529
|
+
ready: { note: "a server is writable. A dead idle connection stays hidden until the monitor writes the server off" },
|
|
530
|
+
failing: { note: "no server is writable. A command waits for one in client_retry_window" },
|
|
531
|
+
closing: { note: "the close of the driver: it fails the commands in flight, sends endSessions, and closes the topology" },
|
|
532
|
+
end: {
|
|
533
|
+
final: true,
|
|
534
|
+
note: "the service closed the client"
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
events: {
|
|
538
|
+
"status.connecting": { kind: "local" },
|
|
539
|
+
"topology.writable": {
|
|
540
|
+
kind: "rx",
|
|
541
|
+
note: "the view of the topology has a writable server"
|
|
542
|
+
},
|
|
543
|
+
"topology.unwritable": {
|
|
544
|
+
kind: "rx",
|
|
545
|
+
note: "the view of the topology has no writable server, for example after a write-off or a step down"
|
|
546
|
+
},
|
|
547
|
+
join: {
|
|
548
|
+
kind: "rx",
|
|
549
|
+
note: "a connection of the pool finished its handshake (D1). The id of the server comes with its first command"
|
|
550
|
+
},
|
|
551
|
+
"connect.failed": {
|
|
552
|
+
kind: "local",
|
|
553
|
+
note: "the startup found no writable server in connect_bound, or the connect of the driver failed"
|
|
554
|
+
},
|
|
555
|
+
"pool.cleared": {
|
|
556
|
+
kind: "rx",
|
|
557
|
+
note: "the driver cleared the pool of a server, and with a write-off also failed its commands in flight"
|
|
558
|
+
},
|
|
559
|
+
"heartbeat.failed": {
|
|
560
|
+
kind: "rx",
|
|
561
|
+
note: "a hello of the monitor failed"
|
|
562
|
+
},
|
|
563
|
+
"client.error": {
|
|
564
|
+
kind: "rx",
|
|
565
|
+
note: "an error event of the driver"
|
|
566
|
+
},
|
|
567
|
+
"command.reissue": { kind: "local" },
|
|
568
|
+
"close.request": { kind: "local" },
|
|
569
|
+
"close.bound": {
|
|
570
|
+
kind: "timer",
|
|
571
|
+
note: "the close of the driver goes on after the bound, in the background"
|
|
572
|
+
},
|
|
573
|
+
"close.destroy": {
|
|
574
|
+
kind: "local",
|
|
575
|
+
note: "the client never records it: the driver gives no socket that the bound can destroy"
|
|
576
|
+
},
|
|
577
|
+
"status.end": { kind: "local" }
|
|
578
|
+
},
|
|
579
|
+
knobs: {
|
|
580
|
+
connect_bound: clientKnob("connectTimeoutMS"),
|
|
581
|
+
close_bound: clientKnob("close.bound")
|
|
582
|
+
},
|
|
583
|
+
timers: {
|
|
584
|
+
connect_timeout: {
|
|
585
|
+
duration: {
|
|
586
|
+
knob: "connect_bound",
|
|
587
|
+
unit: "s"
|
|
588
|
+
},
|
|
589
|
+
arms: ["enter:connecting"],
|
|
590
|
+
cancels: ["topology.writable", "connect.failed"],
|
|
591
|
+
fires: "connect.failed",
|
|
592
|
+
note: "the startup stops its wait, and the connect of the driver goes on in client_retry_window"
|
|
593
|
+
},
|
|
594
|
+
close: closeTimer()
|
|
595
|
+
},
|
|
596
|
+
transitions: [
|
|
597
|
+
transition("connect", "none", "status.connecting", "connecting"),
|
|
598
|
+
transition("writable", ["connecting", "failing"], "topology.writable", "ready"),
|
|
599
|
+
transition("unwritable", "ready", "topology.unwritable", "failing"),
|
|
600
|
+
transition("connect-failed", ["connecting", "failing"], "connect.failed", "failing", { timer: "connect_timeout" }),
|
|
601
|
+
...OPEN.map((state) => transition(`join-${state}`, state, "join", state)),
|
|
602
|
+
...OPEN.map((state) => transition(`pool-cleared-${state}`, state, "pool.cleared", state)),
|
|
603
|
+
...OPEN.map((state) => transition(`heartbeat-failed-${state}`, state, "heartbeat.failed", state)),
|
|
604
|
+
...OPEN.map((state) => transition(`client-error-${state}`, state, "client.error", state)),
|
|
605
|
+
...OPEN.map((state) => transition(`reissue-${state}`, state, "command.reissue", state)),
|
|
606
|
+
transition("close", [...SERVING], "close.request", "closing"),
|
|
607
|
+
transition("close-bound", "closing", "close.bound", "closing", { timer: "close" }),
|
|
608
|
+
transition("end", "closing", "status.end", "end")
|
|
609
|
+
]
|
|
610
|
+
};
|
|
611
|
+
//#endregion
|
|
612
|
+
//#region src/mongodb/starting.ts
|
|
613
|
+
/**
|
|
614
|
+
* This poll is the poll of the checks that `MONGO_STARTING_KNOBS` fits. It is `STARTING_POLL` of
|
|
615
|
+
* the core: each 5 s with a jitter of 20 %, and a timeout of 4 s (B6, O12). The poll does not depend on the driver.
|
|
616
|
+
*/
|
|
617
|
+
const MONGO_STARTING_POLL = STARTING_POLL;
|
|
618
|
+
/**
|
|
619
|
+
* These knobs are a declaration to start from, from what the cells of S1 measured on the driver
|
|
620
|
+
* 7.7.0 on Node 24.21.0 against mongod 9.0.4. The runtime has no defaults: a service gives these
|
|
621
|
+
* knobs, or its own. Each value holds the borders that the cells measured:
|
|
622
|
+
* - `connect_bound` 5 s and `heartbeat_interval` 2 s: the write-off of a silent server came 7 s after
|
|
623
|
+
* its last answer, and the cells T01.border-off.75 and .76 held. At the defaults it came at 40.002 s.
|
|
624
|
+
* - `request_timeout` 9.5 s: in the slowest poll of 10 s (O13). At the defaults the driver has no
|
|
625
|
+
* bound on a read, and only the write-off ends the command.
|
|
626
|
+
* - `statement_timeout` 9 s: below `request_timeout`, thus the server ends a slow command first,
|
|
627
|
+
* with error 50 (O07).
|
|
628
|
+
* - `client_retry_window` 30 s: after an election, the first write took 17 s to 18 s, and the band
|
|
629
|
+
* ended at 26.58 s (T11.defaults.10). The default of the driver, 30 s, holds that band.
|
|
630
|
+
* - `checkout_timeout` 5 s: a full pool fails a checkout in a startup budget. At the defaults the wait has no bound.
|
|
631
|
+
* - `pool_idle_expiry` 10 s: no cell measured it. It is the bound of O03 and O01 that the pg client
|
|
632
|
+
* uses: below a router that forgets an idle flow at 12 s. The driver applies it at the next checkout.
|
|
633
|
+
* - `keepalive_idle` 30 s: the driver always turns keepalive on, at 120 s by default. This value
|
|
634
|
+
* refreshes a held idle connection through the NAT forms of the cells (240 s and 350 s).
|
|
635
|
+
* - `max_connection_age` "infinite": the driver has no age for a connection.
|
|
636
|
+
* - `close_bound` 2 s: behind a black hole the close of the driver waits for `endSessions`.
|
|
637
|
+
* - `retrans_giveup_time` 924.6 s: the give-up of Linux at `tcp_retries2 = 15`. The cells measured 12.936 s at `tcp_retries2 = 5`.
|
|
638
|
+
*/
|
|
639
|
+
const MONGO_STARTING_KNOBS = {
|
|
640
|
+
connect_bound: {
|
|
641
|
+
value: 5,
|
|
642
|
+
unit: "s"
|
|
643
|
+
},
|
|
644
|
+
request_timeout: {
|
|
645
|
+
value: 9.5,
|
|
646
|
+
unit: "s"
|
|
647
|
+
},
|
|
648
|
+
statement_timeout: {
|
|
649
|
+
value: 9,
|
|
650
|
+
unit: "s"
|
|
651
|
+
},
|
|
652
|
+
heartbeat_interval: {
|
|
653
|
+
value: 2,
|
|
654
|
+
unit: "s"
|
|
655
|
+
},
|
|
656
|
+
client_retry_window: {
|
|
657
|
+
value: 30,
|
|
658
|
+
unit: "s"
|
|
659
|
+
},
|
|
660
|
+
checkout_timeout: {
|
|
661
|
+
value: 5,
|
|
662
|
+
unit: "s"
|
|
663
|
+
},
|
|
664
|
+
pool_idle_expiry: {
|
|
665
|
+
value: 10,
|
|
666
|
+
unit: "s"
|
|
667
|
+
},
|
|
668
|
+
keepalive_idle: {
|
|
669
|
+
value: 30,
|
|
670
|
+
unit: "s"
|
|
671
|
+
},
|
|
672
|
+
max_connection_age: {
|
|
673
|
+
value: "infinite",
|
|
674
|
+
unit: "s"
|
|
675
|
+
},
|
|
676
|
+
close_bound: {
|
|
677
|
+
value: 2,
|
|
678
|
+
unit: "s"
|
|
679
|
+
},
|
|
680
|
+
retrans_giveup_time: {
|
|
681
|
+
value: 924.6,
|
|
682
|
+
unit: "s"
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
//#endregion
|
|
686
|
+
export { HELLO, MANIFESTATIONS, MONGO_CLIENT_MACHINE, MONGO_STARTING_KNOBS, MONGO_STARTING_POLL, STARTING_POLL, failureOutcome, firstDetector, helloCommandDial, helloJudge, mongoClient, mongoMeetings, mongoOptions, nodeMongo, specProblems, unwatched };
|
|
687
|
+
|
|
688
|
+
//# sourceMappingURL=mongodb.js.map
|