@pepys/ingest 0.4.1 → 0.4.3
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pepys/ingest",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "pepys ingest: the receive routes, the fact log and the blob store",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
"access": "public"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@impz/client": "
|
|
25
|
-
"@pepys/kit": "0.4.
|
|
26
|
-
"@pepys/schema": "0.4.
|
|
24
|
+
"@impz/client": "7.2.0",
|
|
25
|
+
"@pepys/kit": "0.4.3",
|
|
26
|
+
"@pepys/schema": "0.4.3",
|
|
27
27
|
"@s2-dev/streamstore": "0.30.0",
|
|
28
28
|
"@zgeoff/std": "0.4.3",
|
|
29
29
|
"tiny-invariant": "1.3.3",
|
|
@@ -23,7 +23,7 @@ export function isUnknownStream(error: unknown): boolean {
|
|
|
23
23
|
return error instanceof S2Error && error.status === 404 && error.code === 'stream_not_found';
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function isDefiniteRefusal(error: unknown): boolean {
|
|
26
|
+
export function isDefiniteRefusal(error: unknown): boolean {
|
|
27
27
|
return (
|
|
28
28
|
error instanceof S2Error &&
|
|
29
29
|
error.origin === 'server' &&
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { S2Error } from '@s2-dev/streamstore';
|
|
2
|
+
import { isDefiniteRefusal } from '../fact-log/s2/find-append-outcome.ts';
|
|
3
|
+
|
|
4
|
+
export type StartStep = { kind: 'retry'; delayMs: number } | { kind: 'stop' };
|
|
5
|
+
|
|
6
|
+
const firstDelayMs = 200;
|
|
7
|
+
const maxDelayMs = 30_000;
|
|
8
|
+
|
|
9
|
+
// the failures in a row after which the start ends, as the host's `up` does
|
|
10
|
+
const startRetryLimit = 10;
|
|
11
|
+
|
|
12
|
+
// What the start does after the fact log failed it `failures` times in a row. A failure that a later
|
|
13
|
+
// try can pass, such as a refused connection or a 5xx, waits a delay that doubles from 200 ms up to
|
|
14
|
+
// 30 s. A refusal of the request itself, such as a bad token, fails the same way each time.
|
|
15
|
+
export function pickStartStep(error: unknown, failures: number): StartStep {
|
|
16
|
+
const canPass = error instanceof S2Error && !isDefiniteRefusal(error);
|
|
17
|
+
|
|
18
|
+
if (!canPass || failures >= startRetryLimit) {
|
|
19
|
+
return { kind: 'stop' };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return { kind: 'retry', delayMs: Math.min(firstDelayMs * 2 ** (failures - 1), maxDelayMs) };
|
|
23
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Logger } from '@pepys/kit';
|
|
2
|
+
import { pickStartStep } from './pick-start-step.ts';
|
|
3
|
+
|
|
4
|
+
interface WaitOptions {
|
|
5
|
+
signal: AbortSignal;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface StartRetry {
|
|
9
|
+
// `setTimeout` of `node:timers/promises`: settles after the delay, and rejects on an abort
|
|
10
|
+
wait: (delayMs: number, value: undefined, options: WaitOptions) => Promise<unknown>;
|
|
11
|
+
|
|
12
|
+
// aborts when ingest stops before its start ends
|
|
13
|
+
signal: AbortSignal;
|
|
14
|
+
logger: Logger;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type StartRun<Value> =
|
|
18
|
+
| { kind: 'started'; value: Value }
|
|
19
|
+
| { kind: 'stopped' }
|
|
20
|
+
| { kind: 'failed'; error: unknown };
|
|
21
|
+
|
|
22
|
+
// Runs the part of the start that reads and writes the fact log, and runs it again after each
|
|
23
|
+
// failure that a later try can pass.
|
|
24
|
+
export async function runStartWithRetry<Value>(
|
|
25
|
+
start: () => Promise<Value>,
|
|
26
|
+
retry: StartRetry,
|
|
27
|
+
): Promise<StartRun<Value>> {
|
|
28
|
+
let failures = 0;
|
|
29
|
+
|
|
30
|
+
while (!retry.signal.aborted) {
|
|
31
|
+
try {
|
|
32
|
+
return { kind: 'started', value: await start() };
|
|
33
|
+
} catch (error: unknown) {
|
|
34
|
+
failures += 1;
|
|
35
|
+
|
|
36
|
+
const step = pickStartStep(error, failures);
|
|
37
|
+
|
|
38
|
+
if (step.kind === 'stop') {
|
|
39
|
+
return { kind: 'failed', error };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
retry.logger.error(
|
|
43
|
+
`the fact log failed the start of ingest, which tries again in ${step.delayMs} ms`,
|
|
44
|
+
error,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
await retry.wait(step.delayMs, undefined, { signal: retry.signal }).catch(() => {});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { kind: 'stopped' };
|
|
52
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setTimeout as wait } from 'node:timers/promises';
|
|
1
2
|
import { checkDeployment, createLogger, listReceiveRoutes } from '@pepys/kit';
|
|
2
3
|
import type { Deployment, SourceDefinition, StreamName } from '@pepys/kit';
|
|
3
4
|
import { makeShutdownHandler } from '@pepys/schema';
|
|
@@ -10,6 +11,7 @@ import { formatPullStart } from '../pull/cursors/format-pull-start.ts';
|
|
|
10
11
|
import { readSavedCursors } from '../pull/cursors/read-saved-cursors.ts';
|
|
11
12
|
import { createIngest } from '../receive/create-ingest.ts';
|
|
12
13
|
import { readImpIdentitySenderProof } from '../sender-proof/imp-identity/read-imp-identity-sender-proof.ts';
|
|
14
|
+
import { runStartWithRetry } from './run-start-with-retry.ts';
|
|
13
15
|
import { startPullTimers } from './start-pull-timers.ts';
|
|
14
16
|
|
|
15
17
|
interface ServeIngestOptions {
|
|
@@ -18,11 +20,14 @@ interface ServeIngestOptions {
|
|
|
18
20
|
|
|
19
21
|
// false leaves SIGINT and SIGTERM to the caller, who stops ingest through the returned function
|
|
20
22
|
exitOnSignal?: boolean;
|
|
23
|
+
|
|
24
|
+
// aborts a start that still waits for the fact log, for a caller that owns the stop signals
|
|
25
|
+
signal?: AbortSignal;
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
// The ingest process of a deployment. It exits 1 at start on a failed deployment check,
|
|
24
|
-
//
|
|
25
|
-
//
|
|
28
|
+
// The ingest process of a deployment. It exits 1 at start on a failed deployment check, a missing
|
|
29
|
+
// variable, or a trust folder it cannot load, so no route ever runs with an empty secret. A stop
|
|
30
|
+
// before ingest listens ends the start, and the returned function then stops nothing.
|
|
26
31
|
export async function serveIngest(options: ServeIngestOptions): Promise<() => Promise<void>> {
|
|
27
32
|
const logger = createLogger(process.stderr);
|
|
28
33
|
const checked = checkDeployment(options.deployment);
|
|
@@ -52,14 +57,62 @@ export async function serveIngest(options: ServeIngestOptions): Promise<() => Pr
|
|
|
52
57
|
process.exit(1);
|
|
53
58
|
}
|
|
54
59
|
|
|
60
|
+
const starting = new AbortController();
|
|
61
|
+
|
|
62
|
+
let stop = (): Promise<void> => {
|
|
63
|
+
starting.abort();
|
|
64
|
+
|
|
65
|
+
return Promise.resolve();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (options.exitOnSignal !== false) {
|
|
69
|
+
const shutdown = makeShutdownHandler({
|
|
70
|
+
stop: () => stop(),
|
|
71
|
+
logError: (message, error) => {
|
|
72
|
+
logger.error(message, error);
|
|
73
|
+
},
|
|
74
|
+
exit: (code) => process.exit(code),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
|
|
78
|
+
process.on(signal, () => {
|
|
79
|
+
void shutdown();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
55
84
|
// The S2 adapter creates the basin and each missing stream, so no setup command creates streams.
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
85
|
+
const started = await runStartWithRetry(
|
|
86
|
+
async () => {
|
|
87
|
+
const log = await createS2FactLog({
|
|
88
|
+
...config.value.s2,
|
|
89
|
+
streams: [...collectStreams(sources), cursorStream],
|
|
90
|
+
logger,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return { log, pulls: await readSavedCursors(log, collectTimedPulls(sources)) };
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
wait,
|
|
97
|
+
signal:
|
|
98
|
+
options.signal === undefined
|
|
99
|
+
? starting.signal
|
|
100
|
+
: AbortSignal.any([starting.signal, options.signal]),
|
|
101
|
+
logger,
|
|
102
|
+
},
|
|
103
|
+
);
|
|
61
104
|
|
|
62
|
-
|
|
105
|
+
if (started.kind === 'stopped') {
|
|
106
|
+
return () => Promise.resolve();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (started.kind === 'failed') {
|
|
110
|
+
logger.error('the fact log failed the start of ingest', started.error);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const log = started.value.log;
|
|
115
|
+
const pulls = started.value.pulls;
|
|
63
116
|
|
|
64
117
|
const ingest = createIngest({
|
|
65
118
|
deployment: checked.value,
|
|
@@ -90,30 +143,12 @@ export async function serveIngest(options: ServeIngestOptions): Promise<() => Pr
|
|
|
90
143
|
|
|
91
144
|
console.log(`ingest listening on ${server.hostname}:${server.port}`);
|
|
92
145
|
|
|
93
|
-
|
|
146
|
+
stop = async (): Promise<void> => {
|
|
94
147
|
await stopPulls();
|
|
95
148
|
|
|
96
149
|
await server.stop();
|
|
97
150
|
};
|
|
98
151
|
|
|
99
|
-
if (options.exitOnSignal === false) {
|
|
100
|
-
return stop;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const shutdown = makeShutdownHandler({
|
|
104
|
-
stop,
|
|
105
|
-
logError: (message, error) => {
|
|
106
|
-
logger.error(message, error);
|
|
107
|
-
},
|
|
108
|
-
exit: (code) => process.exit(code),
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
|
|
112
|
-
process.on(signal, () => {
|
|
113
|
-
void shutdown();
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
152
|
return stop;
|
|
118
153
|
}
|
|
119
154
|
|