@substrat-run/kernel 0.158.0 → 0.160.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/dist/billing-retention.d.ts +46 -0
- package/dist/billing-retention.d.ts.map +1 -0
- package/dist/billing-retention.js +78 -0
- package/dist/billing-retention.js.map +1 -0
- package/dist/byte-stream.d.ts +102 -0
- package/dist/byte-stream.d.ts.map +1 -0
- package/dist/byte-stream.js +153 -0
- package/dist/byte-stream.js.map +1 -0
- package/dist/connection-landings.d.ts +15 -3
- package/dist/connection-landings.d.ts.map +1 -1
- package/dist/connection-landings.js +10 -2
- package/dist/connection-landings.js.map +1 -1
- package/dist/deadline-fetch.d.ts +34 -0
- package/dist/deadline-fetch.d.ts.map +1 -0
- package/dist/deadline-fetch.js +231 -0
- package/dist/deadline-fetch.js.map +1 -0
- package/dist/deployment-wire-auth.d.ts +82 -0
- package/dist/deployment-wire-auth.d.ts.map +1 -0
- package/dist/deployment-wire-auth.js +150 -0
- package/dist/deployment-wire-auth.js.map +1 -0
- package/dist/entity-trash.d.ts +65 -8
- package/dist/entity-trash.d.ts.map +1 -1
- package/dist/entity-trash.js +80 -12
- package/dist/entity-trash.js.map +1 -1
- package/dist/export-break.d.ts +6 -0
- package/dist/export-break.d.ts.map +1 -1
- package/dist/export-break.js +11 -0
- package/dist/export-break.js.map +1 -1
- package/dist/index.d.ts +18 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -7
- package/dist/index.js.map +1 -1
- package/dist/invocation-line.d.ts +3 -2
- package/dist/invocation-line.d.ts.map +1 -1
- package/dist/invocation-line.js +2 -0
- package/dist/invocation-line.js.map +1 -1
- package/dist/meter-days.d.ts +107 -0
- package/dist/meter-days.d.ts.map +1 -0
- package/dist/meter-days.js +230 -0
- package/dist/meter-days.js.map +1 -0
- package/dist/meters.d.ts +19 -1
- package/dist/meters.d.ts.map +1 -1
- package/dist/meters.js +0 -0
- package/dist/meters.js.map +1 -1
- package/dist/ops-failure-reference.d.ts +31 -0
- package/dist/ops-failure-reference.d.ts.map +1 -0
- package/dist/ops-failure-reference.js +32 -0
- package/dist/ops-failure-reference.js.map +1 -0
- package/dist/platform-request-cap.d.ts +9 -0
- package/dist/platform-request-cap.d.ts.map +1 -0
- package/dist/platform-request-cap.js +42 -0
- package/dist/platform-request-cap.js.map +1 -0
- package/dist/platform-sweep.d.ts +170 -7
- package/dist/platform-sweep.d.ts.map +1 -1
- package/dist/platform-sweep.js +375 -45
- package/dist/platform-sweep.js.map +1 -1
- package/dist/request-rollup.d.ts +161 -0
- package/dist/request-rollup.d.ts.map +1 -0
- package/dist/request-rollup.js +273 -0
- package/dist/request-rollup.js.map +1 -0
- package/dist/reserved-roles.d.ts +157 -4
- package/dist/reserved-roles.d.ts.map +1 -1
- package/dist/reserved-roles.js +216 -2
- package/dist/reserved-roles.js.map +1 -1
- package/dist/schedule-state.d.ts +5 -0
- package/dist/schedule-state.d.ts.map +1 -1
- package/dist/scope-host.d.ts +260 -15
- package/dist/scope-host.d.ts.map +1 -1
- package/dist/scope-host.js +53 -2
- package/dist/scope-host.js.map +1 -1
- package/dist/scope-lifecycle.d.ts +7 -0
- package/dist/scope-lifecycle.d.ts.map +1 -1
- package/dist/scope-lifecycle.js +7 -0
- package/dist/scope-lifecycle.js.map +1 -1
- package/dist/scope-record.d.ts +16 -5
- package/dist/scope-record.d.ts.map +1 -1
- package/dist/scope-record.js +72 -7
- package/dist/scope-record.js.map +1 -1
- package/dist/spine-restore.d.ts +7 -3
- package/dist/spine-restore.d.ts.map +1 -1
- package/dist/spine-restore.js +14 -3
- package/dist/spine-restore.js.map +1 -1
- package/dist/storage-gauge.d.ts +47 -13
- package/dist/storage-gauge.d.ts.map +1 -1
- package/dist/storage-gauge.js +84 -27
- package/dist/storage-gauge.js.map +1 -1
- package/dist/subject-redaction.d.ts +9 -3
- package/dist/subject-redaction.d.ts.map +1 -1
- package/dist/subject-redaction.js +24 -12
- package/dist/subject-redaction.js.map +1 -1
- package/dist/vertical-owners.d.ts +69 -0
- package/dist/vertical-owners.d.ts.map +1 -0
- package/dist/vertical-owners.js +108 -0
- package/dist/vertical-owners.js.map +1 -0
- package/package.json +2 -2
package/dist/platform-sweep.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { drainedEvent, instant, substratError } from '@substrat-run/contracts';
|
|
2
2
|
import { STORAGE_GAUGE_PRUNE_BATCH, STORED_SCOPE_STATUSES, holdsStore } from './storage-gauge.js';
|
|
3
|
-
import {
|
|
3
|
+
import { BILLING_ROLLUP_RETENTION_MONTHS, billingRollupHorizon, utcDayOf } from './billing-retention.js';
|
|
4
|
+
import { METER_DAYS_PRUNE_BATCH, METER_SNAPSHOT_TIMEOUT_MS } from './meter-days.js';
|
|
5
|
+
import { REQUEST_ROLLUP_DAYS_PER_PASS, REQUEST_ROLLUP_LOOKBACK_DAYS, REQUEST_ROLLUP_MAX_LOOKBACK_DAYS, REQUEST_ROLLUP_PRUNE_BATCH, REQUEST_ROLLUP_READ_TIMEOUT_MS, REQUEST_ROLLUP_RETRY_MS, REQUEST_ROLLUP_SETTLE_MS, isRequestRollupSaturated, requestRollupDueDays, } from './request-rollup.js';
|
|
6
|
+
import { assertRowLimit, backoffAt, fetchLikeOf, TELEMETRY_PRUNE_BATCH } from './scope-host.js';
|
|
4
7
|
import { MIGRATION_FLAG_THRESHOLD, migrationFleet, migrationProgress, scopeMigrationState } from './migration-progress.js';
|
|
5
8
|
import { UNDRAINED_SKIPPED_IDS } from './outbox-event.js';
|
|
6
9
|
import { resolveVerticalInstanceFrom } from './peer.js';
|
|
@@ -188,6 +191,36 @@ function assertSweepBatches(options) {
|
|
|
188
191
|
if (gauge?.batch !== undefined && !(Number.isInteger(gauge.batch) && gauge.batch >= 0)) {
|
|
189
192
|
throw substratError('validation_failed', `storageGauge.batch must be a non-negative integer, got ${String(gauge.batch)}`);
|
|
190
193
|
}
|
|
194
|
+
const rollup = options.requestRollup;
|
|
195
|
+
if (rollup?.pruneBatch !== undefined)
|
|
196
|
+
assertRowLimit('requestRollup.pruneBatch', rollup.pruneBatch);
|
|
197
|
+
if (rollup?.batch !== undefined && !(Number.isInteger(rollup.batch) && rollup.batch >= 0)) {
|
|
198
|
+
throw substratError('validation_failed', `requestRollup.batch must be a non-negative integer, got ${String(rollup.batch)}`);
|
|
199
|
+
}
|
|
200
|
+
if (rollup?.retentionMonths !== undefined)
|
|
201
|
+
assertBillingRetention('requestRollup.retentionMonths', rollup.retentionMonths);
|
|
202
|
+
// Each of these corrupts the kept history silently rather than failing: a lookback past what
|
|
203
|
+
// Analytics Engine keeps records aged-out days as days without traffic, and a negative settle
|
|
204
|
+
// reads the open day and freezes it. So they are refused here, before anything is read.
|
|
205
|
+
const lookback = rollup?.lookbackDays;
|
|
206
|
+
if (lookback !== undefined && !(Number.isInteger(lookback) && lookback >= 1 && lookback <= REQUEST_ROLLUP_MAX_LOOKBACK_DAYS)) {
|
|
207
|
+
throw substratError('validation_failed', `requestRollup.lookbackDays must be a whole number of days from 1 to ${REQUEST_ROLLUP_MAX_LOOKBACK_DAYS}, got ${String(lookback)}`);
|
|
208
|
+
}
|
|
209
|
+
for (const name of ['settleMs', 'retryMs']) {
|
|
210
|
+
const ms = rollup?.[name];
|
|
211
|
+
if (ms !== undefined && !(Number.isFinite(ms) && ms >= 0)) {
|
|
212
|
+
throw substratError('validation_failed', `requestRollup.${name} must be a non-negative number of ms, got ${String(ms)}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (rollup?.readTimeoutMs !== undefined)
|
|
216
|
+
assertPositiveMs('requestRollup.readTimeoutMs', rollup.readTimeoutMs);
|
|
217
|
+
const snapshot = options.meterSnapshot;
|
|
218
|
+
if (snapshot?.pruneBatch !== undefined)
|
|
219
|
+
assertRowLimit('meterSnapshot.pruneBatch', snapshot.pruneBatch);
|
|
220
|
+
if (snapshot?.retentionMonths !== undefined)
|
|
221
|
+
assertBillingRetention('meterSnapshot.retentionMonths', snapshot.retentionMonths);
|
|
222
|
+
if (snapshot?.timeoutMs !== undefined)
|
|
223
|
+
assertPositiveMs('meterSnapshot.timeoutMs', snapshot.timeoutMs);
|
|
191
224
|
const orphans = options.orphanAttachments;
|
|
192
225
|
if (orphans?.batch !== undefined && !(Number.isInteger(orphans.batch) && orphans.batch >= 0)) {
|
|
193
226
|
throw substratError('validation_failed', `orphanAttachments.batch must be a non-negative integer, got ${String(orphans.batch)}`);
|
|
@@ -196,6 +229,69 @@ function assertSweepBatches(options) {
|
|
|
196
229
|
throw substratError('validation_failed', `orphanAttachments.mode must be 'dry-run' or 'delete', got ${String(orphans.mode)}`);
|
|
197
230
|
}
|
|
198
231
|
}
|
|
232
|
+
/** A billing retention option, refused unless `billingRollupHorizon` takes it. */
|
|
233
|
+
function assertBillingRetention(name, months) {
|
|
234
|
+
try {
|
|
235
|
+
billingRollupHorizon(0, months);
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
throw substratError('validation_failed', `${name}: ${message(err)}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** A deadline option, refused unless it is a positive, finite number of ms. */
|
|
242
|
+
function assertPositiveMs(name, ms) {
|
|
243
|
+
if (!(Number.isFinite(ms) && ms > 0)) {
|
|
244
|
+
throw substratError('validation_failed', `${name} must be a positive number of ms, got ${String(ms)}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* How many unreadable directory rows one pass names before it only says there are more: the
|
|
249
|
+
* sweep's `errors`, and the hostname reconcile's `unreadableScopes`, so each pass's report stays
|
|
250
|
+
* bounded however many rows a directory holds.
|
|
251
|
+
*/
|
|
252
|
+
export const UNREADABLE_SCOPE_ROWS_NAMED = 20;
|
|
253
|
+
/**
|
|
254
|
+
* The scope read every fleet-wide phase of one pass shares. One row that does not parse used to
|
|
255
|
+
* throw out of `listScopes` for the whole list, and since no phase reads the directory inside a
|
|
256
|
+
* try, out of the pass: every phase after it went unrun until the row was fixed by hand. Read
|
|
257
|
+
* through `listScopesReadable`, the row is stepped over, and named in `errors` ONCE per pass
|
|
258
|
+
* however many phases met it, by its id and the fields that failed, never their values. At most
|
|
259
|
+
* `UNREADABLE_SCOPE_ROWS_NAMED` are named, then one line says there are more. A host that
|
|
260
|
+
* predates `listScopesReadable` is read through `listScopes`, strict as before.
|
|
261
|
+
*/
|
|
262
|
+
function scopeReaderFor(admin, actor, report) {
|
|
263
|
+
const readable = admin.listScopesReadable?.bind(admin);
|
|
264
|
+
if (!readable)
|
|
265
|
+
return (filter) => admin.listScopes(actor, filter);
|
|
266
|
+
const named = new Set();
|
|
267
|
+
let capped = false;
|
|
268
|
+
return async (filter) => {
|
|
269
|
+
const { scopes, unreadable } = await readable(actor, filter);
|
|
270
|
+
for (const row of unreadable) {
|
|
271
|
+
if (capped)
|
|
272
|
+
break;
|
|
273
|
+
if (named.has(row.scopeId))
|
|
274
|
+
continue;
|
|
275
|
+
if (named.size < UNREADABLE_SCOPE_ROWS_NAMED) {
|
|
276
|
+
named.add(row.scopeId);
|
|
277
|
+
report.errors.push({
|
|
278
|
+
kind: 'scope-row',
|
|
279
|
+
id: row.scopeId,
|
|
280
|
+
error: `the directory row of tenant ${row.tenantId} does not read as a scope (${row.fields.join(', ')}); stepped over this pass`,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
capped = true;
|
|
285
|
+
report.errors.push({
|
|
286
|
+
kind: 'scope-row',
|
|
287
|
+
id: '*',
|
|
288
|
+
error: `more than ${UNREADABLE_SCOPE_ROWS_NAMED} directory rows do not read as a scope; only the first ${UNREADABLE_SCOPE_ROWS_NAMED} are named this pass`,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return scopes;
|
|
293
|
+
};
|
|
294
|
+
}
|
|
199
295
|
export async function runPlatformSweep(host, options) {
|
|
200
296
|
assertSweepBatches(options);
|
|
201
297
|
const concurrency = options.concurrency ?? 8;
|
|
@@ -219,8 +315,13 @@ export async function runPlatformSweep(host, options) {
|
|
|
219
315
|
findings: null,
|
|
220
316
|
storage: null,
|
|
221
317
|
orphanAttachments: null,
|
|
318
|
+
requestRollup: null,
|
|
319
|
+
meterSnapshot: null,
|
|
222
320
|
errors: [],
|
|
223
321
|
};
|
|
322
|
+
// Every fleet-wide read of the directory below goes through this, so one unreadable row is
|
|
323
|
+
// stepped over by each phase and named once. The tenant reap does not: see there.
|
|
324
|
+
const listScopes = scopeReaderFor(host.admin, options.actor, report);
|
|
224
325
|
// -- migration reconciliation (kernel-design §5.3, #49) ---------------------
|
|
225
326
|
// FIRST, deliberately: `drainDue` wakes scopes (and wake migrates lazily), so
|
|
226
327
|
// running this phase after it would mean every failed scope gets its attempt
|
|
@@ -244,9 +345,7 @@ export async function runPlatformSweep(host, options) {
|
|
|
244
345
|
const now = new Date().toISOString();
|
|
245
346
|
// `provisioning` included: a scope stuck there because its migration failed
|
|
246
347
|
// is precisely a straggler (the pure adapter's getScope makes the same call).
|
|
247
|
-
const listed = await
|
|
248
|
-
status: ['active', 'provisioning'],
|
|
249
|
-
});
|
|
348
|
+
const listed = await listScopes({ status: ['active', 'provisioning'] });
|
|
250
349
|
const fleet = migrationFleet(listed);
|
|
251
350
|
// Due = behind and never-failed (wake it — that IS the sweep's job), or
|
|
252
351
|
// failed with its backoff window elapsed. A failed scope inside its window
|
|
@@ -328,7 +427,7 @@ export async function runPlatformSweep(host, options) {
|
|
|
328
427
|
const reachedThisPass = new Map();
|
|
329
428
|
const viaPlatformDrain = Boolean(options.drainPlatformRequestsFn);
|
|
330
429
|
if (options.drainRetries !== false) {
|
|
331
|
-
const scopes = await
|
|
430
|
+
const scopes = await listScopes({ status: 'active' });
|
|
332
431
|
await mapBounded(scopes, concurrency, async (s) => {
|
|
333
432
|
if (failedThisPass.has(s.id))
|
|
334
433
|
return; // fails closed — draining is only noise
|
|
@@ -356,7 +455,7 @@ export async function runPlatformSweep(host, options) {
|
|
|
356
455
|
const drainedThisPass = new Map();
|
|
357
456
|
const drain = options.drainPlatformRequestsFn;
|
|
358
457
|
if (drain) {
|
|
359
|
-
const scopes = await
|
|
458
|
+
const scopes = await listScopes({ status: 'active' });
|
|
360
459
|
await mapBounded(scopes, concurrency, async (s) => {
|
|
361
460
|
if (failedThisPass.has(s.id)) {
|
|
362
461
|
report.platformRequestTotals.skipped += 1;
|
|
@@ -381,7 +480,7 @@ export async function runPlatformSweep(host, options) {
|
|
|
381
480
|
if (options.storageGauge &&
|
|
382
481
|
typeof host.admin.recordScopeStorage === 'function' &&
|
|
383
482
|
typeof host.admin.listScopeStorageAttempts === 'function') {
|
|
384
|
-
report.storage = await sweepStorageGauge(host.admin, options, options.storageGauge, reachedThisPass, report);
|
|
483
|
+
report.storage = await sweepStorageGauge(host.admin, options, options.storageGauge, reachedThisPass, report, listScopes);
|
|
385
484
|
}
|
|
386
485
|
// -- provision reconcile (#1172, #1653) -------------------------------------
|
|
387
486
|
// A vertical's `onProvision` runs once per scope, at install. So a scope serving code
|
|
@@ -404,7 +503,7 @@ export async function runPlatformSweep(host, options) {
|
|
|
404
503
|
unsupported: { count: 0, scopeIds: [] },
|
|
405
504
|
deferred: 0,
|
|
406
505
|
};
|
|
407
|
-
const scopes = (await
|
|
506
|
+
const scopes = (await listScopes({ status: 'active' })).filter((s) => isPrimaryScope(s) && !failedThisPass.has(s.id));
|
|
408
507
|
const serving = await servingPointersFor(host.admin, options.actor, scopes);
|
|
409
508
|
const behind = [];
|
|
410
509
|
for (const s of scopes) {
|
|
@@ -484,21 +583,27 @@ export async function runPlatformSweep(host, options) {
|
|
|
484
583
|
// Primaries only (`isPrimaryScope`, #2005) — a fork, a snapshot or a preview of either
|
|
485
584
|
// kind is a copy or a throwaway; firing its schedules would run real recurring side
|
|
486
585
|
// effects off it. Lineage alone (`forkedFrom === null`) let a clean-room preview through.
|
|
487
|
-
const scopes = (await
|
|
586
|
+
const scopes = (await listScopes({ status: 'active' })).filter(isPrimaryScope);
|
|
488
587
|
await mapBounded(scopes, concurrency, async (s) => {
|
|
489
588
|
if (failedThisPass.has(s.id))
|
|
490
589
|
return;
|
|
491
590
|
let touched = false;
|
|
492
591
|
let platformRequests = 0;
|
|
592
|
+
// #2517: the requests this scope's purge runs enqueued so far in the tick, across modules —
|
|
593
|
+
// nothing drains between them, so a later purge head the cap refuses waits for the next tick.
|
|
594
|
+
let purgeRequests = 0;
|
|
493
595
|
for (const reg of registrations) {
|
|
494
596
|
try {
|
|
495
|
-
const r = await host.runDueSchedules(reg.moduleId, s.tenantId, s.id);
|
|
597
|
+
const r = await host.runDueSchedules(reg.moduleId, s.tenantId, s.id, { earlierPurgeRequests: purgeRequests });
|
|
496
598
|
if (r.fired > 0 || r.failed > 0)
|
|
497
599
|
touched = true;
|
|
498
600
|
schedules.fired += r.fired;
|
|
499
601
|
schedules.skipped += r.skipped;
|
|
500
602
|
schedules.failed += r.failed;
|
|
603
|
+
if (r.deferred)
|
|
604
|
+
schedules.deferred = (schedules.deferred ?? 0) + r.deferred;
|
|
501
605
|
platformRequests += r.platformRequests ?? 0;
|
|
606
|
+
purgeRequests += r.purgeRequests ?? 0;
|
|
502
607
|
for (const e of r.errors) {
|
|
503
608
|
report.errors.push({ kind: 'schedule', id: `${s.id}:${e.operation}`, error: e.error });
|
|
504
609
|
}
|
|
@@ -566,7 +671,7 @@ export async function runPlatformSweep(host, options) {
|
|
|
566
671
|
const freshRegs = host.registeredFreshness().filter((r) => r.freshness.length > 0);
|
|
567
672
|
if (freshRegs.length > 0) {
|
|
568
673
|
// The same primaries the schedule phase fires on (#2005).
|
|
569
|
-
const scopes = (await
|
|
674
|
+
const scopes = (await listScopes({ status: 'active' })).filter(isPrimaryScope);
|
|
570
675
|
await mapBounded(scopes, concurrency, async (s) => {
|
|
571
676
|
if (failedThisPass.has(s.id))
|
|
572
677
|
return;
|
|
@@ -600,7 +705,7 @@ export async function runPlatformSweep(host, options) {
|
|
|
600
705
|
// After the migration phase and skipping what failed there, like every phase that reaches a
|
|
601
706
|
// vertical. Opt-in, and its own window per pass: it lists a bucket prefix and wakes a scope.
|
|
602
707
|
if (options.orphanAttachments) {
|
|
603
|
-
const scopes = (await
|
|
708
|
+
const scopes = (await listScopes({ status: 'active' })).filter((s) => isPrimaryScope(s) && !failedThisPass.has(s.id));
|
|
604
709
|
report.orphanAttachments = await sweepOrphanAttachmentWindow(host, options, options.orphanAttachments, scopes, report);
|
|
605
710
|
}
|
|
606
711
|
if (options.gcSnapshots !== false) {
|
|
@@ -612,7 +717,7 @@ export async function runPlatformSweep(host, options) {
|
|
|
612
717
|
// `deleteSnapshot` re-checks this same predicate, so a mislabeled row fails closed there,
|
|
613
718
|
// never silently deletes.
|
|
614
719
|
const now = new Date().toISOString();
|
|
615
|
-
const scopes = await
|
|
720
|
+
const scopes = await listScopes();
|
|
616
721
|
const expired = scopes.filter((s) => (s.forkedFrom !== null || s.kind === 'preview') && s.expiresAt !== null && s.expiresAt <= now);
|
|
617
722
|
const reap = options.deleteSnapshotFn ??
|
|
618
723
|
((tenantId, scopeId) => host.deleteSnapshot(options.actor, tenantId, scopeId));
|
|
@@ -635,7 +740,7 @@ export async function runPlatformSweep(host, options) {
|
|
|
635
740
|
// status below the seam, so a row that changed underneath fails closed there.
|
|
636
741
|
if (options.reapArchivedAfterDays !== undefined && options.reapArchivedAfterDays >= 0) {
|
|
637
742
|
const cutoff = new Date(Date.now() - options.reapArchivedAfterDays * 86_400_000).toISOString();
|
|
638
|
-
const archived = await
|
|
743
|
+
const archived = await listScopes({ status: ['archived'] });
|
|
639
744
|
const due = archived.filter((s) => s.archivedAt !== null && s.archivedAt <= cutoff);
|
|
640
745
|
const reap = options.reapScopeFn ??
|
|
641
746
|
// Retention reap is a deliberate policy on already-archived scopes: force past the
|
|
@@ -672,6 +777,11 @@ export async function runPlatformSweep(host, options) {
|
|
|
672
777
|
// Co-located default: reap every one of the tenant's scopes through the scope
|
|
673
778
|
// seam (archive-if-needed first — reapScope only accepts `archived`), then clear
|
|
674
779
|
// the tenant's directory rows. A scope already `reaped` is skipped.
|
|
780
|
+
//
|
|
781
|
+
// STRICT, unlike every other read in this pass: `reapTenant` below clears the tenant's
|
|
782
|
+
// directory, so a scope this loop stepped over would never have its storage reaped and
|
|
783
|
+
// would lose the rows that find it. A row that does not parse fails this tenant's reap
|
|
784
|
+
// instead, into `errors` as `reap-tenant`, and the tenant waits for the row to be fixed.
|
|
675
785
|
const scopes = await host.admin.listScopes(options.actor, { tenantId });
|
|
676
786
|
for (const s of scopes) {
|
|
677
787
|
if (s.status === 'reaped')
|
|
@@ -694,7 +804,7 @@ export async function runPlatformSweep(host, options) {
|
|
|
694
804
|
});
|
|
695
805
|
}
|
|
696
806
|
const connections = await host.admin.listConnections(options.actor, {});
|
|
697
|
-
const landings = options.connectionLandings ? new LandingPass(host, options.actor, options.connectionLandings, concurrency) : null;
|
|
807
|
+
const landings = options.connectionLandings ? new LandingPass(host, options.actor, options.connectionLandings, concurrency, listScopes) : null;
|
|
698
808
|
await mapBounded(connections, concurrency, async (c) => {
|
|
699
809
|
// #1232: the connection's durable sweep record. A connection row carries no
|
|
700
810
|
// scope (a connection spans scopes by construction) and no version.
|
|
@@ -734,7 +844,8 @@ export async function runPlatformSweep(host, options) {
|
|
|
734
844
|
report.connectionLandings.failed += landed.failed.length;
|
|
735
845
|
}
|
|
736
846
|
try {
|
|
737
|
-
|
|
847
|
+
// #2498: a sized stream body the connector sends reaches the injected fetch converted.
|
|
848
|
+
const result = await sweeper(host, c.id, { fetch: fetchLikeOf(options.fetch) });
|
|
738
849
|
const answer = options.connectionLandings?.binders[c.provider] ? connectorSweepAnswerOf(result) : null;
|
|
739
850
|
report.connectionsSwept += 1;
|
|
740
851
|
// #2316: a resolved sweep is not a landed one. A scope the connector could not land in, or a
|
|
@@ -769,7 +880,7 @@ export async function runPlatformSweep(host, options) {
|
|
|
769
880
|
// own outbox. An import is never re-emitted into the consumer's outbox, so the two phases
|
|
770
881
|
// never ship the same event twice.
|
|
771
882
|
if (options.crossVertical) {
|
|
772
|
-
report.crossVertical = await sweepCrossVertical(host, options, options.crossVertical, failedThisPass, report);
|
|
883
|
+
report.crossVertical = await sweepCrossVertical(host, options, options.crossVertical, failedThisPass, report, listScopes);
|
|
773
884
|
}
|
|
774
885
|
// -- drain each scope's domain events to Tier 2 (#1334, master-plan §5.3) ---
|
|
775
886
|
// Before the access-log drain below, which is deliberately last: this phase
|
|
@@ -785,7 +896,7 @@ export async function runPlatformSweep(host, options) {
|
|
|
785
896
|
// fleet-wide fault.
|
|
786
897
|
const configured = options.eventDrainBatch ?? EVENT_DRAIN_BATCH;
|
|
787
898
|
const budget = Number.isFinite(configured) && configured >= 1 ? Math.floor(configured) : EVENT_DRAIN_BATCH;
|
|
788
|
-
const drainable = await
|
|
899
|
+
const drainable = await listScopes({ status: 'active' });
|
|
789
900
|
await mapBounded(drainable, concurrency, async (s) => {
|
|
790
901
|
try {
|
|
791
902
|
const { shipped, skipped } = await drainScopeEvents(host, options, sink, {
|
|
@@ -836,6 +947,26 @@ export async function runPlatformSweep(host, options) {
|
|
|
836
947
|
report.errors.push({ kind: 'findings', id: 'findings', error: message(err) });
|
|
837
948
|
}
|
|
838
949
|
}
|
|
950
|
+
// -- the daily meter snapshot (#2501) -----------------------------------------
|
|
951
|
+
// After all the scheduled work, so the day's reading is the directory as this pass leaves it,
|
|
952
|
+
// reaps included, and just before the request rollup: the snapshot is one directory transaction,
|
|
953
|
+
// and a slow Analytics Engine answer must not push its reading later in the day. Its one read of
|
|
954
|
+
// the day records is shipped by the access-log drain below, in the same pass.
|
|
955
|
+
if (options.meterSnapshot &&
|
|
956
|
+
typeof host.admin.recordMeterDay === 'function' &&
|
|
957
|
+
typeof host.admin.listMeterDayTakes === 'function') {
|
|
958
|
+
report.meterSnapshot = await sweepMeterSnapshot(host.admin, options.actor, options.meterSnapshot, report);
|
|
959
|
+
}
|
|
960
|
+
// -- the daily request rollup (#2501) ----------------------------------------
|
|
961
|
+
// After all the scheduled work, deliberately: it waits on Analytics Engine, an outside service,
|
|
962
|
+
// and nothing in the pass depends on it, so a slow answer must not hold back the work that has
|
|
963
|
+
// to happen every pass. Each read is bounded (`RequestRollupSweepOptions.readTimeoutMs`), so the
|
|
964
|
+
// most it can delay is the access-log drain below, which then ships this phase's own reads too.
|
|
965
|
+
if (options.requestRollup &&
|
|
966
|
+
typeof host.admin.recordRequestDay === 'function' &&
|
|
967
|
+
typeof host.admin.listRequestRollupDays === 'function') {
|
|
968
|
+
report.requestRollup = await sweepRequestRollup(host.admin, options.actor, options.requestRollup, report);
|
|
969
|
+
}
|
|
839
970
|
// -- drain the staff access log to Tier 2, then prune it (K-24, §4.4) --------
|
|
840
971
|
// LAST, deliberately: every phase above reads the directory through the audited
|
|
841
972
|
// seam, so each one writes access rows. Running the drain last means a pass ships
|
|
@@ -857,7 +988,7 @@ export async function runPlatformSweep(host, options) {
|
|
|
857
988
|
* One pass over every cross-vertical edge (#1705). Bounded like the event drain: one batch per
|
|
858
989
|
* edge per pass, reported rather than looped, so one busy producer cannot starve the rest.
|
|
859
990
|
*/
|
|
860
|
-
async function sweepCrossVertical(host, options, cv, failedThisPass, report,
|
|
991
|
+
async function sweepCrossVertical(host, options, cv, failedThisPass, report, listScopes,
|
|
861
992
|
/** Run only this producer's outgoing edges (the router kick). Absent: every edge. */
|
|
862
993
|
only) {
|
|
863
994
|
const out = {
|
|
@@ -889,7 +1020,7 @@ only) {
|
|
|
889
1020
|
//
|
|
890
1021
|
// A producer-scoped run reads only that tenant's scopes: both ends of an edge are in one
|
|
891
1022
|
// tenant, so nothing outside it can be a consumer of this producer or change how it resolves.
|
|
892
|
-
const scopes = (await
|
|
1023
|
+
const scopes = (await listScopes({ status: 'active', ...(only ? { tenantId: only.tenantId } : {}) })).filter((s) => isPrimaryScope(s) && s.vertical !== null);
|
|
893
1024
|
// The kick names a scope, and the edges run are the ones whose producer RESOLVES to it. A
|
|
894
1025
|
// fork, a preview, a second install or a scope of another tenant is not a producer, so a
|
|
895
1026
|
// kick naming one runs nothing (the sweep would resolve the same way).
|
|
@@ -1044,7 +1175,8 @@ only) {
|
|
|
1044
1175
|
*/
|
|
1045
1176
|
export async function runCrossVerticalFrom(host, options, producer) {
|
|
1046
1177
|
const report = { errors: [] };
|
|
1047
|
-
const
|
|
1178
|
+
const listScopes = scopeReaderFor(host.admin, options.actor, report);
|
|
1179
|
+
const crossVertical = await sweepCrossVertical(host, options, options.crossVertical, new Set(), report, listScopes, producer);
|
|
1048
1180
|
return { crossVertical, errors: report.errors };
|
|
1049
1181
|
}
|
|
1050
1182
|
/**
|
|
@@ -1590,7 +1722,7 @@ const cmp = (x, y) => (x < y ? -1 : x > y ? 1 : 0);
|
|
|
1590
1722
|
* in one directory call, then prune past retention. A failed read is an error entry; its
|
|
1591
1723
|
* scope keeps its last sample and waits a day like any other.
|
|
1592
1724
|
*/
|
|
1593
|
-
async function sweepStorageGauge(admin, options, gauge, reachedThisPass, report) {
|
|
1725
|
+
async function sweepStorageGauge(admin, options, gauge, reachedThisPass, report, listScopes) {
|
|
1594
1726
|
const out = {
|
|
1595
1727
|
reached: reachedThisPass.size,
|
|
1596
1728
|
resting: 0,
|
|
@@ -1610,7 +1742,7 @@ async function sweepStorageGauge(admin, options, gauge, reachedThisPass, report)
|
|
|
1610
1742
|
if (batch > 0) {
|
|
1611
1743
|
// Not one archived before it ever held data (`holdsStore`): reading it would create the
|
|
1612
1744
|
// spine it never had, and bill it.
|
|
1613
|
-
const resting = (await
|
|
1745
|
+
const resting = (await listScopes({ status: STORED_SCOPE_STATUSES.filter((s) => s !== 'active') })).filter(holdsStore);
|
|
1614
1746
|
out.resting = resting.length;
|
|
1615
1747
|
const candidates = [...reachedThisPass.values(), ...resting.filter((s) => !reachedThisPass.has(s.id))];
|
|
1616
1748
|
const tried = new Map((await admin.listScopeStorageAttempts(options.actor)).map((a) => [a.scopeId, a.attemptedAt]));
|
|
@@ -1639,33 +1771,48 @@ async function sweepStorageGauge(admin, options, gauge, reachedThisPass, report)
|
|
|
1639
1771
|
}
|
|
1640
1772
|
if (!Number.isInteger(bytes) || bytes < 0)
|
|
1641
1773
|
throw new Error(`not a size: ${String(bytes)}`);
|
|
1642
|
-
// The store read
|
|
1643
|
-
// database size just read,
|
|
1644
|
-
|
|
1645
|
-
|
|
1774
|
+
// The store read and the attachment read are each their own: a failure of one must not
|
|
1775
|
+
// throw away the database size just read, nor the other's reading, nor replace what the
|
|
1776
|
+
// day already holds of it. A read the deployment cannot make is not a failure and not a
|
|
1777
|
+
// size of 0: what it would measure is left as it is. Neither waits for the other, so a
|
|
1778
|
+
// slow scope holds its slot for one deadline, not two.
|
|
1779
|
+
const beside = {};
|
|
1780
|
+
const timeoutMs = gauge.storeReadTimeoutMs ?? STORE_READ_TIMEOUT_MS;
|
|
1781
|
+
/** One read beside the database: its failure message, or nothing. */
|
|
1782
|
+
const readBeside = async (read, label, keep) => {
|
|
1783
|
+
if (!read)
|
|
1784
|
+
return undefined;
|
|
1646
1785
|
try {
|
|
1647
|
-
|
|
1786
|
+
const value = await withinDeadline(read.call(gauge, s), timeoutMs, `${label} of ${s.id}`);
|
|
1787
|
+
if (value !== null)
|
|
1788
|
+
keep(value);
|
|
1648
1789
|
}
|
|
1649
1790
|
catch (err) {
|
|
1650
|
-
if (isUnsupported(err))
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
out.unsupported += 1;
|
|
1654
|
-
attempts.push({ ...attempt, bytes });
|
|
1655
|
-
return;
|
|
1656
|
-
}
|
|
1657
|
-
out.failed += 1;
|
|
1658
|
-
attempts.push({ ...attempt, bytes, error: message(err) });
|
|
1659
|
-
report.errors.push({ kind: 'storage', id: s.id, error: message(err) });
|
|
1660
|
-
return;
|
|
1791
|
+
if (!isUnsupported(err))
|
|
1792
|
+
return message(err);
|
|
1793
|
+
out.unsupported += 1;
|
|
1661
1794
|
}
|
|
1795
|
+
return undefined;
|
|
1796
|
+
};
|
|
1797
|
+
// Listed in a fixed order, so a scope failing both always reports them the same way.
|
|
1798
|
+
const failures = (await Promise.all([
|
|
1799
|
+
readBeside(gauge.readStores, 'scope store read', (stores) => {
|
|
1800
|
+
beside.stores = stores;
|
|
1801
|
+
}),
|
|
1802
|
+
readBeside(gauge.readAttachments, 'attachment read', (attachments) => {
|
|
1803
|
+
if (!Number.isInteger(attachments) || attachments < 0)
|
|
1804
|
+
throw new Error(`not a size: ${String(attachments)}`);
|
|
1805
|
+
beside.attachments = attachments;
|
|
1806
|
+
}),
|
|
1807
|
+
])).filter((failure) => failure !== undefined);
|
|
1808
|
+
if (failures.length > 0) {
|
|
1809
|
+
const error = failures.join('; ');
|
|
1810
|
+
out.failed += 1;
|
|
1811
|
+
attempts.push({ ...attempt, bytes, ...beside, error });
|
|
1812
|
+
report.errors.push({ kind: 'storage', id: s.id, error });
|
|
1813
|
+
return;
|
|
1662
1814
|
}
|
|
1663
|
-
|
|
1664
|
-
attempts.push({ ...attempt, bytes });
|
|
1665
|
-
}
|
|
1666
|
-
else {
|
|
1667
|
-
attempts.push({ ...attempt, bytes, ...(stores ? { stores } : {}) });
|
|
1668
|
-
}
|
|
1815
|
+
attempts.push({ ...attempt, bytes, ...beside });
|
|
1669
1816
|
out.read += 1;
|
|
1670
1817
|
}
|
|
1671
1818
|
catch (err) {
|
|
@@ -1691,6 +1838,189 @@ async function sweepStorageGauge(admin, options, gauge, reachedThisPass, report)
|
|
|
1691
1838
|
}
|
|
1692
1839
|
return out;
|
|
1693
1840
|
}
|
|
1841
|
+
const NO_ATTEMPT = { error: null, since: null, failures: 0 };
|
|
1842
|
+
/** The newest of a ledger's attempts (#2501), and the run of identical failures it belongs to when it failed. */
|
|
1843
|
+
function previousAttempt(attempts) {
|
|
1844
|
+
let newest;
|
|
1845
|
+
for (const a of attempts)
|
|
1846
|
+
if (newest === undefined || a.attemptedAt > newest.attemptedAt)
|
|
1847
|
+
newest = a;
|
|
1848
|
+
return newest ? { error: newest.error, since: newest.failingSince ?? newest.attemptedAt, failures: newest.failingCount ?? 1 } : NO_ATTEMPT;
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* A failure's run (#2501): the previous attempt's run carried on when this failure repeats its error,
|
|
1852
|
+
* a new run begun `at` otherwise. A repeated failure is a standing one, which reached the failure
|
|
1853
|
+
* digest when its run began; the run keeps its start and length on the ledger and in the report,
|
|
1854
|
+
* where the pass's log line shows them.
|
|
1855
|
+
*/
|
|
1856
|
+
function extendRun(previous, error, at) {
|
|
1857
|
+
const repeated = error === previous.error;
|
|
1858
|
+
return {
|
|
1859
|
+
repeated,
|
|
1860
|
+
run: { error, since: repeated && previous.since ? previous.since : at, failures: repeated ? previous.failures + 1 : 1 },
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* One pass of the request-rollup phase (#2501): read the oldest due days one at a time, record
|
|
1865
|
+
* each day's counts or its failure, then prune past retention. A failed day writes nothing of the
|
|
1866
|
+
* day, waits `retryMs` like any other attempt, and ends the pass's reads: the days after it stay
|
|
1867
|
+
* due. A failure that repeats the previous attempt's is a standing one (a credential, a dataset
|
|
1868
|
+
* name, an outage), reported when its streak began and not again until a read succeeds.
|
|
1869
|
+
*/
|
|
1870
|
+
async function sweepRequestRollup(admin, actor, rollup, report) {
|
|
1871
|
+
const out = {
|
|
1872
|
+
due: 0,
|
|
1873
|
+
deferred: 0,
|
|
1874
|
+
read: 0,
|
|
1875
|
+
failed: 0,
|
|
1876
|
+
repeated: 0,
|
|
1877
|
+
streak: null,
|
|
1878
|
+
saturated: 0,
|
|
1879
|
+
saturatedDays: [],
|
|
1880
|
+
recorded: 0,
|
|
1881
|
+
pruned: 0,
|
|
1882
|
+
};
|
|
1883
|
+
const batch = rollup.batch ?? REQUEST_ROLLUP_DAYS_PER_PASS;
|
|
1884
|
+
const months = rollup.retentionMonths === undefined ? BILLING_ROLLUP_RETENTION_MONTHS : rollup.retentionMonths;
|
|
1885
|
+
const now = rollup.now ?? (() => new Date().toISOString());
|
|
1886
|
+
try {
|
|
1887
|
+
const nowMs = Date.parse(now());
|
|
1888
|
+
if (batch > 0) {
|
|
1889
|
+
const reads = await admin.listRequestRollupDays(actor);
|
|
1890
|
+
// A saturated day is a state of that day, not a failure of the read, so it belongs to no run.
|
|
1891
|
+
let previous = previousAttempt(reads.filter((r) => !r.saturated));
|
|
1892
|
+
const due = requestRollupDueDays({
|
|
1893
|
+
nowMs,
|
|
1894
|
+
reads,
|
|
1895
|
+
lookbackDays: rollup.lookbackDays ?? REQUEST_ROLLUP_LOOKBACK_DAYS,
|
|
1896
|
+
settleMs: rollup.settleMs ?? REQUEST_ROLLUP_SETTLE_MS,
|
|
1897
|
+
retryMs: rollup.retryMs ?? REQUEST_ROLLUP_RETRY_MS,
|
|
1898
|
+
horizon: billingRollupHorizon(nowMs, months),
|
|
1899
|
+
});
|
|
1900
|
+
const take = due.slice(0, batch);
|
|
1901
|
+
out.due = due.length;
|
|
1902
|
+
out.deferred = due.length - take.length;
|
|
1903
|
+
// One day at a time: each is one Analytics Engine query, and the account's query budget
|
|
1904
|
+
// is shared with every observability read.
|
|
1905
|
+
const saturated = [];
|
|
1906
|
+
for (const day of take) {
|
|
1907
|
+
try {
|
|
1908
|
+
const counts = await withinDeadline(rollup.read(day), rollup.readTimeoutMs ?? REQUEST_ROLLUP_READ_TIMEOUT_MS, 'the request rollup read');
|
|
1909
|
+
out.recorded += (await admin.recordRequestDay(actor, { day, attemptedAt: now(), counts })).recorded;
|
|
1910
|
+
out.read += 1;
|
|
1911
|
+
// A read that succeeds ends any run, so a failure later in this pass starts a new one.
|
|
1912
|
+
previous = NO_ATTEMPT;
|
|
1913
|
+
}
|
|
1914
|
+
catch (err) {
|
|
1915
|
+
const error = message(err);
|
|
1916
|
+
const attemptedAt = now();
|
|
1917
|
+
if (isRequestRollupSaturated(err)) {
|
|
1918
|
+
// A standing state of this day, not a failed read: kept visible on the day's record,
|
|
1919
|
+
// never read again, and no reason to stop, so the days after it are still rolled up.
|
|
1920
|
+
out.saturated += 1;
|
|
1921
|
+
saturated.push(day);
|
|
1922
|
+
await admin.recordRequestDay(actor, { day, attemptedAt, error, saturated: true }).catch(() => undefined);
|
|
1923
|
+
continue;
|
|
1924
|
+
}
|
|
1925
|
+
out.failed += 1;
|
|
1926
|
+
const { run, repeated } = extendRun(previous, error, attemptedAt);
|
|
1927
|
+
out.streak = run;
|
|
1928
|
+
if (repeated)
|
|
1929
|
+
out.repeated += 1;
|
|
1930
|
+
else
|
|
1931
|
+
report.errors.push({ kind: 'request-rollup', id: day, error });
|
|
1932
|
+
// The failure is the day's attempt, so the day waits `retryMs` rather than taking a slot
|
|
1933
|
+
// on every pass. If even this write fails, the day is simply due again next pass.
|
|
1934
|
+
await admin
|
|
1935
|
+
.recordRequestDay(actor, { day, attemptedAt, error, failingSince: run.since, failingCount: run.failures })
|
|
1936
|
+
.catch(() => undefined);
|
|
1937
|
+
// The next day would most likely fail the same way, and each read is a fleet-wide query
|
|
1938
|
+
// against the account's shared budget: one failure ends the pass's reads.
|
|
1939
|
+
break;
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
// No read failed this pass and none succeeded (a success resets `previous`): a run the ledger
|
|
1943
|
+
// holds still stands, though this pass had nothing due to retry. It is reported as it is,
|
|
1944
|
+
// with no attempt added and nothing pushed to the digest, which mailed it when it began.
|
|
1945
|
+
if (out.streak === null && previous.error !== null && previous.since !== null) {
|
|
1946
|
+
out.streak = { error: previous.error, since: previous.since, failures: previous.failures };
|
|
1947
|
+
}
|
|
1948
|
+
// Each saturated day reaches the digest once, on the pass that found it: it is not read again.
|
|
1949
|
+
out.saturatedDays = saturated;
|
|
1950
|
+
if (saturated.length > 0) {
|
|
1951
|
+
report.errors.push({
|
|
1952
|
+
kind: 'request-rollup',
|
|
1953
|
+
id: 'saturated',
|
|
1954
|
+
error: `${saturated.length} day(s) hold more groups than one read answers and are kept unread: ${saturated.join(', ')}`,
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
if (typeof admin.pruneRequestDays === 'function') {
|
|
1959
|
+
out.pruned = await admin.pruneRequestDays(actor, rollup.pruneBatch ?? REQUEST_ROLLUP_PRUNE_BATCH, months);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
catch (err) {
|
|
1963
|
+
// A directory read or the prune failed: whatever days were recorded stay recorded, and the
|
|
1964
|
+
// next pass finds the rest due.
|
|
1965
|
+
report.errors.push({ kind: 'request-rollup', id: 'directory', error: message(err) });
|
|
1966
|
+
}
|
|
1967
|
+
return out;
|
|
1968
|
+
}
|
|
1969
|
+
const DAY_MS = 86_400_000;
|
|
1970
|
+
/**
|
|
1971
|
+
* One pass of the meter-snapshot phase (#2501): take the snapshot of the UTC day the pass runs in,
|
|
1972
|
+
* unless the day already has one, then prune past retention. A failed attempt writes nothing of the
|
|
1973
|
+
* day and is recorded, and the next pass of the day tries again. A failure that repeats the attempt
|
|
1974
|
+
* before it, across days too, is a standing one: reported when its run began, and not again until
|
|
1975
|
+
* a snapshot is taken.
|
|
1976
|
+
*/
|
|
1977
|
+
async function sweepMeterSnapshot(admin, actor, snapshot, report) {
|
|
1978
|
+
const out = { day: null, taken: false, tenants: 0, failed: 0, repeated: 0, streak: null, pruned: 0 };
|
|
1979
|
+
const months = snapshot.retentionMonths === undefined ? BILLING_ROLLUP_RETENTION_MONTHS : snapshot.retentionMonths;
|
|
1980
|
+
const now = snapshot.now ?? (() => new Date().toISOString());
|
|
1981
|
+
try {
|
|
1982
|
+
const at = new Date(now()).toISOString();
|
|
1983
|
+
const day = at.slice(0, 10);
|
|
1984
|
+
out.day = day;
|
|
1985
|
+
// The attempt before this pass is today's, or yesterday's when today has none: one a day at the
|
|
1986
|
+
// least while the phase runs, so two days hold it.
|
|
1987
|
+
const takes = await admin.listMeterDayTakes(actor, { since: utcDayOf(Date.parse(at) - DAY_MS), until: day });
|
|
1988
|
+
// A day already taken is not tried again, so a later pass of the day can never report a failure
|
|
1989
|
+
// about a snapshot that stands. The directory checks again inside its own transaction, which is
|
|
1990
|
+
// what settles two passes racing for the same day.
|
|
1991
|
+
if (!takes.some((t) => t.day === day && t.takenAt !== null)) {
|
|
1992
|
+
try {
|
|
1993
|
+
const taken = await withinDeadline(admin.recordMeterDay(actor, { at }), snapshot.timeoutMs ?? METER_SNAPSHOT_TIMEOUT_MS, 'the meter snapshot');
|
|
1994
|
+
out.taken = taken.taken;
|
|
1995
|
+
out.tenants = taken.tenants;
|
|
1996
|
+
}
|
|
1997
|
+
catch (err) {
|
|
1998
|
+
const error = message(err);
|
|
1999
|
+
out.failed = 1;
|
|
2000
|
+
const { run, repeated } = extendRun(previousAttempt(takes), error, at);
|
|
2001
|
+
out.streak = run;
|
|
2002
|
+
if (repeated)
|
|
2003
|
+
out.repeated = 1;
|
|
2004
|
+
else
|
|
2005
|
+
report.errors.push({ kind: 'meter-snapshot', id: day, error });
|
|
2006
|
+
// The attempt's own instant, so its record lands on its day even when the deadline ran past
|
|
2007
|
+
// midnight. If even this write fails, the next pass simply tries again.
|
|
2008
|
+
await admin
|
|
2009
|
+
.recordMeterDay(actor, { at, error, failingSince: run.since, failingCount: run.failures })
|
|
2010
|
+
.catch(() => undefined);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
if (typeof admin.pruneMeterDays === 'function') {
|
|
2014
|
+
out.pruned = await admin.pruneMeterDays(actor, snapshot.pruneBatch ?? METER_DAYS_PRUNE_BATCH, months);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
catch (err) {
|
|
2018
|
+
// A directory read or the prune failed: a snapshot already taken stays taken, and the next pass
|
|
2019
|
+
// tries the day again if it was not.
|
|
2020
|
+
report.errors.push({ kind: 'meter-snapshot', id: 'directory', error: message(err) });
|
|
2021
|
+
}
|
|
2022
|
+
return out;
|
|
2023
|
+
}
|
|
1694
2024
|
const pushCapped = (bucket, id) => {
|
|
1695
2025
|
bucket.count += 1;
|
|
1696
2026
|
if (bucket.scopeIds.length < ORPHAN_ATTACHMENT_REPORTED_IDS)
|