@opentermsarchive/engine 16.0.1 → 16.1.0
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/config/default.json +13 -0
- package/config/test.json +12 -0
- package/package.json +1 -1
- package/scripts/dataset/export/index.js +1 -1
- package/scripts/declarations/validate/index.mocha.js +7 -0
- package/scripts/import/index.js +1 -1
- package/scripts/import/loadCommits.js +1 -1
- package/scripts/rewrite/initializer/index.js +1 -1
- package/scripts/rewrite/rewrite-snapshots.js +1 -1
- package/scripts/rewrite/rewrite-versions.js +1 -1
- package/src/archivist/collection/index.test.js +2 -1
- package/src/archivist/fetcher/htmlOnlyFetcher.js +8 -0
- package/src/archivist/fetcher/index.test.js +12 -0
- package/src/archivist/index.js +106 -23
- package/src/archivist/index.test.js +659 -9
- package/src/archivist/recorder/repositories/factory.js +3 -2
- package/src/archivist/recorder/repositories/git/dataMapper.js +2 -1
- package/src/archivist/recorder/repositories/git/index.js +30 -33
- package/src/archivist/recorder/repositories/git/index.test.js +98 -2
- package/src/archivist/recorder/repositories/mongo/index.js +14 -3
- package/src/archivist/recorder/repositories/mongo/index.test.js +27 -1
- package/src/archivist/services/index.js +45 -1
- package/src/archivist/services/index.test.js +52 -1
- package/src/archivist/services/sourceDocument.js +8 -2
- package/src/archivist/services/sourceDocument.test.js +37 -0
- package/src/archivist/tracking-results/errors.js +5 -0
- package/src/archivist/tracking-results/index.js +233 -0
- package/src/archivist/tracking-results/index.test.js +406 -0
- package/src/archivist/tracking-results/recorder.js +156 -0
- package/src/archivist/tracking-results/recorder.test.js +559 -0
- package/src/archivist/tracking-results/repository.js +136 -0
- package/src/archivist/tracking-results/repository.test.js +763 -0
- package/src/archivist/tracking-results/run/dataMapper.js +51 -0
- package/src/archivist/tracking-results/run/dataMapper.test.js +168 -0
- package/src/archivist/tracking-results/run/index.js +115 -0
- package/src/archivist/tracking-results/run/index.test.js +221 -0
- package/src/archivist/tracking-results/terms-result/dataMapper.js +200 -0
- package/src/archivist/tracking-results/terms-result/dataMapper.test.js +575 -0
- package/src/archivist/tracking-results/terms-result/index.js +42 -0
- package/src/archivist/tracking-results/terms-result/index.test.js +228 -0
- package/src/collection-api/routes/index.js +2 -2
- package/src/git/errors.js +1 -0
- package/src/{archivist/recorder/repositories/git/git.js → git/index.js} +79 -23
- package/src/git/index.test.js +266 -0
- package/src/git/pathSegment.js +18 -0
- package/src/git/pathSegment.test.js +33 -0
- package/src/index.js +5 -4
- package/src/reporter/index.js +9 -4
- package/src/reporter/index.test.js +47 -9
- package/src/archivist/recorder/repositories/git/git.test.js +0 -114
- /package/src/{archivist/recorder/repositories/git → git}/trailers.js +0 -0
- /package/src/{archivist/recorder/repositories/git → git}/trailers.test.js +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { UnreadableRunError } from './errors.js';
|
|
2
|
+
import Run, { RUN_STATUSES } from './run/index.js';
|
|
3
|
+
import { TRANSITIONS_BY_EVENT_TYPE, termsKey } from './terms-result/dataMapper.js';
|
|
4
|
+
import TermsResult, { STATUSES } from './terms-result/index.js';
|
|
5
|
+
|
|
6
|
+
export const RUN_ID_TRAILER_KEY = 'x-run-id'; // Git trailer tying every commit of a run to its runId, so run membership stays greppable without relying on commit ranges between two run.json commits; parseTrailers lowercases keys, hence the casing
|
|
7
|
+
|
|
8
|
+
export default class TrackingResultsRecorder {
|
|
9
|
+
constructor({ repository, collectionId, schedule, engineVersion }) {
|
|
10
|
+
this.repository = repository;
|
|
11
|
+
this.collectionId = collectionId;
|
|
12
|
+
this.schedule = schedule;
|
|
13
|
+
this.engineVersion = engineVersion;
|
|
14
|
+
this.currentRun = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async initialize() {
|
|
18
|
+
await this.repository.initialize();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
finalize() {
|
|
22
|
+
return this.repository.finalize();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async startRun({ declarationsCommit, servicesCount, termsCount, skippedTerms = [] }) {
|
|
26
|
+
const run = new Run({
|
|
27
|
+
runId: Run.generateId(),
|
|
28
|
+
collectionId: this.collectionId,
|
|
29
|
+
schedule: this.schedule,
|
|
30
|
+
startDate: new Date().toISOString(),
|
|
31
|
+
engineVersion: this.engineVersion,
|
|
32
|
+
declarationsCommit,
|
|
33
|
+
servicesCount,
|
|
34
|
+
termsCount,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
skippedTerms.forEach(skipped => run.addSkipped(skipped)); // Skips known at run start (e.g. terms not selected by a partial run) are persisted in the run-start commit: a crash during the run must not let recovery attribute them to the crash
|
|
38
|
+
|
|
39
|
+
await this.repository.saveRun(run, { trailers: runTrailers(run) });
|
|
40
|
+
|
|
41
|
+
this.currentRun = run; // Assigned only once the run-start commit landed: a failed start must not leave a half-open run that later recordings would attach to
|
|
42
|
+
|
|
43
|
+
return run;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async recordTermsOutcome({ serviceId, termsType, serviceName, sourceDocuments, status, reasons, transientErrorReasons }) {
|
|
47
|
+
const run = this.activeRun;
|
|
48
|
+
|
|
49
|
+
const event = {
|
|
50
|
+
date: new Date().toISOString(),
|
|
51
|
+
serviceName,
|
|
52
|
+
sourceDocuments,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (status === STATUSES.failed) {
|
|
56
|
+
event.reasons = reasons;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (transientErrorReasons?.length) {
|
|
60
|
+
event.transientError = { reasons: transientErrorReasons };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const newResult = new TermsResult({ serviceId, termsType, status, event });
|
|
64
|
+
const { eventType } = await this.repository.saveTermsResult(newResult, { trailers: runTrailers(run) });
|
|
65
|
+
|
|
66
|
+
run.coverage.processed++; // Counted per call: every terms of a run is either recorded once or persisted as skipped at run start; the Archivist is responsible for that invariant
|
|
67
|
+
run.tracked[status]++;
|
|
68
|
+
|
|
69
|
+
if (event.transientError) {
|
|
70
|
+
run.transientErrors++;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const transitionType = TRANSITIONS_BY_EVENT_TYPE[eventType];
|
|
74
|
+
|
|
75
|
+
if (transitionType) {
|
|
76
|
+
run.recordTransition(transitionType, { serviceId, termsType });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async completeRun() {
|
|
81
|
+
const run = this.activeRun;
|
|
82
|
+
|
|
83
|
+
run.markCompleted(new Date().toISOString());
|
|
84
|
+
|
|
85
|
+
await this.repository.saveRun(run, { trailers: runTrailers(run) });
|
|
86
|
+
|
|
87
|
+
this.currentRun = null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async recoverCrashedRunIfAny({ getDeclaredTermsAtCommit }) { // Must be invoked before startRun: a new run-start commit would hide the crashed run's reference SHA from findLatestRunCommitSha. The callback returns the terms declared at a given declarations commit, since the recorder has no knowledge of the declarations module
|
|
91
|
+
let run;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
run = await this.repository.findLatestRun();
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw error.code ? error : new UnreadableRunError(error.message, { cause: error }); // System errors carry a code and are worth a retry, unlike a content that cannot be parsed or validated
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!run || run.lastRun.status !== RUN_STATUSES.inProgress) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const runStartSha = await this.repository.findLatestRunCommitSha();
|
|
104
|
+
const committedTerms = await this.repository.findCommittedTermsResultsSince(runStartSha);
|
|
105
|
+
const persistedSkipped = run.coverage.skipped; // Skips persisted by the run-start commit (e.g. terms not selected by a partial run) are preserved with their original reason, not re-attributed to the crash
|
|
106
|
+
const accountedKeys = new Set([ ...committedTerms, ...persistedSkipped ].map(({ serviceId, termsType }) => termsKey(serviceId, termsType)));
|
|
107
|
+
const declaredTerms = await getDeclaredTermsAtCommit(run.declarations.commit);
|
|
108
|
+
const crashSkipped = declaredTerms
|
|
109
|
+
.filter(({ serviceId, termsType }) => !accountedKeys.has(termsKey(serviceId, termsType)))
|
|
110
|
+
.map(({ serviceId, termsType }) => ({ serviceId, termsType, reason: 'no outcome recorded before the engine crashed' })); // Terms processed without a substantive change leave no commit, so they cannot be told apart from unprocessed ones and end up here, understating `processed`: the reason states the absence of a record rather than claiming the terms was not processed
|
|
111
|
+
|
|
112
|
+
run.coverage = { processed: committedTerms.length, skipped: [ ...persistedSkipped, ...crashSkipped ] };
|
|
113
|
+
({ tracked: run.tracked, transientErrors: run.transientErrors } = await this.deriveCounts(committedTerms));
|
|
114
|
+
run.markCrashed(new Date().toISOString()); // run.transitions is left as persisted by the run-start commit (empty): deriving it back would mean parsing commit subjects, and the transitions of a crashed run remain derivable by consumers from its per-terms commits
|
|
115
|
+
await this.repository.saveRun(run, { trailers: runTrailers(run) }); // Carries the crashed run's id, so the finalization commit is greppable alongside the run it closes
|
|
116
|
+
|
|
117
|
+
return run;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async deriveCounts(committedTerms) { // The committed terms files still hold the crashed run's last written state, since recovery runs before any new startRun
|
|
121
|
+
const tracked = { ok: 0, failed: 0 };
|
|
122
|
+
let transientErrors = 0;
|
|
123
|
+
|
|
124
|
+
for (const { serviceId, termsType } of committedTerms) {
|
|
125
|
+
const result = await this.repository.findLatestTermsResult(serviceId, termsType);
|
|
126
|
+
|
|
127
|
+
if (!result) { // Defensive: a file removed by hand since the crash leaves the counts unchanged rather than failing the recovery
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
tracked[result.status]++;
|
|
132
|
+
|
|
133
|
+
if (result.event.transientError) {
|
|
134
|
+
transientErrors++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { tracked, transientErrors };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
discardCurrentRun() { // Test entry point to drop the run in progress without persisting anything
|
|
142
|
+
this.currentRun = null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
get activeRun() { // Single guarded access path to the run in progress: dereferencing it before startRun throws, so no mutating method needs a separate assertion. External readers keep using the nullable currentRun property
|
|
146
|
+
if (!this.currentRun) {
|
|
147
|
+
throw new Error(`${this.constructor.name}: no tracking run in progress; call startRun first`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return this.currentRun;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function runTrailers(run) {
|
|
155
|
+
return { [RUN_ID_TRAILER_KEY]: run.runId };
|
|
156
|
+
}
|