@harperfast/harper 5.3.0-beta.1 → 5.3.0-beta.2
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/components/DESIGN.md +421 -0
- package/components/mcp/DESIGN.md +109 -0
- package/components/mcp/audit.ts +21 -17
- package/config/DESIGN.md +306 -0
- package/dataLayer/DESIGN.md +179 -0
- package/dataLayer/restoreMarker.ts +92 -25
- package/dist/components/mcp/audit.d.ts +2 -1
- package/dist/components/mcp/audit.js +21 -17
- package/dist/components/mcp/audit.js.map +1 -1
- package/dist/dataLayer/restoreMarker.d.ts +21 -8
- package/dist/dataLayer/restoreMarker.js +94 -27
- package/dist/dataLayer/restoreMarker.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/resources/RecordEncoder.js +25 -5
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/Table.js +48 -2
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.d.ts +3 -0
- package/dist/resources/analytics/write.js +39 -13
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/crdt.d.ts +10 -0
- package/dist/resources/crdt.js +22 -0
- package/dist/resources/crdt.js.map +1 -1
- package/dist/resources/databases.js +1 -1
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +2 -2
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +43 -24
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/indexes/fullTextDerivedIndex.d.ts +81 -0
- package/dist/resources/indexes/fullTextDerivedIndex.js +1004 -0
- package/dist/resources/indexes/fullTextDerivedIndex.js.map +1 -0
- package/dist/resources/indexes/fullTextNativeBinding.d.ts +78 -0
- package/dist/resources/indexes/fullTextNativeBinding.js +85 -0
- package/dist/resources/indexes/fullTextNativeBinding.js.map +1 -0
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.d.ts +24 -0
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js +149 -0
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js.map +1 -0
- package/dist/resources/recordLockCoordinator.d.ts +5 -5
- package/dist/resources/recordLockCoordinator.js +48 -16
- package/dist/resources/recordLockCoordinator.js.map +1 -1
- package/dist/resources/transactionBroadcast.js +4 -6
- package/dist/resources/transactionBroadcast.js.map +1 -1
- package/dist/security/auth.js +59 -23
- package/dist/security/auth.js.map +1 -1
- package/dist/security/deferredAuthentication.d.ts +11 -0
- package/dist/security/deferredAuthentication.js +25 -3
- package/dist/security/deferredAuthentication.js.map +1 -1
- package/dist/security/jsLoader.js +3 -0
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/server/REST.js +6 -3
- package/dist/server/REST.js.map +1 -1
- package/dist/server/mqtt.js +5 -1
- package/dist/server/mqtt.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.d.ts +3 -3
- package/dist/server/serverHelpers/serverUtilities.js +14 -3
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/serverHelpers/uwsServer.js +4 -1
- package/dist/server/serverHelpers/uwsServer.js.map +1 -1
- package/dist/server/serverHelpers/webSocketCloseReason.d.ts +2 -0
- package/dist/server/serverHelpers/webSocketCloseReason.js +29 -0
- package/dist/server/serverHelpers/webSocketCloseReason.js.map +1 -0
- package/dist/utility/logging/harper_logger.js +1 -0
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/index.ts +3 -0
- package/npm-shrinkwrap.json +104 -104
- package/package.json +6 -5
- package/resources/DESIGN.md +568 -3
- package/resources/RecordEncoder.ts +27 -5
- package/resources/Table.ts +49 -3
- package/resources/analytics/DESIGN.md +38 -0
- package/resources/analytics/write.ts +40 -14
- package/resources/crdt.ts +22 -0
- package/resources/databases.ts +1 -1
- package/resources/indexes/DESIGN.md +833 -0
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +44 -25
- package/resources/indexes/fullTextDerivedIndex.ts +1165 -0
- package/resources/indexes/fullTextNativeBinding.ts +146 -0
- package/resources/indexes/nativeFullTextDerivedIndexLifecycle.ts +181 -0
- package/resources/record-locks.md +1407 -0
- package/resources/recordLockCoordinator.ts +61 -22
- package/resources/scheduler/DESIGN.md +40 -0
- package/resources/transactionBroadcast.ts +4 -4
- package/security/DESIGN.md +175 -0
- package/security/auth.ts +53 -24
- package/security/deferredAuthentication.ts +24 -2
- package/security/jsLoader.ts +3 -0
- package/server/DESIGN.md +264 -0
- package/server/REST.ts +6 -3
- package/server/mqtt.ts +6 -4
- package/server/serverHelpers/serverUtilities.ts +14 -3
- package/server/serverHelpers/uwsServer.ts +4 -1
- package/server/serverHelpers/webSocketCloseReason.ts +25 -0
- package/studio/web/assets/{Chat-D3j-1yY1.js → Chat-DADFFGe_.js} +1 -1
- package/studio/web/assets/{FloatingChat-BxJGYcfB.js → FloatingChat-D_mI-rZ7.js} +3 -3
- package/studio/web/assets/{apiToken-CT55oWOe.js → apiToken-c2NiSDHa.js} +1 -1
- package/studio/web/assets/{applications-D9Ct9_vm.js → applications-DktUqh7G.js} +1 -1
- package/studio/web/assets/{cssMode-DV8H7VwA.js → cssMode-Cs_75Xhw.js} +1 -1
- package/studio/web/assets/{editor-uatc0unt.js → editor-19b-Y1IN.js} +1 -1
- package/studio/web/assets/{html-Bm6D6paN.js → html-DiYEQMpB.js} +1 -1
- package/studio/web/assets/{htmlMode-CEn7tpLG.js → htmlMode-CmR0y7P_.js} +1 -1
- package/studio/web/assets/{index-BIXW6Pu4.js → index-Dm0rfkJ7.js} +5 -5
- package/studio/web/assets/{index.lazy-UI7L-Vrk.js → index.lazy-7vqt2CC3.js} +1 -1
- package/studio/web/assets/{javascript-CJ0G3AFZ.js → javascript-BWtCFuOt.js} +1 -1
- package/studio/web/assets/{jsonMode-DQADAYEa.js → jsonMode-Buzzbv9y.js} +1 -1
- package/studio/web/assets/{languageServices-CAQJXWcI.js → languageServices-SqsFWfTM.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-CCQ8P5sY.js → lspLanguageFeatures-EMV5cmjo.js} +1 -1
- package/studio/web/assets/{notifications-Cvb3P1lB.js → notifications-CAB-LZWT.js} +1 -1
- package/studio/web/assets/{notifications-BbxTU6Aw.js → notifications-DRzmSRxM.js} +1 -1
- package/studio/web/assets/{profile-Yyb7gsvL.js → profile-BNKAl79n.js} +1 -1
- package/studio/web/assets/{regions-OgjGHlU5.js → regions-CUow_Zw2.js} +1 -1
- package/studio/web/assets/{register-6qwNEOY3.js → register-Dkt3WUMp.js} +2 -2
- package/studio/web/assets/{setComponentFile-BilDMtgB.js → setComponentFile-BZRfMD0N.js} +1 -1
- package/studio/web/assets/{setup-J6qJ7OIU.js → setup-D_yiEPO2.js} +2 -2
- package/studio/web/assets/{status-0RWGcfyD.js → status-DhHh1Ge-.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-BIn-vErT.js → toggleHighContrast-D7L1PDtV.js} +1 -1
- package/studio/web/assets/{tsMode-DgUXku4d.js → tsMode-CCwLk1YS.js} +1 -1
- package/studio/web/assets/{typescript-C9orXcsM.js → typescript-BP1j1mjn.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-BEoXXbUB.js → useEntityRestURL-D7bnYxLw.js} +1 -1
- package/studio/web/assets/{workers-JVzSDmgx.js → workers-tOuCNT17.js} +1 -1
- package/studio/web/assets/{xml-Cq-S8S4X.js → xml-BSG_3mQT.js} +1 -1
- package/studio/web/assets/{yaml-sfoRdh1M.js → yaml-DxiLprBB.js} +1 -1
- package/studio/web/index.html +1 -1
- package/utility/DESIGN.md +55 -0
- package/utility/logging/harper_logger.ts +1 -0
|
@@ -163,6 +163,10 @@ export class RecordObject {
|
|
|
163
163
|
return entryMap.get(this)?.expiresAt;
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
|
+
// msgpackr's RESERVE_START_SPACE option carries the reserved byte count in the LOW BYTE of the same
|
|
167
|
+
// word as its flags (`encodeOptions & 0xff`, msgpackr `pack`), so the metadata prefix this encoder
|
|
168
|
+
// reserves cannot exceed it.
|
|
169
|
+
const MAX_RESERVED_START_SPACE = 255;
|
|
166
170
|
export let lastValueEncoding: Buffer | undefined;
|
|
167
171
|
let timestampNextEncoding = 0,
|
|
168
172
|
metadataInNextEncoding = -1,
|
|
@@ -355,6 +359,7 @@ export class RecordEncoder extends StructonEncoder {
|
|
|
355
359
|
const residencyId = residencyIdAtNextEncoding;
|
|
356
360
|
const nodeId = nodeIdAtNextEncoding;
|
|
357
361
|
const additionalAuditRefs = additionalAuditRefsNextEncoding;
|
|
362
|
+
let auditRefsToEncode = 0;
|
|
358
363
|
if (metadata >= 0) {
|
|
359
364
|
valueStart += 4; // make room for metadata bytes
|
|
360
365
|
metadataInNextEncoding = -1; // reset indicator to mean no metadata
|
|
@@ -380,7 +385,18 @@ export class RecordEncoder extends StructonEncoder {
|
|
|
380
385
|
}
|
|
381
386
|
}
|
|
382
387
|
if (additionalAuditRefs && additionalAuditRefs.length > 0) {
|
|
383
|
-
|
|
388
|
+
// `valueStart` is OR'd into msgpackr's encode options below, where the reserved-space field
|
|
389
|
+
// is the LOW BYTE (RESERVE_START_SPACE): a valueStart over 255 wraps, so the record is
|
|
390
|
+
// written with a prefix neither side agrees on and decodes as garbage. The persisted ref
|
|
391
|
+
// count is one byte too. Refs are best-effort — a later in-order write drops them outright —
|
|
392
|
+
// so over budget the MIDDLE is dropped: the first entry is the addressable audit head when
|
|
393
|
+
// the record and log clocks diverge, and the last is the identity this write is matched on
|
|
394
|
+
// by the re-delivery guard, so neither end may go. (harper#2642)
|
|
395
|
+
auditRefsToEncode = Math.min(
|
|
396
|
+
additionalAuditRefs.length,
|
|
397
|
+
Math.floor((MAX_RESERVED_START_SPACE - valueStart - 1) / 12)
|
|
398
|
+
);
|
|
399
|
+
valueStart += 1 + auditRefsToEncode * 12; // 1 byte for count + 8 bytes version + 4 bytes nodeId per ref
|
|
384
400
|
additionalAuditRefsNextEncoding = undefined;
|
|
385
401
|
}
|
|
386
402
|
}
|
|
@@ -402,7 +418,7 @@ export class RecordEncoder extends StructonEncoder {
|
|
|
402
418
|
position += 8;
|
|
403
419
|
}
|
|
404
420
|
if (blobsWereEncoded) metadata |= HAS_BLOBS;
|
|
405
|
-
if (
|
|
421
|
+
if (auditRefsToEncode > 0) metadata |= HAS_ADDITIONAL_AUDIT_REFS;
|
|
406
422
|
if (metadata >= 0) {
|
|
407
423
|
dataView.setUint32(position, metadata | (ACTION_32_BIT << 24)); // use the extended action byte
|
|
408
424
|
position += 4;
|
|
@@ -418,9 +434,15 @@ export class RecordEncoder extends StructonEncoder {
|
|
|
418
434
|
dataView.setUint32(position, nodeId);
|
|
419
435
|
position += 4;
|
|
420
436
|
}
|
|
421
|
-
if (
|
|
422
|
-
encoded[position++] =
|
|
423
|
-
|
|
437
|
+
if (auditRefsToEncode > 0) {
|
|
438
|
+
encoded[position++] = auditRefsToEncode;
|
|
439
|
+
// Written as [0, …tail]: the dropped window is the middle, and it is empty when nothing
|
|
440
|
+
// had to be dropped.
|
|
441
|
+
const dropFrom = 1;
|
|
442
|
+
const dropTo = additionalAuditRefs.length - auditRefsToEncode + 1;
|
|
443
|
+
for (let index = 0; index < additionalAuditRefs.length; index++) {
|
|
444
|
+
if (index >= dropFrom && index < dropTo) continue;
|
|
445
|
+
const ref = additionalAuditRefs[index];
|
|
424
446
|
dataView.setFloat64(position, ref.version);
|
|
425
447
|
position += 8;
|
|
426
448
|
dataView.setUint32(position, ref.nodeId);
|
package/resources/Table.ts
CHANGED
|
@@ -108,6 +108,7 @@ import {
|
|
|
108
108
|
LOCAL_ONLY,
|
|
109
109
|
auditRetention,
|
|
110
110
|
removeAuditEntry,
|
|
111
|
+
getAuditFloor,
|
|
111
112
|
raiseAuditFloor,
|
|
112
113
|
boundedAuditPruneEnd,
|
|
113
114
|
isLockControlType,
|
|
@@ -135,7 +136,7 @@ import {
|
|
|
135
136
|
storedFieldsOnly,
|
|
136
137
|
} from './RecordEncoder.ts';
|
|
137
138
|
import { recordAction, recordActionBinary } from './analytics/write.ts';
|
|
138
|
-
import { rebuildUpdateBefore } from './crdt.ts';
|
|
139
|
+
import { commutativeOpsOf, rebuildUpdateBefore } from './crdt.ts';
|
|
139
140
|
import { appendHeader } from '../server/serverHelpers/Headers.ts';
|
|
140
141
|
import fs from 'node:fs';
|
|
141
142
|
import { Blob, deleteBlobsInObject, findBlobsInObject, startPreCommitBlobsForRecord } from './blob.ts';
|
|
@@ -3642,6 +3643,8 @@ export function makeTable(options) {
|
|
|
3642
3643
|
// existing timestamp, which means that we received updates out of order, and must resequence the application
|
|
3643
3644
|
// of the updates to the record to ensure consistency across the cluster
|
|
3644
3645
|
// TODO: can the previous version be older, but even more previous version be newer?
|
|
3646
|
+
let belowAuditFloor = false;
|
|
3647
|
+
let dedupVersionCouldBeRetained: (version: number) => boolean;
|
|
3645
3648
|
if (audit) {
|
|
3646
3649
|
// A re-delivered out-of-order write (full-copy audit-replay re-delivers writes) must not have
|
|
3647
3650
|
// its commutative ops re-folded. additionalAuditRefs is the record's own list of folded
|
|
@@ -3687,7 +3690,7 @@ export function makeTable(options) {
|
|
|
3687
3690
|
// Resolve the oldest retained entry once, for the same log the dedup reads.
|
|
3688
3691
|
let oldestRetainedAuditTime: number | undefined;
|
|
3689
3692
|
let oldestRetainedAuditTimeResolved = false;
|
|
3690
|
-
|
|
3693
|
+
dedupVersionCouldBeRetained = (version: number): boolean => {
|
|
3691
3694
|
if (!isRocksDB) return true; // LMDB keeps its exact, unbounded lookup (keyed by local audit time)
|
|
3692
3695
|
if (!oldestRetainedAuditTimeResolved) {
|
|
3693
3696
|
oldestRetainedAuditTimeResolved = true;
|
|
@@ -3734,6 +3737,19 @@ export function makeTable(options) {
|
|
|
3734
3737
|
return; // duplicate already applied; avoid the resequencing walk
|
|
3735
3738
|
}
|
|
3736
3739
|
}
|
|
3740
|
+
// The walk terminates at this write only by reaching an audit entry whose log key is at or below
|
|
3741
|
+
// txnTime (the loop condition below), so below the floor it cannot: it runs the whole retained
|
|
3742
|
+
// chain to an outcome the floor already determines (harper#2642). txnTime is the coordinate
|
|
3743
|
+
// because it is the one that loop compares; txnLogKey addresses this write's own entry, a
|
|
3744
|
+
// different question that dedupVersionCouldBeRetained already answers. An unknown floor is
|
|
3745
|
+
// Infinity, which fails closed for a cursor check but has to fail OPEN here — walk rather than
|
|
3746
|
+
// discard. RocksDB only: LMDB keeps its exact, unbounded reconciliation.
|
|
3747
|
+
if (isRocksDB && precedesExisting < 0) {
|
|
3748
|
+
const auditFloor = getAuditFloor(auditStore);
|
|
3749
|
+
belowAuditFloor = Number.isFinite(auditFloor) && txnTime < auditFloor;
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
if (audit && !belowAuditFloor) {
|
|
3737
3753
|
// incremental CRDT updates are only available with audit logging on
|
|
3738
3754
|
const initialAuditHead = isRocksDB
|
|
3739
3755
|
? resolveAuditHead(id, existingEntry.version, existingEntry.nodeId, existingEntry.additionalAuditRefs)
|
|
@@ -3751,7 +3767,12 @@ export function makeTable(options) {
|
|
|
3751
3767
|
new Date(localTime)
|
|
3752
3768
|
);
|
|
3753
3769
|
|
|
3754
|
-
|
|
3770
|
+
// Normalized here and not at the lookup, because the two sources of an undefined nodeId do not
|
|
3771
|
+
// mean the same thing: a record's own nodeId is resolved by the same expression as the id its
|
|
3772
|
+
// audit entry is logged under, which applies `?? 0` (RecordEncoder), so absent means log 0;
|
|
3773
|
+
// `previousNodeId` is never encoded, so absent there means the log is unknown and only the
|
|
3774
|
+
// aggregate lookup can resolve a cross-origin predecessor.
|
|
3775
|
+
let nodeId = initialAuditHead.nodeId ?? 0;
|
|
3755
3776
|
const succeedingUpdates = []; // record the "future" updates, as we need to apply the updates in reverse order
|
|
3756
3777
|
const auditRefsToVisit: Array<{ localTime: number; nodeId: number }> = existingEntry.additionalAuditRefs
|
|
3757
3778
|
? existingEntry.additionalAuditRefs.map((ref) => ({ localTime: ref.version, nodeId: ref.nodeId }))
|
|
@@ -4015,6 +4036,31 @@ export function makeTable(options) {
|
|
|
4015
4036
|
write.skipped = true;
|
|
4016
4037
|
return;
|
|
4017
4038
|
}
|
|
4039
|
+
} else if (belowAuditFloor) {
|
|
4040
|
+
// The walk cannot reach this write, so it may contribute only what is order-independent: its
|
|
4041
|
+
// commutative ops. Whether a plain field survives depends on what newer writes did to that key,
|
|
4042
|
+
// which is what the purged history no longer answers, so applying one would resurrect state a
|
|
4043
|
+
// newer put may have erased. Everything else loses to the strictly newer head: a full update, a
|
|
4044
|
+
// head carrying no record (a delete or a residency-omitted record, which an op must not
|
|
4045
|
+
// resurrect), and an op-less patch. Bare return, no writeCommit, so no audit record references
|
|
4046
|
+
// the losing update's pre-saved blobs.
|
|
4047
|
+
incrementalUpdateToApply = fullUpdate || existingRecord == null ? null : commutativeOpsOf(recordUpdate);
|
|
4048
|
+
if (!incrementalUpdateToApply) {
|
|
4049
|
+
write.skipped = true;
|
|
4050
|
+
return;
|
|
4051
|
+
}
|
|
4052
|
+
// The surviving head's addressable log-key pointer lives in these, wherever the record and log
|
|
4053
|
+
// clocks differ.
|
|
4054
|
+
if (existingEntry.additionalAuditRefs) {
|
|
4055
|
+
for (const ref of existingEntry.additionalAuditRefs) {
|
|
4056
|
+
additionalAuditRefs.push(ref);
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
// Once the walk no longer runs, this ref is what the read-your-writes check above matches a
|
|
4060
|
+
// re-delivery of these ops on. Best-effort, like every other guard here: the encoder bounds
|
|
4061
|
+
// the persisted list, so an identity can age out of it (harper#1148's full-copy convergence
|
|
4062
|
+
// is the backstop).
|
|
4063
|
+
additionalAuditRefs.push({ version: txnLogKey, nodeId: options?.nodeId });
|
|
4018
4064
|
} else if (fullUpdate) {
|
|
4019
4065
|
// if no audit, we can't accurately do incremental updates, so we just assume the last update
|
|
4020
4066
|
// was the same type. Assuming a full update this record update loses and there are no changes —
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# resources/analytics/ — Design notes
|
|
2
|
+
|
|
3
|
+
Analytics recording and aggregation.
|
|
4
|
+
|
|
5
|
+
**Read this when:** touching `write.ts` aggregation or the raw-analytics cursor.
|
|
6
|
+
|
|
7
|
+
Index of every design note: [DESIGN.md](../../DESIGN.md).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Analytics aggregation resumes from a raw cursor, which is not a clock (`resources/analytics/write.ts`)
|
|
12
|
+
|
|
13
|
+
`aggregation()` rolls up the raw reports in `hdb_raw_analytics` one `toPeriod` window at a time —
|
|
14
|
+
it stops at `firstForPeriod + toPeriod` so one cycle's main-thread msgpackr decode stays bounded
|
|
15
|
+
(#1538) — and resumes, exclusive, from `rawCursor`. Two rules hold that cursor:
|
|
16
|
+
|
|
17
|
+
- **It only ever moves to a record a cycle actually read.** It used to be one variable with the
|
|
18
|
+
cadence marker, stamped with the cycle's end-of-run `Date.now()`, which discarded the remainder of
|
|
19
|
+
a backlog longer than one window and every report written while the cycle ran, because nothing
|
|
20
|
+
reads below the cursor again.
|
|
21
|
+
- **A cycle that reads nothing leaves it where it is.** An empty scan is not proof that nothing is
|
|
22
|
+
there: `recordAnalytics` does not await its `primaryStore.put`, so a report can be uncommitted and
|
|
23
|
+
invisible to the scan while carrying a key older than anything the cycle could compare against. No
|
|
24
|
+
wall-clock reading is safe to put in the cursor for the same reason.
|
|
25
|
+
|
|
26
|
+
`lastAggregationTime` stays a pure cadence marker, stamped `now` at the end of every completed
|
|
27
|
+
cycle, so an idle node still aggregates once per period (#1538). A cycle that stopped at its window
|
|
28
|
+
edge sets `aggregationBehind`, which lets the next tick skip that guard: a backlog then drains one
|
|
29
|
+
window per half-period tick instead of waiting out the cadence it is already behind on.
|
|
30
|
+
|
|
31
|
+
Cycles must not overlap. `setInterval` does not await its async callback, so `runAggregationCycle`
|
|
32
|
+
holds a single-flight flag — two cycles reading the same cursor roll the same window up twice and
|
|
33
|
+
double every count in it.
|
|
34
|
+
|
|
35
|
+
Still open: `findLastAggregationTime` seeds both markers after a restart from the newest stored
|
|
36
|
+
analytics record, whose `time` is an end-of-cycle stamp, so a restart mid-drain can skip the rest of
|
|
37
|
+
the backlog; and `storeMetric` discards `table.put()`'s result, so the cursor advances past raw
|
|
38
|
+
records whose aggregate rows failed to commit. Both need a cursor persisted with the rows.
|
|
@@ -877,6 +877,12 @@ async function storeNodeStorageMetric(analyticsTable: Table) {
|
|
|
877
877
|
const MAX_LAST_AGGREGATION_SCAN = 1000;
|
|
878
878
|
// Time of this node's last completed aggregation, tracked in O(1) across cycles within a process.
|
|
879
879
|
let lastAggregationTime: number | undefined;
|
|
880
|
+
// Resume point into the raw analytics table: it moves only over records a cycle read, so it is not
|
|
881
|
+
// a clock and never passes an unread report.
|
|
882
|
+
let rawCursor: number | undefined;
|
|
883
|
+
// A cycle stopped at its window edge with raw records still behind it, so the next one skips the
|
|
884
|
+
// cadence guard and drains the next window.
|
|
885
|
+
let aggregationBehind = false;
|
|
880
886
|
|
|
881
887
|
/**
|
|
882
888
|
* Find the time of the local node's most recent aggregation record by scanning back from the
|
|
@@ -901,6 +907,20 @@ export function findLastAggregationTime(
|
|
|
901
907
|
return undefined;
|
|
902
908
|
}
|
|
903
909
|
|
|
910
|
+
let aggregationRunning = false;
|
|
911
|
+
|
|
912
|
+
/** Two cycles reading the same cursor roll the same window up twice, and the scheduler does not
|
|
913
|
+
* await its callback. */
|
|
914
|
+
export async function runAggregationCycle(fromPeriod, toPeriod = 60000) {
|
|
915
|
+
if (aggregationRunning) return;
|
|
916
|
+
aggregationRunning = true;
|
|
917
|
+
try {
|
|
918
|
+
await aggregation(fromPeriod, toPeriod);
|
|
919
|
+
} finally {
|
|
920
|
+
aggregationRunning = false;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
904
924
|
async function aggregation(fromPeriod, toPeriod = 60000) {
|
|
905
925
|
const rawAnalyticsTable = getRawAnalyticsTable();
|
|
906
926
|
const analyticsTable = getAnalyticsTable();
|
|
@@ -924,28 +944,35 @@ async function aggregation(fromPeriod, toPeriod = 60000) {
|
|
|
924
944
|
// first never touch the table. On the first cycle after boot we seed it once from the table
|
|
925
945
|
// via a bounded reverse scan (see findLastAggregationTime). Caching the seeded value keeps
|
|
926
946
|
// the next cycle O(1) even when this run early-returns below as "too recent"; a bound-hit
|
|
927
|
-
// (no match) leaves it undefined so we don't early-return, proceed to aggregate, and
|
|
928
|
-
// marker
|
|
947
|
+
// (no match) leaves it undefined so we don't early-return, proceed to aggregate, and stamp the
|
|
948
|
+
// cadence marker at the end of the cycle instead (#1538).
|
|
929
949
|
let lastForPeriod = lastAggregationTime;
|
|
930
950
|
if (lastForPeriod === undefined) {
|
|
931
951
|
lastForPeriod = findLastAggregationTime(analyticsTable.primaryStore, localNodeId);
|
|
932
|
-
if (lastForPeriod !== undefined)
|
|
952
|
+
if (lastForPeriod !== undefined) {
|
|
953
|
+
lastAggregationTime = lastForPeriod;
|
|
954
|
+
rawCursor ??= lastForPeriod;
|
|
955
|
+
}
|
|
933
956
|
}
|
|
934
957
|
// was the last aggregation too recent to calculate a whole period?
|
|
935
|
-
if (lastForPeriod !== undefined && Date.now() - toPeriod < lastForPeriod) return;
|
|
958
|
+
if (!aggregationBehind && lastForPeriod !== undefined && Date.now() - toPeriod < lastForPeriod) return;
|
|
936
959
|
let firstForPeriod;
|
|
937
960
|
const aggregateActions = new Map();
|
|
938
961
|
const distributions = new Map();
|
|
939
962
|
const threadsToAverage = [];
|
|
940
|
-
let lastTime: number;
|
|
963
|
+
let lastTime: number | undefined;
|
|
964
|
+
let stoppedAtWindowEdge = false;
|
|
941
965
|
for (const { key, value } of rawAnalyticsTable.primaryStore.getRange({
|
|
942
|
-
start:
|
|
966
|
+
start: rawCursor || false,
|
|
943
967
|
exclusiveStart: true,
|
|
944
968
|
end: Infinity,
|
|
945
969
|
})) {
|
|
946
970
|
if (!value) continue;
|
|
947
971
|
if (firstForPeriod) {
|
|
948
|
-
if (key > firstForPeriod + toPeriod)
|
|
972
|
+
if (key > firstForPeriod + toPeriod) {
|
|
973
|
+
stoppedAtWindowEdge = true;
|
|
974
|
+
break; // outside the period of interest
|
|
975
|
+
}
|
|
949
976
|
} else firstForPeriod = key;
|
|
950
977
|
lastTime = key;
|
|
951
978
|
const { metrics, threadId } = value;
|
|
@@ -1091,12 +1118,11 @@ async function aggregation(fromPeriod, toPeriod = 60000) {
|
|
|
1091
1118
|
};
|
|
1092
1119
|
storeMetric(analyticsTable, cruMetric);
|
|
1093
1120
|
lastResourceUsage = resourceUsage;
|
|
1094
|
-
//
|
|
1095
|
-
//
|
|
1096
|
-
//
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
// scan-based behavior rather than letting a stale marker run aggregation on every tick (#1538).
|
|
1121
|
+
// The cursor may only pass records this cycle read, and nothing reads below it again: the scan
|
|
1122
|
+
// covers one `toPeriod` window, and a raw report whose `put` has not committed is invisible to
|
|
1123
|
+
// it however recent its key is. An empty scan therefore leaves the cursor where it was.
|
|
1124
|
+
if (lastTime !== undefined) rawCursor = lastTime;
|
|
1125
|
+
aggregationBehind = stoppedAtWindowEdge;
|
|
1100
1126
|
lastAggregationTime = now;
|
|
1101
1127
|
|
|
1102
1128
|
// `system` is set as non-enumerable on the object returned by getDatabases() so most
|
|
@@ -1208,7 +1234,7 @@ function startScheduledTasks() {
|
|
|
1208
1234
|
const aggregateRetentionMs = envGet(CONFIG_PARAMS.ANALYTICS_AGGREGATERETENTIONMS) ?? AGGREGATE_EXPIRATION;
|
|
1209
1235
|
setInterval(
|
|
1210
1236
|
async () => {
|
|
1211
|
-
await
|
|
1237
|
+
await runAggregationCycle(analyticsDelay, AGGREGATE_PERIOD);
|
|
1212
1238
|
await cleanup(getRawAnalyticsTable(), rawRetentionMs);
|
|
1213
1239
|
// 0 means "keep forever" — skip aggregate cleanup, matching storageInterval: 0 convention
|
|
1214
1240
|
if (aggregateRetentionMs) await cleanup(getAnalyticsTable(), aggregateRetentionMs);
|
package/resources/crdt.ts
CHANGED
|
@@ -74,6 +74,28 @@ export function rebuildUpdateBefore(update: any, newerUpdate: any, fullUpdate?:
|
|
|
74
74
|
}
|
|
75
75
|
return newUpdate;
|
|
76
76
|
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The commutative operations in an out-of-order `update` — the only part of it whose outcome does not
|
|
80
|
+
* depend on where it lands in the write order. Used when the audit history the update would have been
|
|
81
|
+
* resequenced against is no longer retained.
|
|
82
|
+
*
|
|
83
|
+
* Not `rebuildUpdateBefore`, which folds an update against another UPDATE: against a materialized
|
|
84
|
+
* record its plain-newer-value rule drops the incoming `add` on any key the record already has a
|
|
85
|
+
* value for.
|
|
86
|
+
*/
|
|
87
|
+
export function commutativeOpsOf(update: any) {
|
|
88
|
+
let ops = null;
|
|
89
|
+
for (const key in update) {
|
|
90
|
+
const value = update[key];
|
|
91
|
+
if (value?.__op__) {
|
|
92
|
+
if (!ops) ops = {};
|
|
93
|
+
ops[key] = value;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return ops;
|
|
97
|
+
}
|
|
98
|
+
|
|
77
99
|
export function applyReverse(record, update, unknowns: Set<string>) {
|
|
78
100
|
for (const key in update) {
|
|
79
101
|
const value = update[key];
|
package/resources/databases.ts
CHANGED
|
@@ -1014,7 +1014,7 @@ function initStores(
|
|
|
1014
1014
|
{ defaultTable, auditPath, isLegacy, destination, storeName, openedStores }: InitStoresOptions = {}
|
|
1015
1015
|
) {
|
|
1016
1016
|
// a store with no tables never reaches the per-table loop below, and blob roots resolve from this
|
|
1017
|
-
rootStore.databaseName
|
|
1017
|
+
rootStore.databaseName ??= storeName ?? databaseName;
|
|
1018
1018
|
const envInit = new OpenEnvironmentObject(path, isReadOnlyMode());
|
|
1019
1019
|
const internalDbiInit = createOpenDBIObject(false);
|
|
1020
1020
|
let attributesDbi = rootStore.dbisDb;
|