@lde/distribution-probe 0.1.9 → 0.1.11
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 +3 -1
- package/dist/probe.d.ts +11 -0
- package/dist/probe.d.ts.map +1 -1
- package/dist/probe.js +63 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -32,4 +32,6 @@ Sends `HEAD` with `Accept: <distribution.mimeType>` and `Accept-Encoding: identi
|
|
|
32
32
|
|
|
33
33
|
### Network errors
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
A thrown exception from `fetch` (DNS failure, connection refused, socket reset, TLS error, timeout after the configured `timeoutMs` – default 5 000 ms) is a connection-level failure. The probe retries these up to `retries` times (default 2) with a short backoff before giving up and returning a `NetworkError`. This turns a transient transport blip into a reliable single measurement without looking backward across checks. A genuine outage still resolves to a `NetworkError` on the current check – every attempt fails – but note each attempt gets its own `timeoutMs`, so an endpoint that fails only by timing out takes up to `(retries + 1) × timeoutMs` (plus backoff) to be reported down. HTTP error responses (4xx/5xx) and content-validation failures are real ‘down’ states and are **never** retried.
|
|
36
|
+
|
|
37
|
+
`NetworkError.message` includes the underlying `error.cause` (e.g. `ECONNRESET`, `UND_ERR_SOCKET “other side closed”`) when Node wraps one, so observations record what actually failed rather than a bare ‘fetch failed’.
|
package/dist/probe.d.ts
CHANGED
|
@@ -17,6 +17,17 @@ export interface ProbeOptions {
|
|
|
17
17
|
* distributions. Defaults to `SELECT * { ?s ?p ?o } LIMIT 1`.
|
|
18
18
|
*/
|
|
19
19
|
sparqlQuery?: string;
|
|
20
|
+
/**
|
|
21
|
+
* How many times to retry a connection-level failure (DNS, connection
|
|
22
|
+
* refused, socket reset, TLS error, timeout) before returning a
|
|
23
|
+
* {@link NetworkError}. Only transport errors are retried within the same
|
|
24
|
+
* check, so a transient blip does not flip an otherwise healthy distribution
|
|
25
|
+
* to ‘unavailable’; HTTP error responses and content-validation failures are
|
|
26
|
+
* genuine ‘down’ states and are never retried. Set to `0` to disable.
|
|
27
|
+
* Defaults to `2`. A non-integer or otherwise invalid value falls back to
|
|
28
|
+
* the default; negative values are clamped to `0`.
|
|
29
|
+
*/
|
|
30
|
+
retries?: number;
|
|
20
31
|
}
|
|
21
32
|
/**
|
|
22
33
|
* Result of a network error during probing.
|
package/dist/probe.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,YAAY,EAAE,MAAM,cAAc,CAAC;AAInE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,YAAY,EAAE,MAAM,cAAc,CAAC;AAInE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AASD;;GAEG;AACH,qBAAa,YAAY;aAEL,GAAG,EAAE,MAAM;aACX,OAAO,EAAE,MAAM;aACf,cAAc,EAAE,MAAM;gBAFtB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,MAAM;CAEzC;AAED;;GAEG;AACH,uBAAe,WAAW;aAUN,GAAG,EAAE,MAAM;IAT7B,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,SAAgB,YAAY,EAAE,IAAI,GAAG,IAAI,CAAQ;IACjD,SAAgB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,SAAgB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,SAAgB,QAAQ,EAAE,MAAM,EAAE,CAAM;IACxC,SAAgB,cAAc,EAAE,MAAM,CAAC;gBAGrB,GAAG,EAAE,MAAM,EAC3B,QAAQ,EAAE,QAAQ,EAClB,cAAc,EAAE,MAAM,EACtB,aAAa,GAAE,MAAM,GAAG,IAAW;IAa9B,SAAS,IAAI,OAAO;CAO5B;AAMD;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,WAAW;IAChD;;;;;OAKG;IACH,SAAgB,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;gBAGtD,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,EAClB,cAAc,EAAE,MAAM,EACtB,oBAAoB,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EAChD,aAAa,GAAE,MAAM,GAAG,IAAW;IAS5B,SAAS,IAAI,OAAO;CAQ9B;AAED;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,WAAW;IAClD,SAAgB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAQ;gBAGhD,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,EAClB,cAAc,EAAE,MAAM,EACtB,aAAa,GAAE,MAAM,GAAG,IAAW;CAQtC;AAED,MAAM,MAAM,eAAe,GACvB,iBAAiB,GACjB,mBAAmB,GACnB,YAAY,CAAC;AAIjB;;;;;;;;GAQG;AACH,wBAAsB,KAAK,CACzB,YAAY,EAAE,YAAY,EAC1B,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,eAAe,CAAC,CAqD1B"}
|
package/dist/probe.js
CHANGED
|
@@ -3,6 +3,9 @@ import { rdfParser } from 'rdf-parse';
|
|
|
3
3
|
import { Readable } from 'node:stream';
|
|
4
4
|
const DEFAULT_SPARQL_QUERY = 'SELECT * { ?s ?p ?o } LIMIT 1';
|
|
5
5
|
const DEFAULT_TIMEOUT_MS = 5000;
|
|
6
|
+
const DEFAULT_RETRIES = 2;
|
|
7
|
+
/** Base backoff between retries; the nth retry waits `n × base`. */
|
|
8
|
+
const RETRY_BACKOFF_MS = 250;
|
|
6
9
|
/**
|
|
7
10
|
* Result of a network error during probing.
|
|
8
11
|
*/
|
|
@@ -100,23 +103,75 @@ export async function probe(distribution, options) {
|
|
|
100
103
|
const [authUrl, authHeaders] = distribution.accessUrl !== undefined
|
|
101
104
|
? extractUrlCredentials(distribution.accessUrl, resolved.headers)
|
|
102
105
|
: [new URL(url), new Headers(resolved.headers)];
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
// Retry only connection-level failures (a thrown `fetch`): HTTP error
|
|
107
|
+
// responses and content-validation failures are returned as result objects,
|
|
108
|
+
// never thrown, so they exit the loop on the first attempt and are not
|
|
109
|
+
// retried. A genuine outage still resolves to a NetworkError – every attempt
|
|
110
|
+
// fails – but note each attempt gets its own `timeoutMs`, so an endpoint that
|
|
111
|
+
// fails only by timing out takes up to (retries + 1) × timeoutMs (plus
|
|
112
|
+
// backoff) to be reported down.
|
|
113
|
+
const overallStart = performance.now();
|
|
114
|
+
let lastError;
|
|
115
|
+
for (let attempt = 0; attempt <= resolved.retries; attempt++) {
|
|
116
|
+
if (attempt > 0) {
|
|
117
|
+
await delay(RETRY_BACKOFF_MS * attempt);
|
|
118
|
+
}
|
|
119
|
+
const start = performance.now();
|
|
120
|
+
try {
|
|
121
|
+
if (distribution.isSparql()) {
|
|
122
|
+
return await probeSparqlEndpoint(authUrl.toString(), distribution, resolved, authHeaders, start);
|
|
123
|
+
}
|
|
124
|
+
return await probeDataDump(authUrl.toString(), distribution, resolved, authHeaders, start);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
lastError = error;
|
|
107
128
|
}
|
|
108
|
-
return await probeDataDump(authUrl.toString(), distribution, resolved, authHeaders, start);
|
|
109
129
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
130
|
+
// A successful probe reports its own attempt's latency (computed inside the
|
|
131
|
+
// probe functions); a NetworkError reports the total time spent failing,
|
|
132
|
+
// across every attempt and backoff, so observations do not understate the
|
|
133
|
+
// real cost of a down endpoint.
|
|
134
|
+
return new NetworkError(url, describeNetworkError(lastError), Math.round(performance.now() - overallStart));
|
|
135
|
+
}
|
|
136
|
+
function delay(milliseconds) {
|
|
137
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Describe a thrown fetch error for a {@link NetworkError} message. undici wraps
|
|
141
|
+
* * the real reason (`ECONNRESET`, `UND_ERR_SOCKET “other side closed”`, TLS
|
|
142
|
+
* errors, …) in `error.cause`, while `error.message` is usually a bare
|
|
143
|
+
* ‘fetch failed’. Including the cause’s code and message preserves the
|
|
144
|
+
* diagnostic detail that would otherwise be discarded.
|
|
145
|
+
*/
|
|
146
|
+
function describeNetworkError(error) {
|
|
147
|
+
if (!(error instanceof Error)) {
|
|
148
|
+
return String(error);
|
|
149
|
+
}
|
|
150
|
+
const { cause } = error;
|
|
151
|
+
if (cause === undefined || cause === null) {
|
|
152
|
+
return error.message;
|
|
113
153
|
}
|
|
154
|
+
const detail = cause instanceof Error
|
|
155
|
+
? [cause.code, cause.message]
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.join(': ')
|
|
158
|
+
: String(cause);
|
|
159
|
+
return detail && detail !== error.message
|
|
160
|
+
? `${error.message} (${detail})`
|
|
161
|
+
: error.message;
|
|
114
162
|
}
|
|
115
163
|
function resolveOptions(options) {
|
|
164
|
+
const retries = options?.retries;
|
|
116
165
|
return {
|
|
117
166
|
timeoutMs: options?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
118
167
|
headers: options?.headers ?? new Headers(),
|
|
119
168
|
sparqlQuery: options?.sparqlQuery ?? DEFAULT_SPARQL_QUERY,
|
|
169
|
+
// Guard the loop bound: a non-integer (NaN, Infinity, fractional) would
|
|
170
|
+
// otherwise either skip the loop entirely or never terminate. Negatives
|
|
171
|
+
// clamp to 0 (retries disabled).
|
|
172
|
+
retries: retries === undefined || !Number.isInteger(retries)
|
|
173
|
+
? DEFAULT_RETRIES
|
|
174
|
+
: Math.max(0, retries),
|
|
120
175
|
};
|
|
121
176
|
}
|
|
122
177
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/distribution-probe",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"repository": {
|
|
5
5
|
"url": "git+https://github.com/ldelements/lde.git",
|
|
6
6
|
"directory": "packages/distribution-probe"
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"!**/*.tsbuildinfo"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@lde/dataset": "0.7.
|
|
27
|
+
"@lde/dataset": "0.7.7",
|
|
28
28
|
"rdf-parse": "^5.0.0",
|
|
29
29
|
"tslib": "^2.3.0"
|
|
30
30
|
}
|