@compforge/harness-toolbox 0.4.3 → 0.4.5
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 +18 -0
- package/dist/chunk-835fssgz.js +37 -0
- package/dist/{chunk-0s27bgga.js → chunk-ae84rzq5.js} +1 -1
- package/dist/{chunk-age65q4n.js → chunk-db4an9td.js} +1 -1
- package/dist/{chunk-ftkrjqkc.js → chunk-dpcge1xt.js} +1 -1
- package/dist/{chunk-grpygk6f.js → chunk-dx6yvj81.js} +2 -2
- package/dist/{chunk-sqbew04a.js → chunk-f3vq900j.js} +2 -2
- package/dist/{chunk-yp6tcw22.js → chunk-f98qrpx1.js} +38 -78
- package/dist/{chunk-8kts470p.js → chunk-g2bac2jd.js} +12 -6
- package/dist/chunk-gm4swndx.js +106 -0
- package/dist/{chunk-shsabpc1.js → chunk-r3rnsvwv.js} +1 -1
- package/dist/{chunk-20kv4wc3.js → chunk-rkt41za2.js} +7 -1
- package/dist/{chunk-8mb6natq.js → chunk-sbt20n84.js} +53 -35
- package/dist/{chunk-h73h2b7y.js → chunk-t4hrnaj0.js} +44 -39
- package/dist/{chunk-kkts16w9.js → chunk-ww11q58v.js} +1 -1
- package/dist/{chunk-4ms037k2.js → chunk-x0yhfg29.js} +2 -2
- package/dist/chunk-z15gwjvg.js +145 -0
- package/dist/connection/acquire.d.ts +34 -0
- package/dist/connection/acquire.js +9 -0
- package/dist/datasource.js +1 -1
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +3 -1
- package/dist/file/client.js +2 -2
- package/dist/file/index.js +4 -4
- package/dist/file/local.js +3 -3
- package/dist/file/pod.js +3 -3
- package/dist/kubernetes/client.d.ts +4 -0
- package/dist/kubernetes/client.js +6 -6
- package/dist/kubernetes/compute-model.d.ts +115 -0
- package/dist/kubernetes/compute-model.js +0 -0
- package/dist/kubernetes/compute-quantity.d.ts +18 -0
- package/dist/kubernetes/compute-quantity.js +23 -0
- package/dist/kubernetes/compute.d.ts +17 -0
- package/dist/kubernetes/compute.js +211 -0
- package/dist/kubernetes/environment.js +6 -6
- package/dist/kubernetes/exec-transport.js +2 -2
- package/dist/kubernetes/pod-log-datasource.js +1 -1
- package/dist/kubernetes/resource-transport.js +3 -3
- package/dist/kubernetes/resources.js +2 -2
- package/dist/kubernetes/workload.js +2 -2
- package/dist/mysql/client.d.ts +5 -2
- package/dist/mysql/client.js +5 -4
- package/dist/mysql/connection.d.ts +3 -0
- package/dist/mysql/connection.js +4 -2
- package/dist/mysql/index.js +6 -5
- package/dist/mysql/readonly.js +2 -2
- package/dist/opensearch/client.d.ts +3 -1
- package/dist/opensearch/client.js +3 -2
- package/dist/opensearch/index.js +3 -2
- package/dist/redis/client.js +3 -2
- package/dist/redis/index.js +7 -6
- package/dist/s3/datasource.js +1 -1
- package/dist/s3/index.js +1 -1
- package/package.json +4 -1
- package/dist/chunk-136egyff.js +0 -11
package/README.md
CHANGED
|
@@ -173,6 +173,9 @@ the root owns cancellation and must join child work before disposing clients.
|
|
|
173
173
|
|
|
174
174
|
Catch ToolboxError from the root entry or `@compforge/harness-toolbox/errors`; use `kind` and
|
|
175
175
|
`code` for decisions. Native `cause` may contain sensitive details and is debug-only.
|
|
176
|
+
Connection acquisition deadlines and Redis handshake/command deadlines raise `TimeoutError`
|
|
177
|
+
from `@compforge/harness-toolbox/errors`. It extends `ToolboxError`, retaining
|
|
178
|
+
`kind: "timeout"` and `code: "ETIMEDOUT"`; OpenSearch preserves it unchanged.
|
|
176
179
|
|
|
177
180
|
For coordinated common/toolbox development, install and build common before installing toolbox:
|
|
178
181
|
|
|
@@ -376,3 +379,18 @@ reason. The caller owns shared transports such as `PodRelayTransport` and their
|
|
|
376
379
|
|
|
377
380
|
Routed HTTPS preserves the original TLS server name. Bun disables HTTP keep-alive because its reused
|
|
378
381
|
socket recovery can replay a failed request below the driver; Node retains connection pooling.
|
|
382
|
+
|
|
383
|
+
## Kubernetes compute evidence
|
|
384
|
+
|
|
385
|
+
`kubernetes/compute` exports `collectClusterCompute` and `collectWorkloadCompute`. They borrow
|
|
386
|
+
ResourceAccess, use explicit deadlines/item/page budgets, and retain read coverage independently.
|
|
387
|
+
Use `client.clusterResources()` for Nodes and all-namespace Pods; normal client methods retain their
|
|
388
|
+
configured namespace. A forbidden cluster read records `permission_denied`; it does not block a
|
|
389
|
+
separate namespace-scoped Workload read. See [compute collection](../../../docs/toolbox.md#kubernetes-compute-evidence).
|
|
390
|
+
|
|
391
|
+
### Connection acquisition deadlines
|
|
392
|
+
|
|
393
|
+
MySQL, OpenSearch and Redis use the same acquisition lifecycle. `connectTimeoutMs` includes route mapping
|
|
394
|
+
and handshake/probe; optional `acquireTimeoutMs` bounds all routes together (default: per-route budget × route count).
|
|
395
|
+
Cancellation preserves the caller reason and prevents late mappings from starting a handshake. Query and command
|
|
396
|
+
timeouts remain separate, and failed operations are never replayed on another transport.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/mysql/connection.ts
|
|
2
|
+
import { createConnection } from "mysql2";
|
|
3
|
+
async function connectMysql(options, signal) {
|
|
4
|
+
signal?.throwIfAborted();
|
|
5
|
+
const native = createConnection(options);
|
|
6
|
+
const connection = native.promise();
|
|
7
|
+
let abort;
|
|
8
|
+
try {
|
|
9
|
+
await new Promise((resolve, reject) => {
|
|
10
|
+
abort = () => {
|
|
11
|
+
destroyMysqlConnection(connection);
|
|
12
|
+
reject(signal.reason);
|
|
13
|
+
};
|
|
14
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
15
|
+
native.connect((error) => error ? reject(error) : resolve());
|
|
16
|
+
if (signal?.aborted)
|
|
17
|
+
abort();
|
|
18
|
+
});
|
|
19
|
+
signal?.throwIfAborted();
|
|
20
|
+
return connection;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
destroyMysqlConnection(connection);
|
|
23
|
+
throw error;
|
|
24
|
+
} finally {
|
|
25
|
+
signal?.removeEventListener("abort", abort);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function destroyMysqlConnection(connection) {
|
|
29
|
+
const native = connection.connection;
|
|
30
|
+
try {
|
|
31
|
+
connection.destroy();
|
|
32
|
+
} finally {
|
|
33
|
+
native.stream.destroy();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { connectMysql, destroyMysqlConnection };
|
|
@@ -2,10 +2,10 @@ import {
|
|
|
2
2
|
BoundedFileClient,
|
|
3
3
|
fileClientOptions,
|
|
4
4
|
filePaths
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-ww11q58v.js";
|
|
6
6
|
import {
|
|
7
7
|
FileError
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-rkt41za2.js";
|
|
9
9
|
|
|
10
10
|
// src/file/pod.ts
|
|
11
11
|
import { posix } from "node:path";
|
|
@@ -2,10 +2,10 @@ import {
|
|
|
2
2
|
BoundedFileClient,
|
|
3
3
|
fileClientOptions,
|
|
4
4
|
filePaths
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-ww11q58v.js";
|
|
6
6
|
import {
|
|
7
7
|
FileError
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-rkt41za2.js";
|
|
9
9
|
|
|
10
10
|
// src/file/local.ts
|
|
11
11
|
import { constants } from "node:fs";
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
acquireConnection,
|
|
3
|
+
awaitWithSignal
|
|
4
|
+
} from "./chunk-gm4swndx.js";
|
|
1
5
|
import {
|
|
2
6
|
discoverRedisTopology
|
|
3
7
|
} from "./chunk-5tbzf9wd.js";
|
|
4
8
|
import {
|
|
5
|
-
|
|
6
|
-
} from "./chunk-
|
|
9
|
+
TimeoutError
|
|
10
|
+
} from "./chunk-rkt41za2.js";
|
|
7
11
|
import {
|
|
8
12
|
isConnectionNetworkError
|
|
9
13
|
} from "./chunk-h8fwx0k7.js";
|
|
@@ -41,26 +45,6 @@ function parseRedisInfo(raw) {
|
|
|
41
45
|
}
|
|
42
46
|
return result;
|
|
43
47
|
}
|
|
44
|
-
async function abortable(pending, signal, cancel) {
|
|
45
|
-
if (signal.aborted) {
|
|
46
|
-
cancel?.();
|
|
47
|
-
pending.catch(() => {});
|
|
48
|
-
signal.throwIfAborted();
|
|
49
|
-
}
|
|
50
|
-
let abort;
|
|
51
|
-
const cancelled = new Promise((_, reject) => {
|
|
52
|
-
abort = () => {
|
|
53
|
-
cancel?.();
|
|
54
|
-
reject(signal.reason);
|
|
55
|
-
};
|
|
56
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
57
|
-
});
|
|
58
|
-
try {
|
|
59
|
-
return await Promise.race([pending, cancelled]);
|
|
60
|
-
} finally {
|
|
61
|
-
signal.removeEventListener("abort", abort);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
48
|
|
|
65
49
|
class RedisConnection {
|
|
66
50
|
endpoint;
|
|
@@ -101,7 +85,7 @@ class RedisConnection {
|
|
|
101
85
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
102
86
|
});
|
|
103
87
|
const timeout = new AbortController;
|
|
104
|
-
const timer = setTimeout(() => timeout.abort(new
|
|
88
|
+
const timer = setTimeout(() => timeout.abort(new TimeoutError("Redis handshake timed out")), config.connectTimeoutMs ?? config.timeoutMs);
|
|
105
89
|
const signal = AbortSignal.any([
|
|
106
90
|
timeout.signal,
|
|
107
91
|
...config.signal ? [config.signal] : [],
|
|
@@ -112,7 +96,7 @@ class RedisConnection {
|
|
|
112
96
|
client.destroy();
|
|
113
97
|
};
|
|
114
98
|
try {
|
|
115
|
-
await
|
|
99
|
+
await awaitWithSignal(client.connect(), signal, close);
|
|
116
100
|
signal.throwIfAborted();
|
|
117
101
|
return new RedisConnection(endpoint, client, config.timeoutMs, () => lastError, config.signal);
|
|
118
102
|
} catch (error) {
|
|
@@ -132,10 +116,10 @@ class RedisConnection {
|
|
|
132
116
|
throw new Error(`Redis connection is closed${reason ? `: ${reason}` : ""}`);
|
|
133
117
|
}
|
|
134
118
|
const timeout = new AbortController;
|
|
135
|
-
const timer = setTimeout(() => timeout.abort(new
|
|
119
|
+
const timer = setTimeout(() => timeout.abort(new TimeoutError(`Redis ${operationName} timed out after ${this.timeoutMs}ms (${this.endpoint.host}:${this.endpoint.port})`)), this.timeoutMs);
|
|
136
120
|
const signal = this.signal ? AbortSignal.any([this.signal, timeout.signal]) : timeout.signal;
|
|
137
121
|
try {
|
|
138
|
-
return await
|
|
122
|
+
return await awaitWithSignal(Promise.resolve().then(() => {
|
|
139
123
|
signal.throwIfAborted();
|
|
140
124
|
return operation();
|
|
141
125
|
}), signal, () => this.close());
|
|
@@ -229,58 +213,34 @@ class RedisAccess {
|
|
|
229
213
|
});
|
|
230
214
|
return pending;
|
|
231
215
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
});
|
|
259
|
-
const prepared = pending.then((connection2) => {
|
|
260
|
-
if (attemptSignal.aborted) {
|
|
261
|
-
connection2.close();
|
|
262
|
-
attemptSignal.throwIfAborted();
|
|
263
|
-
}
|
|
264
|
-
return connection2;
|
|
265
|
-
});
|
|
266
|
-
const connection = await abortable(prepared, attemptSignal);
|
|
267
|
-
this.lifecycle.onRoute?.({ transport: mapped.host === endpoint.host && mapped.port === endpoint.port ? "direct" : transport.name, reason });
|
|
268
|
-
return connection;
|
|
269
|
-
} catch (failure) {
|
|
270
|
-
signal.throwIfAborted();
|
|
271
|
-
const error = attemptSignal.aborted ? attemptSignal.reason : failure;
|
|
272
|
-
const network = isConnectionNetworkError(error) || error instanceof SocketClosedUnexpectedlyError || error instanceof ConnectionTimeoutError;
|
|
273
|
-
if (!network || index === this.#transports.length - 1)
|
|
274
|
-
throw error;
|
|
275
|
-
reason = error instanceof SocketClosedUnexpectedlyError ? "connection_lost" : error instanceof ConnectionTimeoutError ? "ETIMEDOUT" : String(error.code);
|
|
276
|
-
} finally {
|
|
277
|
-
clearTimeout(attemptTimer);
|
|
278
|
-
}
|
|
216
|
+
#open(endpoint, database, credentials) {
|
|
217
|
+
return acquireConnection(this.#transports, {
|
|
218
|
+
name: "Redis connection",
|
|
219
|
+
signal: this.#signal,
|
|
220
|
+
connectTimeoutMs: this.baseConfig.connectTimeoutMs ?? this.baseConfig.timeoutMs,
|
|
221
|
+
acquireTimeoutMs: this.baseConfig.acquireTimeoutMs,
|
|
222
|
+
onRoute: this.lifecycle.onRoute,
|
|
223
|
+
open: async (transport, attempt) => {
|
|
224
|
+
const mapped = await transport.connect(endpoint);
|
|
225
|
+
const connection = await this.connectionFactory(endpoint, mapped, {
|
|
226
|
+
...this.baseConfig,
|
|
227
|
+
...credentials,
|
|
228
|
+
database,
|
|
229
|
+
connectTimeoutMs: attempt.remainingMs(),
|
|
230
|
+
signal: this.#signal,
|
|
231
|
+
connectSignal: attempt.signal
|
|
232
|
+
});
|
|
233
|
+
return { connection, transport: mapped.host === endpoint.host && mapped.port === endpoint.port ? "direct" : transport.name };
|
|
234
|
+
},
|
|
235
|
+
close: (connection) => connection.close(),
|
|
236
|
+
retryReason: (error) => {
|
|
237
|
+
if (error instanceof SocketClosedUnexpectedlyError)
|
|
238
|
+
return "connection_lost";
|
|
239
|
+
if (error instanceof ConnectionTimeoutError)
|
|
240
|
+
return "ETIMEDOUT";
|
|
241
|
+
return isConnectionNetworkError(error) ? String(error.code) : undefined;
|
|
279
242
|
}
|
|
280
|
-
|
|
281
|
-
} finally {
|
|
282
|
-
clearTimeout(timer);
|
|
283
|
-
}
|
|
243
|
+
});
|
|
284
244
|
}
|
|
285
245
|
close() {
|
|
286
246
|
return this.#closing ??= (async () => {
|
|
@@ -319,7 +279,7 @@ class RedisClient {
|
|
|
319
279
|
initialize() {
|
|
320
280
|
this.signal.throwIfAborted();
|
|
321
281
|
return this.#initialization ??= (async () => {
|
|
322
|
-
this.#target = await this.source.resolve();
|
|
282
|
+
this.#target = await awaitWithSignal(this.source.resolve(), this.signal);
|
|
323
283
|
this.signal.throwIfAborted();
|
|
324
284
|
if (!this.source.transports.length || this.source.transports.some((transport) => transport.kind !== "tcp")) {
|
|
325
285
|
throw new Error("Redis requires TCP transports");
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
KubernetesResourceTransport
|
|
3
|
-
} from "./chunk-4ms037k2.js";
|
|
4
1
|
import {
|
|
5
2
|
resolveWorkload
|
|
6
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-ae84rzq5.js";
|
|
7
4
|
import {
|
|
8
5
|
ServicePortForwarder
|
|
9
6
|
} from "./chunk-2171tajs.js";
|
|
7
|
+
import {
|
|
8
|
+
KubernetesResourceTransport
|
|
9
|
+
} from "./chunk-x0yhfg29.js";
|
|
10
10
|
import {
|
|
11
11
|
KubernetesExecTransport
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-r3rnsvwv.js";
|
|
13
13
|
import {
|
|
14
14
|
KubernetesError
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-rkt41za2.js";
|
|
16
16
|
import {
|
|
17
17
|
KubectlExecutor
|
|
18
18
|
} from "./chunk-hmah9t8e.js";
|
|
@@ -83,6 +83,12 @@ class KubernetesClient {
|
|
|
83
83
|
list(query, options) {
|
|
84
84
|
return this.#track(() => this.#resourceAccess().list({ ...query, namespace: query.namespace ?? this.kube.namespace }, this.#options(options)));
|
|
85
85
|
}
|
|
86
|
+
clusterResources() {
|
|
87
|
+
return {
|
|
88
|
+
get: (ref, options) => this.#track(() => this.#resourceAccess().get(ref, this.#options(options))),
|
|
89
|
+
list: (query, options) => this.#track(() => this.#resourceAccess().list(query, this.#options(options)))
|
|
90
|
+
};
|
|
91
|
+
}
|
|
86
92
|
inNamespace(namespace) {
|
|
87
93
|
if (!namespace.trim())
|
|
88
94
|
throw new KubernetesError("Namespace is required", { kind: "invalid_argument" });
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TimeoutError,
|
|
3
|
+
ToolboxError
|
|
4
|
+
} from "./chunk-rkt41za2.js";
|
|
5
|
+
|
|
6
|
+
// src/connection/acquire.ts
|
|
7
|
+
async function awaitWithSignal(pending, signal, cancel) {
|
|
8
|
+
if (signal.aborted) {
|
|
9
|
+
cancel?.();
|
|
10
|
+
pending.catch(() => {});
|
|
11
|
+
signal.throwIfAborted();
|
|
12
|
+
}
|
|
13
|
+
let abort;
|
|
14
|
+
const cancelled = new Promise((_, reject) => {
|
|
15
|
+
abort = () => {
|
|
16
|
+
cancel?.();
|
|
17
|
+
reject(signal.reason);
|
|
18
|
+
};
|
|
19
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
20
|
+
});
|
|
21
|
+
try {
|
|
22
|
+
return await Promise.race([pending, cancelled]);
|
|
23
|
+
} finally {
|
|
24
|
+
signal.removeEventListener("abort", abort);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function acquireConnection(routes, options) {
|
|
28
|
+
const budget = options.acquireTimeoutMs ?? options.connectTimeoutMs * routes.length;
|
|
29
|
+
if (!routes.length)
|
|
30
|
+
throw new ToolboxError(`${options.name} requires a transport`, { kind: "invalid_argument" });
|
|
31
|
+
if ([options.connectTimeoutMs, budget].some((value) => !Number.isFinite(value) || value <= 0 || value > 2147483647)) {
|
|
32
|
+
throw new ToolboxError(`${options.name} timeouts must be positive milliseconds within timer range`, { kind: "invalid_argument" });
|
|
33
|
+
}
|
|
34
|
+
options.signal?.throwIfAborted();
|
|
35
|
+
const timeout = new AbortController;
|
|
36
|
+
const timedOut = (message) => new TimeoutError(message);
|
|
37
|
+
const deadline = performance.now() + budget;
|
|
38
|
+
const timer = setTimeout(() => timeout.abort(timedOut(`${options.name} timed out after ${budget}ms`)), budget);
|
|
39
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeout.signal]) : timeout.signal;
|
|
40
|
+
let reason;
|
|
41
|
+
try {
|
|
42
|
+
for (const [index, route] of routes.entries()) {
|
|
43
|
+
if (performance.now() >= deadline)
|
|
44
|
+
timeout.abort(timedOut(`${options.name} timed out after ${budget}ms`));
|
|
45
|
+
signal.throwIfAborted();
|
|
46
|
+
const attempt = new AbortController;
|
|
47
|
+
const end = Math.min(deadline, performance.now() + options.connectTimeoutMs);
|
|
48
|
+
const expire = () => attempt.abort(timedOut(`${options.name} attempt timed out (${route.name})`));
|
|
49
|
+
const attemptTimer = setTimeout(expire, Math.max(1, Math.ceil(end - performance.now())));
|
|
50
|
+
const attemptSignal = AbortSignal.any([signal, attempt.signal]);
|
|
51
|
+
const context = {
|
|
52
|
+
signal: attemptSignal,
|
|
53
|
+
remainingMs: () => {
|
|
54
|
+
if (performance.now() >= end)
|
|
55
|
+
expire();
|
|
56
|
+
attemptSignal.throwIfAborted();
|
|
57
|
+
return Math.max(1, Math.ceil(end - performance.now()));
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
let candidate;
|
|
61
|
+
let discarded = false;
|
|
62
|
+
const discard = async () => {
|
|
63
|
+
if (!candidate || discarded)
|
|
64
|
+
return;
|
|
65
|
+
discarded = true;
|
|
66
|
+
await options.close(candidate.connection);
|
|
67
|
+
};
|
|
68
|
+
let result;
|
|
69
|
+
try {
|
|
70
|
+
const prepared = options.open(route, context).then(async (value) => {
|
|
71
|
+
candidate = value;
|
|
72
|
+
if (attemptSignal.aborted) {
|
|
73
|
+
await discard();
|
|
74
|
+
attemptSignal.throwIfAborted();
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
});
|
|
78
|
+
result = await awaitWithSignal(prepared, attemptSignal);
|
|
79
|
+
} catch (failure) {
|
|
80
|
+
await discard();
|
|
81
|
+
signal.throwIfAborted();
|
|
82
|
+
const error = attemptSignal.aborted ? attemptSignal.reason : failure;
|
|
83
|
+
reason = options.retryReason(error);
|
|
84
|
+
if (reason === undefined || index === routes.length - 1)
|
|
85
|
+
throw error;
|
|
86
|
+
continue;
|
|
87
|
+
} finally {
|
|
88
|
+
clearTimeout(attemptTimer);
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
attemptSignal.throwIfAborted();
|
|
92
|
+
options.onRoute?.({ transport: result.transport, reason });
|
|
93
|
+
attemptSignal.throwIfAborted();
|
|
94
|
+
return result.connection;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
await discard();
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
throw new Error("Connection routes exhausted");
|
|
101
|
+
} finally {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export { awaitWithSignal, acquireConnection };
|
|
@@ -15,6 +15,12 @@ class KubernetesError extends ToolboxError {
|
|
|
15
15
|
|
|
16
16
|
class FileError extends ToolboxError {
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
class TimeoutError extends ToolboxError {
|
|
20
|
+
constructor(message, options) {
|
|
21
|
+
super(message, { kind: "timeout", code: "ETIMEDOUT", cause: options?.cause });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
18
24
|
function kubernetesHttpError(status) {
|
|
19
25
|
const kind = {
|
|
20
26
|
401: "authentication_failed",
|
|
@@ -41,4 +47,4 @@ function kubernetesCommandError(result) {
|
|
|
41
47
|
return status ? kubernetesHttpError(status) : new KubernetesError("Kubernetes resource request failed", { kind: "operation_failed" });
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
export { ToolboxError, KubernetesError, FileError, kubernetesHttpError, kubernetesCommandError };
|
|
50
|
+
export { ToolboxError, KubernetesError, FileError, TimeoutError, kubernetesHttpError, kubernetesCommandError };
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
acquireConnection,
|
|
3
|
+
awaitWithSignal
|
|
4
|
+
} from "./chunk-gm4swndx.js";
|
|
1
5
|
import {
|
|
2
6
|
ToolboxError
|
|
3
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-rkt41za2.js";
|
|
4
8
|
import {
|
|
5
9
|
isConnectionNetworkError
|
|
6
10
|
} from "./chunk-h8fwx0k7.js";
|
|
@@ -68,6 +72,15 @@ class OpenSearchEngine {
|
|
|
68
72
|
return this.client.close();
|
|
69
73
|
}
|
|
70
74
|
}
|
|
75
|
+
function openSearchRetryReason(error) {
|
|
76
|
+
if (isConnectionNetworkError(error))
|
|
77
|
+
return String(error.code);
|
|
78
|
+
if (error instanceof errors.TimeoutError)
|
|
79
|
+
return "ETIMEDOUT";
|
|
80
|
+
if (error instanceof errors.ConnectionError)
|
|
81
|
+
return error.name;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
71
84
|
|
|
72
85
|
class OpenSearchClient {
|
|
73
86
|
source;
|
|
@@ -86,44 +99,49 @@ class OpenSearchClient {
|
|
|
86
99
|
initialize() {
|
|
87
100
|
this.signal.throwIfAborted();
|
|
88
101
|
return this.#initialization ??= (async () => {
|
|
89
|
-
const target = await this.source.resolve();
|
|
102
|
+
const target = await awaitWithSignal(this.source.resolve(), this.signal);
|
|
90
103
|
this.signal.throwIfAborted();
|
|
91
104
|
const original = new URL(target.node);
|
|
92
|
-
|
|
93
|
-
|
|
105
|
+
try {
|
|
106
|
+
this.#engine = await acquireConnection(this.source.transports, {
|
|
107
|
+
name: "OpenSearch connection",
|
|
108
|
+
signal: this.signal,
|
|
109
|
+
connectTimeoutMs: target.connectTimeoutMs ?? target.requestTimeoutMs ?? 1e4,
|
|
110
|
+
acquireTimeoutMs: target.acquireTimeoutMs,
|
|
111
|
+
onRoute: this.lifecycle.onRoute,
|
|
112
|
+
open: async (transport, attempt) => {
|
|
113
|
+
if (transport.kind !== "tcp")
|
|
114
|
+
throw new Error("OpenSearch requires a TCP transport");
|
|
115
|
+
const endpoint = await transport.connect({
|
|
116
|
+
host: original.hostname,
|
|
117
|
+
port: Number(original.port || (original.protocol === "https:" ? 443 : 80)),
|
|
118
|
+
servername: target.servername ?? original.hostname
|
|
119
|
+
});
|
|
120
|
+
attempt.signal.throwIfAborted();
|
|
121
|
+
const url = new URL(original);
|
|
122
|
+
url.hostname = endpoint.host;
|
|
123
|
+
url.port = String(endpoint.port);
|
|
124
|
+
const engine = new OpenSearchEngine({ ...target, node: url.toString(), servername: endpoint.servername ?? target.servername ?? original.hostname });
|
|
125
|
+
try {
|
|
126
|
+
await engine.ping(attempt.signal, attempt.remainingMs());
|
|
127
|
+
return { connection: engine, transport: transport.name };
|
|
128
|
+
} catch (error) {
|
|
129
|
+
await engine.close();
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
close: (engine) => engine.close(),
|
|
134
|
+
retryReason: openSearchRetryReason
|
|
135
|
+
});
|
|
136
|
+
} catch (error) {
|
|
94
137
|
this.signal.throwIfAborted();
|
|
95
|
-
if (
|
|
96
|
-
throw
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
port: Number(original.port || (original.protocol === "https:" ? 443 : 80)),
|
|
102
|
-
servername: target.servername ?? original.hostname
|
|
103
|
-
});
|
|
104
|
-
this.signal.throwIfAborted();
|
|
105
|
-
const url = new URL(original);
|
|
106
|
-
url.hostname = endpoint.host;
|
|
107
|
-
url.port = String(endpoint.port);
|
|
108
|
-
engine = new OpenSearchEngine({ ...target, node: url.toString(), servername: endpoint.servername ?? target.servername ?? original.hostname });
|
|
109
|
-
await engine.ping(this.signal, target.connectTimeoutMs ?? target.requestTimeoutMs ?? 1e4);
|
|
110
|
-
this.signal.throwIfAborted();
|
|
111
|
-
this.lifecycle.onRoute?.({ transport: transport.name, reason });
|
|
112
|
-
this.#engine = engine;
|
|
113
|
-
return;
|
|
114
|
-
} catch (error) {
|
|
115
|
-
await engine?.close();
|
|
116
|
-
this.signal.throwIfAborted();
|
|
117
|
-
const network = isConnectionNetworkError(error);
|
|
118
|
-
if (!network && !(error instanceof errors.ConnectionError) && !(error instanceof errors.TimeoutError))
|
|
119
|
-
throw error;
|
|
120
|
-
reason = network ? String(error.code) : error.name;
|
|
121
|
-
if (index === this.source.transports.length - 1) {
|
|
122
|
-
throw new ToolboxError(`OpenSearch connect failed (${reason})`, { kind: "connection_failed", code: reason, cause: error });
|
|
123
|
-
}
|
|
124
|
-
}
|
|
138
|
+
if (error instanceof ToolboxError && error.kind === "timeout")
|
|
139
|
+
throw error;
|
|
140
|
+
const reason = openSearchRetryReason(error);
|
|
141
|
+
if (reason === undefined)
|
|
142
|
+
throw error;
|
|
143
|
+
throw new ToolboxError(`OpenSearch connect failed (${reason})`, { kind: "connection_failed", code: reason, cause: error });
|
|
125
144
|
}
|
|
126
|
-
throw new Error("OpenSearch DataSource has no transport");
|
|
127
145
|
})();
|
|
128
146
|
}
|
|
129
147
|
#ready() {
|