@harperfast/harper-pro 5.0.25 → 5.0.26
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/core/package-lock.json +38 -38
- package/core/resources/RocksTransactionLogStore.ts +44 -26
- package/core/resources/Table.ts +60 -11
- package/core/resources/auditStore.ts +13 -0
- package/core/resources/replayLogs.ts +20 -0
- package/dist/core/resources/RocksTransactionLogStore.js +44 -26
- package/dist/core/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/core/resources/Table.js +52 -6
- package/dist/core/resources/Table.js.map +1 -1
- package/dist/core/resources/auditStore.js +13 -0
- package/dist/core/resources/auditStore.js.map +1 -1
- package/dist/core/resources/replayLogs.js +19 -0
- package/dist/core/resources/replayLogs.js.map +1 -1
- package/npm-shrinkwrap.json +38 -38
- package/package.json +2 -2
- package/studio/web/assets/{index-CmtPP0YO.js → index-tpQxqcfB.js} +5 -5
- package/studio/web/assets/{index-CmtPP0YO.js.map → index-tpQxqcfB.js.map} +1 -1
- package/studio/web/assets/{index.lazy-C8jvGtlu.js → index.lazy-Cr82BZoJ.js} +2 -2
- package/studio/web/assets/{index.lazy-C8jvGtlu.js.map → index.lazy-Cr82BZoJ.js.map} +1 -1
- package/studio/web/assets/{profile-O0DYlJUv.js → profile-CfwQpiKM.js} +2 -2
- package/studio/web/assets/{profile-O0DYlJUv.js.map → profile-CfwQpiKM.js.map} +1 -1
- package/studio/web/assets/{status-BIlJkJby.js → status-fDIUHt_X.js} +2 -2
- package/studio/web/assets/{status-BIlJkJby.js.map → status-fDIUHt_X.js.map} +1 -1
- package/studio/web/index.html +1 -1
|
@@ -94,6 +94,11 @@ const TEST_WRITE_KEY_BUFFER = Buffer.allocUnsafeSlow(8192);
|
|
|
94
94
|
const MAX_KEY_BYTES = 1978;
|
|
95
95
|
const EVENT_HIGH_WATER_MARK = 100;
|
|
96
96
|
const REPLAY_YIELD_INTERVAL = 100; // yield to the event loop every N records during subscription replay
|
|
97
|
+
// Cap for the out-of-order write reconciliation audit-chain walk in commit(). A pathologically deep
|
|
98
|
+
// audit history (e.g. a replication full-copy of a large-history database) would otherwise walk and
|
|
99
|
+
// buffer the entire backward chain per record, synchronously, on every worker — pinning the JS heap
|
|
100
|
+
// until the worker OOMs (issue #1114). Beyond this depth we fall back to a bounded reconciliation.
|
|
101
|
+
const MAX_OUT_OF_ORDER_AUDIT_DEPTH = 1000;
|
|
97
102
|
const FULL_PERMISSIONS = {
|
|
98
103
|
read: true,
|
|
99
104
|
insert: true,
|
|
@@ -1620,8 +1625,18 @@ function makeTable(options) {
|
|
|
1620
1625
|
}
|
|
1621
1626
|
let addedAuditRef = false;
|
|
1622
1627
|
let nextRef;
|
|
1628
|
+
let walkSteps = 0;
|
|
1629
|
+
let auditWalkCapped = false;
|
|
1623
1630
|
do {
|
|
1624
1631
|
while (localTime > txnTime || (auditedVersion >= txnTime && localTime > 0)) {
|
|
1632
|
+
// Bound the walk only for RocksDB, where the OOM was observed (issue #1114): each step
|
|
1633
|
+
// is a transaction-log range scan + msgpackr decode, and the per-node logs can be huge.
|
|
1634
|
+
// LMDB audit entries are keyed by local audit time (not version), so the duplicate
|
|
1635
|
+
// shortcut below would not apply — keep its exact, unbounded reconciliation.
|
|
1636
|
+
if (isRocksDB && ++walkSteps > MAX_OUT_OF_ORDER_AUDIT_DEPTH) {
|
|
1637
|
+
auditWalkCapped = true;
|
|
1638
|
+
break;
|
|
1639
|
+
}
|
|
1625
1640
|
const auditRecord = auditStore.get(localTime, tableId, id, nodeId);
|
|
1626
1641
|
if (!auditRecord)
|
|
1627
1642
|
break;
|
|
@@ -1643,8 +1658,11 @@ function makeTable(options) {
|
|
|
1643
1658
|
}
|
|
1644
1659
|
if (auditRecord.type === 'patch') {
|
|
1645
1660
|
logger_ts_1.logger.debug?.('out of order patch will be applied', id, auditRecord);
|
|
1646
|
-
//
|
|
1647
|
-
|
|
1661
|
+
// Materialize the patch value now and keep only { version, value } rather than the
|
|
1662
|
+
// audit record itself, so its backing transaction-log buffer and decoders can be
|
|
1663
|
+
// reclaimed immediately. Only these two fields are needed for the ordered fold below;
|
|
1664
|
+
// retaining the full records is what pins the heap on a deep chain (issue #1114).
|
|
1665
|
+
succeedingUpdates.push({ version: auditedVersion, value: auditRecord.getValue(primaryStore) });
|
|
1648
1666
|
auditRecordToStore = recordUpdate; // use the original update for the audit record
|
|
1649
1667
|
}
|
|
1650
1668
|
else if (auditRecord.type === 'put' || auditRecord.type === 'delete') {
|
|
@@ -1676,6 +1694,8 @@ function makeTable(options) {
|
|
|
1676
1694
|
nodeId = auditRecord.previousNodeId;
|
|
1677
1695
|
}
|
|
1678
1696
|
// Check if we need to scan additional audit refs from this record
|
|
1697
|
+
if (auditWalkCapped)
|
|
1698
|
+
break;
|
|
1679
1699
|
nextRef = auditRefsToVisit.shift();
|
|
1680
1700
|
if (nextRef) {
|
|
1681
1701
|
localTime = auditedVersion = nextRef.localTime;
|
|
@@ -1683,14 +1703,40 @@ function makeTable(options) {
|
|
|
1683
1703
|
logger_ts_1.logger.debug?.('Following additional audit ref to continue scanning', { localTime, nodeId });
|
|
1684
1704
|
}
|
|
1685
1705
|
} while (nextRef);
|
|
1686
|
-
if (!localTime) {
|
|
1706
|
+
if (!localTime && !auditWalkCapped) {
|
|
1687
1707
|
// if we reached the end of the audit trail, we can just apply the update
|
|
1688
1708
|
logger_ts_1.logger.debug?.('No further audit history, applying incremental updates based on available history', id, 'existing version preserved', existingEntry);
|
|
1689
1709
|
}
|
|
1710
|
+
if (auditWalkCapped) {
|
|
1711
|
+
// The out-of-order audit chain exceeded MAX_OUT_OF_ORDER_AUDIT_DEPTH (a pathologically deep
|
|
1712
|
+
// history, seen during a replication full-copy of a large-history database — issue #1114).
|
|
1713
|
+
// Walking and buffering the whole chain per record OOMs the worker, so we stopped at the cap
|
|
1714
|
+
// and reconcile against only the most recent MAX_OUT_OF_ORDER_AUDIT_DEPTH updates (the fold
|
|
1715
|
+
// below). That is an approximation for histories deeper than the cap — updates older than the
|
|
1716
|
+
// retained window are not layered in — but the authoritative full-copy record restores exact
|
|
1717
|
+
// convergence. Because we stopped before reaching txnTime, the inline duplicate detection in
|
|
1718
|
+
// the walk never ran; full-copy audit-replay re-delivers writes, and re-applying one would
|
|
1719
|
+
// double-apply its commutative ops, so rule that out here with a single O(1) lookup at txnTime
|
|
1720
|
+
// (RocksDB audit logs are keyed by version, and the cap is RocksDB-only).
|
|
1721
|
+
logger_ts_1.logger.warn?.('Out-of-order audit reconciliation exceeded depth cap; reconciling against most recent updates only', {
|
|
1722
|
+
table: tableName,
|
|
1723
|
+
id,
|
|
1724
|
+
depth: walkSteps,
|
|
1725
|
+
});
|
|
1726
|
+
const duplicate = auditStore.get(txnTime, tableId, id, options?.nodeId);
|
|
1727
|
+
if (duplicate &&
|
|
1728
|
+
duplicate.version === txnTime &&
|
|
1729
|
+
precedesExistingVersion(txnTime, { version: txnTime, localTime: txnTime, key: id, nodeId: duplicate.nodeId }, options?.nodeId) === 0) {
|
|
1730
|
+
write.skipped = true;
|
|
1731
|
+
return; // duplicate write already applied
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
// Fold the retained succeeding updates (the full chain, or — when capped — the most recent
|
|
1735
|
+
// window) onto this older write so newer fields win; for a capped walk this layers in only
|
|
1736
|
+
// what we collected before the cap.
|
|
1690
1737
|
succeedingUpdates.sort((a, b) => a.version - b.version); // order the patches
|
|
1691
|
-
for (const
|
|
1692
|
-
|
|
1693
|
-
logger_ts_1.logger.debug?.('Rebuilding update with future patch:', new Date(auditRecord.version), newerUpdate, auditRecord);
|
|
1738
|
+
for (const { version: patchVersion, value: newerUpdate } of succeedingUpdates) {
|
|
1739
|
+
logger_ts_1.logger.debug?.('Rebuilding update with future patch:', new Date(patchVersion), newerUpdate);
|
|
1694
1740
|
incrementalUpdateToApply = (0, crdt_ts_1.rebuildUpdateBefore)(incrementalUpdateToApply ?? recordUpdate, newerUpdate, fullUpdate);
|
|
1695
1741
|
if (!incrementalUpdateToApply)
|
|
1696
1742
|
return writeCommit(false); // if all changes are overwritten, nothing left to do
|