@harperfast/harper 5.2.9 → 5.2.10
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 +9 -0
- package/dist/resources/databases.js +315 -76
- 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 +296 -70
- package/resources/search.ts +32 -7
- package/server/storageReclamation.ts +10 -0
- package/server/threads/manageThreads.js +7 -0
- package/studio/web/assets/Chat-Br06zdMA.js +2267 -0
- package/studio/web/assets/FloatingChat-BWImX5fA.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-DN0nmDsq.js} +1 -1
- package/studio/web/assets/applications-kSxVoyeU.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-C1JeufH5.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-qoo9CrGO.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-Dt4IIy04.js} +1 -1
- package/studio/web/assets/{htmlMode-DyO31v-P.js → htmlMode-DXgKKr4C.js} +1 -1
- package/studio/web/assets/index-6onkYFOG.js +824 -0
- package/studio/web/assets/{index-Cxj2_wsl.css → index-7RMEgVG1.css} +1 -1
- package/studio/web/assets/index.lazy-BrCFnpNJ.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-DNCQGUBc.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-CR6HWruP.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-BM4fI4rS.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-B4pCF1zO.js → lspLanguageFeatures-DSa1ttcD.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-D3tIQ4sg.js → notifications-BHXLnh6x.js} +1 -1
- package/studio/web/assets/notifications-CUmtIA6z.js +1 -0
- 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-8BeFSF3j.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-C8qR0HhD.js} +1 -1
- package/studio/web/assets/{register-BUyhWjBO.js → register-B4n5i0SD.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-g0_B0lgX.js} +1 -1
- package/studio/web/assets/{setup-D2kn7cAA.js → setup-B0CTj_Q5.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-C6Yib7-K.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-Dgta7bVi.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-CH_jHvU-.js} +1 -1
- package/studio/web/assets/{typescript-DApRQir3.js → typescript-Co9LCXd5.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-DB6JStU1.js → useEntityRestURL-wKC8NsC_.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-CWeLxCXA.js} +1 -1
- package/studio/web/assets/x-DIzaLEdK.js +1 -0
- package/studio/web/assets/{xml-HWd01lU-.js → xml-BadC-0Rk.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-BiUfxPbC.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';
|
|
@@ -348,6 +350,20 @@ _assignPackageExport('databases', databases);
|
|
|
348
350
|
_assignPackageExport('tables', tables);
|
|
349
351
|
|
|
350
352
|
const NEXT_TABLE_ID = Symbol.for('next-table-id');
|
|
353
|
+
/**
|
|
354
|
+
* True when a descriptor claims an index build no live operation in this process can own. The PID and
|
|
355
|
+
* worker generation cannot answer that alone: a container reuses PID 1 and starts the in-memory
|
|
356
|
+
* generation back at 1 while the persisted one is higher. A descriptor with no incarnation was written
|
|
357
|
+
* before the field existed, so it belongs to an earlier process; a thread started without one of its
|
|
358
|
+
* own cannot judge, and falls back rather than declaring a live build dead.
|
|
359
|
+
*/
|
|
360
|
+
function isAbandonedIndexBuild(descriptor: any, currentRestartGeneration: number): boolean {
|
|
361
|
+
if (!descriptor) return false;
|
|
362
|
+
if (descriptor.indexingPID && descriptor.indexingPID !== process.pid) return true;
|
|
363
|
+
if (descriptor.restartNumber < currentRestartGeneration) return true;
|
|
364
|
+
const incarnation = manageThreads.processIncarnation;
|
|
365
|
+
return !!descriptor.indexingPID && incarnation != null && descriptor.indexingIncarnation !== incarnation;
|
|
366
|
+
}
|
|
351
367
|
// How many times the schema load will try to finish a tombstoned drop before
|
|
352
368
|
// giving up for the rest of this process's lifetime. A drop that fails once
|
|
353
369
|
// almost always fails identically forever - the usual cause is a RocksDB
|
|
@@ -637,6 +653,7 @@ export function readMetaDb(
|
|
|
637
653
|
lmdbDatabaseEnvs.set(path, rootStore);
|
|
638
654
|
}
|
|
639
655
|
|
|
656
|
+
rootStore.dbisDb?.resetReadTxn();
|
|
640
657
|
return initStores(path, rootStore, databaseName, defaultTable, auditPath, isLegacy);
|
|
641
658
|
} catch (error) {
|
|
642
659
|
error.message += ` opening database ${path}`;
|
|
@@ -896,6 +913,8 @@ function initStores(
|
|
|
896
913
|
indices[attribute.name] = dbi;
|
|
897
914
|
indices[attribute.name].indexNulls = attribute.indexNulls;
|
|
898
915
|
}
|
|
916
|
+
// the only way a thread that never declares the schema reaches Table.indices
|
|
917
|
+
indices[attribute.name].isIndexing = !!attribute.indexingPID;
|
|
899
918
|
const existingAttribute = existingAttributes.find(
|
|
900
919
|
(existingAttribute) => existingAttribute.name === attribute.name
|
|
901
920
|
);
|
|
@@ -1253,6 +1272,10 @@ export async function dropDatabase(databaseName) {
|
|
|
1253
1272
|
databaseEventsEmitter.emit('dropDatabase', databaseName);
|
|
1254
1273
|
|
|
1255
1274
|
if (rootStore) {
|
|
1275
|
+
// awaited: retirement stops the loop admitting work, the barrier is what says the pass that was
|
|
1276
|
+
// already running has released the stores this is about to close and unlink
|
|
1277
|
+
await rootStore.auditStore?.stopAuditCleanup?.();
|
|
1278
|
+
removeStorageReclamation(rootStore.path);
|
|
1256
1279
|
if (rootStore.status === 'open') {
|
|
1257
1280
|
if (rootStore instanceof RocksDatabase) {
|
|
1258
1281
|
rootStore.close();
|
|
@@ -1267,6 +1290,8 @@ export async function dropDatabase(databaseName) {
|
|
|
1267
1290
|
// a tableless database resolves its root store here rather than in the loop above, so take
|
|
1268
1291
|
// the drop lock now (still before any destructive step)
|
|
1269
1292
|
if (rootStore instanceof RocksDatabase) lockDatabaseForDrop(rootStore.path, databaseName, restoreLocks);
|
|
1293
|
+
await rootStore.auditStore?.stopAuditCleanup?.();
|
|
1294
|
+
removeStorageReclamation(rootStore.path);
|
|
1270
1295
|
if (rootStore instanceof RocksDatabase) {
|
|
1271
1296
|
rootStore.close();
|
|
1272
1297
|
rootStore.destroy();
|
|
@@ -1304,17 +1329,27 @@ export function closeDatabase(databaseName: string): boolean {
|
|
|
1304
1329
|
const table: any = dbTables[tableName];
|
|
1305
1330
|
if (!table?.primaryStore) continue;
|
|
1306
1331
|
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
1332
|
}
|
|
1312
1333
|
// a database with no tables (an empty schema, or one whose tables were all dropped) still holds
|
|
1313
1334
|
// an open root store, tracked only on the defined-database entry rather than any table — include
|
|
1314
1335
|
// it so its handles are released too (the Set dedupes it against the per-table root stores above)
|
|
1315
1336
|
const definedRoot = (definedDatabases?.get(databaseName) as any)?.rootStore;
|
|
1316
1337
|
if (definedRoot) rootStores.add(definedRoot);
|
|
1338
|
+
// before any table store closes, so no further pass is admitted. This is synchronous, so it cannot
|
|
1339
|
+
// await the drain barrier stopAuditCleanup() returns; what covers it is the in-pass status checks,
|
|
1340
|
+
// plus the fact that its production callers reach it only for RocksDB databases, whose pass is one
|
|
1341
|
+
// synchronous purgeLogs() call with nothing suspended mid-removal.
|
|
1342
|
+
for (const rootStore of rootStores) rootStore.auditStore?.stopAuditCleanup?.();
|
|
1343
|
+
for (const tableName in dbTables) {
|
|
1344
|
+
const table: any = dbTables[tableName];
|
|
1345
|
+
if (!table?.primaryStore) continue;
|
|
1346
|
+
for (const indexName in table.indices || {}) {
|
|
1347
|
+
closeStore(table.indices[indexName], `index ${tableName}.${indexName}`);
|
|
1348
|
+
}
|
|
1349
|
+
closeStore(table.primaryStore, `table ${tableName}`);
|
|
1350
|
+
}
|
|
1317
1351
|
for (const rootStore of rootStores) {
|
|
1352
|
+
removeStorageReclamation(rootStore.path);
|
|
1318
1353
|
closeStore(rootStore.dbisDb, 'attributes store');
|
|
1319
1354
|
closeStore(rootStore, 'root store');
|
|
1320
1355
|
lmdbDatabaseEnvs.delete(rootStore.path);
|
|
@@ -1882,8 +1917,7 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
1882
1917
|
changed ||
|
|
1883
1918
|
indexFormatNeedsPersist ||
|
|
1884
1919
|
attributeDescriptor?.indexingFailed ||
|
|
1885
|
-
(attributeDescriptor
|
|
1886
|
-
attributeDescriptor?.restartNumber < currentRestartGeneration
|
|
1920
|
+
isAbandonedIndexBuild(attributeDescriptor, currentRestartGeneration)
|
|
1887
1921
|
) {
|
|
1888
1922
|
hasChanges = true;
|
|
1889
1923
|
exclusiveLock();
|
|
@@ -1891,8 +1925,7 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
1891
1925
|
if (
|
|
1892
1926
|
structurallyChanged ||
|
|
1893
1927
|
attributeDescriptor?.indexingFailed ||
|
|
1894
|
-
(attributeDescriptor
|
|
1895
|
-
attributeDescriptor?.restartNumber < currentRestartGeneration
|
|
1928
|
+
isAbandonedIndexBuild(attributeDescriptor, currentRestartGeneration)
|
|
1896
1929
|
) {
|
|
1897
1930
|
hasChanges = true;
|
|
1898
1931
|
if (attribute.indexNulls === undefined) attribute.indexNulls = true;
|
|
@@ -1912,9 +1945,17 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
1912
1945
|
// resumes rather than restarts. Canonicalized to match structurallyChanged above.
|
|
1913
1946
|
const indexOptionsChanged =
|
|
1914
1947
|
canonicalIndexKey(attributeDescriptor?.indexed) !== canonicalIndexKey(attribute.indexed);
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1948
|
+
// Only a checkpoint runIndexing stamped with its own key resumes: earlier releases advanced
|
|
1949
|
+
// lastIndexedKey past failed and unflushed index writes, so any other is a full rebuild.
|
|
1950
|
+
const uncertifiedCheckpoint =
|
|
1951
|
+
attributeDescriptor?.lastIndexedKey !== undefined &&
|
|
1952
|
+
(attributeDescriptor.checkpointCertified === undefined ||
|
|
1953
|
+
compareKeys(attributeDescriptor.checkpointCertified, attributeDescriptor.lastIndexedKey) !== 0);
|
|
1954
|
+
attribute.lastIndexedKey =
|
|
1955
|
+
indexOptionsChanged || uncertifiedCheckpoint
|
|
1956
|
+
? undefined
|
|
1957
|
+
: (attributeDescriptor?.lastIndexedKey ?? undefined);
|
|
1958
|
+
if (attribute.lastIndexedKey !== undefined) attribute.checkpointCertified = attribute.lastIndexedKey;
|
|
1918
1959
|
// Explicit reindex is the upgrade path from a legacy (un-versioned) custom-index
|
|
1919
1960
|
// object store to the versioned, VT-cacheable format. A full rebuild from scratch
|
|
1920
1961
|
// (lastIndexedKey === undefined) clears the store and rewrites every node, so the
|
|
@@ -1938,6 +1979,9 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
1938
1979
|
// the new process reuses the old PID. Cleared on clean completion; left in place
|
|
1939
1980
|
// on failure/crash so the next, higher-numbered restart re-triggers the backfill.
|
|
1940
1981
|
attribute.restartNumber = currentRestartGeneration;
|
|
1982
|
+
if (manageThreads.processIncarnation != null)
|
|
1983
|
+
attribute.indexingIncarnation = manageThreads.processIncarnation;
|
|
1984
|
+
attribute.indexingBuildId = randomBytes(8).toString('hex');
|
|
1941
1985
|
delete attribute.indexingFailed; // clear failure flag for the new run
|
|
1942
1986
|
dbi.isIndexing = true;
|
|
1943
1987
|
Object.defineProperty(attribute, 'dbi', { value: dbi, configurable: true, enumerable: false });
|
|
@@ -1951,6 +1995,13 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
1951
1995
|
if (attributeDescriptor?.indexingPID && attributeDescriptor.indexingPID !== process.pid)
|
|
1952
1996
|
reindexReasons.push(`crash-recovery(pid=${attributeDescriptor.indexingPID})`);
|
|
1953
1997
|
if (attributeDescriptor?.restartNumber < currentRestartGeneration) reindexReasons.push('restart-number');
|
|
1998
|
+
if (uncertifiedCheckpoint) reindexReasons.push('uncertified-checkpoint');
|
|
1999
|
+
if (
|
|
2000
|
+
attributeDescriptor?.indexingPID === process.pid &&
|
|
2001
|
+
manageThreads.processIncarnation != null &&
|
|
2002
|
+
attributeDescriptor.indexingIncarnation !== manageThreads.processIncarnation
|
|
2003
|
+
)
|
|
2004
|
+
reindexReasons.push('abandoned-build(previous process incarnation)');
|
|
1954
2005
|
logger.info(
|
|
1955
2006
|
`reindex ${databaseName}.${tableName}.${attribute.name}: reason=${reindexReasons.join(',') || 'unknown'}`
|
|
1956
2007
|
);
|
|
@@ -1964,9 +2015,13 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
1964
2015
|
// workers / a reload would treat the still-partial index as ready and return incomplete results.
|
|
1965
2016
|
attribute.indexingPID = attributeDescriptor.indexingPID;
|
|
1966
2017
|
attribute.lastIndexedKey = attributeDescriptor.lastIndexedKey;
|
|
2018
|
+
if (attributeDescriptor.checkpointCertified !== undefined)
|
|
2019
|
+
attribute.checkpointCertified = attributeDescriptor.checkpointCertified;
|
|
1967
2020
|
// Carry the in-progress restart generation too, so persisting this metadata-only
|
|
1968
2021
|
// change doesn't drop it and break the crash-recovery trigger for the running backfill.
|
|
1969
2022
|
attribute.restartNumber = attributeDescriptor.restartNumber;
|
|
2023
|
+
attribute.indexingIncarnation = attributeDescriptor.indexingIncarnation;
|
|
2024
|
+
attribute.indexingBuildId = attributeDescriptor.indexingBuildId;
|
|
1970
2025
|
if (attributeDescriptor.indexingFailed) attribute.indexingFailed = attributeDescriptor.indexingFailed;
|
|
1971
2026
|
}
|
|
1972
2027
|
attributesDbi.put(dbiKey, attribute);
|
|
@@ -1994,7 +2049,10 @@ export function table<TableResourceType>(tableDefinition: TableDefinition): Tabl
|
|
|
1994
2049
|
}
|
|
1995
2050
|
logger.trace(`${tableName} table loading, running index`);
|
|
1996
2051
|
if (attributesToIndex.length > 0 || indicesToRemove.length > 0) {
|
|
1997
|
-
|
|
2052
|
+
// captured before the backfill can rewrite the attributes
|
|
2053
|
+
const buildIds = new Map(attributesToIndex.map((attribute) => [attribute, attribute.indexingBuildId]));
|
|
2054
|
+
const markSettled = () => markAbandonedIndexBuild(Table, rootStore, buildIds);
|
|
2055
|
+
Table.indexingOperation = runIndexing(Table, attributesToIndex, indicesToRemove).then(markSettled, markSettled);
|
|
1998
2056
|
} else if (hasChanges)
|
|
1999
2057
|
signalling.signalSchemaChange(
|
|
2000
2058
|
new SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName)
|
|
@@ -2066,7 +2124,139 @@ export function canonicalizeIndexOptions(value: any): any {
|
|
|
2066
2124
|
}
|
|
2067
2125
|
const MAX_OUTSTANDING_INDEXING = 1000;
|
|
2068
2126
|
const MIN_OUTSTANDING_INDEXING = 10;
|
|
2127
|
+
const INDEXING_YIELD_INTERVAL = 100;
|
|
2128
|
+
// A resumable checkpoint is written only after a flush (see flushIndexStores), at most once per period
|
|
2129
|
+
// and never before this many more records: the flush seals every column family in the database, so a
|
|
2130
|
+
// slow backfill must not impose the period's flush rate on unrelated tables.
|
|
2131
|
+
let indexingCheckpointPeriodMs = 5000;
|
|
2132
|
+
let indexingCheckpointMinRecords = 10000;
|
|
2133
|
+
export function setIndexingCheckpointPeriod(ms: number, minRecords = indexingCheckpointMinRecords) {
|
|
2134
|
+
const previous = { ms: indexingCheckpointPeriodMs, minRecords: indexingCheckpointMinRecords };
|
|
2135
|
+
indexingCheckpointPeriodMs = ms;
|
|
2136
|
+
indexingCheckpointMinRecords = minRecords;
|
|
2137
|
+
return previous;
|
|
2138
|
+
}
|
|
2139
|
+
const yieldEventTurn = () => new Promise((resolve) => setImmediate(resolve));
|
|
2140
|
+
// RocksDB index stores have no WAL (openRocksDatabase defaults disableWAL), so a flush is what makes the
|
|
2141
|
+
// entries a checkpoint certifies durable. A flush only covers writes issued before it started, so a caller
|
|
2142
|
+
// never joins one in flight: it joins the next one, which every backfill on that database asking meanwhile
|
|
2143
|
+
// shares — at most one in flight and one queued.
|
|
2144
|
+
const indexingFlushes = new WeakMap<object, { inFlight?: Promise<void>; queued?: Promise<void> }>();
|
|
2145
|
+
function flushIndexStores(rootStore: any): Promise<void> | undefined {
|
|
2146
|
+
if (!(rootStore instanceof RocksDatabase)) return;
|
|
2147
|
+
let flushes = indexingFlushes.get(rootStore);
|
|
2148
|
+
if (!flushes) indexingFlushes.set(rootStore, (flushes = {}));
|
|
2149
|
+
if (flushes.queued) return flushes.queued;
|
|
2150
|
+
const start = () => {
|
|
2151
|
+
flushes.queued = undefined;
|
|
2152
|
+
const flush = rootStore.flush().finally(() => {
|
|
2153
|
+
if (flushes.inFlight === flush) flushes.inFlight = undefined;
|
|
2154
|
+
});
|
|
2155
|
+
flushes.inFlight = flush;
|
|
2156
|
+
return flush;
|
|
2157
|
+
};
|
|
2158
|
+
if (!flushes.inFlight) return start();
|
|
2159
|
+
return (flushes.queued = flushes.inFlight.then(start, start));
|
|
2160
|
+
}
|
|
2161
|
+
export function resumeStartKey(attributes: { lastIndexedKey?: any }[]): any {
|
|
2162
|
+
let start: any;
|
|
2163
|
+
for (const attribute of attributes) {
|
|
2164
|
+
if (attribute.lastIndexedKey == undefined) return undefined;
|
|
2165
|
+
if (start === undefined || compareKeys(attribute.lastIndexedKey, start) < 0) start = attribute.lastIndexedKey;
|
|
2166
|
+
}
|
|
2167
|
+
return start;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
// Bounded, unlike the exclusiveLock() spin above: that one runs on the declaring path, where the
|
|
2171
|
+
// caller is waiting on the result and there is nothing useful to do without the lock. This one runs
|
|
2172
|
+
// after a backfill has already settled, so a holder that never releases would wedge the worker's
|
|
2173
|
+
// event loop for nothing. Giving up costs only the marker, and the next load of the table
|
|
2174
|
+
// re-triggers the build regardless.
|
|
2175
|
+
export const ABANDONED_MARK_LOCK_TIMEOUT = 10000;
|
|
2176
|
+
const abandonedMarkLockWait = new Int32Array(new SharedArrayBuffer(4));
|
|
2177
|
+
// `timeout` is the test seam; production callers take the default.
|
|
2178
|
+
export function tryAcquireUpdateAttributesLock(
|
|
2179
|
+
rootStore: RocksDatabase,
|
|
2180
|
+
timeout = ABANDONED_MARK_LOCK_TIMEOUT
|
|
2181
|
+
): boolean {
|
|
2182
|
+
if (rootStore.tryLock('update-attributes')) return true;
|
|
2183
|
+
const startTime = performance.now();
|
|
2184
|
+
let waitTime = 1;
|
|
2185
|
+
while (!rootStore.tryLock('update-attributes')) {
|
|
2186
|
+
const elapsed = performance.now() - startTime;
|
|
2187
|
+
if (elapsed >= timeout) return false;
|
|
2188
|
+
// Atomics.wait rather than a busy spin: the section this guards is synchronous, so the wait
|
|
2189
|
+
// blocks this thread either way, but sleeping does not burn a core while it does.
|
|
2190
|
+
if (elapsed >= 2) {
|
|
2191
|
+
Atomics.wait(abandonedMarkLockWait, 0, 0, Math.min(waitTime, timeout - elapsed));
|
|
2192
|
+
if (waitTime < 16) waitTime *= 2;
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
return true;
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
/**
|
|
2199
|
+
* Persists the failure marker for a build that ended without running one of runIndexing's own exit
|
|
2200
|
+
* paths, so something re-triggers it. Fenced on `indexingBuildId` inside the storage engine's catalog
|
|
2201
|
+
* serialization boundary, because a replacement generation (or another thread declaring different index
|
|
2202
|
+
* options) can claim the attribute before an outgoing build's promise settles, and marking that would fail
|
|
2203
|
+
* a live build. The fence read and write stay synchronous, and nothing here may throw because
|
|
2204
|
+
* `Table.indexingOperation` reaches operations-API callers.
|
|
2205
|
+
*/
|
|
2206
|
+
async function markAbandonedIndexBuild(Table, rootStore, buildIds: Map<any, string>) {
|
|
2207
|
+
for (const [attribute, buildId] of buildIds) {
|
|
2208
|
+
try {
|
|
2209
|
+
let marked;
|
|
2210
|
+
if (buildId == null || Table.dbisDB.getSync(attribute.key)?.indexingBuildId !== buildId) continue;
|
|
2211
|
+
const markIfOwned = () => {
|
|
2212
|
+
const descriptor = Table.dbisDB.getSync(attribute.key);
|
|
2213
|
+
if (descriptor?.indexingBuildId === buildId && !descriptor.indexingFailed) {
|
|
2214
|
+
Table.dbisDB.putSync(attribute.key, { ...descriptor, indexingFailed: true });
|
|
2215
|
+
marked = true;
|
|
2216
|
+
}
|
|
2217
|
+
};
|
|
2218
|
+
if (rootStore instanceof RocksDatabase) {
|
|
2219
|
+
if (!tryAcquireUpdateAttributesLock(rootStore)) {
|
|
2220
|
+
logger.warn(
|
|
2221
|
+
`Could not mark the abandoned index build of ${Table.databaseName}.${Table.tableName}.${attribute.name}: ` +
|
|
2222
|
+
`timed out after ${ABANDONED_MARK_LOCK_TIMEOUT}ms waiting for the exclusive 'update-attributes' lock. ` +
|
|
2223
|
+
`The index stays incomplete and the next load of the table re-triggers the backfill.`
|
|
2224
|
+
);
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
try {
|
|
2228
|
+
markIfOwned();
|
|
2229
|
+
} finally {
|
|
2230
|
+
rootStore.unlock('update-attributes');
|
|
2231
|
+
}
|
|
2232
|
+
} else {
|
|
2233
|
+
rootStore.transactionSync(markIfOwned);
|
|
2234
|
+
}
|
|
2235
|
+
if (marked)
|
|
2236
|
+
logger.warn(
|
|
2237
|
+
`Indexing of ${Table.databaseName}.${Table.tableName}.${attribute.name} ended without completing. ` +
|
|
2238
|
+
`The index stays incomplete and every query on the attribute reports it as not indexed yet; ` +
|
|
2239
|
+
`the next load of the table retries the backfill from the last checkpoint (indexingFailed=true).`
|
|
2240
|
+
);
|
|
2241
|
+
} catch (error) {
|
|
2242
|
+
// A store closed by shutdown is the common case, and it cannot be written to at all.
|
|
2243
|
+
try {
|
|
2244
|
+
logger.debug(`Could not mark the abandoned index build of ${Table.tableName}.${attribute.name}`, error);
|
|
2245
|
+
} catch {}
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2069
2249
|
async function runIndexing(Table, attributes, indicesToRemove) {
|
|
2250
|
+
let checkpointing;
|
|
2251
|
+
let hadIndexingErrors = false;
|
|
2252
|
+
const attributeErrorReported = {};
|
|
2253
|
+
const onIndexPutRejected = (property, error) => {
|
|
2254
|
+
hadIndexingErrors = true;
|
|
2255
|
+
if (attributeErrorReported[property]) return;
|
|
2256
|
+
attributeErrorReported[property] = true;
|
|
2257
|
+
logger.error(`Error indexing attribute ${property}`, error);
|
|
2258
|
+
};
|
|
2259
|
+
const putRejectionHandlers = attributes.map((attribute) => (error) => onIndexPutRejected(attribute.name, error));
|
|
2070
2260
|
try {
|
|
2071
2261
|
logger.info(`Indexing ${Table.tableName} attributes`, attributes);
|
|
2072
2262
|
await signalling.signalSchemaChange(
|
|
@@ -2075,20 +2265,16 @@ async function runIndexing(Table, attributes, indicesToRemove) {
|
|
|
2075
2265
|
let lastResolution;
|
|
2076
2266
|
for (const index of indicesToRemove) {
|
|
2077
2267
|
lastResolution = index.drop();
|
|
2268
|
+
if (lastResolution?.then) lastResolution.then(undefined, (error) => onIndexPutRejected(index.name, error));
|
|
2078
2269
|
}
|
|
2079
2270
|
let interrupted;
|
|
2080
|
-
let hadIndexingErrors = false;
|
|
2081
|
-
const attributeErrorReported = {};
|
|
2082
2271
|
let indexed = 0;
|
|
2083
2272
|
const attributesLength = attributes.length;
|
|
2084
2273
|
await new Promise((resolve) => setImmediate(resolve)); // yield event turn, indexing should consistently take at least one event turn
|
|
2085
2274
|
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
|
|
2275
|
+
const start = resumeStartKey(attributes);
|
|
2276
|
+
if (start === undefined) {
|
|
2277
|
+
for (const attribute of attributes) {
|
|
2092
2278
|
if (attribute.dbi.clearAsync) {
|
|
2093
2279
|
// LMDB, note that we don't need to wait for this to complete, just gets enqueued in front of the other writes
|
|
2094
2280
|
attribute.dbi.clearAsync();
|
|
@@ -2098,6 +2284,26 @@ async function runIndexing(Table, attributes, indicesToRemove) {
|
|
|
2098
2284
|
}
|
|
2099
2285
|
}
|
|
2100
2286
|
let outstanding = 0;
|
|
2287
|
+
// A resumed scan starts at the checkpoint, so it must only name a key whose every predecessor is
|
|
2288
|
+
// durably indexed: persisted once the writes it covers have settled and flushed, frozen after any
|
|
2289
|
+
// record fails so the retry re-covers it, and stamped with its own key (see the trigger in table()).
|
|
2290
|
+
const persistCheckpoint = async (key) => {
|
|
2291
|
+
if (hadIndexingErrors) return;
|
|
2292
|
+
try {
|
|
2293
|
+
await flushIndexStores(Table.primaryStore.rootStore);
|
|
2294
|
+
const puts = [];
|
|
2295
|
+
for (const attribute of attributes) {
|
|
2296
|
+
attribute.lastIndexedKey = key;
|
|
2297
|
+
attribute.checkpointCertified = key;
|
|
2298
|
+
puts.push(Table.dbisDB.put(attribute.key, attribute));
|
|
2299
|
+
}
|
|
2300
|
+
await Promise.all(puts);
|
|
2301
|
+
} catch (error) {
|
|
2302
|
+
logger.warn(`Could not persist the indexing checkpoint for ${Table.tableName}`, error);
|
|
2303
|
+
}
|
|
2304
|
+
};
|
|
2305
|
+
let nextCheckpointAt = performance.now() + indexingCheckpointPeriodMs;
|
|
2306
|
+
let nextCheckpointRecord = indexingCheckpointMinRecords;
|
|
2101
2307
|
// this means that a new attribute has been introduced that needs to be indexed
|
|
2102
2308
|
for (const { key, value: record } of Table.primaryStore.getRange({
|
|
2103
2309
|
start,
|
|
@@ -2105,7 +2311,7 @@ async function runIndexing(Table, attributes, indicesToRemove) {
|
|
|
2105
2311
|
versions: true,
|
|
2106
2312
|
snapshot: false, // don't hold a read transaction this whole time
|
|
2107
2313
|
})) {
|
|
2108
|
-
|
|
2314
|
+
const atInterval = ++indexed % INDEXING_YIELD_INTERVAL === 0;
|
|
2109
2315
|
// TODO: Do we ever need to interrupt due to a schema change that was not a restart?
|
|
2110
2316
|
//if (Table.schemaVersion !== schemaVersion) return; // break out if there are any schema changes and let someone else pick it up
|
|
2111
2317
|
outstanding++;
|
|
@@ -2117,71 +2323,77 @@ async function runIndexing(Table, attributes, indicesToRemove) {
|
|
|
2117
2323
|
// we index, that's fine because indexing is idempotent, we can just put the same values again. If it changes
|
|
2118
2324
|
// during the indexing, the indexing here will fail. This is also fine because it means the other thread will have
|
|
2119
2325
|
// 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
|
-
|
|
2326
|
+
if (record) {
|
|
2327
|
+
for (let i = 0; i < attributesLength; i++) {
|
|
2328
|
+
const attribute = attributes[i];
|
|
2329
|
+
const property = attribute.name;
|
|
2330
|
+
const index = attribute.dbi;
|
|
2331
|
+
const onPutRejected = putRejectionHandlers[i];
|
|
2332
|
+
try {
|
|
2333
|
+
const resolver = attribute.resolve;
|
|
2334
|
+
const value = record && (resolver ? resolver(record) : record[property]);
|
|
2335
|
+
if (index.customIndex) {
|
|
2336
|
+
index.customIndex.index(key, value);
|
|
2337
|
+
didSynchronousIndexing = true;
|
|
2338
|
+
continue;
|
|
2339
|
+
}
|
|
2340
|
+
const values = getIndexedValues(value, index.indexNulls);
|
|
2341
|
+
if (values) {
|
|
2342
|
+
for (let i = 0, l = values.length; i < l; i++) {
|
|
2343
|
+
lastResolution = index.put(values[i], key);
|
|
2344
|
+
if (lastResolution?.then) lastResolution.then(undefined, onPutRejected);
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
} catch (error) {
|
|
2348
|
+
hadIndexingErrors = true;
|
|
2349
|
+
if (!attributeErrorReported[property]) {
|
|
2350
|
+
// just report an indexing error once per attribute so we don't spam the logs.
|
|
2351
|
+
// A store closed by worker shutdown surfaces here as "Database not open"; that is
|
|
2352
|
+
// a benign interruption (the next generation re-runs the backfill), so don't log
|
|
2353
|
+
// it as an error — the outer catch returns quietly once the iterator also throws.
|
|
2354
|
+
attributeErrorReported[property] = true;
|
|
2355
|
+
if (Table.primaryStore?.rootStore?.status === 'closed')
|
|
2356
|
+
logger.debug(`Indexing attribute ${property} interrupted by store shutdown`, error);
|
|
2357
|
+
else logger.error(`Error indexing attribute ${property}`, error);
|
|
2136
2358
|
}
|
|
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
2359
|
}
|
|
2150
2360
|
}
|
|
2151
2361
|
}
|
|
2152
2362
|
when(
|
|
2153
2363
|
lastResolution,
|
|
2154
2364
|
() => outstanding--,
|
|
2155
|
-
(
|
|
2156
|
-
outstanding--;
|
|
2157
|
-
hadIndexingErrors = true;
|
|
2158
|
-
logger.error(error);
|
|
2159
|
-
}
|
|
2365
|
+
() => outstanding--
|
|
2160
2366
|
);
|
|
2161
2367
|
if (workerData && workerData.restartNumber !== manageThreads.restartNumber) {
|
|
2162
2368
|
interrupted = true;
|
|
2163
2369
|
}
|
|
2164
|
-
if (
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2370
|
+
if (interrupted) {
|
|
2371
|
+
try {
|
|
2372
|
+
await lastResolution;
|
|
2373
|
+
} catch {
|
|
2374
|
+
// already counted and logged by the rejection handler above
|
|
2169
2375
|
}
|
|
2170
|
-
|
|
2376
|
+
await checkpointing;
|
|
2377
|
+
await persistCheckpoint(key);
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
if (atInterval && indexed >= nextCheckpointRecord && performance.now() >= nextCheckpointAt) {
|
|
2381
|
+
nextCheckpointAt = performance.now() + indexingCheckpointPeriodMs;
|
|
2382
|
+
nextCheckpointRecord = indexed + indexingCheckpointMinRecords;
|
|
2383
|
+
await checkpointing;
|
|
2384
|
+
checkpointing = when(
|
|
2385
|
+
lastResolution,
|
|
2386
|
+
() => persistCheckpoint(key),
|
|
2387
|
+
() => {}
|
|
2388
|
+
);
|
|
2171
2389
|
}
|
|
2172
2390
|
if (outstanding > MAX_OUTSTANDING_INDEXING) await lastResolution;
|
|
2173
|
-
|
|
2174
|
-
await new Promise((resolve) => setImmediate(resolve)); // yield event turn, don't want to use all computation
|
|
2175
|
-
else if (didSynchronousIndexing) await new Promise((resolve) => setImmediate(resolve)); // custom indexes (e.g. HNSW) index synchronously and never raise `outstanding`; without this yield a large backfill runs in a single event-loop turn, starving keepalive/replication and queries and never letting the isIndexing flag be observed
|
|
2391
|
+
if (atInterval || didSynchronousIndexing || outstanding > MIN_OUTSTANDING_INDEXING) await yieldEventTurn();
|
|
2176
2392
|
}
|
|
2177
2393
|
}
|
|
2178
|
-
|
|
2179
|
-
//
|
|
2180
|
-
//
|
|
2181
|
-
// itself rejects (i.e. the very last put in the loop failed) which would otherwise
|
|
2182
|
-
// throw past the hadIndexingErrors check to the outer catch. The broader issue of
|
|
2183
|
-
// unhandled rejections from non-last puts in multi-value attributes is pre-existing
|
|
2184
|
-
// and out of scope for this fix.
|
|
2394
|
+
await checkpointing;
|
|
2395
|
+
// Await the last pending put. If it rejects, that is also an indexing error (already counted by
|
|
2396
|
+
// onIndexPutRejected); catching it here keeps it from escaping to the outer catch.
|
|
2185
2397
|
try {
|
|
2186
2398
|
await lastResolution;
|
|
2187
2399
|
} catch (error) {
|
|
@@ -2192,6 +2404,16 @@ async function runIndexing(Table, attributes, indicesToRemove) {
|
|
|
2192
2404
|
// microtasks when their tracked promise settles) have a chance to set hadIndexingErrors
|
|
2193
2405
|
// before we decide whether to mark indexing as complete.
|
|
2194
2406
|
await new Promise((resolve) => setImmediate(resolve));
|
|
2407
|
+
// the tail since the last checkpoint is not durable until flushed; announcing the index complete
|
|
2408
|
+
// before that would outlive a crash that loses it
|
|
2409
|
+
if (!hadIndexingErrors) {
|
|
2410
|
+
try {
|
|
2411
|
+
await flushIndexStores(Table.primaryStore.rootStore);
|
|
2412
|
+
} catch (error) {
|
|
2413
|
+
hadIndexingErrors = true;
|
|
2414
|
+
logger.error(`Could not flush the indexes of ${Table.tableName} before marking them complete`, error);
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2195
2417
|
if (hadIndexingErrors) {
|
|
2196
2418
|
// Some records failed to index. Persist the failure marker in the descriptor so
|
|
2197
2419
|
// the next call to table() (including after a restart with a fresh PID) re-triggers
|
|
@@ -2220,9 +2442,12 @@ async function runIndexing(Table, attributes, indicesToRemove) {
|
|
|
2220
2442
|
// update the attributes to indicate that we are finished
|
|
2221
2443
|
for (const attribute of attributes) {
|
|
2222
2444
|
delete attribute.lastIndexedKey;
|
|
2445
|
+
delete attribute.checkpointCertified;
|
|
2223
2446
|
delete attribute.indexingPID;
|
|
2224
2447
|
delete attribute.indexingFailed;
|
|
2225
2448
|
delete attribute.restartNumber;
|
|
2449
|
+
delete attribute.indexingIncarnation;
|
|
2450
|
+
delete attribute.indexingBuildId;
|
|
2226
2451
|
attribute.dbi.isIndexing = false;
|
|
2227
2452
|
// Also clear isIndexing on the currently-active dbi in Table.indices, which may
|
|
2228
2453
|
// differ from attribute.dbi if a resetDatabases() call during this migration
|
|
@@ -2239,6 +2464,7 @@ async function runIndexing(Table, attributes, indicesToRemove) {
|
|
|
2239
2464
|
logger.info(`Finished indexing ${Table.tableName} attributes`, attributes);
|
|
2240
2465
|
}
|
|
2241
2466
|
} catch (error) {
|
|
2467
|
+
await checkpointing;
|
|
2242
2468
|
// A worker shutting down closes its stores mid-backfill, so the range iterator or a
|
|
2243
2469
|
// put throws (e.g. "Database not open" / "Iterator not initialized"). This is an
|
|
2244
2470
|
// interruption, not a data error: the next worker generation re-runs the backfill via
|
package/resources/search.ts
CHANGED
|
@@ -1055,7 +1055,7 @@ export function filterByType(searchCondition, Table, context, filtered, isPrimar
|
|
|
1055
1055
|
canUseIndex =
|
|
1056
1056
|
canUseIndex && // is it a comparator that makes sense to use index
|
|
1057
1057
|
!isPrimaryKey && // no need to use index for primary keys, since we will be iterating over the primary keys
|
|
1058
|
-
Table
|
|
1058
|
+
!!(Table && usableIndex(Table, attribute)) && // is there a usable (complete) index for this attribute
|
|
1059
1059
|
estimatedIncomingCount > 3; // do we have a valid estimate of multiple incoming records (that is worth using an index for)
|
|
1060
1060
|
if (canUseIndex) {
|
|
1061
1061
|
if (searchCondition.estimated_count == undefined) estimateCondition(Table)(searchCondition);
|
|
@@ -1112,6 +1112,28 @@ export function filterByType(searchCondition, Table, context, filtered, isPrimar
|
|
|
1112
1112
|
}
|
|
1113
1113
|
}
|
|
1114
1114
|
|
|
1115
|
+
/** The index a condition can be driven by; searchByIndex refuses a rebuilding one, so it reads as absent. */
|
|
1116
|
+
function usableIndex(table, attributeName): any {
|
|
1117
|
+
const index = attributeName == null ? undefined : table.indices[attributeName];
|
|
1118
|
+
return index?.isIndexing ? undefined : index;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* True when an index this attribute would have to be driven by is still being built, following a
|
|
1123
|
+
* relationship path to the local join index and on to the related table's leaf index.
|
|
1124
|
+
*/
|
|
1125
|
+
function drivesRebuildingIndex(table, attributeName, relationshipOffset = 0): boolean {
|
|
1126
|
+
if (!Array.isArray(attributeName))
|
|
1127
|
+
return attributeName != null && attributeName !== table.primaryKey && !!table.indices[attributeName]?.isIndexing;
|
|
1128
|
+
if (relationshipOffset >= attributeName.length - 1)
|
|
1129
|
+
return drivesRebuildingIndex(table, attributeName[relationshipOffset]);
|
|
1130
|
+
const attribute = findAttribute(table.attributes, attributeName[relationshipOffset]);
|
|
1131
|
+
if (!attribute) return false;
|
|
1132
|
+
if (table.indices[attribute.relationship?.from]?.isIndexing) return true;
|
|
1133
|
+
const relatedTable = attribute.definition?.tableClass || attribute.elements?.definition?.tableClass;
|
|
1134
|
+
return !!relatedTable && drivesRebuildingIndex(relatedTable, attributeName, relationshipOffset + 1);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1115
1137
|
export function estimateCondition(table) {
|
|
1116
1138
|
function estimateConditionForTable(condition) {
|
|
1117
1139
|
if (condition.estimated_count === undefined) {
|
|
@@ -1141,7 +1163,10 @@ export function estimateCondition(table) {
|
|
|
1141
1163
|
// skip if it is cached
|
|
1142
1164
|
let searchType = condition.comparator || condition.search_type;
|
|
1143
1165
|
searchType = ALTERNATE_COMPARATOR_NAMES[searchType] || searchType;
|
|
1144
|
-
|
|
1166
|
+
// before comparator dispatch: several branches fall back to a finite table-fraction heuristic
|
|
1167
|
+
if (drivesRebuildingIndex(table, condition[0] ?? condition.attribute)) {
|
|
1168
|
+
condition.estimated_count = Infinity;
|
|
1169
|
+
} else if (searchType === SEARCH_TYPES.EQUALS || !searchType) {
|
|
1145
1170
|
const attribute_name = condition[0] ?? condition.attribute;
|
|
1146
1171
|
if (attribute_name == null || attribute_name === table.primaryKey) condition.estimated_count = 1;
|
|
1147
1172
|
else if (Array.isArray(attribute_name) && attribute_name.length > 1) {
|
|
@@ -1167,12 +1192,12 @@ export function estimateCondition(table) {
|
|
|
1167
1192
|
}
|
|
1168
1193
|
} else {
|
|
1169
1194
|
// we only attempt to estimate count on equals operator because that's really all that LMDB supports (some other key-value stores like libmdbx could be considered if we need to do estimated counts of ranges at some point)
|
|
1170
|
-
const index = table
|
|
1195
|
+
const index = usableIndex(table, attribute_name);
|
|
1171
1196
|
condition.estimated_count = index ? index.getValuesCount(condition[1] ?? condition.value) : Infinity;
|
|
1172
1197
|
}
|
|
1173
1198
|
} else if (searchType === 'contains' || searchType === 'ends_with' || searchType === 'ne') {
|
|
1174
1199
|
const attribute_name = condition[0] ?? condition.attribute;
|
|
1175
|
-
const index = table
|
|
1200
|
+
const index = usableIndex(table, attribute_name);
|
|
1176
1201
|
if (condition.value === null && searchType === 'ne') {
|
|
1177
1202
|
condition.estimated_count = Math.max(
|
|
1178
1203
|
estimatedEntryCount(table.primaryStore) - (index ? index.getValuesCount(null) : 0),
|
|
@@ -1181,7 +1206,7 @@ export function estimateCondition(table) {
|
|
|
1181
1206
|
} else condition.estimated_count = Infinity;
|
|
1182
1207
|
} else if (searchType === 'in') {
|
|
1183
1208
|
const attribute_name = condition[0] ?? condition.attribute;
|
|
1184
|
-
const index = table
|
|
1209
|
+
const index = usableIndex(table, attribute_name);
|
|
1185
1210
|
if (Array.isArray(condition.value) && index) {
|
|
1186
1211
|
// Sum of per-value matches (over-counts duplicates but is a fine ceiling)
|
|
1187
1212
|
let estimate = 0;
|
|
@@ -1199,7 +1224,7 @@ export function estimateCondition(table) {
|
|
|
1199
1224
|
condition.estimated_count = BETWEEN_ESTIMATE * estimatedEntryCount(table.primaryStore) + 1;
|
|
1200
1225
|
else if (searchType === 'sort') {
|
|
1201
1226
|
const attribute_name = condition[0] ?? condition.attribute;
|
|
1202
|
-
const index = table
|
|
1227
|
+
const index = usableIndex(table, attribute_name);
|
|
1203
1228
|
if (index?.customIndex?.estimateCountAsSort)
|
|
1204
1229
|
// allow custom index to define its own estimation of counts
|
|
1205
1230
|
condition.estimated_count = index.customIndex.estimateCountAsSort(condition);
|
|
@@ -1207,7 +1232,7 @@ export function estimateCondition(table) {
|
|
|
1207
1232
|
} else {
|
|
1208
1233
|
// for the search types that use the broadest range, try do them last
|
|
1209
1234
|
const attribute_name = condition[0] ?? condition.attribute;
|
|
1210
|
-
const index = table
|
|
1235
|
+
const index = usableIndex(table, attribute_name);
|
|
1211
1236
|
if (index?.customIndex?.estimateCount)
|
|
1212
1237
|
// allow custom index to define its own estimation of counts
|
|
1213
1238
|
condition.estimated_count = index.customIndex.estimateCount(condition.value);
|