Repository navigation
Expand file tree
/
Copy pathextension.js
More file actions
141 lines (132 loc) · 7.7 KB
/
Copy pathextension.js
File metadata and controls
141 lines (132 loc) · 7.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/**
* Prerender plugin module (Harper Plugin API).
*
* `handleApplication` runs once per worker after the plugin's resources and
* schema have loaded. It reads the host app's scoped options (`scope.options`),
* merges the stored override layer on top, applies the result onto the live
* `config`, re-applies on every change to either layer (live reload), and starts
* the background schedulers once config is in effect.
*/
import { applyOptions, resolveConfig } from './src/config.js';
import {
loadOverrideLayer,
overridesEnabledFor,
seedOverrideFingerprint,
startOverrideWatch,
} from './src/util/configOverride.js';
import { startQueueStatusSync } from './src/resources/RenderQueue.js';
import { startSitemapRefreshScheduler } from './src/resources/Sitemap.js';
import { startScheduleReconciler } from './src/util/reconcile.js';
import { startChangeProbeScheduler } from './src/util/changeProbe.js';
import { startUnroutedReporter } from './src/util/unrouted.js';
import { startBacklogSnapshotter } from './src/util/backlogSnapshot.js';
import { startQueueKeeper } from './src/util/queueKeeperService.js';
import { startInvalidationWatch } from './src/util/invalidation.js';
import { startRequestSampling } from './src/util/sampling.js';
import { startHealthLine } from './src/util/health.js';
import { keeperHealth } from './src/util/renderSchedule.js';
import { probeHealth } from './src/util/changeProbe.js';
import { sitemapHealth } from './src/resources/Sitemap.js';
export async function handleApplication(scope) {
await scope.ready;
const hostOptions = () => scope.options.getAll();
// The stored-override layer, as the table last reported it. Held raw — the kill switch is
// consulted at apply time rather than here, so flipping `management.overrides.enabled` in the
// config file takes effect on the next apply without having to re-read the table.
let overrides = {};
// Each row's recorded file hash, by path. Applied nowhere: it is what the layers view and the
// apply-time warning judge `masking` against (see `overrideDrift` in src/config.js).
let recorded = {};
// BOTH LAYERS ON EVERY APPLY. `applyOptions` rebuilds the whole config from defaults each time,
// so applying one layer alone silently drops the other: a config.yaml edit would wipe every
// override until the next override change, and vice versa.
// FAILING OPEN IS THE WHOLE CONTRACT, and it has to hold for a throw as well as for a read that
// times out. Component load is raced against a hard timeout and an exception here does not merely
// delay it — `handleApplication` rejecting marks the component failed on this worker, so a single
// unusable override row would take the plugin down across the cluster rather than degrade it. A
// deployment running its committed config.yaml with a loud warning is always the better outcome,
// so the override layer is dropped rather than allowed to be fatal.
const apply = () => {
try {
return applyOptions(hostOptions(), overridesEnabledFor(hostOptions()) ? overrides : {}, recorded);
} catch (e) {
scope.logger.error(
`[prerender] Could not apply stored config overrides (${e.message}) — running the deployed ` +
`configuration without them. Fix or clear the offending row; the layer stays inert until then.`
);
overrides = {};
return applyOptions(hostOptions(), {});
}
};
// SUBSCRIBE BEFORE READING. A write that lands between the read and a later subscribe is seen by
// neither, and the resulting staleness would persist — invisibly — until the backstop poll. The
// watcher's own settings come from a pure resolve of the file layer, because nothing has been
// applied yet and reading the live config here would see schema defaults.
await startOverrideWatch((next, nextRecorded) => {
overrides = next;
recorded = nextRecorded ?? {};
apply();
}, resolveConfig(hostOptions(), null).config.management.overrides);
// Requests cannot reach this worker until handleApplication resolves — the worker's socket
// delivery is wired inside loadRootComponents().then(...) — so awaiting the override read here is
// what guarantees no request and no timer ever observes a pre-override config. The read is
// bounded and fails open for the same reason it has to be awaited: component load is raced
// against a hard timeout, and overrunning it fails the component rather than delaying it.
const layer = await loadOverrideLayer(hostOptions());
// Seeded from what the TABLE says, not from what was applied, so a change made while the kill
// switch is off is still detected as a change (and still correctly ignored) rather than mistaken
// for the steady state.
//
// It returns false when the watcher already applied something while this read was in flight — the
// doorbell fired in the window that subscribing early exists to cover. In that case the watcher
// has strictly fresher data and this boot read must NOT overwrite it; applying anyway would
// reinstate the pre-edit config and file it under a fingerprint that says nothing is pending.
if (seedOverrideFingerprint(layer.overrides, layer.recorded)) {
overrides = layer.overrides;
recorded = layer.recorded ?? {};
apply();
}
// Live reload: re-apply whenever the host config changes. The background schedulers
// below subscribe to applyOptions (onConfigApplied) and re-arm themselves, so their
// gates and intervals — including the sitemap-refresh pinning — follow config without
// a restart. The few boot-only options (schema scope 'restart') are diffed on
// re-apply and reported via pendingRestartChanges instead of taking effect silently.
scope.options.on('change', () => {
try {
apply();
} catch (e) {
scope.logger.error(e);
}
});
// Start background work now that config is applied. All are idempotent and
// self-gate by worker/node. The reconciler is deliberately NOT pinned to one node:
// every node repairs the schedule rows it owns (see util/reconcile.js).
startQueueStatusSync();
// Worker 0: the in-memory render queue every claim on this node is served from.
startQueueKeeper();
startSitemapRefreshScheduler();
startScheduleReconciler();
// Owner-scoped on every node, like the reconciler: each node probes the URLs it owns.
// Inert unless changeProbe.enabled with at least one usable rule.
startChangeProbeScheduler();
// Keeps the console's backlog snapshot off the page-load path: its table counts are scans, so it
// recomputes on this slow cadence instead.
startBacklogSnapshotter();
// Unlike the three above, this one runs on EVERY worker: its counters are in-process, so
// each worker has to flush its own tally (see util/unrouted.js).
startUnroutedReporter();
// EVERY worker too, and for two different reasons in one call: it starts this worker's subscribed
// in-memory view of the invalidation table (the serve path resolves epochs from it; until it loads,
// the handler — installed at module load, before this runs — reads per request), and it reports any
// recorded scope that no longer names a configured route. That second half must re-run on config
// changes, because a route RENAMED by a live edit un-invalidates a corpus somebody deliberately
// invalidated, with nothing else to notice.
startInvalidationWatch();
// EVERY worker: each samples the requests it serves into its own ring and writes its own batches
// (util/sampling.js). Compiles now and on every config apply, so samplers are live.
startRequestSampling();
// EVERY worker arms the health line's timer, and whichever claims an interval boundary writes that
// node's line (util/health.js) — so a busy or restarting worker cannot hold it back. The sections are
// read by the writer, from state each module already keeps.
startHealthLine({ keeper: keeperHealth, probe: probeHealth, sitemap: sitemapHealth });
}