@harperfast/harper 5.2.9 → 5.2.12
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/resources/DatabaseTransaction.d.ts +3 -0
- package/dist/resources/DatabaseTransaction.js +90 -2
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.js +2 -1
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/RocksIndexStore.js +2 -1
- package/dist/resources/RocksIndexStore.js.map +1 -1
- package/dist/resources/Table.js +13 -5
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/auditStore.js +142 -53
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/databases.d.ts +10 -0
- package/dist/resources/databases.js +369 -97
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/search.js +33 -7
- package/dist/resources/search.js.map +1 -1
- package/dist/server/storageReclamation.d.ts +7 -0
- package/dist/server/storageReclamation.js +10 -0
- package/dist/server/storageReclamation.js.map +1 -1
- package/dist/server/threads/manageThreads.d.ts +1 -0
- package/dist/server/threads/manageThreads.js +7 -0
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/npm-shrinkwrap.json +41 -41
- package/package.json +2 -2
- package/resources/DatabaseTransaction.ts +88 -2
- package/resources/PrimaryRocksDatabase.ts +2 -1
- package/resources/RocksIndexStore.ts +2 -1
- package/resources/Table.ts +13 -5
- package/resources/auditStore.ts +144 -52
- package/resources/databases.ts +351 -93
- package/resources/search.ts +32 -7
- package/server/storageReclamation.ts +10 -0
- package/server/threads/manageThreads.js +7 -0
- package/studio/web/assets/Chat-CokJOjyX.js +2267 -0
- package/studio/web/assets/FloatingChat-h9BJo6YG.js +23 -0
- package/studio/web/assets/{abnfDiagram-VCTEODGH-C0_BAZyO.js → abnfDiagram-VCTEODGH-B0BebmD2.js} +1 -1
- package/studio/web/assets/{alertDialog-DIHt7Z0r.js → alertDialog-CQyAJJhl.js} +1 -1
- package/studio/web/assets/{apiToken-c3Rd-w6g.js → apiToken-B_g12QrM.js} +1 -1
- package/studio/web/assets/applications-Dlq5kJ2O.js +296 -0
- package/studio/web/assets/architecture-7GRP2DOG-LB-MLAAb.js +1 -0
- package/studio/web/assets/{architectureDiagram-5GKGNRK7-BWzrASgm.js → architectureDiagram-5GKGNRK7-7SW3GD-K.js} +1 -1
- package/studio/web/assets/authStore-C3Nfubqr.js +3 -0
- package/studio/web/assets/{blockDiagram-NRAW4CY4-BdJX9Khj.js → blockDiagram-I7D4REHJ-BqguiadH.js} +2 -2
- package/studio/web/assets/{button-DhiX-njv.js → button-BIsUKRZq.js} +2 -2
- package/studio/web/assets/{c4Diagram-UCG6FXSJ-CI6MzGmQ.js → c4Diagram-7LVT6UL2-LBNf8t_X.js} +1 -1
- package/studio/web/assets/channel-yictG-U-.js +1 -0
- package/studio/web/assets/{chevron-up-Df2c8uoU.js → chevron-up-DtKGqDn3.js} +1 -1
- package/studio/web/assets/{chunk-TEH6E4GO-P87k5mNi.js → chunk-4HAMMTFA-DWtTut21.js} +1 -1
- package/studio/web/assets/{chunk-75Z2AOVW-BT8tVmks.js → chunk-75Z2AOVW-QGQD6th2.js} +1 -1
- package/studio/web/assets/{chunk-DU6HZSFF-9kAOOmI6.js → chunk-DU6HZSFF-Chq20Ba5.js} +1 -1
- package/studio/web/assets/{chunk-F27PBJKO-BW7ao8AY.js → chunk-F27PBJKO-BVA5EPhV.js} +1 -1
- package/studio/web/assets/{chunk-GMAD6QVW-BNyXpoQO.js → chunk-GMAD6QVW-BeS7S07A.js} +1 -1
- package/studio/web/assets/{chunk-OBVCFTLP-D4wWOqDQ.js → chunk-GVQU2GXP-sbwVIQ8i.js} +1 -1
- package/studio/web/assets/{chunk-G27WJ6UU-COyLMcgK.js → chunk-IMKFNOWR-Bnh3tAVd.js} +1 -1
- package/studio/web/assets/{chunk-JQ64N6SF-Cyz1IeLf.js → chunk-L3NEJ4N5-COfUyKII.js} +1 -1
- package/studio/web/assets/chunk-OSK3NFVY-ByciRftO.js +10 -0
- package/studio/web/assets/{chunk-P2QGCYS3-DmIFY4d7.js → chunk-P2QGCYS3-CP1VhG_c.js} +1 -1
- package/studio/web/assets/{chunk-POPQ4Y6H-BPrvMyKz.js → chunk-POPQ4Y6H-ClWhhkwW.js} +1 -1
- package/studio/web/assets/{chunk-PWAF6VOD-2zB6IW9i.js → chunk-PWAF6VOD-1z1THyS5.js} +1 -1
- package/studio/web/assets/{chunk-RHFEMEQ7-2FgyI8YU.js → chunk-SHT3W25Y-LpQkMsah.js} +2 -2
- package/studio/web/assets/{chunk-SVP7TREG-FwtbH2QC.js → chunk-SVP7TREG-jtdAHw0S.js} +1 -1
- package/studio/web/assets/{chunk-LCL6LL3I-HOzK_ppE.js → chunk-TICWLB2K-VOwzetX-.js} +1 -1
- package/studio/web/assets/classDiagram-ZZMXUADV-VaEwSy_g.js +1 -0
- package/studio/web/assets/classDiagram-v2-VYDZK3BY-VaEwSy_g.js +1 -0
- package/studio/web/assets/{createLucideIcon-BKGPfjm2.js → createLucideIcon-CzW9508A.js} +1 -1
- package/studio/web/assets/{cssMode-CEN2mzSA.js → cssMode-DAjzPoBf.js} +1 -1
- package/studio/web/assets/{cynefin-OW5HDTMX-BRkpLFQV.js → cynefin-OW5HDTMX-BbdbCvub.js} +1 -1
- package/studio/web/assets/{cynefinDiagram-5FMLGOSQ-CHT1DaX6.js → cynefinDiagram-5FMLGOSQ-TP-aIqbt.js} +1 -1
- package/studio/web/assets/{dagre-3AP2YEHR-DpUXBh63.js → dagre-GXQ25YYZ-DShnGpGo.js} +1 -1
- package/studio/web/assets/{diagram-S7CK7UJ4-BuymVFZT.js → diagram-S7CK7UJ4-aoCVTtcy.js} +1 -1
- package/studio/web/assets/{diagram-UQ7AKVKN-CyP148RM.js → diagram-UQ7AKVKN-DglXtQ6x.js} +1 -1
- package/studio/web/assets/{diagram-VSXAHHWV-CoCAg3M9.js → diagram-VSXAHHWV-fhEdmkwM.js} +1 -1
- package/studio/web/assets/{diagram-VX7I27RA-BpOqCFca.js → diagram-VX7I27RA-DccVJet6.js} +1 -1
- package/studio/web/assets/{diagram-Z3DM3KII-Bfpw7Vbj.js → diagram-Z3DM3KII-D-RyJJb7.js} +1 -1
- package/studio/web/assets/{dialog-CBf0Mr1d.js → dialog-Cn2uWgD4.js} +1 -1
- package/studio/web/assets/{dist-lkA3O3eM.js → dist-DP8UjMB_.js} +1 -1
- package/studio/web/assets/{download-BtTOBem-.js → download-B5T5r7ss.js} +1 -1
- package/studio/web/assets/{ebnfDiagram-PWID7BFC-DS_6aWqL.js → ebnfDiagram-PWID7BFC-DJGpIpz_.js} +1 -1
- package/studio/web/assets/{editor-D8oDeCTL.js → editor-VUNQ-5Ye.js} +1 -1
- package/studio/web/assets/{erDiagram-SSCWMZ5O-DJNk6Fgw.js → erDiagram-RLTQ6QDP-CIfNlgkC.js} +1 -1
- package/studio/web/assets/eventmodeling-NTZA5JFV-CLxnp2CR.js +1 -0
- package/studio/web/assets/flowDiagram-HODETNUW-BIbhmz9f.js +1 -0
- package/studio/web/assets/{ganttDiagram-EL5Y4UJY-2pOExxMY.js → ganttDiagram-EL5Y4UJY-BxToTzzD.js} +1 -1
- package/studio/web/assets/{getAnalytics-D4LKGeVy.js → getAnalytics-GHK8ORfM.js} +1 -1
- package/studio/web/assets/{gitGraph-4MIJSDKK-CH5ZxwzF.js → gitGraph-4MIJSDKK-D2s2w8lE.js} +1 -1
- package/studio/web/assets/{gitGraphDiagram-WWUBYQGX-DVIsIhbO.js → gitGraphDiagram-WWUBYQGX-Dwntd4-x.js} +1 -1
- package/studio/web/assets/{html-u3vOg7LJ.js → html-CFqhtdfa.js} +1 -1
- package/studio/web/assets/{htmlMode-DyO31v-P.js → htmlMode-CpgOTfjz.js} +1 -1
- package/studio/web/assets/{index-Cxj2_wsl.css → index-7RMEgVG1.css} +1 -1
- package/studio/web/assets/index-ZQ1upnh7.js +824 -0
- package/studio/web/assets/index.lazy-HDafSRxx.js +2 -0
- package/studio/web/assets/{info-A6RAGUB7-CPQfTnaG.js → info-A6RAGUB7-DYjkvb0C.js} +1 -1
- package/studio/web/assets/{infoDiagram-RXCK75RN-DlwLYlwm.js → infoDiagram-27XIBGKW-Bnp1FJE5.js} +1 -1
- package/studio/web/assets/{ishikawaDiagram-5VMMS53U-BRXRp29U.js → ishikawaDiagram-5VMMS53U-D9Xh2r6X.js} +1 -1
- package/studio/web/assets/{javascript-CUvxOyTC.js → javascript-9l5JXJwc.js} +1 -1
- package/studio/web/assets/{journeyDiagram-EYS64GPL-B0ou8k0n.js → journeyDiagram-3NMN7TZE-CokIi6ll.js} +2 -2
- package/studio/web/assets/{jsonMode-f_IwbF3D.js → jsonMode-BJBH-iR0.js} +1 -1
- package/studio/web/assets/{kanban-definition-3QL26DDD-uYg7iYzp.js → kanban-definition-UXKFOSKX-CukSFJfX.js} +1 -1
- package/studio/web/assets/{languageServices-DXtZ6rEF.js → languageServices-0ZvgO63b.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-B4pCF1zO.js → lspLanguageFeatures-sNVDopx3.js} +1 -1
- package/studio/web/assets/{mermaid-parser.core-Ck-fC8b7.js → mermaid-parser.core-BlEsOWNO.js} +3 -3
- package/studio/web/assets/{mermaid.core-CP8aNNYm.js → mermaid.core-BlkGaMIH.js} +5 -5
- package/studio/web/assets/{mindmap-definition-FBJOCRG2-CgTZ-rit.js → mindmap-definition-YA3MSWOX-IprMc_0j.js} +1 -1
- package/studio/web/assets/notifications-BukjVuVe.js +1 -0
- package/studio/web/assets/{notifications-D3tIQ4sg.js → notifications-DvBJIIO0.js} +1 -1
- package/studio/web/assets/{packet-AYTQ26CC-DEyoPtPb.js → packet-AYTQ26CC-Bi3V04Zi.js} +1 -1
- package/studio/web/assets/{pegDiagram-XKGWAZYB-DrD-7sD9.js → pegDiagram-XKGWAZYB-BNuPDLZY.js} +1 -1
- package/studio/web/assets/{pie-WAS4IAKB-wjj-EI1d.js → pie-WAS4IAKB-_6DoDbng.js} +1 -1
- package/studio/web/assets/{pieDiagram-E7YTZNPT-GntqDCzv.js → pieDiagram-E7YTZNPT-DqNb6Ht2.js} +1 -1
- package/studio/web/assets/{profile-DZWU7MgT.js → profile-B-jyYdzV.js} +1 -1
- package/studio/web/assets/{quadrantDiagram-AXDQQJYC-0UeqXQGd.js → quadrantDiagram-AXDQQJYC-BGH9E2YR.js} +1 -1
- package/studio/web/assets/{radar-RG4KPBEZ-DFSA5h7k.js → radar-RG4KPBEZ-DDdVczcL.js} +1 -1
- package/studio/web/assets/{railroad-74A4TZTK-CaOUG9wR.js → railroad-74A4TZTK-BJUP4Jds.js} +1 -1
- package/studio/web/assets/railroad-abnf-HS5TGJTU-Bm3L1L0h.js +1 -0
- package/studio/web/assets/railroad-ebnf-LZEXJU2U-CKzLGlkw.js +1 -0
- package/studio/web/assets/railroad-peg-WCYAUIDC-S8xLjslx.js +1 -0
- package/studio/web/assets/{railroadDiagram-O6MQD6OU-yHUELZaV.js → railroadDiagram-O6MQD6OU-DGTPh2KZ.js} +1 -1
- package/studio/web/assets/{regions-CkyurXzE.js → regions-GN6Mu5aH.js} +1 -1
- package/studio/web/assets/{register-BUyhWjBO.js → register-CU-gCfLP.js} +3 -3
- package/studio/web/assets/{requirementDiagram-EFPCY7ZU-DNEGFjuW.js → requirementDiagram-BXWQKSXE-BJnO6uLz.js} +1 -1
- package/studio/web/assets/{sankeyDiagram-P5KCCOFB-DpyAmSVR.js → sankeyDiagram-P5KCCOFB-0vSOdymH.js} +1 -1
- package/studio/web/assets/{sequenceDiagram-WJ2MYXX4-TyaT7xNk.js → sequenceDiagram-WJ2MYXX4-hETizDWE.js} +1 -1
- package/studio/web/assets/{setComponentFile-CeyKSZAa.js → setComponentFile-D7Fww0lC.js} +1 -1
- package/studio/web/assets/{setup-D2kn7cAA.js → setup-DG3Ws0Gg.js} +2 -2
- package/studio/web/assets/{stateDiagram-HBIQ2CUA-CeEdTArZ.js → stateDiagram-D77RDMKH-CdYQ_KtC.js} +1 -1
- package/studio/web/assets/stateDiagram-v2-MP3YSRHH-CdKuzQMT.js +1 -0
- package/studio/web/assets/status-koJd66XF.js +61 -0
- package/studio/web/assets/{swimlanes-XN3QIQJK-B54FmF46.js → swimlanes-42K2YHIH-B8cHIpU4.js} +1 -1
- package/studio/web/assets/swimlanesDiagram-VR7AAH4N-DmOSJwaH.js +8 -0
- package/studio/web/assets/{tabs-B_G5zscN.js → tabs-BrHu7gJi.js} +1 -1
- package/studio/web/assets/{timeline-definition-24CTP7MA-D-a9ujbo.js → timeline-definition-24CTP7MA-BJWYSXqF.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-C0UW6rI2.js → toggleHighContrast-CGksgPgv.js} +1 -1
- package/studio/web/assets/{treeView-Q6P3EWNA-CrW_6JnS.js → treeView-Q6P3EWNA-qxe_v6CQ.js} +1 -1
- package/studio/web/assets/{treemap-WGGIJYW6-BxyYLdP_.js → treemap-WGGIJYW6-dDo97XXF.js} +1 -1
- package/studio/web/assets/{tsMode-DBC0zmDx.js → tsMode-D3T_5QzW.js} +1 -1
- package/studio/web/assets/{typescript-DApRQir3.js → typescript-BegjUvtB.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-DB6JStU1.js → useEntityRestURL-BicFpx_N.js} +1 -1
- package/studio/web/assets/{useLocalStorage-Dtj1QS8_.js → useLocalStorage-BqMR3D8_.js} +1 -1
- package/studio/web/assets/vendor-core-c2JRRJpV.js +58 -0
- package/studio/web/assets/vendor-datadog-CLUcJXOo.js +6 -0
- package/studio/web/assets/{vendor-react-Dyj4O3HE.js → vendor-react-CJV_K1u4.js} +1 -1
- package/studio/web/assets/vendor-tanstack-DxzraizX.js +1 -0
- package/studio/web/assets/{vendor-ui-vhu-UHhF.js → vendor-ui-BUjK0h8a.js} +2 -2
- package/studio/web/assets/{vennDiagram-4TSXK5OY-Cy7s7Mpy.js → vennDiagram-4TSXK5OY-A3i-lCdl.js} +1 -1
- package/studio/web/assets/{wardley-WFR3VGLG-BeBL35g2.js → wardley-WFR3VGLG-B0ik-_6g.js} +1 -1
- package/studio/web/assets/{wardleyDiagram-VM6X3IG4-BylmIGSg.js → wardleyDiagram-VM6X3IG4-CjrkKWUR.js} +1 -1
- package/studio/web/assets/{workers-C0bFIedw.js → workers-Bmx4l_9B.js} +1 -1
- package/studio/web/assets/x-DIzaLEdK.js +1 -0
- package/studio/web/assets/{xml-HWd01lU-.js → xml-Bg40Rcra.js} +1 -1
- package/studio/web/assets/{xychartDiagram-S5SC5T6Z-CoKALMXr.js → xychartDiagram-S5SC5T6Z-Biok4GYV.js} +1 -1
- package/studio/web/assets/{yaml-CIH0Nt-h.js → yaml-DolDvfXE.js} +1 -1
- package/studio/web/index.html +14 -14
- package/studio/web/assets/Chat-JpO8EtUu.js +0 -2067
- package/studio/web/assets/FloatingChat-Bcj3xSZu.js +0 -23
- package/studio/web/assets/applications-ByqLRKyZ.js +0 -296
- package/studio/web/assets/architecture-7GRP2DOG-DNdx5tEU.js +0 -1
- package/studio/web/assets/authStore-qKmCZcaf.js +0 -3
- package/studio/web/assets/channel-DtCV8PTL.js +0 -1
- package/studio/web/assets/chunk-R7TYR2AO-Irip67yr.js +0 -10
- package/studio/web/assets/classDiagram-DTDB5LWJ-DbO_dCNE.js +0 -1
- package/studio/web/assets/classDiagram-v2-JRS7N3AN-DbO_dCNE.js +0 -1
- package/studio/web/assets/eventmodeling-NTZA5JFV-5jbe4A5P.js +0 -1
- package/studio/web/assets/flowDiagram-A5DVABFB-Dp9Ezlow.js +0 -1
- package/studio/web/assets/index-aSt5tY-L.js +0 -824
- package/studio/web/assets/index.lazy-B9jiPwT8.js +0 -2
- package/studio/web/assets/notifications-CUoYgU98.js +0 -1
- package/studio/web/assets/railroad-abnf-HS5TGJTU-Bc0Qi0WH.js +0 -1
- package/studio/web/assets/railroad-ebnf-LZEXJU2U-G8rVVZ2C.js +0 -1
- package/studio/web/assets/railroad-peg-WCYAUIDC-CrehKBhC.js +0 -1
- package/studio/web/assets/stateDiagram-v2-4QOOHH4V-D4tuw9Su.js +0 -1
- package/studio/web/assets/status-D7Xn5ePA.js +0 -61
- package/studio/web/assets/swimlanesDiagram-VK2B7HYN-XOhmNEvq.js +0 -8
- package/studio/web/assets/vendor-core-RCcadM3e.js +0 -73
- package/studio/web/assets/vendor-datadog-BRv-mOv1.js +0 -6
- package/studio/web/assets/vendor-tanstack-BiFWSB3W.js +0 -1
- package/studio/web/assets/x-B9o9hsep.js +0 -1
- /package/studio/web/assets/{sizeCapture-X5ZJPWSS-B0uUizjq.js → sizeCapture-INFHLROL-B0uUizjq.js} +0 -0
package/resources/databases.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
2
3
|
import { initSync, getHdbBasePath, get as envGet } from '../utility/environment/environmentManager.ts';
|
|
3
4
|
import { INTERNAL_DBIS_NAME } from '../utility/lmdb/terms.ts';
|
|
4
5
|
import { open, compareKeys, type Database, type RootDatabase } from 'lmdb';
|
|
@@ -29,6 +30,7 @@ import harperLogger from '../utility/logging/harper_logger.ts';
|
|
|
29
30
|
const { forComponent } = harperLogger;
|
|
30
31
|
import * as manageThreads from '../server/threads/manageThreads.js';
|
|
31
32
|
import { openAuditStore, readAuditEntry, createAuditEntry, type AuditRecord } from './auditStore.ts';
|
|
33
|
+
import { removeStorageReclamation } from '../server/storageReclamation.ts';
|
|
32
34
|
import { handleLocalTimeForGets } from './RecordEncoder.ts';
|
|
33
35
|
import { deleteRootBlobPathsForDB } from './blob.ts';
|
|
34
36
|
import { CUSTOM_INDEXES } from './indexes/customIndexes.ts';
|
|
@@ -38,7 +40,6 @@ import { PrimaryRocksDatabase } from './PrimaryRocksDatabase.ts';
|
|
|
38
40
|
import { replayLogs } from './replayLogs.ts';
|
|
39
41
|
import { totalmem } from 'node:os';
|
|
40
42
|
import { RocksIndexStore } from './RocksIndexStore.ts';
|
|
41
|
-
import { when } from '../utility/when.ts';
|
|
42
43
|
import { resolveRocksMemoryConfig } from '../utility/rocksMemoryConfig.ts';
|
|
43
44
|
import { isProcessRunning } from '../utility/processManagement/processManagement.js';
|
|
44
45
|
import {
|
|
@@ -348,6 +349,20 @@ _assignPackageExport('databases', databases);
|
|
|
348
349
|
_assignPackageExport('tables', tables);
|
|
349
350
|
|
|
350
351
|
const NEXT_TABLE_ID = Symbol.for('next-table-id');
|
|
352
|
+
/**
|
|
353
|
+
* True when a descriptor claims an index build no live operation in this process can own. The PID and
|
|
354
|
+
* worker generation cannot answer that alone: a container reuses PID 1 and starts the in-memory
|
|
355
|
+
* generation back at 1 while the persisted one is higher. A descriptor with no incarnation was written
|
|
356
|
+
* before the field existed, so it belongs to an earlier process; a thread started without one of its
|
|
357
|
+
* own cannot judge, and falls back rather than declaring a live build dead.
|
|
358
|
+
*/
|
|
359
|
+
function isAbandonedIndexBuild(descriptor: any, currentRestartGeneration: number): boolean {
|
|
360
|
+
if (!descriptor) return false;
|
|
361
|
+
if (descriptor.indexingPID && descriptor.indexingPID !== process.pid) return true;
|
|
362
|
+
if (descriptor.restartNumber < currentRestartGeneration) return true;
|
|
363
|
+
const incarnation = manageThreads.processIncarnation;
|
|
364
|
+
return !!descriptor.indexingPID && incarnation != null && descriptor.indexingIncarnation !== incarnation;
|
|
365
|
+
}
|
|
351
366
|
// How many times the schema load will try to finish a tombstoned drop before
|
|
352
367
|
// giving up for the rest of this process's lifetime. A drop that fails once
|
|
353
368
|
// almost always fails identically forever - the usual cause is a RocksDB
|
|
@@ -637,6 +652,7 @@ export function readMetaDb(
|
|
|
637
652
|
lmdbDatabaseEnvs.set(path, rootStore);
|
|
638
653
|
}
|
|
639
654
|
|
|
655
|
+
rootStore.dbisDb?.resetReadTxn();
|
|
640
656
|
return initStores(path, rootStore, databaseName, defaultTable, auditPath, isLegacy);
|
|
641
657
|
} catch (error) {
|
|
642
658
|
error.message += ` opening database ${path}`;
|
|
@@ -896,6 +912,8 @@ function initStores(
|
|
|
896
912
|
indices[attribute.name] = dbi;
|
|
897
913
|
indices[attribute.name].indexNulls = attribute.indexNulls;
|
|
898
914
|
}
|
|
915
|
+
// the only way a thread that never declares the schema reaches Table.indices
|
|
916
|
+
indices[attribute.name].isIndexing = !!attribute.indexingPID;
|
|
899
917
|
const existingAttribute = existingAttributes.find(
|
|
900
918
|
(existingAttribute) => existingAttribute.name === attribute.name
|
|
901
919
|
);
|
|
@@ -1253,6 +1271,10 @@ export async function dropDatabase(databaseName) {
|
|
|
1253
1271
|
databaseEventsEmitter.emit('dropDatabase', databaseName);
|
|
1254
1272
|
|
|
1255
1273
|
if (rootStore) {
|
|
1274
|
+
// awaited: retirement stops the loop admitting work, the barrier is what says the pass that was
|
|
1275
|
+
// already running has released the stores this is about to close and unlink
|
|
1276
|
+
await rootStore.auditStore?.stopAuditCleanup?.();
|
|
1277
|
+
removeStorageReclamation(rootStore.path);
|
|
1256
1278
|
if (rootStore.status === 'open') {
|
|
1257
1279
|
if (rootStore instanceof RocksDatabase) {
|
|
1258
1280
|
rootStore.close();
|
|
@@ -1267,6 +1289,8 @@ export async function dropDatabase(databaseName) {
|
|
|
1267
1289
|
// a tableless database resolves its root store here rather than in the loop above, so take
|
|
1268
1290
|
// the drop lock now (still before any destructive step)
|
|
1269
1291
|
if (rootStore instanceof RocksDatabase) lockDatabaseForDrop(rootStore.path, databaseName, restoreLocks);
|
|
1292
|
+
await rootStore.auditStore?.stopAuditCleanup?.();
|
|
1293
|
+
removeStorageReclamation(rootStore.path);
|
|
1270
1294
|
if (rootStore instanceof RocksDatabase) {
|
|
1271
1295
|
rootStore.close();
|
|
1272
1296
|
rootStore.destroy();
|
|
@@ -1304,17 +1328,27 @@ export function closeDatabase(databaseName: string): boolean {
|
|
|
1304
1328
|
const table: any = dbTables[tableName];
|
|
1305
1329
|
if (!table?.primaryStore) continue;
|
|
1306
1330
|
if (table.primaryStore.rootStore) rootStores.add(table.primaryStore.rootStore);
|
|
1307
|
-
for (const indexName in table.indices || {}) {
|
|
1308
|
-
closeStore(table.indices[indexName], `index ${tableName}.${indexName}`);
|
|
1309
|
-
}
|
|
1310
|
-
closeStore(table.primaryStore, `table ${tableName}`);
|
|
1311
1331
|
}
|
|
1312
1332
|
// a database with no tables (an empty schema, or one whose tables were all dropped) still holds
|
|
1313
1333
|
// an open root store, tracked only on the defined-database entry rather than any table — include
|
|
1314
1334
|
// it so its handles are released too (the Set dedupes it against the per-table root stores above)
|
|
1315
1335
|
const definedRoot = (definedDatabases?.get(databaseName) as any)?.rootStore;
|
|
1316
1336
|
if (definedRoot) rootStores.add(definedRoot);
|
|
1337
|
+
// before any table store closes, so no further pass is admitted. This is synchronous, so it cannot
|
|
1338
|
+
// await the drain barrier stopAuditCleanup() returns; what covers it is the in-pass status checks,
|
|
1339
|
+
// plus the fact that its production callers reach it only for RocksDB databases, whose pass is one
|
|
1340
|
+
// synchronous purgeLogs() call with nothing suspended mid-removal.
|
|
1341
|
+
for (const rootStore of rootStores) rootStore.auditStore?.stopAuditCleanup?.();
|
|
1342
|
+
for (const tableName in dbTables) {
|
|
1343
|
+
const table: any = dbTables[tableName];
|
|
1344
|
+
if (!table?.primaryStore) continue;
|
|
1345
|
+
for (const indexName in table.indices || {}) {
|
|
1346
|
+
closeStore(table.indices[indexName], `index ${tableName}.${indexName}`);
|
|
1347
|
+
}
|
|
1348
|
+
closeStore(table.primaryStore, `table ${tableName}`);
|
|
1349
|
+
}
|
|
1317
1350
|
for (const rootStore of rootStores) {
|
|
1351
|
+
removeStorageReclamation(rootStore.path);
|
|
1318
1352
|
closeStore(rootStore.dbisDb, 'attributes store');
|
|
1319
1353
|
closeStore(rootStore, 'root store');
|
|
1320
1354
|
lmdbDatabaseEnvs.delete(rootStore.path);
|
|
@@ -1882,8 +1916,7 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
1882
1916
|
changed ||
|
|
1883
1917
|
indexFormatNeedsPersist ||
|
|
1884
1918
|
attributeDescriptor?.indexingFailed ||
|
|
1885
|
-
(attributeDescriptor
|
|
1886
|
-
attributeDescriptor?.restartNumber < currentRestartGeneration
|
|
1919
|
+
isAbandonedIndexBuild(attributeDescriptor, currentRestartGeneration)
|
|
1887
1920
|
) {
|
|
1888
1921
|
hasChanges = true;
|
|
1889
1922
|
exclusiveLock();
|
|
@@ -1891,8 +1924,7 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
1891
1924
|
if (
|
|
1892
1925
|
structurallyChanged ||
|
|
1893
1926
|
attributeDescriptor?.indexingFailed ||
|
|
1894
|
-
(attributeDescriptor
|
|
1895
|
-
attributeDescriptor?.restartNumber < currentRestartGeneration
|
|
1927
|
+
isAbandonedIndexBuild(attributeDescriptor, currentRestartGeneration)
|
|
1896
1928
|
) {
|
|
1897
1929
|
hasChanges = true;
|
|
1898
1930
|
if (attribute.indexNulls === undefined) attribute.indexNulls = true;
|
|
@@ -1912,9 +1944,21 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
1912
1944
|
// resumes rather than restarts. Canonicalized to match structurallyChanged above.
|
|
1913
1945
|
const indexOptionsChanged =
|
|
1914
1946
|
canonicalIndexKey(attributeDescriptor?.indexed) !== canonicalIndexKey(attribute.indexed);
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1947
|
+
// Only a checkpoint runIndexing stamped with its own key resumes: earlier releases advanced
|
|
1948
|
+
// lastIndexedKey past failed and unflushed index writes, so any other is a full rebuild.
|
|
1949
|
+
const uncertifiedCheckpoint =
|
|
1950
|
+
attributeDescriptor?.lastIndexedKey !== undefined &&
|
|
1951
|
+
(attributeDescriptor.checkpointCertified === undefined ||
|
|
1952
|
+
attributeDescriptor.checkpointAlgorithm !== CHECKPOINT_ALGORITHM ||
|
|
1953
|
+
compareKeys(attributeDescriptor.checkpointCertified, attributeDescriptor.lastIndexedKey) !== 0);
|
|
1954
|
+
attribute.lastIndexedKey =
|
|
1955
|
+
indexOptionsChanged || uncertifiedCheckpoint
|
|
1956
|
+
? undefined
|
|
1957
|
+
: (attributeDescriptor?.lastIndexedKey ?? undefined);
|
|
1958
|
+
if (attribute.lastIndexedKey !== undefined) {
|
|
1959
|
+
attribute.checkpointCertified = attribute.lastIndexedKey;
|
|
1960
|
+
attribute.checkpointAlgorithm = CHECKPOINT_ALGORITHM;
|
|
1961
|
+
}
|
|
1918
1962
|
// Explicit reindex is the upgrade path from a legacy (un-versioned) custom-index
|
|
1919
1963
|
// object store to the versioned, VT-cacheable format. A full rebuild from scratch
|
|
1920
1964
|
// (lastIndexedKey === undefined) clears the store and rewrites every node, so the
|
|
@@ -1938,6 +1982,9 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
1938
1982
|
// the new process reuses the old PID. Cleared on clean completion; left in place
|
|
1939
1983
|
// on failure/crash so the next, higher-numbered restart re-triggers the backfill.
|
|
1940
1984
|
attribute.restartNumber = currentRestartGeneration;
|
|
1985
|
+
if (manageThreads.processIncarnation != null)
|
|
1986
|
+
attribute.indexingIncarnation = manageThreads.processIncarnation;
|
|
1987
|
+
attribute.indexingBuildId = randomBytes(8).toString('hex');
|
|
1941
1988
|
delete attribute.indexingFailed; // clear failure flag for the new run
|
|
1942
1989
|
dbi.isIndexing = true;
|
|
1943
1990
|
Object.defineProperty(attribute, 'dbi', { value: dbi, configurable: true, enumerable: false });
|
|
@@ -1951,6 +1998,13 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
1951
1998
|
if (attributeDescriptor?.indexingPID && attributeDescriptor.indexingPID !== process.pid)
|
|
1952
1999
|
reindexReasons.push(`crash-recovery(pid=${attributeDescriptor.indexingPID})`);
|
|
1953
2000
|
if (attributeDescriptor?.restartNumber < currentRestartGeneration) reindexReasons.push('restart-number');
|
|
2001
|
+
if (uncertifiedCheckpoint) reindexReasons.push('uncertified-checkpoint');
|
|
2002
|
+
if (
|
|
2003
|
+
attributeDescriptor?.indexingPID === process.pid &&
|
|
2004
|
+
manageThreads.processIncarnation != null &&
|
|
2005
|
+
attributeDescriptor.indexingIncarnation !== manageThreads.processIncarnation
|
|
2006
|
+
)
|
|
2007
|
+
reindexReasons.push('abandoned-build(previous process incarnation)');
|
|
1954
2008
|
logger.info(
|
|
1955
2009
|
`reindex ${databaseName}.${tableName}.${attribute.name}: reason=${reindexReasons.join(',') || 'unknown'}`
|
|
1956
2010
|
);
|
|
@@ -1964,11 +2018,21 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
1964
2018
|
// workers / a reload would treat the still-partial index as ready and return incomplete results.
|
|
1965
2019
|
attribute.indexingPID = attributeDescriptor.indexingPID;
|
|
1966
2020
|
attribute.lastIndexedKey = attributeDescriptor.lastIndexedKey;
|
|
2021
|
+
if (attributeDescriptor.checkpointCertified !== undefined) {
|
|
2022
|
+
attribute.checkpointCertified = attributeDescriptor.checkpointCertified;
|
|
2023
|
+
attribute.checkpointAlgorithm = attributeDescriptor.checkpointAlgorithm;
|
|
2024
|
+
}
|
|
1967
2025
|
// Carry the in-progress restart generation too, so persisting this metadata-only
|
|
1968
2026
|
// change doesn't drop it and break the crash-recovery trigger for the running backfill.
|
|
1969
2027
|
attribute.restartNumber = attributeDescriptor.restartNumber;
|
|
2028
|
+
attribute.indexingIncarnation = attributeDescriptor.indexingIncarnation;
|
|
2029
|
+
attribute.indexingBuildId = attributeDescriptor.indexingBuildId;
|
|
1970
2030
|
if (attributeDescriptor.indexingFailed) attribute.indexingFailed = attributeDescriptor.indexingFailed;
|
|
1971
2031
|
}
|
|
2032
|
+
// The declared attribute never carries the stamp, so any rewrite of a descriptor that has
|
|
2033
|
+
// one would drop it and make a completed index look like a pre-stamp build.
|
|
2034
|
+
if (attribute.checkpointAlgorithm === undefined && attributeDescriptor?.checkpointAlgorithm !== undefined)
|
|
2035
|
+
attribute.checkpointAlgorithm = attributeDescriptor.checkpointAlgorithm;
|
|
1972
2036
|
attributesDbi.put(dbiKey, attribute);
|
|
1973
2037
|
}
|
|
1974
2038
|
// If a migration is in progress (indexingPID set), any newly opened dbi must also
|
|
@@ -1994,7 +2058,10 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
1994
2058
|
}
|
|
1995
2059
|
logger.trace(`${tableName} table loading, running index`);
|
|
1996
2060
|
if (attributesToIndex.length > 0 || indicesToRemove.length > 0) {
|
|
1997
|
-
|
|
2061
|
+
// captured before the backfill can rewrite the attributes
|
|
2062
|
+
const buildIds = new Map(attributesToIndex.map((attribute) => [attribute, attribute.indexingBuildId]));
|
|
2063
|
+
const markSettled = () => markAbandonedIndexBuild(Table, rootStore, buildIds);
|
|
2064
|
+
Table.indexingOperation = runIndexing(Table, attributesToIndex, indicesToRemove).then(markSettled, markSettled);
|
|
1998
2065
|
} else if (hasChanges)
|
|
1999
2066
|
signalling.signalSchemaChange(
|
|
2000
2067
|
new SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName)
|
|
@@ -2064,40 +2131,231 @@ export function canonicalizeIndexOptions(value: any): any {
|
|
|
2064
2131
|
}
|
|
2065
2132
|
return value;
|
|
2066
2133
|
}
|
|
2134
|
+
// Bumped when a change alters which keys a checkpoint may certify. A descriptor stamped by any other
|
|
2135
|
+
// version resumes as uncertified (full rebuild) rather than being trusted, and a completed index keeps
|
|
2136
|
+
// the stamp of the build that wrote it.
|
|
2137
|
+
export const CHECKPOINT_ALGORITHM = 2;
|
|
2067
2138
|
const MAX_OUTSTANDING_INDEXING = 1000;
|
|
2068
2139
|
const MIN_OUTSTANDING_INDEXING = 10;
|
|
2140
|
+
const INDEXING_YIELD_INTERVAL = 100;
|
|
2141
|
+
// A resumable checkpoint is written only after a flush (see flushIndexStores), at most once per period
|
|
2142
|
+
// and never before this many more records: the flush seals every column family in the database, so a
|
|
2143
|
+
// slow backfill must not impose the period's flush rate on unrelated tables.
|
|
2144
|
+
let indexingCheckpointPeriodMs = 5000;
|
|
2145
|
+
let indexingCheckpointMinRecords = 10000;
|
|
2146
|
+
export function setIndexingCheckpointPeriod(ms: number, minRecords = indexingCheckpointMinRecords) {
|
|
2147
|
+
const previous = { ms: indexingCheckpointPeriodMs, minRecords: indexingCheckpointMinRecords };
|
|
2148
|
+
indexingCheckpointPeriodMs = ms;
|
|
2149
|
+
indexingCheckpointMinRecords = minRecords;
|
|
2150
|
+
return previous;
|
|
2151
|
+
}
|
|
2152
|
+
const yieldEventTurn = () => new Promise((resolve) => setImmediate(resolve));
|
|
2153
|
+
// RocksDB index stores have no WAL (openRocksDatabase defaults disableWAL), so a flush is what makes the
|
|
2154
|
+
// entries a checkpoint certifies durable. A flush only covers writes issued before it started, so a caller
|
|
2155
|
+
// never joins one in flight: it joins the next one, which every backfill on that database asking meanwhile
|
|
2156
|
+
// shares — at most one in flight and one queued.
|
|
2157
|
+
const indexingFlushes = new WeakMap<object, { inFlight?: Promise<void>; queued?: Promise<void> }>();
|
|
2158
|
+
function flushIndexStores(rootStore: any): Promise<void> | undefined {
|
|
2159
|
+
if (!(rootStore instanceof RocksDatabase)) return;
|
|
2160
|
+
let flushes = indexingFlushes.get(rootStore);
|
|
2161
|
+
if (!flushes) indexingFlushes.set(rootStore, (flushes = {}));
|
|
2162
|
+
if (flushes.queued) return flushes.queued;
|
|
2163
|
+
const start = () => {
|
|
2164
|
+
flushes.queued = undefined;
|
|
2165
|
+
const flush = rootStore.flush().finally(() => {
|
|
2166
|
+
if (flushes.inFlight === flush) flushes.inFlight = undefined;
|
|
2167
|
+
});
|
|
2168
|
+
flushes.inFlight = flush;
|
|
2169
|
+
return flush;
|
|
2170
|
+
};
|
|
2171
|
+
if (!flushes.inFlight) return start();
|
|
2172
|
+
return (flushes.queued = flushes.inFlight.then(start, start));
|
|
2173
|
+
}
|
|
2174
|
+
export function resumeStartKey(attributes: { lastIndexedKey?: any }[]): any {
|
|
2175
|
+
let start: any;
|
|
2176
|
+
for (const attribute of attributes) {
|
|
2177
|
+
if (attribute.lastIndexedKey == undefined) return undefined;
|
|
2178
|
+
if (start === undefined || compareKeys(attribute.lastIndexedKey, start) < 0) start = attribute.lastIndexedKey;
|
|
2179
|
+
}
|
|
2180
|
+
return start;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
// Bounded, unlike the exclusiveLock() spin above: that one runs on the declaring path, where the
|
|
2184
|
+
// caller is waiting on the result and there is nothing useful to do without the lock. This one runs
|
|
2185
|
+
// after a backfill has already settled, so a holder that never releases would wedge the worker's
|
|
2186
|
+
// event loop for nothing. Giving up costs only the marker, and the next load of the table
|
|
2187
|
+
// re-triggers the build regardless.
|
|
2188
|
+
export const ABANDONED_MARK_LOCK_TIMEOUT = 10000;
|
|
2189
|
+
const abandonedMarkLockWait = new Int32Array(new SharedArrayBuffer(4));
|
|
2190
|
+
// `timeout` is the test seam; production callers take the default.
|
|
2191
|
+
export function tryAcquireUpdateAttributesLock(
|
|
2192
|
+
rootStore: RocksDatabase,
|
|
2193
|
+
timeout = ABANDONED_MARK_LOCK_TIMEOUT
|
|
2194
|
+
): boolean {
|
|
2195
|
+
if (rootStore.tryLock('update-attributes')) return true;
|
|
2196
|
+
const startTime = performance.now();
|
|
2197
|
+
let waitTime = 1;
|
|
2198
|
+
while (!rootStore.tryLock('update-attributes')) {
|
|
2199
|
+
const elapsed = performance.now() - startTime;
|
|
2200
|
+
if (elapsed >= timeout) return false;
|
|
2201
|
+
// Atomics.wait rather than a busy spin: the section this guards is synchronous, so the wait
|
|
2202
|
+
// blocks this thread either way, but sleeping does not burn a core while it does.
|
|
2203
|
+
if (elapsed >= 2) {
|
|
2204
|
+
Atomics.wait(abandonedMarkLockWait, 0, 0, Math.min(waitTime, timeout - elapsed));
|
|
2205
|
+
if (waitTime < 16) waitTime *= 2;
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
return true;
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
/**
|
|
2212
|
+
* Persists the failure marker for a build that ended without running one of runIndexing's own exit
|
|
2213
|
+
* paths, so something re-triggers it. Fenced on `indexingBuildId` inside the storage engine's catalog
|
|
2214
|
+
* serialization boundary, because a replacement generation (or another thread declaring different index
|
|
2215
|
+
* options) can claim the attribute before an outgoing build's promise settles, and marking that would fail
|
|
2216
|
+
* a live build. The fence read and write stay synchronous, and nothing here may throw because
|
|
2217
|
+
* `Table.indexingOperation` reaches operations-API callers.
|
|
2218
|
+
*/
|
|
2219
|
+
async function markAbandonedIndexBuild(Table, rootStore, buildIds: Map<any, string>) {
|
|
2220
|
+
for (const [attribute, buildId] of buildIds) {
|
|
2221
|
+
try {
|
|
2222
|
+
let marked;
|
|
2223
|
+
if (buildId == null || Table.dbisDB.getSync(attribute.key)?.indexingBuildId !== buildId) continue;
|
|
2224
|
+
const markIfOwned = () => {
|
|
2225
|
+
const descriptor = Table.dbisDB.getSync(attribute.key);
|
|
2226
|
+
if (descriptor?.indexingBuildId === buildId && !descriptor.indexingFailed) {
|
|
2227
|
+
Table.dbisDB.putSync(attribute.key, { ...descriptor, indexingFailed: true });
|
|
2228
|
+
marked = true;
|
|
2229
|
+
}
|
|
2230
|
+
};
|
|
2231
|
+
if (rootStore instanceof RocksDatabase) {
|
|
2232
|
+
if (!tryAcquireUpdateAttributesLock(rootStore)) {
|
|
2233
|
+
logger.warn(
|
|
2234
|
+
`Could not mark the abandoned index build of ${Table.databaseName}.${Table.tableName}.${attribute.name}: ` +
|
|
2235
|
+
`timed out after ${ABANDONED_MARK_LOCK_TIMEOUT}ms waiting for the exclusive 'update-attributes' lock. ` +
|
|
2236
|
+
`The index stays incomplete and the next load of the table re-triggers the backfill.`
|
|
2237
|
+
);
|
|
2238
|
+
continue;
|
|
2239
|
+
}
|
|
2240
|
+
try {
|
|
2241
|
+
markIfOwned();
|
|
2242
|
+
} finally {
|
|
2243
|
+
rootStore.unlock('update-attributes');
|
|
2244
|
+
}
|
|
2245
|
+
} else {
|
|
2246
|
+
rootStore.transactionSync(markIfOwned);
|
|
2247
|
+
}
|
|
2248
|
+
if (marked)
|
|
2249
|
+
logger.warn(
|
|
2250
|
+
`Indexing of ${Table.databaseName}.${Table.tableName}.${attribute.name} ended without completing. ` +
|
|
2251
|
+
`The index stays incomplete and every query on the attribute reports it as not indexed yet; ` +
|
|
2252
|
+
`the next load of the table retries the backfill from the last checkpoint (indexingFailed=true).`
|
|
2253
|
+
);
|
|
2254
|
+
} catch (error) {
|
|
2255
|
+
// A store closed by shutdown is the common case, and it cannot be written to at all.
|
|
2256
|
+
try {
|
|
2257
|
+
logger.debug(`Could not mark the abandoned index build of ${Table.tableName}.${attribute.name}`, error);
|
|
2258
|
+
} catch {}
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2069
2262
|
async function runIndexing(Table, attributes, indicesToRemove) {
|
|
2263
|
+
let checkpointing;
|
|
2264
|
+
let hadIndexingErrors = false;
|
|
2265
|
+
const attributeErrorReported = {};
|
|
2266
|
+
const onIndexPutRejected = (property, error) => {
|
|
2267
|
+
hadIndexingErrors = true;
|
|
2268
|
+
if (attributeErrorReported[property]) return;
|
|
2269
|
+
attributeErrorReported[property] = true;
|
|
2270
|
+
logger.error(`Error indexing attribute ${property}`, error);
|
|
2271
|
+
};
|
|
2272
|
+
const putRejectionHandlers = attributes.map((attribute) => (error) => onIndexPutRejected(attribute.name, error));
|
|
2070
2273
|
try {
|
|
2071
2274
|
logger.info(`Indexing ${Table.tableName} attributes`, attributes);
|
|
2072
2275
|
await signalling.signalSchemaChange(
|
|
2073
2276
|
new SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName)
|
|
2074
2277
|
);
|
|
2075
2278
|
let lastResolution;
|
|
2279
|
+
// The checkpoint and completion barriers have to cover every mutation still in flight: any of them
|
|
2280
|
+
// may reject after those barriers read hadIndexingErrors.
|
|
2281
|
+
const pendingMutations = new Set();
|
|
2282
|
+
let settleWaiter;
|
|
2283
|
+
const track = (result, onRejected) => {
|
|
2284
|
+
if (!result?.then) return result;
|
|
2285
|
+
const tracked = result.then(
|
|
2286
|
+
() => {
|
|
2287
|
+
pendingMutations.delete(tracked);
|
|
2288
|
+
settleWaiter?.();
|
|
2289
|
+
return false;
|
|
2290
|
+
},
|
|
2291
|
+
(error) => {
|
|
2292
|
+
pendingMutations.delete(tracked);
|
|
2293
|
+
settleWaiter?.();
|
|
2294
|
+
onRejected(error);
|
|
2295
|
+
return true;
|
|
2296
|
+
}
|
|
2297
|
+
);
|
|
2298
|
+
pendingMutations.add(tracked);
|
|
2299
|
+
return result;
|
|
2300
|
+
};
|
|
2301
|
+
// The tracked promises absorb their own rejections, so one failure never abandons its siblings.
|
|
2302
|
+
const drainMutations = async () => {
|
|
2303
|
+
if (!pendingMutations.size) return false;
|
|
2304
|
+
return (await Promise.all([...pendingMutations])).some(Boolean);
|
|
2305
|
+
};
|
|
2306
|
+
// Waiting on a chosen entry would stall behind a slow one the others have already overtaken.
|
|
2307
|
+
const nextSettlement = () =>
|
|
2308
|
+
new Promise((resolve) => {
|
|
2309
|
+
settleWaiter = () => {
|
|
2310
|
+
settleWaiter = undefined;
|
|
2311
|
+
resolve(undefined);
|
|
2312
|
+
};
|
|
2313
|
+
});
|
|
2076
2314
|
for (const index of indicesToRemove) {
|
|
2077
|
-
|
|
2315
|
+
track(index.drop(), (error) => onIndexPutRejected(index.name, error));
|
|
2078
2316
|
}
|
|
2079
2317
|
let interrupted;
|
|
2080
|
-
let hadIndexingErrors = false;
|
|
2081
|
-
const attributeErrorReported = {};
|
|
2082
2318
|
let indexed = 0;
|
|
2083
2319
|
const attributesLength = attributes.length;
|
|
2084
2320
|
await new Promise((resolve) => setImmediate(resolve)); // yield event turn, indexing should consistently take at least one event turn
|
|
2085
2321
|
if (attributesLength > 0) {
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
if (compareKeys(attribute.lastIndexedKey, start) < 0) start = attribute.lastIndexedKey;
|
|
2090
|
-
if (attribute.lastIndexedKey == undefined) {
|
|
2091
|
-
// if we are starting from the beginning, clear out any previous index entries since we are rewriting
|
|
2322
|
+
const start = resumeStartKey(attributes);
|
|
2323
|
+
if (start === undefined) {
|
|
2324
|
+
for (const attribute of attributes) {
|
|
2092
2325
|
if (attribute.dbi.clearAsync) {
|
|
2093
|
-
// LMDB
|
|
2094
|
-
|
|
2326
|
+
// LMDB enqueues this ahead of the index writes, so the scan need not wait for it — but the
|
|
2327
|
+
// barriers must, or a rejected clear certifies a checkpoint over stale entries.
|
|
2328
|
+
track(attribute.dbi.clearAsync(), (error) => onIndexPutRejected(attribute.name, error));
|
|
2095
2329
|
} else {
|
|
2096
2330
|
await attribute.dbi.clear();
|
|
2097
2331
|
}
|
|
2098
2332
|
}
|
|
2099
2333
|
}
|
|
2100
|
-
|
|
2334
|
+
// A resumed scan starts at the checkpoint, so it must only name a key whose every predecessor is
|
|
2335
|
+
// durably indexed: persisted once the writes it covers have settled and flushed, frozen after any
|
|
2336
|
+
// record fails so the retry re-covers it, and stamped with its own key (see the trigger in table()).
|
|
2337
|
+
const persistCheckpoint = async (key) => {
|
|
2338
|
+
if (hadIndexingErrors) return;
|
|
2339
|
+
try {
|
|
2340
|
+
// Everything still in flight was issued for a key at or before this one: the scan has not
|
|
2341
|
+
// moved past it yet. So a failure among them is a failure this checkpoint would cover.
|
|
2342
|
+
const failed = await drainMutations();
|
|
2343
|
+
if (failed) return;
|
|
2344
|
+
await flushIndexStores(Table.primaryStore.rootStore);
|
|
2345
|
+
const puts = [];
|
|
2346
|
+
for (const attribute of attributes) {
|
|
2347
|
+
attribute.lastIndexedKey = key;
|
|
2348
|
+
attribute.checkpointCertified = key;
|
|
2349
|
+
attribute.checkpointAlgorithm = CHECKPOINT_ALGORITHM;
|
|
2350
|
+
puts.push(Table.dbisDB.put(attribute.key, attribute));
|
|
2351
|
+
}
|
|
2352
|
+
await Promise.all(puts);
|
|
2353
|
+
} catch (error) {
|
|
2354
|
+
logger.warn(`Could not persist the indexing checkpoint for ${Table.tableName}`, error);
|
|
2355
|
+
}
|
|
2356
|
+
};
|
|
2357
|
+
let nextCheckpointAt = performance.now() + indexingCheckpointPeriodMs;
|
|
2358
|
+
let nextCheckpointRecord = indexingCheckpointMinRecords;
|
|
2101
2359
|
// this means that a new attribute has been introduced that needs to be indexed
|
|
2102
2360
|
for (const { key, value: record } of Table.primaryStore.getRange({
|
|
2103
2361
|
start,
|
|
@@ -2105,93 +2363,86 @@ async function runIndexing(Table, attributes, indicesToRemove) {
|
|
|
2105
2363
|
versions: true,
|
|
2106
2364
|
snapshot: false, // don't hold a read transaction this whole time
|
|
2107
2365
|
})) {
|
|
2108
|
-
|
|
2366
|
+
const atInterval = ++indexed % INDEXING_YIELD_INTERVAL === 0;
|
|
2109
2367
|
// TODO: Do we ever need to interrupt due to a schema change that was not a restart?
|
|
2110
2368
|
//if (Table.schemaVersion !== schemaVersion) return; // break out if there are any schema changes and let someone else pick it up
|
|
2111
|
-
|
|
2112
|
-
//
|
|
2113
|
-
// outstanding-based yield below never fires for them. Track that this row did synchronous
|
|
2369
|
+
// Custom indexes (e.g. HNSW) index synchronously and leave pendingMutations empty, so the
|
|
2370
|
+
// backpressure yield below never fires for them. Track that this row did synchronous
|
|
2114
2371
|
// indexing work so we can still yield the event loop after it.
|
|
2115
2372
|
let didSynchronousIndexing = false;
|
|
2116
2373
|
// every index operation needs to be guarded by the version still be the same. If it has already changed before
|
|
2117
2374
|
// we index, that's fine because indexing is idempotent, we can just put the same values again. If it changes
|
|
2118
2375
|
// during the indexing, the indexing here will fail. This is also fine because it means the other thread will have
|
|
2119
2376
|
// performed indexing and we don't need to do anything further
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
const
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2377
|
+
if (record) {
|
|
2378
|
+
for (let i = 0; i < attributesLength; i++) {
|
|
2379
|
+
const attribute = attributes[i];
|
|
2380
|
+
const property = attribute.name;
|
|
2381
|
+
const index = attribute.dbi;
|
|
2382
|
+
const onPutRejected = putRejectionHandlers[i];
|
|
2383
|
+
try {
|
|
2384
|
+
const resolver = attribute.resolve;
|
|
2385
|
+
const value = record && (resolver ? resolver(record) : record[property]);
|
|
2386
|
+
if (index.customIndex) {
|
|
2387
|
+
index.customIndex.index(key, value);
|
|
2388
|
+
didSynchronousIndexing = true;
|
|
2389
|
+
continue;
|
|
2390
|
+
}
|
|
2391
|
+
const values = getIndexedValues(value, index.indexNulls);
|
|
2392
|
+
if (values) {
|
|
2393
|
+
for (let i = 0, l = values.length; i < l; i++) {
|
|
2394
|
+
track(index.put(values[i], key), onPutRejected);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
} catch (error) {
|
|
2398
|
+
hadIndexingErrors = true;
|
|
2399
|
+
if (!attributeErrorReported[property]) {
|
|
2400
|
+
// just report an indexing error once per attribute so we don't spam the logs.
|
|
2401
|
+
// A store closed by worker shutdown surfaces here as "Database not open"; that is
|
|
2402
|
+
// a benign interruption (the next generation re-runs the backfill), so don't log
|
|
2403
|
+
// it as an error — the outer catch returns quietly once the iterator also throws.
|
|
2404
|
+
attributeErrorReported[property] = true;
|
|
2405
|
+
if (Table.primaryStore?.rootStore?.status === 'closed')
|
|
2406
|
+
logger.debug(`Indexing attribute ${property} interrupted by store shutdown`, error);
|
|
2407
|
+
else logger.error(`Error indexing attribute ${property}`, error);
|
|
2136
2408
|
}
|
|
2137
|
-
}
|
|
2138
|
-
} catch (error) {
|
|
2139
|
-
hadIndexingErrors = true;
|
|
2140
|
-
if (!attributeErrorReported[property]) {
|
|
2141
|
-
// just report an indexing error once per attribute so we don't spam the logs.
|
|
2142
|
-
// A store closed by worker shutdown surfaces here as "Database not open"; that is
|
|
2143
|
-
// a benign interruption (the next generation re-runs the backfill), so don't log
|
|
2144
|
-
// it as an error — the outer catch returns quietly once the iterator also throws.
|
|
2145
|
-
attributeErrorReported[property] = true;
|
|
2146
|
-
if (Table.primaryStore?.rootStore?.status === 'closed')
|
|
2147
|
-
logger.debug(`Indexing attribute ${property} interrupted by store shutdown`, error);
|
|
2148
|
-
else logger.error(`Error indexing attribute ${property}`, error);
|
|
2149
2409
|
}
|
|
2150
2410
|
}
|
|
2151
2411
|
}
|
|
2152
|
-
when(
|
|
2153
|
-
lastResolution,
|
|
2154
|
-
() => outstanding--,
|
|
2155
|
-
(error) => {
|
|
2156
|
-
outstanding--;
|
|
2157
|
-
hadIndexingErrors = true;
|
|
2158
|
-
logger.error(error);
|
|
2159
|
-
}
|
|
2160
|
-
);
|
|
2161
2412
|
if (workerData && workerData.restartNumber !== manageThreads.restartNumber) {
|
|
2162
2413
|
interrupted = true;
|
|
2163
2414
|
}
|
|
2164
|
-
if (
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
}
|
|
2170
|
-
if (interrupted) return;
|
|
2415
|
+
if (interrupted) {
|
|
2416
|
+
await drainMutations();
|
|
2417
|
+
await checkpointing;
|
|
2418
|
+
await persistCheckpoint(key);
|
|
2419
|
+
return;
|
|
2171
2420
|
}
|
|
2172
|
-
if (
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2421
|
+
if (atInterval && indexed >= nextCheckpointRecord && performance.now() >= nextCheckpointAt) {
|
|
2422
|
+
nextCheckpointAt = performance.now() + indexingCheckpointPeriodMs;
|
|
2423
|
+
nextCheckpointRecord = indexed + indexingCheckpointMinRecords;
|
|
2424
|
+
await checkpointing;
|
|
2425
|
+
checkpointing = persistCheckpoint(key);
|
|
2426
|
+
}
|
|
2427
|
+
// Checked once per record, so a record's own fan-out can overshoot before the bound applies.
|
|
2428
|
+
while (pendingMutations.size > MAX_OUTSTANDING_INDEXING) await nextSettlement();
|
|
2429
|
+
if (atInterval || didSynchronousIndexing || pendingMutations.size > MIN_OUTSTANDING_INDEXING)
|
|
2430
|
+
await yieldEventTurn();
|
|
2176
2431
|
}
|
|
2177
2432
|
}
|
|
2178
|
-
|
|
2179
|
-
//
|
|
2180
|
-
|
|
2181
|
-
//
|
|
2182
|
-
//
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2433
|
+
await checkpointing;
|
|
2434
|
+
// A mutation that rejects after completion is declared has no build left to park.
|
|
2435
|
+
await drainMutations();
|
|
2436
|
+
// the tail since the last checkpoint is not durable until flushed; announcing the index complete
|
|
2437
|
+
// before that would outlive a crash that loses it
|
|
2438
|
+
if (!hadIndexingErrors) {
|
|
2439
|
+
try {
|
|
2440
|
+
await flushIndexStores(Table.primaryStore.rootStore);
|
|
2441
|
+
} catch (error) {
|
|
2442
|
+
hadIndexingErrors = true;
|
|
2443
|
+
logger.error(`Could not flush the indexes of ${Table.tableName} before marking them complete`, error);
|
|
2444
|
+
}
|
|
2190
2445
|
}
|
|
2191
|
-
// Yield one more event turn so any queued when() error callbacks (which fire as
|
|
2192
|
-
// microtasks when their tracked promise settles) have a chance to set hadIndexingErrors
|
|
2193
|
-
// before we decide whether to mark indexing as complete.
|
|
2194
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
2195
2446
|
if (hadIndexingErrors) {
|
|
2196
2447
|
// Some records failed to index. Persist the failure marker in the descriptor so
|
|
2197
2448
|
// the next call to table() (including after a restart with a fresh PID) re-triggers
|
|
@@ -2220,9 +2471,15 @@ async function runIndexing(Table, attributes, indicesToRemove) {
|
|
|
2220
2471
|
// update the attributes to indicate that we are finished
|
|
2221
2472
|
for (const attribute of attributes) {
|
|
2222
2473
|
delete attribute.lastIndexedKey;
|
|
2474
|
+
delete attribute.checkpointCertified;
|
|
2475
|
+
// Survives completion, unlike the checkpoint fields: without it an index built here is
|
|
2476
|
+
// indistinguishable from one a release that could skip a failed record declared complete.
|
|
2477
|
+
attribute.checkpointAlgorithm = CHECKPOINT_ALGORITHM;
|
|
2223
2478
|
delete attribute.indexingPID;
|
|
2224
2479
|
delete attribute.indexingFailed;
|
|
2225
2480
|
delete attribute.restartNumber;
|
|
2481
|
+
delete attribute.indexingIncarnation;
|
|
2482
|
+
delete attribute.indexingBuildId;
|
|
2226
2483
|
attribute.dbi.isIndexing = false;
|
|
2227
2484
|
// Also clear isIndexing on the currently-active dbi in Table.indices, which may
|
|
2228
2485
|
// differ from attribute.dbi if a resetDatabases() call during this migration
|
|
@@ -2239,6 +2496,7 @@ async function runIndexing(Table, attributes, indicesToRemove) {
|
|
|
2239
2496
|
logger.info(`Finished indexing ${Table.tableName} attributes`, attributes);
|
|
2240
2497
|
}
|
|
2241
2498
|
} catch (error) {
|
|
2499
|
+
await checkpointing;
|
|
2242
2500
|
// A worker shutting down closes its stores mid-backfill, so the range iterator or a
|
|
2243
2501
|
// put throws (e.g. "Database not open" / "Iterator not initialized"). This is an
|
|
2244
2502
|
// interruption, not a data error: the next worker generation re-runs the backfill via
|