@pepys/ingest 0.3.1 → 0.4.1
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 +3 -3
- package/src/fact-log/make-batch-appender.ts +43 -128
- package/src/fact-log/memory/create-memory-fact-log.ts +1 -1
- package/src/fact-log/pick-attempt-step.ts +53 -0
- package/src/fact-log/pick-retry-step.ts +47 -0
- package/src/fact-log/pick-settled-attempt.ts +30 -0
- package/src/fact-log/read-run.ts +34 -0
- package/src/fact-log/s2/create-s2-fact-log.ts +17 -6
- package/src/index.ts +3 -3
- package/src/pull/collect-timed-pulls.ts +23 -0
- package/src/pull/cursors/build-cursor-entity.ts +8 -0
- package/src/pull/cursors/build-cursor-record.ts +32 -0
- package/src/pull/cursors/cursor-stream.ts +6 -0
- package/src/pull/cursors/format-pull-start.ts +13 -0
- package/src/pull/cursors/read-saved-cursors.ts +55 -0
- package/src/pull/make-pull-runner.ts +98 -51
- package/src/pull/with-source-lock.ts +76 -0
- package/src/receive/answers/build-failure-answer.ts +2 -1
- package/src/receive/create-ingest.ts +10 -3
- package/src/receive/make-receive-handler.ts +21 -37
- package/src/receive/pick-received-step.ts +56 -0
- package/src/receive/write-batch.ts +8 -6
- package/src/sender-proof/imp-identity/create-imp-identity-sender-proof.ts +8 -2
- package/src/sender-proof/imp-identity/read-imp-identity-sender-proof.ts +5 -1
- package/src/serve/serve-ingest.ts +43 -14
- package/src/serve/start-pull-timers.ts +47 -39
- package/src/{receive → utils}/find-batch-problem.ts +4 -1
- package/src/utils/pick-batch-plan.ts +37 -0
- package/src/{receive → utils}/plan-batch.ts +1 -1
- package/src/source-port/types.ts +0 -64
- /package/src/{fact-log → utils}/check-append-batch.ts +0 -0
- /package/src/{fact-log → utils}/parse-stored-body.ts +0 -0
- /package/src/{fact-log → utils}/plan-appends.ts +0 -0
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { checkDeployment, listReceiveRoutes } from '@pepys/kit';
|
|
1
|
+
import { checkDeployment, createLogger, listReceiveRoutes } from '@pepys/kit';
|
|
2
2
|
import type { Deployment, SourceDefinition, StreamName } from '@pepys/kit';
|
|
3
3
|
import { makeShutdownHandler } from '@pepys/schema';
|
|
4
4
|
import { toTagged } from '@zgeoff/std';
|
|
5
5
|
import { readIngestConfig } from '../config/read-ingest-config.ts';
|
|
6
6
|
import { createS2FactLog } from '../fact-log/s2/create-s2-fact-log.ts';
|
|
7
|
+
import { collectTimedPulls } from '../pull/collect-timed-pulls.ts';
|
|
8
|
+
import { cursorStream } from '../pull/cursors/cursor-stream.ts';
|
|
9
|
+
import { formatPullStart } from '../pull/cursors/format-pull-start.ts';
|
|
10
|
+
import { readSavedCursors } from '../pull/cursors/read-saved-cursors.ts';
|
|
7
11
|
import { createIngest } from '../receive/create-ingest.ts';
|
|
8
12
|
import { readImpIdentitySenderProof } from '../sender-proof/imp-identity/read-imp-identity-sender-proof.ts';
|
|
9
13
|
import { startPullTimers } from './start-pull-timers.ts';
|
|
@@ -11,16 +15,20 @@ import { startPullTimers } from './start-pull-timers.ts';
|
|
|
11
15
|
interface ServeIngestOptions {
|
|
12
16
|
deployment: Deployment;
|
|
13
17
|
environment: Readonly<Record<string, string | undefined>>;
|
|
18
|
+
|
|
19
|
+
// false leaves SIGINT and SIGTERM to the caller, who stops ingest through the returned function
|
|
20
|
+
exitOnSignal?: boolean;
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
// The ingest process of a deployment. It exits 1 at start on a failed deployment check, on any
|
|
17
24
|
// missing variable, or on a trust folder it cannot load, so no route ever runs with an empty secret.
|
|
18
25
|
// A path whose trust documents or audience are unset starts, and refuses every delivery.
|
|
19
|
-
export async function serveIngest(options: ServeIngestOptions): Promise<void
|
|
26
|
+
export async function serveIngest(options: ServeIngestOptions): Promise<() => Promise<void>> {
|
|
27
|
+
const logger = createLogger(process.stderr);
|
|
20
28
|
const checked = checkDeployment(options.deployment);
|
|
21
29
|
|
|
22
30
|
if (!checked.ok) {
|
|
23
|
-
|
|
31
|
+
logger.error('the deployment fails its check', checked.failure);
|
|
24
32
|
process.exit(1);
|
|
25
33
|
}
|
|
26
34
|
|
|
@@ -28,7 +36,7 @@ export async function serveIngest(options: ServeIngestOptions): Promise<void> {
|
|
|
28
36
|
const config = readIngestConfig(options.environment, collectSecretNames(sources));
|
|
29
37
|
|
|
30
38
|
if (!config.ok) {
|
|
31
|
-
|
|
39
|
+
logger.error('the ingest config is incomplete', config.failure);
|
|
32
40
|
process.exit(1);
|
|
33
41
|
}
|
|
34
42
|
|
|
@@ -36,15 +44,22 @@ export async function serveIngest(options: ServeIngestOptions): Promise<void> {
|
|
|
36
44
|
sources,
|
|
37
45
|
environment: options.environment,
|
|
38
46
|
now: () => Date.now(),
|
|
47
|
+
logger,
|
|
39
48
|
});
|
|
40
49
|
|
|
41
50
|
if (!senderProof.ok) {
|
|
42
|
-
|
|
51
|
+
logger.error('the sender proof cannot start', senderProof.failure);
|
|
43
52
|
process.exit(1);
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
// The S2 adapter creates the basin and each missing stream, so no setup command creates streams.
|
|
47
|
-
const log = await createS2FactLog({
|
|
56
|
+
const log = await createS2FactLog({
|
|
57
|
+
...config.value.s2,
|
|
58
|
+
streams: [...collectStreams(sources), cursorStream],
|
|
59
|
+
logger,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const pulls = await readSavedCursors(log, collectTimedPulls(sources));
|
|
48
63
|
|
|
49
64
|
const ingest = createIngest({
|
|
50
65
|
deployment: checked.value,
|
|
@@ -53,30 +68,42 @@ export async function serveIngest(options: ServeIngestOptions): Promise<void> {
|
|
|
53
68
|
senderProof: senderProof.value,
|
|
54
69
|
appendRetry: { maxAttempts: 5, retryDelayMs: 200 },
|
|
55
70
|
now: () => Date.now(),
|
|
71
|
+
logger,
|
|
56
72
|
});
|
|
57
73
|
|
|
58
74
|
for (const route of listReceiveRoutes(options.deployment)) {
|
|
59
75
|
console.log(`route ${route.route}`, JSON.stringify(route));
|
|
60
76
|
}
|
|
61
77
|
|
|
62
|
-
|
|
63
|
-
|
|
78
|
+
// The server starts first, so a failed listen leaves no timer behind.
|
|
64
79
|
const server = Bun.serve({
|
|
65
80
|
hostname: config.value.host,
|
|
66
81
|
port: config.value.port,
|
|
67
82
|
fetch: ingest.handleRequest,
|
|
68
83
|
});
|
|
69
84
|
|
|
85
|
+
for (const pull of pulls) {
|
|
86
|
+
console.log(formatPullStart(pull));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const stopPulls = startPullTimers(pulls, ingest.runPull, logger);
|
|
90
|
+
|
|
70
91
|
console.log(`ingest listening on ${server.hostname}:${server.port}`);
|
|
71
92
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
stopPulls();
|
|
93
|
+
const stop = async (): Promise<void> => {
|
|
94
|
+
await stopPulls();
|
|
75
95
|
|
|
76
|
-
|
|
77
|
-
|
|
96
|
+
await server.stop();
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (options.exitOnSignal === false) {
|
|
100
|
+
return stop;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const shutdown = makeShutdownHandler({
|
|
104
|
+
stop,
|
|
78
105
|
logError: (message, error) => {
|
|
79
|
-
|
|
106
|
+
logger.error(message, error);
|
|
80
107
|
},
|
|
81
108
|
exit: (code) => process.exit(code),
|
|
82
109
|
});
|
|
@@ -86,6 +113,8 @@ export async function serveIngest(options: ServeIngestOptions): Promise<void> {
|
|
|
86
113
|
void shutdown();
|
|
87
114
|
});
|
|
88
115
|
}
|
|
116
|
+
|
|
117
|
+
return stop;
|
|
89
118
|
}
|
|
90
119
|
|
|
91
120
|
// every receive path's secret, and every name a pull path's `env` lists
|
|
@@ -1,60 +1,68 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Logger } from '@pepys/kit';
|
|
2
2
|
import { match } from 'ts-pattern';
|
|
3
|
-
import type {
|
|
3
|
+
import type { ResumedPull } from '../pull/cursors/read-saved-cursors.ts';
|
|
4
|
+
import type { PullRun, PullRunner } from '../pull/make-pull-runner.ts';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
-
// pull from no cursor. A run never overlaps the one before it. Returns the function that stops them.
|
|
6
|
+
// One timer per pull, whose first run starts from the pull's saved cursor. A timer starts no run
|
|
7
|
+
// while its last one waits for its source or runs. Returns the function that stops the timers: it
|
|
8
|
+
// aborts each run, waiting or in flight, and settles once every run has ended.
|
|
9
9
|
export function startPullTimers(
|
|
10
|
-
|
|
10
|
+
pulls: readonly ResumedPull[],
|
|
11
11
|
runPull: PullRunner,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
logger: Logger,
|
|
13
|
+
): () => Promise<void> {
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
const activeRuns = new Set<Promise<void>>();
|
|
16
|
+
|
|
17
|
+
const timers = pulls.map((pull) => {
|
|
18
|
+
let cursor = pull.cursor;
|
|
19
|
+
let isRunning = false;
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
const runOnce = async (): Promise<void> => {
|
|
22
|
+
if (isRunning || controller.signal.aborted) {
|
|
23
|
+
return;
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
let isRunning = false;
|
|
26
|
+
isRunning = true;
|
|
23
27
|
|
|
24
|
-
const
|
|
25
|
-
if (isRunning) {
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
+
const ended = Promise.withResolvers<void>();
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
activeRuns.add(ended.promise);
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
try {
|
|
33
|
+
const run = await runPull(pull.source, pull.path, cursor, controller.signal);
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
cursor = pickNextCursor(run, cursor);
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
// a run that the stop cut short is no failure
|
|
38
|
+
if (run.kind !== 'stored' && !controller.signal.aborted) {
|
|
39
|
+
logger.error(`the ${pull.path} pull of ${pull.source} did not store`, run);
|
|
40
|
+
}
|
|
41
|
+
} catch (error: unknown) {
|
|
42
|
+
if (!controller.signal.aborted) {
|
|
43
|
+
logger.error(`the ${pull.path} pull of ${pull.source} threw`, error);
|
|
43
44
|
}
|
|
44
|
-
}
|
|
45
|
+
} finally {
|
|
46
|
+
isRunning = false;
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
activeRuns.delete(ended.promise);
|
|
49
|
+
ended.resolve();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return setInterval(() => {
|
|
54
|
+
void runOnce();
|
|
55
|
+
}, pull.intervalMs);
|
|
56
|
+
});
|
|
53
57
|
|
|
54
|
-
return () => {
|
|
58
|
+
return async () => {
|
|
55
59
|
for (const timer of timers) {
|
|
56
60
|
clearInterval(timer);
|
|
57
61
|
}
|
|
62
|
+
|
|
63
|
+
controller.abort();
|
|
64
|
+
|
|
65
|
+
await Promise.all(activeRuns);
|
|
58
66
|
};
|
|
59
67
|
}
|
|
60
68
|
|
|
@@ -3,7 +3,10 @@ import type { Batch, SourceContext } from '@pepys/kit';
|
|
|
3
3
|
|
|
4
4
|
// Every fact is checked before any is written: its stream must be one of the source's families, and
|
|
5
5
|
// its envelope must parse.
|
|
6
|
-
export function findBatchProblem(
|
|
6
|
+
export function findBatchProblem(
|
|
7
|
+
source: Pick<SourceContext, 'name' | 'families'>,
|
|
8
|
+
batch: Batch,
|
|
9
|
+
): string | undefined {
|
|
7
10
|
const streams = new Set<string>(source.families.map((family) => family.stream));
|
|
8
11
|
|
|
9
12
|
for (const [index, pending] of batch.facts.entries()) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Batch, Result, SourceContext } from '@pepys/kit';
|
|
2
|
+
import { findBatchProblem } from './find-batch-problem.ts';
|
|
3
|
+
import { planBatch } from './plan-batch.ts';
|
|
4
|
+
import type { BatchPlan } from './plan-batch.ts';
|
|
5
|
+
|
|
6
|
+
export interface BatchRefusal {
|
|
7
|
+
kind: 'bad-batch' | 'has-blob' | 'too-large';
|
|
8
|
+
reason: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// The checks a batch passes before any of it is written, in order: every fact's stream and
|
|
12
|
+
// envelope, no fact with a blob, then the cut into appends.
|
|
13
|
+
export function pickBatchPlan(
|
|
14
|
+
source: Pick<SourceContext, 'name' | 'families'>,
|
|
15
|
+
batch: Batch,
|
|
16
|
+
): Result<BatchPlan, BatchRefusal> {
|
|
17
|
+
const problem = findBatchProblem(source, batch);
|
|
18
|
+
|
|
19
|
+
if (problem !== undefined) {
|
|
20
|
+
return { ok: false, failure: { kind: 'bad-batch', reason: problem } };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (batch.facts.some((pending) => pending.blobs.length > 0)) {
|
|
24
|
+
return { ok: false, failure: { kind: 'has-blob', reason: 'ingest has no blob store' } };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const plan = planBatch(batch);
|
|
28
|
+
|
|
29
|
+
if (!plan.ok) {
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
failure: { kind: 'too-large', reason: 'one fact is larger than one append of the log takes' },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return plan;
|
|
37
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Batch, FactEnvelope, Result, StreamName } from '@pepys/kit';
|
|
2
|
-
import { planAppends } from '
|
|
2
|
+
import { planAppends } from './plan-appends.ts';
|
|
3
3
|
|
|
4
4
|
export type BatchPlan = ReadonlyMap<StreamName, readonly (readonly FactEnvelope<unknown>[])[]>;
|
|
5
5
|
|
package/src/source-port/types.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
BlobHash,
|
|
3
|
-
ConsumerDeclaration,
|
|
4
|
-
EntityKind,
|
|
5
|
-
FactEnvelope,
|
|
6
|
-
LinkTypeDeclaration,
|
|
7
|
-
Result,
|
|
8
|
-
StreamName,
|
|
9
|
-
} from '@pepys/kit';
|
|
10
|
-
|
|
11
|
-
export interface FactFamily<Payload> {
|
|
12
|
-
stream: StreamName;
|
|
13
|
-
types: readonly string[];
|
|
14
|
-
api: string;
|
|
15
|
-
parsePayload: (raw: unknown) => Result<Payload, { reason: string }>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Provisional: source-retention
|
|
19
|
-
export interface SourceDefinition {
|
|
20
|
-
name: string;
|
|
21
|
-
families: readonly FactFamily<unknown>[];
|
|
22
|
-
entityKinds: readonly EntityKind[];
|
|
23
|
-
linkTypes: readonly LinkTypeDeclaration[];
|
|
24
|
-
consumers: readonly ConsumerDeclaration[];
|
|
25
|
-
live: ReceivePath | PullPath;
|
|
26
|
-
recovery: PullPath;
|
|
27
|
-
backfill: PullPath;
|
|
28
|
-
resync: PullPath;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface ReceivePath {
|
|
32
|
-
kind: 'receive';
|
|
33
|
-
route: string;
|
|
34
|
-
receive: (request: Request) => Promise<Received>;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export type Received =
|
|
38
|
-
| { kind: 'refused'; status: 400 | 401 | 405 | 422; reason: string }
|
|
39
|
-
| { kind: 'ignored' }
|
|
40
|
-
| { kind: 'batch'; batch: Batch };
|
|
41
|
-
|
|
42
|
-
export interface PullPath {
|
|
43
|
-
kind: 'pull';
|
|
44
|
-
name: string;
|
|
45
|
-
intervalMs: number | null;
|
|
46
|
-
pull: (cursor: string | null) => Promise<Pulled>;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Provisional: source-retention
|
|
50
|
-
export type Pulled =
|
|
51
|
-
| { kind: 'batch'; batch: Batch; cursor: string; isDone: boolean }
|
|
52
|
-
| { kind: 'expired' }
|
|
53
|
-
| { kind: 'unavailable'; retryAfterMs: number }
|
|
54
|
-
| { kind: 'rejected'; reason: string };
|
|
55
|
-
|
|
56
|
-
export interface Batch {
|
|
57
|
-
facts: readonly PendingFact[];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface PendingFact {
|
|
61
|
-
stream: StreamName;
|
|
62
|
-
fact: FactEnvelope<unknown>;
|
|
63
|
-
blobs: readonly { hash: BlobHash; bytes: Uint8Array }[];
|
|
64
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|