@harperfast/harper 5.3.0-alpha.1 → 5.3.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cliOperations.ts +12 -0
- package/bin/restart.ts +66 -6
- package/components/Application.ts +1134 -112
- package/components/DESIGN.md +421 -0
- package/components/OptionsWatcher.ts +368 -102
- package/components/Scope.ts +20 -1
- package/components/componentLoader.ts +43 -4
- package/components/deploymentOperations.ts +4 -1
- package/components/deploymentRecorder.ts +9 -2
- package/components/mcp/DESIGN.md +109 -0
- package/components/mcp/audit.ts +21 -17
- package/components/operations.js +284 -52
- package/components/operationsValidation.js +49 -2
- package/components/packageComponent.ts +25 -1
- package/components/requestRestart.ts +11 -0
- package/config/DESIGN.md +306 -0
- package/config/RootConfigWatcher.ts +191 -37
- package/config/configReadRetry.ts +62 -0
- package/config/configUtils.ts +78 -26
- package/config/parseConfigFile.ts +34 -0
- package/config/readConfigFileSync.ts +44 -0
- package/config/watcherArming.ts +59 -0
- package/config-root.schema.json +4 -0
- package/dataLayer/DESIGN.md +179 -0
- package/dataLayer/harperBridge/ResourceBridge.ts +28 -2
- package/dataLayer/restoreMarker.ts +92 -25
- package/dist/bin/cliOperations.js +13 -0
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/restart.js +42 -6
- package/dist/bin/restart.js.map +1 -1
- package/dist/components/Application.d.ts +104 -9
- package/dist/components/Application.js +954 -102
- package/dist/components/Application.js.map +1 -1
- package/dist/components/OptionsWatcher.d.ts +4 -1
- package/dist/components/OptionsWatcher.js +378 -104
- package/dist/components/OptionsWatcher.js.map +1 -1
- package/dist/components/Scope.js +15 -1
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/componentLoader.js +35 -3
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/deploymentOperations.js +4 -1
- package/dist/components/deploymentOperations.js.map +1 -1
- package/dist/components/deploymentRecorder.d.ts +4 -2
- package/dist/components/deploymentRecorder.js +1 -0
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/mcp/audit.d.ts +2 -1
- package/dist/components/mcp/audit.js +21 -17
- package/dist/components/mcp/audit.js.map +1 -1
- package/dist/components/operations.d.ts +28 -0
- package/dist/components/operations.js +263 -54
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +48 -2
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/packageComponent.js +24 -0
- package/dist/components/packageComponent.js.map +1 -1
- package/dist/components/requestRestart.d.ts +1 -0
- package/dist/components/requestRestart.js +7 -0
- package/dist/components/requestRestart.js.map +1 -1
- package/dist/config/RootConfigWatcher.d.ts +2 -0
- package/dist/config/RootConfigWatcher.js +189 -35
- package/dist/config/RootConfigWatcher.js.map +1 -1
- package/dist/config/configReadRetry.d.ts +8 -0
- package/dist/config/configReadRetry.js +62 -0
- package/dist/config/configReadRetry.js.map +1 -0
- package/dist/config/configUtils.d.ts +10 -9
- package/dist/config/configUtils.js +63 -27
- package/dist/config/configUtils.js.map +1 -1
- package/dist/config/parseConfigFile.d.ts +4 -0
- package/dist/config/parseConfigFile.js +35 -0
- package/dist/config/parseConfigFile.js.map +1 -0
- package/dist/config/readConfigFileSync.d.ts +1 -0
- package/dist/config/readConfigFileSync.js +47 -0
- package/dist/config/readConfigFileSync.js.map +1 -0
- package/dist/config/watcherArming.d.ts +15 -0
- package/dist/config/watcherArming.js +59 -0
- package/dist/config/watcherArming.js.map +1 -0
- package/dist/dataLayer/harperBridge/ResourceBridge.js +21 -2
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/restoreMarker.d.ts +21 -8
- package/dist/dataLayer/restoreMarker.js +94 -27
- package/dist/dataLayer/restoreMarker.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +3 -0
- package/dist/resources/DatabaseTransaction.d.ts +25 -0
- package/dist/resources/DatabaseTransaction.js +224 -18
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +2 -1
- package/dist/resources/LMDBTransaction.js +22 -3
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.js +22 -6
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/RecordEncoder.d.ts +1 -1
- package/dist/resources/RecordEncoder.js +46 -10
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/Resource.js +97 -13
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +8 -0
- package/dist/resources/RocksIndexStore.js +2 -1
- package/dist/resources/RocksIndexStore.js.map +1 -1
- package/dist/resources/RocksTransactionLogStore.d.ts +10 -0
- package/dist/resources/RocksTransactionLogStore.js +104 -33
- package/dist/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/resources/Table.d.ts +58 -7
- package/dist/resources/Table.js +1143 -348
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.d.ts +3 -0
- package/dist/resources/analytics/write.js +49 -16
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/auditStore.d.ts +170 -0
- package/dist/resources/auditStore.js +457 -11
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/crdt.d.ts +10 -0
- package/dist/resources/crdt.js +22 -0
- package/dist/resources/crdt.js.map +1 -1
- package/dist/resources/dataLoader.js +3 -4
- package/dist/resources/dataLoader.js.map +1 -1
- package/dist/resources/databases.d.ts +16 -13
- package/dist/resources/databases.js +625 -177
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/derivedIndexRegistry.d.ts +5 -0
- package/dist/resources/derivedIndexRegistry.js +68 -0
- package/dist/resources/derivedIndexRegistry.js.map +1 -0
- package/dist/resources/derivedIndexRuntime.d.ts +215 -0
- package/dist/resources/derivedIndexRuntime.js +2027 -0
- package/dist/resources/derivedIndexRuntime.js.map +1 -0
- package/dist/resources/graphql.js +3 -2
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +102 -9
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +867 -39
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/indexes/fullTextDerivedIndex.d.ts +81 -0
- package/dist/resources/indexes/fullTextDerivedIndex.js +1004 -0
- package/dist/resources/indexes/fullTextDerivedIndex.js.map +1 -0
- package/dist/resources/indexes/fullTextNativeBinding.d.ts +78 -0
- package/dist/resources/indexes/fullTextNativeBinding.js +85 -0
- package/dist/resources/indexes/fullTextNativeBinding.js.map +1 -0
- package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
- package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
- package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
- package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
- package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
- package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.d.ts +24 -0
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js +149 -0
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js.map +1 -0
- package/dist/resources/nodeIdMapping.d.ts +5 -0
- package/dist/resources/nodeIdMapping.js +49 -0
- package/dist/resources/nodeIdMapping.js.map +1 -1
- package/dist/resources/recordLock.d.ts +47 -4
- package/dist/resources/recordLock.js +138 -7
- package/dist/resources/recordLock.js.map +1 -1
- package/dist/resources/recordLockCoordinator.d.ts +557 -0
- package/dist/resources/recordLockCoordinator.js +2597 -0
- package/dist/resources/recordLockCoordinator.js.map +1 -0
- package/dist/resources/replayLogs.js +5 -0
- package/dist/resources/replayLogs.js.map +1 -1
- package/dist/resources/replicatedApplyFailure.d.ts +16 -0
- package/dist/resources/replicatedApplyFailure.js +63 -0
- package/dist/resources/replicatedApplyFailure.js.map +1 -0
- package/dist/resources/scheduler/scheduler.js +3 -3
- package/dist/resources/scheduler/scheduler.js.map +1 -1
- package/dist/resources/search.d.ts +10 -4
- package/dist/resources/search.js +160 -40
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/tracked.d.ts +5 -1
- package/dist/resources/tracked.js +74 -23
- package/dist/resources/tracked.js.map +1 -1
- package/dist/resources/transactionBroadcast.js +4 -6
- package/dist/resources/transactionBroadcast.js.map +1 -1
- package/dist/security/auth.js +59 -23
- package/dist/security/auth.js.map +1 -1
- package/dist/security/deferredAuthentication.d.ts +11 -0
- package/dist/security/deferredAuthentication.js +25 -3
- package/dist/security/deferredAuthentication.js.map +1 -1
- package/dist/security/jsLoader.js +9 -4
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/server/REST.js +39 -5
- package/dist/server/REST.js.map +1 -1
- package/dist/server/http.d.ts +5 -1
- package/dist/server/http.js +34 -2
- package/dist/server/http.js.map +1 -1
- package/dist/server/mqtt.js +5 -1
- package/dist/server/mqtt.js.map +1 -1
- package/dist/server/serverHelpers/Headers.d.ts +2 -0
- package/dist/server/serverHelpers/Headers.js +6 -0
- package/dist/server/serverHelpers/Headers.js.map +1 -1
- package/dist/server/serverHelpers/NodeAdapterResponse.d.ts +48 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js +220 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -0
- package/dist/server/serverHelpers/Request.d.ts +5 -10
- package/dist/server/serverHelpers/Request.js +38 -136
- package/dist/server/serverHelpers/Request.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.d.ts +2 -0
- package/dist/server/serverHelpers/contentTypes.js +189 -15
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.d.ts +3 -3
- package/dist/server/serverHelpers/serverUtilities.js +110 -20
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/serverHelpers/uwsServer.js +4 -1
- package/dist/server/serverHelpers/uwsServer.js.map +1 -1
- package/dist/server/serverHelpers/webSocketCloseReason.d.ts +2 -0
- package/dist/server/serverHelpers/webSocketCloseReason.js +29 -0
- package/dist/server/serverHelpers/webSocketCloseReason.js.map +1 -0
- package/dist/server/storageReclamation.js +1 -1
- package/dist/server/storageReclamation.js.map +1 -1
- package/dist/server/threads/isolatedApplications.d.ts +47 -0
- package/dist/server/threads/isolatedApplications.js +171 -0
- package/dist/server/threads/isolatedApplications.js.map +1 -0
- package/dist/server/threads/logRotationTransport.d.ts +1 -0
- package/dist/server/threads/logRotationTransport.js +33 -0
- package/dist/server/threads/logRotationTransport.js.map +1 -0
- package/dist/server/threads/manageThreads.d.ts +64 -6
- package/dist/server/threads/manageThreads.js +261 -12
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/socketRouter.d.ts +1 -0
- package/dist/server/threads/socketRouter.js +196 -13
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadServer.js +30 -7
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +24 -0
- package/dist/utility/errors/hdbError.js +58 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/hdbTerms.d.ts +2 -0
- package/dist/utility/hdbTerms.js +2 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/logging/harper_logger.js +218 -38
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/logging/logGenerationCoordinator.d.ts +35 -0
- package/dist/utility/logging/logGenerationCoordinator.js +184 -0
- package/dist/utility/logging/logGenerationCoordinator.js.map +1 -0
- package/dist/utility/logging/logRotation.d.ts +46 -0
- package/dist/utility/logging/logRotation.js +365 -0
- package/dist/utility/logging/logRotation.js.map +1 -0
- package/dist/utility/logging/logRotator.d.ts +1 -1
- package/dist/utility/logging/logRotator.js +172 -92
- package/dist/utility/logging/logRotator.js.map +1 -1
- package/dist/utility/npmUtilities.js +6 -4
- package/dist/utility/npmUtilities.js.map +1 -1
- package/dist/utility/watcherFallback.d.ts +0 -45
- package/dist/utility/watcherFallback.js +1 -125
- package/dist/utility/watcherFallback.js.map +1 -1
- package/dist/validation/configValidator.js +6 -3
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +9 -0
- package/json/systemSchema.json +3 -0
- package/npm-shrinkwrap.json +177 -87
- package/package.json +13 -5
- package/resources/DESIGN.md +689 -19
- package/resources/DatabaseTransaction.ts +230 -17
- package/resources/LMDBTransaction.ts +21 -3
- package/resources/PrimaryRocksDatabase.ts +20 -7
- package/resources/RecordEncoder.ts +52 -10
- package/resources/Resource.ts +97 -13
- package/resources/ResourceInterface.ts +8 -0
- package/resources/RocksIndexStore.ts +2 -1
- package/resources/RocksTransactionLogStore.ts +111 -31
- package/resources/Table.ts +1273 -396
- package/resources/analytics/DESIGN.md +38 -0
- package/resources/analytics/write.ts +50 -17
- package/resources/auditStore.ts +460 -11
- package/resources/crdt.ts +22 -0
- package/resources/dataLoader.ts +3 -4
- package/resources/databases.ts +611 -147
- package/resources/derivedIndexRegistry.ts +56 -0
- package/resources/derivedIndexRuntime.ts +2292 -0
- package/resources/graphql.ts +3 -2
- package/resources/indexes/DESIGN.md +833 -0
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +924 -46
- package/resources/indexes/fullTextDerivedIndex.ts +1165 -0
- package/resources/indexes/fullTextNativeBinding.ts +146 -0
- package/resources/indexes/hnswDerivedIndex.ts +531 -0
- package/resources/indexes/hnswPlaneBinding.ts +174 -0
- package/resources/indexes/nativeFullTextDerivedIndexLifecycle.ts +181 -0
- package/resources/nodeIdMapping.ts +50 -0
- package/resources/record-locks.md +1407 -0
- package/resources/recordLock.ts +173 -7
- package/resources/recordLockCoordinator.ts +3082 -0
- package/resources/replayLogs.ts +5 -0
- package/resources/replicatedApplyFailure.ts +77 -0
- package/resources/scheduler/DESIGN.md +40 -0
- package/resources/scheduler/scheduler.ts +4 -4
- package/resources/search.ts +169 -49
- package/resources/tracked.ts +73 -22
- package/resources/transactionBroadcast.ts +4 -4
- package/security/DESIGN.md +175 -0
- package/security/auth.ts +53 -24
- package/security/deferredAuthentication.ts +24 -2
- package/security/jsLoader.ts +9 -4
- package/server/DESIGN.md +275 -0
- package/server/REST.ts +42 -6
- package/server/http.ts +34 -2
- package/server/mqtt.ts +6 -4
- package/server/serverHelpers/Headers.ts +5 -1
- package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
- package/server/serverHelpers/Request.ts +33 -131
- package/server/serverHelpers/contentTypes.ts +188 -15
- package/server/serverHelpers/serverUtilities.ts +157 -27
- package/server/serverHelpers/uwsServer.ts +4 -1
- package/server/serverHelpers/webSocketCloseReason.ts +25 -0
- package/server/storageReclamation.ts +2 -2
- package/server/threads/isolatedApplications.ts +157 -0
- package/server/threads/logRotationTransport.ts +40 -0
- package/server/threads/manageThreads.js +254 -12
- package/server/threads/socketRouter.ts +217 -11
- package/server/threads/threadServer.js +30 -7
- package/studio/web/assets/{Chat-BnCBegQz.js → Chat-DADFFGe_.js} +1 -1
- package/studio/web/assets/{FloatingChat-CoDW1ySS.js → FloatingChat-D_mI-rZ7.js} +3 -3
- package/studio/web/assets/{apiToken-Bwk5BLXW.js → apiToken-c2NiSDHa.js} +1 -1
- package/studio/web/assets/{applications-DHxGi7JH.js → applications-DktUqh7G.js} +1 -1
- package/studio/web/assets/{cssMode-s0cWI-_M.js → cssMode-Cs_75Xhw.js} +1 -1
- package/studio/web/assets/{editor-DNcRHK54.js → editor-19b-Y1IN.js} +1 -1
- package/studio/web/assets/{html-Bdssedlg.js → html-DiYEQMpB.js} +1 -1
- package/studio/web/assets/{htmlMode-CoDlJ3fw.js → htmlMode-CmR0y7P_.js} +1 -1
- package/studio/web/assets/{index-D6sxmFLR.js → index-Dm0rfkJ7.js} +5 -5
- package/studio/web/assets/{index.lazy-tmU5BS8s.js → index.lazy-7vqt2CC3.js} +1 -1
- package/studio/web/assets/{javascript-B8meVSTH.js → javascript-BWtCFuOt.js} +1 -1
- package/studio/web/assets/{jsonMode-DpIPd35T.js → jsonMode-Buzzbv9y.js} +1 -1
- package/studio/web/assets/{languageServices-C_5FMJzQ.js → languageServices-SqsFWfTM.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-BIzNBzPK.js → lspLanguageFeatures-EMV5cmjo.js} +1 -1
- package/studio/web/assets/{notifications-CvZivSbh.js → notifications-CAB-LZWT.js} +1 -1
- package/studio/web/assets/{notifications-CQf18QKb.js → notifications-DRzmSRxM.js} +1 -1
- package/studio/web/assets/{profile-DdOwtntb.js → profile-BNKAl79n.js} +1 -1
- package/studio/web/assets/{regions-n69fwagr.js → regions-CUow_Zw2.js} +1 -1
- package/studio/web/assets/{register-PfWTCXWB.js → register-Dkt3WUMp.js} +2 -2
- package/studio/web/assets/{setComponentFile-Bg6O7X0S.js → setComponentFile-BZRfMD0N.js} +1 -1
- package/studio/web/assets/{setup-CUx_aUDl.js → setup-D_yiEPO2.js} +2 -2
- package/studio/web/assets/{status-D7BVKqX9.js → status-DhHh1Ge-.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-DBSyXzMr.js → toggleHighContrast-D7L1PDtV.js} +1 -1
- package/studio/web/assets/{tsMode-BByKCjBS.js → tsMode-CCwLk1YS.js} +1 -1
- package/studio/web/assets/{typescript-DDLnLpw9.js → typescript-BP1j1mjn.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-31CHGaHk.js → useEntityRestURL-D7bnYxLw.js} +1 -1
- package/studio/web/assets/{workers-pR3jRY9D.js → workers-tOuCNT17.js} +1 -1
- package/studio/web/assets/{xml-2iRnMhQO.js → xml-BSG_3mQT.js} +1 -1
- package/studio/web/assets/{yaml-Bf92gJpd.js → yaml-DxiLprBB.js} +1 -1
- package/studio/web/index.html +1 -1
- package/utility/DESIGN.md +55 -0
- package/utility/errors/hdbError.ts +54 -0
- package/utility/hdbTerms.ts +2 -0
- package/utility/logging/harper_logger.ts +210 -30
- package/utility/logging/logGenerationCoordinator.ts +196 -0
- package/utility/logging/logRotation.ts +367 -0
- package/utility/logging/logRotator.ts +196 -91
- package/utility/npmUtilities.ts +6 -4
- package/utility/watcherFallback.ts +0 -122
- package/validation/configValidator.ts +6 -3
package/resources/databases.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
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';
|
|
5
|
-
import { join, extname, basename } from 'path';
|
|
6
|
-
import {
|
|
6
|
+
import { join, extname, basename } from 'node:path';
|
|
7
|
+
import {
|
|
8
|
+
closeSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
openSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
readdirSync,
|
|
14
|
+
realpathSync,
|
|
15
|
+
unlinkSync,
|
|
16
|
+
} from 'node:fs';
|
|
7
17
|
import { unlink } from 'node:fs/promises';
|
|
8
18
|
import {
|
|
9
19
|
getBaseSchemaPath,
|
|
@@ -28,19 +38,26 @@ import { workerData } from 'worker_threads';
|
|
|
28
38
|
import harperLogger from '../utility/logging/harper_logger.ts';
|
|
29
39
|
const { forComponent } = harperLogger;
|
|
30
40
|
import * as manageThreads from '../server/threads/manageThreads.js';
|
|
31
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
establishAuditFloor,
|
|
43
|
+
openAuditStore,
|
|
44
|
+
readAuditEntry,
|
|
45
|
+
createAuditEntry,
|
|
46
|
+
type AuditRecord,
|
|
47
|
+
} from './auditStore.ts';
|
|
32
48
|
import { handleLocalTimeForGets } from './RecordEncoder.ts';
|
|
33
49
|
import { databasePaths, deleteRootBlobPathsForDB } from './blob.ts';
|
|
34
50
|
import { removeStorageReclamation } from '../server/storageReclamation.ts';
|
|
35
51
|
import { commonValidators, schemaRegex } from '../validation/common_validators.ts';
|
|
36
52
|
import { CUSTOM_INDEXES } from './indexes/customIndexes.ts';
|
|
53
|
+
import { planeFilePathFor, planeStalePathFor } from './indexes/hnswPlaneBinding.ts';
|
|
37
54
|
import { OpenDBIObject } from '../utility/lmdb/OpenDBIObject.ts';
|
|
38
55
|
import { RocksDatabase, supportedCompression, type RocksDatabaseOptions } from '@harperfast/rocksdb-js';
|
|
39
56
|
import { PrimaryRocksDatabase } from './PrimaryRocksDatabase.ts';
|
|
40
57
|
import { replayLogs } from './replayLogs.ts';
|
|
58
|
+
import { attachDerivedIndexes } from './indexes/hnswDerivedIndex.ts';
|
|
41
59
|
import { totalmem } from 'node:os';
|
|
42
60
|
import { RocksIndexStore } from './RocksIndexStore.ts';
|
|
43
|
-
import { when } from '../utility/when.ts';
|
|
44
61
|
import { resolveRocksMemoryConfig } from '../utility/rocksMemoryConfig.ts';
|
|
45
62
|
import { isProcessRunning } from '../utility/processManagement/processManagement.js';
|
|
46
63
|
import {
|
|
@@ -446,6 +463,21 @@ function applyDurableDeclaration(attribute: any, descriptor: any) {
|
|
|
446
463
|
else delete attribute[field];
|
|
447
464
|
}
|
|
448
465
|
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* True when a descriptor claims an index build no live operation in this process can own. The PID and
|
|
469
|
+
* worker generation cannot answer that alone: a container reuses PID 1 and starts the in-memory
|
|
470
|
+
* generation back at 1 while the persisted one is higher. A descriptor with no incarnation was written
|
|
471
|
+
* before the field existed, so it belongs to an earlier process; a thread started without one of its
|
|
472
|
+
* own cannot judge, and falls back rather than declaring a live build dead.
|
|
473
|
+
*/
|
|
474
|
+
function isAbandonedIndexBuild(descriptor: any, currentRestartGeneration: number): boolean {
|
|
475
|
+
if (!descriptor) return false;
|
|
476
|
+
if (descriptor.indexingPID && descriptor.indexingPID !== process.pid) return true;
|
|
477
|
+
if (descriptor.restartNumber < currentRestartGeneration) return true;
|
|
478
|
+
const incarnation = manageThreads.processIncarnation;
|
|
479
|
+
return !!descriptor.indexingPID && incarnation != null && descriptor.indexingIncarnation !== incarnation;
|
|
480
|
+
}
|
|
449
481
|
// How many times the schema load will try to finish a tombstoned drop before
|
|
450
482
|
// giving up for the rest of this process's lifetime. A drop that fails once
|
|
451
483
|
// almost always fails identically forever - the usual cause is a RocksDB
|
|
@@ -910,6 +942,7 @@ export function readMetaDb(
|
|
|
910
942
|
lmdbDatabaseEnvs.set(path, rootStore);
|
|
911
943
|
}
|
|
912
944
|
|
|
945
|
+
rootStore.dbisDb?.resetReadTxn();
|
|
913
946
|
return initStores(path, rootStore, databaseName, { defaultTable, auditPath, isLegacy });
|
|
914
947
|
} catch (error) {
|
|
915
948
|
error.message += ` opening database ${path}`;
|
|
@@ -981,7 +1014,7 @@ function initStores(
|
|
|
981
1014
|
{ defaultTable, auditPath, isLegacy, destination, storeName, openedStores }: InitStoresOptions = {}
|
|
982
1015
|
) {
|
|
983
1016
|
// a store with no tables never reaches the per-table loop below, and blob roots resolve from this
|
|
984
|
-
rootStore.databaseName
|
|
1017
|
+
rootStore.databaseName ??= storeName ?? databaseName;
|
|
985
1018
|
const envInit = new OpenEnvironmentObject(path, isReadOnlyMode());
|
|
986
1019
|
const internalDbiInit = createOpenDBIObject(false);
|
|
987
1020
|
let attributesDbi = rootStore.dbisDb;
|
|
@@ -1016,6 +1049,9 @@ function initStores(
|
|
|
1016
1049
|
}) as any;
|
|
1017
1050
|
}
|
|
1018
1051
|
auditStore.isLegacy = true;
|
|
1052
|
+
// A legacy standalone audit root skips openAuditStore, so give it a floor here or it
|
|
1053
|
+
// reports its retention horizon as permanently unknown.
|
|
1054
|
+
establishAuditFloor(auditStore);
|
|
1019
1055
|
}
|
|
1020
1056
|
} else {
|
|
1021
1057
|
auditStore = openAuditStore(rootStore);
|
|
@@ -1141,6 +1177,9 @@ function initStores(
|
|
|
1141
1177
|
// unless its store was migrated to a different engine (e.g. LMDB to RocksDB on startup)
|
|
1142
1178
|
const recreateForEngineChange =
|
|
1143
1179
|
!!table && (table as any).primaryStore?.rootStore instanceof RocksDatabase !== rootStore instanceof RocksDatabase;
|
|
1180
|
+
const recreateForTableIdChange =
|
|
1181
|
+
!!table && primaryAttribute.tableId != null && table.tableId !== primaryAttribute.tableId;
|
|
1182
|
+
const recreateTable = recreateForEngineChange || recreateForTableIdChange;
|
|
1144
1183
|
let indices = {},
|
|
1145
1184
|
existingAttributes = [];
|
|
1146
1185
|
let tableId;
|
|
@@ -1154,7 +1193,8 @@ function initStores(
|
|
|
1154
1193
|
const cacheControl = primaryAttribute.cacheControl;
|
|
1155
1194
|
const splitSegments = primaryAttribute.splitSegments;
|
|
1156
1195
|
const replicate = primaryAttribute.replicate;
|
|
1157
|
-
if (table && !
|
|
1196
|
+
if (table && !recreateTable) {
|
|
1197
|
+
if (primaryAttribute.audit === true && table.audit !== true) table.enableAuditing();
|
|
1158
1198
|
indices = table.indices;
|
|
1159
1199
|
existingAttributes = table.attributes;
|
|
1160
1200
|
table.schemaVersion++;
|
|
@@ -1204,6 +1244,8 @@ function initStores(
|
|
|
1204
1244
|
indices[attribute.name] = dbi;
|
|
1205
1245
|
indices[attribute.name].indexNulls = attribute.indexNulls;
|
|
1206
1246
|
}
|
|
1247
|
+
// the only way a thread that never declares the schema reaches Table.indices
|
|
1248
|
+
indices[attribute.name].isIndexing = !!attribute.indexingPID;
|
|
1207
1249
|
const existingAttribute = existingAttributes.find(
|
|
1208
1250
|
(existingAttribute) => existingAttribute.name === attribute.name
|
|
1209
1251
|
);
|
|
@@ -1269,12 +1311,13 @@ function initStores(
|
|
|
1269
1311
|
existingAttributes.splice(existingAttributes.indexOf(existingAttribute), 1);
|
|
1270
1312
|
attributesUpdated = true;
|
|
1271
1313
|
}
|
|
1272
|
-
if (table && !
|
|
1314
|
+
if (table && !recreateTable) {
|
|
1273
1315
|
if (attributesUpdated) {
|
|
1274
1316
|
table.schemaVersion++;
|
|
1275
1317
|
table.updatedAttributes();
|
|
1276
1318
|
}
|
|
1277
1319
|
} else {
|
|
1320
|
+
if (recreateForTableIdChange) table.cleanup();
|
|
1278
1321
|
table = setTable(
|
|
1279
1322
|
tables,
|
|
1280
1323
|
tableName,
|
|
@@ -1305,6 +1348,8 @@ function initStores(
|
|
|
1305
1348
|
table.schemaVersion = 1;
|
|
1306
1349
|
if (!destination) databaseEventsEmitter.emit('updateTable', table);
|
|
1307
1350
|
}
|
|
1351
|
+
void table.derivedIndexRuntime?.close();
|
|
1352
|
+
table.derivedIndexRuntime = attachDerivedIndexes(table);
|
|
1308
1353
|
if (Array.isArray(primaryAttribute.relationships)) {
|
|
1309
1354
|
relationshipsToHydrate.push({ table, databaseName, tableName, definitions: primaryAttribute.relationships });
|
|
1310
1355
|
} else if (primaryAttribute.relationships !== undefined) {
|
|
@@ -1640,6 +1685,8 @@ export function openBranchDatabase(
|
|
|
1640
1685
|
openBranches.set(path, undefined);
|
|
1641
1686
|
retakeBranchIdentity(storeName);
|
|
1642
1687
|
try {
|
|
1688
|
+
// before the open: table load schedules TTL, eviction and audit cleanup, which ask who owns this store
|
|
1689
|
+
manageThreads.markBranchStorePath(path);
|
|
1643
1690
|
rootStore = readRocksMetaDb(path, null, databaseName, { destination: tables, storeName, openedStores });
|
|
1644
1691
|
// Pin the handle to the roots the caller proved this branch was published with, before it is
|
|
1645
1692
|
// handed out. A row's `storageIndex` is a position in that list, so resolving through current
|
|
@@ -1649,6 +1696,7 @@ export function openBranchDatabase(
|
|
|
1649
1696
|
if (blobRoots) databasePaths.set(rootStore as unknown as RootDatabase, blobRoots);
|
|
1650
1697
|
} catch (error) {
|
|
1651
1698
|
openBranches.delete(path);
|
|
1699
|
+
manageThreads.markBranchStorePath(path, false);
|
|
1652
1700
|
releaseBranchIdentity(storeName);
|
|
1653
1701
|
const stranded = rocksdbDatabaseEnvs.get(path);
|
|
1654
1702
|
rocksdbDatabaseEnvs.delete(path);
|
|
@@ -1672,6 +1720,7 @@ export function openBranchDatabase(
|
|
|
1672
1720
|
openBranches.delete(path);
|
|
1673
1721
|
releaseBranchIdentity(storeName);
|
|
1674
1722
|
rocksdbDatabaseEnvs.delete(path);
|
|
1723
|
+
manageThreads.markBranchStorePath(path, false);
|
|
1675
1724
|
closeBranchHandles(path, rootStore, openedStores, tables);
|
|
1676
1725
|
},
|
|
1677
1726
|
};
|
|
@@ -1765,6 +1814,8 @@ interface TableDefinition {
|
|
|
1765
1814
|
// default Cache-Control for anonymous REST reads; null = schema explicitly has none (clears a
|
|
1766
1815
|
// prior value on reload), undefined = caller is not schema-defining (leave the current value)
|
|
1767
1816
|
cacheControl?: string | null;
|
|
1817
|
+
/** Internal: this declaration came from the application owned by the current dedicated worker. */
|
|
1818
|
+
isolatedApplicationOwner?: boolean;
|
|
1768
1819
|
}
|
|
1769
1820
|
/**
|
|
1770
1821
|
* Ensure that we have this database object (that holds a set of tables) set up
|
|
@@ -2244,6 +2295,9 @@ function openIndex(dbiKey: string, rootStore: RootDatabaseKind, attribute: any)
|
|
|
2244
2295
|
const CustomIndex = CUSTOM_INDEXES[attribute.indexed.type];
|
|
2245
2296
|
if (CustomIndex) {
|
|
2246
2297
|
indexStore.customIndex = new CustomIndex(indexStore, attribute.indexed);
|
|
2298
|
+
// derived state whose maintaining option is now off must not linger to be adopted
|
|
2299
|
+
// stale on a later re-enable
|
|
2300
|
+
indexStore.customIndex.cleanupDisabledPlane?.();
|
|
2247
2301
|
} else {
|
|
2248
2302
|
logger.error(`The indexing type '${attribute.indexed.type}' is unknown`);
|
|
2249
2303
|
}
|
|
@@ -2298,13 +2352,18 @@ const GLOBAL_TARGET: TableTarget = {
|
|
|
2298
2352
|
/**
|
|
2299
2353
|
* The factory a branched application declares tables through: each declaration goes to the branch
|
|
2300
2354
|
* of the database it names, or to `table()` itself for a database the application did not branch.
|
|
2301
|
-
* An unbranched application gets `table` by identity
|
|
2355
|
+
* An unbranched, shared application gets `table` by identity. An isolated application still gets a
|
|
2356
|
+
* wrapper so its own declarations can claim their single-threaded maintenance work.
|
|
2302
2357
|
*/
|
|
2303
|
-
export function scopedTableFactory(
|
|
2304
|
-
|
|
2358
|
+
export function scopedTableFactory(
|
|
2359
|
+
branches?: Map<string, BranchDatabase>,
|
|
2360
|
+
isolatedApplicationOwner = false
|
|
2361
|
+
): typeof table {
|
|
2362
|
+
if (!branches?.size && !isolatedApplicationOwner) return table;
|
|
2305
2363
|
return function scopedTable<TableResourceType>(tableDefinition: TableDefinition): TableResourceType {
|
|
2364
|
+
if (isolatedApplicationOwner) tableDefinition = { ...tableDefinition, isolatedApplicationOwner: true };
|
|
2306
2365
|
// `||`, not `??`: `table()` resolves every falsy name to the default database
|
|
2307
|
-
const branch = branches
|
|
2366
|
+
const branch = branches?.get(tableDefinition.database || DEFAULT_DATABASE_NAME);
|
|
2308
2367
|
return branch ? declareTable(branchTarget(branch), tableDefinition) : table(tableDefinition);
|
|
2309
2368
|
};
|
|
2310
2369
|
}
|
|
@@ -2382,7 +2441,10 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
2382
2441
|
properties,
|
|
2383
2442
|
hidden,
|
|
2384
2443
|
cacheControl,
|
|
2444
|
+
isolatedApplicationOwner,
|
|
2385
2445
|
} = tableDefinition;
|
|
2446
|
+
const auditExplicitlyEnabled = audit === true;
|
|
2447
|
+
const auditExplicitlyDisabled = audit === false;
|
|
2386
2448
|
if (!databaseName) databaseName = DEFAULT_DATABASE_NAME;
|
|
2387
2449
|
// Reject reserved names here too, not only at the operations API: a database
|
|
2388
2450
|
// is also created by schema authoring — a `schema.graphql` `@table(database:)`
|
|
@@ -2417,25 +2479,135 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
2417
2479
|
if (schemaDefined == undefined) schemaDefined = true;
|
|
2418
2480
|
const relationshipDefinitions = schemaRelationshipsDefined ? normalizeRelationships(attributes) : undefined;
|
|
2419
2481
|
const internalDbiInit = createOpenDBIObject(false);
|
|
2482
|
+
let releaseExclusiveLock: (() => void) | undefined;
|
|
2420
2483
|
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2484
|
+
const hasHnswAtEntry = attributes.some((attribute) => attribute.indexed?.type === 'HNSW');
|
|
2485
|
+
const persistedPrimaryDescriptor = (catalog: any) => {
|
|
2486
|
+
const declaredPrimaryKey = attributes.find((attribute) => attribute.isPrimaryKey)?.name ?? Table?.primaryKey;
|
|
2487
|
+
if (declaredPrimaryKey) {
|
|
2488
|
+
const key = `${tableName}/${declaredPrimaryKey}`;
|
|
2489
|
+
const descriptor = catalog?.getSync(key);
|
|
2490
|
+
if (descriptor?.isPrimaryKey) return { key, descriptor };
|
|
2491
|
+
}
|
|
2492
|
+
const key = `${tableName}/`;
|
|
2493
|
+
return { key, descriptor: catalog?.getSync(key) };
|
|
2494
|
+
};
|
|
2495
|
+
const hasLegacyHnswStateAtEntry =
|
|
2496
|
+
Table &&
|
|
2497
|
+
origin !== 'cluster' &&
|
|
2498
|
+
attributes.some((attribute) => {
|
|
2499
|
+
if (attribute.indexed?.type !== 'HNSW') return false;
|
|
2500
|
+
const persisted = Table.dbisDB?.getSync(`${tableName}/${attribute.name || attribute.attribute || ''}`)?.indexed;
|
|
2501
|
+
if (persisted?.type !== 'HNSW') return false;
|
|
2502
|
+
if (Object.hasOwn(persisted, 'nativePlane') && typeof persisted.nativePlane !== 'boolean') return true;
|
|
2503
|
+
for (const name of CUSTOM_INDEXES.HNSW.numericOptions)
|
|
2504
|
+
if (Object.hasOwn(persisted, name) && typeof persisted[name] !== 'number') return true;
|
|
2505
|
+
return false;
|
|
2506
|
+
});
|
|
2507
|
+
try {
|
|
2508
|
+
if (
|
|
2509
|
+
Table &&
|
|
2510
|
+
hasHnswAtEntry &&
|
|
2511
|
+
origin !== 'cluster' &&
|
|
2512
|
+
(rootStore instanceof RocksDatabase || hasLegacyHnswStateAtEntry)
|
|
2513
|
+
)
|
|
2514
|
+
exclusiveLock();
|
|
2515
|
+
const persistedAuditAtEntry =
|
|
2516
|
+
hasHnswAtEntry || (Table && Table.audit !== true)
|
|
2517
|
+
? persistedPrimaryDescriptor(Table?.dbisDB).descriptor?.audit
|
|
2518
|
+
: undefined;
|
|
2519
|
+
if (!auditExplicitlyDisabled && persistedAuditAtEntry === true && Table?.audit !== true) Table.enableAuditing();
|
|
2520
|
+
for (const attribute of attributes) {
|
|
2521
|
+
if (attribute.attribute && !attribute.name) {
|
|
2522
|
+
// there is some legacy code that calls the attribute's name the attribute's attribute
|
|
2523
|
+
attribute.name = attribute.attribute;
|
|
2524
|
+
attribute.indexed = true;
|
|
2525
|
+
} else attribute.attribute = attribute.name;
|
|
2526
|
+
if (attribute.expiresAt) attribute.indexed = true;
|
|
2527
|
+
if (attribute.indexed?.type === 'HNSW' && origin !== 'cluster') {
|
|
2528
|
+
const existingAttribute = Table?.attributes.find((existing: any) => existing.name === attribute.name);
|
|
2529
|
+
const persistedIndexed =
|
|
2530
|
+
Table?.dbisDB?.getSync(`${tableName}/${attribute.name || ''}`)?.indexed ?? existingAttribute?.indexed;
|
|
2531
|
+
CUSTOM_INDEXES.HNSW.normalizeDeclarationOptions(attribute.indexed, persistedIndexed);
|
|
2532
|
+
if (attribute.indexed.nativePlane != null) {
|
|
2533
|
+
const persistedNativePlane = persistedIndexed?.nativePlane;
|
|
2534
|
+
const matchesPersistedLegacySpelling =
|
|
2535
|
+
persistedIndexed?.type === 'HNSW' &&
|
|
2536
|
+
Object.hasOwn(persistedIndexed, 'nativePlane') &&
|
|
2537
|
+
typeof persistedNativePlane !== 'boolean' &&
|
|
2538
|
+
(Object.is(persistedNativePlane, attribute.indexed.nativePlane) ||
|
|
2539
|
+
(typeof persistedNativePlane === 'string' &&
|
|
2540
|
+
typeof attribute.indexed.nativePlane === 'number' &&
|
|
2541
|
+
persistedNativePlane.trim() !== '' &&
|
|
2542
|
+
Number(persistedNativePlane) === attribute.indexed.nativePlane));
|
|
2543
|
+
if (matchesPersistedLegacySpelling) {
|
|
2544
|
+
attribute.indexed.nativePlane = persistedNativePlane;
|
|
2545
|
+
} else {
|
|
2546
|
+
attribute.indexed.nativePlane = CUSTOM_INDEXES.HNSW.normalizeNativePlaneDeclaration(
|
|
2547
|
+
attribute.indexed.nativePlane
|
|
2548
|
+
);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
const auditEnabledAtEntry =
|
|
2554
|
+
auditExplicitlyEnabled ||
|
|
2555
|
+
(!auditExplicitlyDisabled &&
|
|
2556
|
+
(persistedAuditAtEntry === true || (persistedAuditAtEntry == null && Table?.audit === true)));
|
|
2557
|
+
if (
|
|
2558
|
+
origin !== 'cluster' &&
|
|
2559
|
+
attributes.some((attribute) => {
|
|
2560
|
+
if (attribute.indexed?.type !== 'HNSW') return false;
|
|
2561
|
+
if (attribute.indexed.nativePlane != null) return Boolean(attribute.indexed.nativePlane);
|
|
2562
|
+
const existingAttribute = Table?.attributes.find(
|
|
2563
|
+
(existing: any) => existing.name === attribute.name && existing.indexed?.type === 'HNSW'
|
|
2564
|
+
);
|
|
2565
|
+
return Boolean(existingAttribute?.indexed.nativePlane);
|
|
2566
|
+
}) &&
|
|
2567
|
+
!auditEnabledAtEntry
|
|
2568
|
+
) {
|
|
2569
|
+
throw new ClientError(
|
|
2570
|
+
`Table '${databaseName}.${tableName}' must enable audit logging before using nativePlane because its transaction log is the derived-index recovery source; set nativePlane: false to use the JS index`
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
} catch (error) {
|
|
2574
|
+
releaseLock();
|
|
2575
|
+
throw error;
|
|
2428
2576
|
}
|
|
2577
|
+
const validateHnswOptions = (catalog: any, auditQualifiesDefault: boolean) => {
|
|
2578
|
+
for (const attribute of attributes) {
|
|
2579
|
+
const indexed = attribute.indexed;
|
|
2580
|
+
if (indexed?.type !== 'HNSW') continue;
|
|
2581
|
+
const persistedIndexed = catalog?.getSync(`${tableName}/${attribute.name || ''}`)?.indexed;
|
|
2582
|
+
if (indexed.nativePlane) {
|
|
2583
|
+
CUSTOM_INDEXES.HNSW.validateNativePlaneOptions(rootStore, indexed);
|
|
2584
|
+
continue;
|
|
2585
|
+
}
|
|
2586
|
+
if (indexed.nativePlane != null) continue;
|
|
2587
|
+
if (persistedIndexed?.type === 'HNSW' && Object.hasOwn(persistedIndexed, 'nativePlane')) {
|
|
2588
|
+
if (persistedIndexed.nativePlane)
|
|
2589
|
+
CUSTOM_INDEXES.HNSW.validateNativePlaneOptions(rootStore, {
|
|
2590
|
+
...indexed,
|
|
2591
|
+
nativePlane: persistedIndexed.nativePlane,
|
|
2592
|
+
});
|
|
2593
|
+
continue;
|
|
2594
|
+
}
|
|
2595
|
+
if (!auditQualifiesDefault) continue;
|
|
2596
|
+
if (persistedIndexed?.type !== 'HNSW') CUSTOM_INDEXES.HNSW.canDefaultToNativePlane(rootStore, indexed);
|
|
2597
|
+
}
|
|
2598
|
+
};
|
|
2599
|
+
if (!Table && origin !== 'cluster') validateHnswOptions(undefined, auditExplicitlyEnabled);
|
|
2429
2600
|
let hasChanges;
|
|
2430
2601
|
let refreshRelationshipAttributes = false;
|
|
2602
|
+
let refreshedLiveAttributes = false;
|
|
2431
2603
|
let deferredPrimaryRow: any;
|
|
2432
2604
|
let unpublishedPrimaryStore: any;
|
|
2433
2605
|
let published = false;
|
|
2434
|
-
let releaseExclusiveLock: (() => void) | undefined;
|
|
2435
2606
|
const attributesToIndex = [];
|
|
2436
2607
|
const indicesToRemove = [];
|
|
2437
2608
|
try {
|
|
2438
2609
|
if (Table) {
|
|
2610
|
+
refreshedLiveAttributes = true;
|
|
2439
2611
|
primaryKey = Table.primaryKey;
|
|
2440
2612
|
if (Table.primaryStore.rootStore.status === 'closed') {
|
|
2441
2613
|
throw new Error(`Can not use a closed data store from ${tableName} class`);
|
|
@@ -2464,10 +2636,17 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
2464
2636
|
}
|
|
2465
2637
|
}
|
|
2466
2638
|
// Acquire before the first mutation of the live Table below, so a lost race leaves no
|
|
2467
|
-
// attributes this worker describes but never persisted. Only the RocksDB acquire is bounded
|
|
2468
|
-
//
|
|
2469
|
-
// environment-wide write transaction that cannot time out, so it stays lazy.
|
|
2639
|
+
// attributes this worker describes but never persisted. Only the RocksDB acquire is bounded;
|
|
2640
|
+
// ordinary LMDB declarations stay lazy, while legacy HNSW normalization locks at entry.
|
|
2470
2641
|
if (rootStore instanceof RocksDatabase) exclusiveLock();
|
|
2642
|
+
if (origin !== 'cluster') {
|
|
2643
|
+
const lockedAttributesDbi = Table.dbisDB;
|
|
2644
|
+
const persistedAuditUnderLock = persistedPrimaryDescriptor(lockedAttributesDbi).descriptor?.audit;
|
|
2645
|
+
validateHnswOptions(
|
|
2646
|
+
lockedAttributesDbi,
|
|
2647
|
+
auditExplicitlyEnabled || (!auditExplicitlyDisabled && persistedAuditUnderLock === true)
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2471
2650
|
// it table already exists, get the split segments setting
|
|
2472
2651
|
if (splitSegments == undefined) splitSegments = Table.splitSegments;
|
|
2473
2652
|
if (origin === 'cluster') {
|
|
@@ -2497,6 +2676,13 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
2497
2676
|
);
|
|
2498
2677
|
}
|
|
2499
2678
|
attributes = merged;
|
|
2679
|
+
} else if (!attributes.some((attribute) => attribute.isPrimaryKey)) {
|
|
2680
|
+
const existingPrimary = Table.attributes.find((attribute: any) => attribute.isPrimaryKey);
|
|
2681
|
+
if (existingPrimary && attributes.some((attribute) => attribute.name === existingPrimary.name))
|
|
2682
|
+
throw new ClientError(
|
|
2683
|
+
`Cannot remove the primary key designation from '${databaseName}.${tableName}.${existingPrimary.name}'`
|
|
2684
|
+
);
|
|
2685
|
+
if (existingPrimary) attributes = [existingPrimary, ...attributes];
|
|
2500
2686
|
}
|
|
2501
2687
|
Table.attributes.splice(0, Table.attributes.length, ...attributes);
|
|
2502
2688
|
// Re-assert from the live declaration so a stale value on disk (replicated event,
|
|
@@ -2681,7 +2867,7 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
2681
2867
|
const attribute = attributes.find((attribute) => attribute.name === attribute_name);
|
|
2682
2868
|
const removeIndex = !attribute?.indexed && value.indexed && !value.isPrimaryKey;
|
|
2683
2869
|
// rows already present under a create are aborted state
|
|
2684
|
-
const staleRow = !attribute || Boolean(deferredPrimaryRow);
|
|
2870
|
+
const staleRow = (!attribute && !value.isPrimaryKey) || Boolean(deferredPrimaryRow);
|
|
2685
2871
|
if (staleRow || removeIndex) {
|
|
2686
2872
|
exclusiveLock();
|
|
2687
2873
|
hasChanges = true;
|
|
@@ -2692,9 +2878,79 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
2692
2878
|
}
|
|
2693
2879
|
}
|
|
2694
2880
|
}
|
|
2881
|
+
const hasHnswDeclaration = attributes.some((attribute) => attribute.indexed?.type === 'HNSW');
|
|
2882
|
+
const persistedAudit = hasHnswDeclaration ? persistedPrimaryDescriptor(attributesDbi).descriptor?.audit : undefined;
|
|
2883
|
+
// A cluster declaration can apply audit on a create, but deliberately cannot rewrite an existing
|
|
2884
|
+
// table's primary row. Do not let an incoming audit value qualify a replicated native descriptor
|
|
2885
|
+
// that this node would then persist beside its durable audit:false row.
|
|
2886
|
+
const explicitAuditCanBeApplied = origin !== 'cluster' || Boolean(deferredPrimaryRow);
|
|
2887
|
+
const auditEnabledForNativeDefault =
|
|
2888
|
+
(auditExplicitlyEnabled && explicitAuditCanBeApplied) || (!auditExplicitlyDisabled && persistedAudit === true);
|
|
2889
|
+
const auditEnabledForNativePlane =
|
|
2890
|
+
!auditExplicitlyDisabled && (auditEnabledForNativeDefault || (persistedAudit == null && Table.audit === true));
|
|
2891
|
+
for (const attribute of attributes) {
|
|
2892
|
+
const indexed = attribute.indexed;
|
|
2893
|
+
if (!indexed || typeof indexed !== 'object' || indexed.type !== 'HNSW') continue;
|
|
2894
|
+
const descriptor = attributesDbi.getSync(tableName + '/' + (attribute.name || ''));
|
|
2895
|
+
const existingHnsw = descriptor?.indexed?.type === 'HNSW';
|
|
2896
|
+
if (indexed.nativePlane == null) {
|
|
2897
|
+
if (existingHnsw) {
|
|
2898
|
+
if (Object.hasOwn(descriptor.indexed, 'nativePlane')) {
|
|
2899
|
+
indexed.nativePlane = descriptor.indexed.nativePlane;
|
|
2900
|
+
}
|
|
2901
|
+
} else if (
|
|
2902
|
+
origin !== 'cluster' &&
|
|
2903
|
+
auditEnabledForNativeDefault &&
|
|
2904
|
+
CUSTOM_INDEXES.HNSW.canDefaultToNativePlane(rootStore, indexed)
|
|
2905
|
+
) {
|
|
2906
|
+
indexed.nativePlane = true;
|
|
2907
|
+
}
|
|
2908
|
+
} else if (origin === 'cluster' && !existingHnsw && indexed.nativePlane) {
|
|
2909
|
+
let canRunNative = false;
|
|
2910
|
+
try {
|
|
2911
|
+
canRunNative = auditEnabledForNativeDefault && CUSTOM_INDEXES.HNSW.canRunNativePlane(rootStore, indexed);
|
|
2912
|
+
} catch {}
|
|
2913
|
+
if (!canRunNative) {
|
|
2914
|
+
logger.warn(
|
|
2915
|
+
`Using the JS HNSW index for replicated attribute ${databaseName}.${tableName}.${attribute.name} because this node does not satisfy the nativePlane requirements`
|
|
2916
|
+
);
|
|
2917
|
+
indexed.nativePlane = false;
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
const nativePlaneEnabled =
|
|
2922
|
+
origin !== 'cluster' &&
|
|
2923
|
+
attributes.some((attribute) => attribute.indexed?.type === 'HNSW' && attribute.indexed.nativePlane);
|
|
2924
|
+
if (nativePlaneEnabled && !auditEnabledForNativePlane) {
|
|
2925
|
+
throw new ClientError(
|
|
2926
|
+
`Table '${databaseName}.${tableName}' must enable audit logging before using nativePlane because its transaction log is the derived-index recovery source; set nativePlane: false to use the JS index`
|
|
2927
|
+
);
|
|
2928
|
+
}
|
|
2929
|
+
if (nativePlaneEnabled && persistedAudit !== true) audit = true;
|
|
2930
|
+
if (nativePlaneEnabled && persistedAudit !== true && !attributes.some((attribute) => attribute.isPrimaryKey)) {
|
|
2931
|
+
exclusiveLock();
|
|
2932
|
+
const primaryKey = primaryDescriptorKey();
|
|
2933
|
+
const primaryDescriptor = attributesDbi.getSync(primaryKey);
|
|
2934
|
+
if (primaryDescriptor && !tableIsDropping(primaryDescriptor, primaryKey)) {
|
|
2935
|
+
Table.enableAuditing();
|
|
2936
|
+
attributesDbi.put(primaryKey, { ...primaryDescriptor, audit: true });
|
|
2937
|
+
hasChanges = true;
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2695
2940
|
// TODO: If we have attributes and the schemaDefined flag is not set, turn it on
|
|
2696
2941
|
// iterate through the attributes to ensure that we have all the dbis created and indexed
|
|
2697
|
-
|
|
2942
|
+
const attributesInPersistenceOrder = nativePlaneEnabled
|
|
2943
|
+
? [
|
|
2944
|
+
...attributes.filter((attribute) => attribute.isPrimaryKey),
|
|
2945
|
+
...attributes.filter((attribute) => !attribute.isPrimaryKey),
|
|
2946
|
+
]
|
|
2947
|
+
: auditExplicitlyDisabled
|
|
2948
|
+
? [
|
|
2949
|
+
...attributes.filter((attribute) => !attribute.isPrimaryKey),
|
|
2950
|
+
...attributes.filter((attribute) => attribute.isPrimaryKey),
|
|
2951
|
+
]
|
|
2952
|
+
: attributes;
|
|
2953
|
+
for (const attribute of attributesInPersistenceOrder) {
|
|
2698
2954
|
if (attribute.relationship) {
|
|
2699
2955
|
refreshRelationshipAttributes = true;
|
|
2700
2956
|
continue;
|
|
@@ -2717,7 +2973,7 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
2717
2973
|
if (
|
|
2718
2974
|
origin !== 'cluster' &&
|
|
2719
2975
|
(schemaDefinedMismatch ||
|
|
2720
|
-
(audit
|
|
2976
|
+
(typeof audit === 'boolean' && audit !== attributeDescriptor.audit) ||
|
|
2721
2977
|
(sealed !== undefined && sealed !== Table.sealed) ||
|
|
2722
2978
|
(replicate !== undefined && replicate !== Table.replicate) ||
|
|
2723
2979
|
(+expiration || undefined) !== (+attributeDescriptor.expiration || undefined) ||
|
|
@@ -2754,8 +3010,7 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
2754
3010
|
const abandonedIndexBuild =
|
|
2755
3011
|
attribute.indexed &&
|
|
2756
3012
|
(attributeDescriptor.indexingFailed ||
|
|
2757
|
-
(attributeDescriptor
|
|
2758
|
-
attributeDescriptor.restartNumber < (workerData?.restartNumber ?? manageThreads.restartNumber));
|
|
3013
|
+
isAbandonedIndexBuild(attributeDescriptor, workerData?.restartNumber ?? manageThreads.restartNumber));
|
|
2759
3014
|
if (abandonedIndexBuild) {
|
|
2760
3015
|
// Recovery is the exception to skipping the handling below, because without it `isIndexing`
|
|
2761
3016
|
// stays pinned on with nothing left to clear it and every query on the attribute fails with
|
|
@@ -2845,8 +3100,7 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
2845
3100
|
changed ||
|
|
2846
3101
|
indexFormatNeedsPersist ||
|
|
2847
3102
|
attributeDescriptor?.indexingFailed ||
|
|
2848
|
-
(attributeDescriptor
|
|
2849
|
-
attributeDescriptor?.restartNumber < currentRestartGeneration
|
|
3103
|
+
isAbandonedIndexBuild(attributeDescriptor, currentRestartGeneration)
|
|
2850
3104
|
) {
|
|
2851
3105
|
hasChanges = true;
|
|
2852
3106
|
exclusiveLock();
|
|
@@ -2854,8 +3108,7 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
2854
3108
|
if (
|
|
2855
3109
|
structurallyChanged ||
|
|
2856
3110
|
attributeDescriptor?.indexingFailed ||
|
|
2857
|
-
(attributeDescriptor
|
|
2858
|
-
attributeDescriptor?.restartNumber < currentRestartGeneration
|
|
3111
|
+
isAbandonedIndexBuild(attributeDescriptor, currentRestartGeneration)
|
|
2859
3112
|
) {
|
|
2860
3113
|
hasChanges = true;
|
|
2861
3114
|
if (attribute.indexNulls === undefined) attribute.indexNulls = true;
|
|
@@ -2875,9 +3128,21 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
2875
3128
|
// resumes rather than restarts. Canonicalized to match structurallyChanged above.
|
|
2876
3129
|
const indexOptionsChanged =
|
|
2877
3130
|
canonicalIndexKey(attributeDescriptor?.indexed) !== canonicalIndexKey(attribute.indexed);
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
3131
|
+
// Only a checkpoint runIndexing stamped with its own key resumes: earlier releases advanced
|
|
3132
|
+
// lastIndexedKey past failed and unflushed index writes, so any other is a full rebuild.
|
|
3133
|
+
const uncertifiedCheckpoint =
|
|
3134
|
+
attributeDescriptor?.lastIndexedKey !== undefined &&
|
|
3135
|
+
(attributeDescriptor.checkpointCertified === undefined ||
|
|
3136
|
+
attributeDescriptor.checkpointAlgorithm !== CHECKPOINT_ALGORITHM ||
|
|
3137
|
+
compareKeys(attributeDescriptor.checkpointCertified, attributeDescriptor.lastIndexedKey) !== 0);
|
|
3138
|
+
attribute.lastIndexedKey =
|
|
3139
|
+
indexOptionsChanged || uncertifiedCheckpoint
|
|
3140
|
+
? undefined
|
|
3141
|
+
: (attributeDescriptor?.lastIndexedKey ?? undefined);
|
|
3142
|
+
if (attribute.lastIndexedKey !== undefined) {
|
|
3143
|
+
attribute.checkpointCertified = attribute.lastIndexedKey;
|
|
3144
|
+
attribute.checkpointAlgorithm = CHECKPOINT_ALGORITHM;
|
|
3145
|
+
}
|
|
2881
3146
|
// Explicit reindex is the upgrade path from a legacy (un-versioned) custom-index
|
|
2882
3147
|
// object store to the versioned, VT-cacheable format. A full rebuild from scratch
|
|
2883
3148
|
// (lastIndexedKey === undefined) clears the store and rewrites every node, so the
|
|
@@ -2901,6 +3166,9 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
2901
3166
|
// the new process reuses the old PID. Cleared on clean completion; left in place
|
|
2902
3167
|
// on failure/crash so the next, higher-numbered restart re-triggers the backfill.
|
|
2903
3168
|
attribute.restartNumber = currentRestartGeneration;
|
|
3169
|
+
if (manageThreads.processIncarnation != null)
|
|
3170
|
+
attribute.indexingIncarnation = manageThreads.processIncarnation;
|
|
3171
|
+
attribute.indexingBuildId = randomBytes(8).toString('hex');
|
|
2904
3172
|
delete attribute.indexingFailed; // clear failure flag for the new run
|
|
2905
3173
|
dbi.isIndexing = true;
|
|
2906
3174
|
Object.defineProperty(attribute, 'dbi', { value: dbi, configurable: true, enumerable: false });
|
|
@@ -2914,6 +3182,13 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
2914
3182
|
if (attributeDescriptor?.indexingPID && attributeDescriptor.indexingPID !== process.pid)
|
|
2915
3183
|
reindexReasons.push(`crash-recovery(pid=${attributeDescriptor.indexingPID})`);
|
|
2916
3184
|
if (attributeDescriptor?.restartNumber < currentRestartGeneration) reindexReasons.push('restart-number');
|
|
3185
|
+
if (uncertifiedCheckpoint) reindexReasons.push('uncertified-checkpoint');
|
|
3186
|
+
if (
|
|
3187
|
+
attributeDescriptor?.indexingPID === process.pid &&
|
|
3188
|
+
manageThreads.processIncarnation != null &&
|
|
3189
|
+
attributeDescriptor.indexingIncarnation !== manageThreads.processIncarnation
|
|
3190
|
+
)
|
|
3191
|
+
reindexReasons.push('abandoned-build(previous process incarnation)');
|
|
2917
3192
|
logger.info(
|
|
2918
3193
|
`reindex ${databaseName}.${tableName}.${attribute.name}: reason=${reindexReasons.join(',') || 'unknown'}`
|
|
2919
3194
|
);
|
|
@@ -2927,11 +3202,21 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
2927
3202
|
// workers / a reload would treat the still-partial index as ready and return incomplete results.
|
|
2928
3203
|
attribute.indexingPID = attributeDescriptor.indexingPID;
|
|
2929
3204
|
attribute.lastIndexedKey = attributeDescriptor.lastIndexedKey;
|
|
3205
|
+
if (attributeDescriptor.checkpointCertified !== undefined) {
|
|
3206
|
+
attribute.checkpointCertified = attributeDescriptor.checkpointCertified;
|
|
3207
|
+
attribute.checkpointAlgorithm = attributeDescriptor.checkpointAlgorithm;
|
|
3208
|
+
}
|
|
2930
3209
|
// Carry the in-progress restart generation too, so persisting this metadata-only
|
|
2931
3210
|
// change doesn't drop it and break the crash-recovery trigger for the running backfill.
|
|
2932
3211
|
attribute.restartNumber = attributeDescriptor.restartNumber;
|
|
3212
|
+
attribute.indexingIncarnation = attributeDescriptor.indexingIncarnation;
|
|
3213
|
+
attribute.indexingBuildId = attributeDescriptor.indexingBuildId;
|
|
2933
3214
|
if (attributeDescriptor.indexingFailed) attribute.indexingFailed = attributeDescriptor.indexingFailed;
|
|
2934
3215
|
}
|
|
3216
|
+
// The declared attribute never carries the stamp, so any rewrite of a descriptor that has
|
|
3217
|
+
// one would drop it and make a completed index look like a pre-stamp build.
|
|
3218
|
+
if (attribute.checkpointAlgorithm === undefined && attributeDescriptor?.checkpointAlgorithm !== undefined)
|
|
3219
|
+
attribute.checkpointAlgorithm = attributeDescriptor.checkpointAlgorithm;
|
|
2935
3220
|
attributesDbi.put(dbiKey, attribute);
|
|
2936
3221
|
}
|
|
2937
3222
|
// If a migration is in progress (indexingPID set), any newly opened dbi must also
|
|
@@ -2987,35 +3272,43 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
2987
3272
|
} finally {
|
|
2988
3273
|
releaseLock();
|
|
2989
3274
|
}
|
|
2990
|
-
if (hasChanges || refreshRelationshipAttributes)
|
|
2991
|
-
|
|
2992
|
-
Table.updatedAttributes();
|
|
2993
|
-
}
|
|
3275
|
+
if (hasChanges || refreshRelationshipAttributes) Table.schemaVersion++;
|
|
3276
|
+
if (hasChanges || refreshRelationshipAttributes || refreshedLiveAttributes) Table.updatedAttributes();
|
|
2994
3277
|
logger.trace(`${tableName} table loading, running index`);
|
|
2995
3278
|
const branchPath = target.branch?.path;
|
|
2996
3279
|
if (attributesToIndex.length > 0 || indicesToRemove.length > 0) {
|
|
2997
|
-
|
|
3280
|
+
// captured before the backfill can rewrite the attributes
|
|
3281
|
+
const buildIds = new Map(attributesToIndex.map((attribute) => [attribute, attribute.indexingBuildId]));
|
|
3282
|
+
const markSettled = () => markAbandonedIndexBuild(Table, rootStore, buildIds);
|
|
3283
|
+
Table.indexingOperation = runIndexing(Table, attributesToIndex, indicesToRemove, branchPath).then(
|
|
3284
|
+
markSettled,
|
|
3285
|
+
markSettled
|
|
3286
|
+
);
|
|
2998
3287
|
} else if (hasChanges)
|
|
2999
3288
|
signalling.signalSchemaChange(
|
|
3000
3289
|
new SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName, undefined, branchPath)
|
|
3001
3290
|
);
|
|
3291
|
+
void Table.derivedIndexRuntime?.close();
|
|
3292
|
+
Table.derivedIndexRuntime = attachDerivedIndexes(Table);
|
|
3002
3293
|
|
|
3003
3294
|
Table.origin = origin;
|
|
3004
3295
|
// scope-private: replication and other global subscribers must not learn of a branch class
|
|
3005
3296
|
if ((hasChanges || refreshRelationshipAttributes) && !target.branch) {
|
|
3006
3297
|
databaseEventsEmitter.emit('updateTable', Table, origin !== 'cluster');
|
|
3007
3298
|
}
|
|
3008
|
-
if (expiration || eviction || scanInterval)
|
|
3299
|
+
if (expiration || eviction || scanInterval || attributes.some((attribute) => attribute.expiresAt))
|
|
3009
3300
|
Table.setTTLExpiration({
|
|
3010
3301
|
expiration,
|
|
3011
3302
|
eviction,
|
|
3012
3303
|
scanInterval,
|
|
3304
|
+
fromSchema: true,
|
|
3305
|
+
isolatedApplicationOwner,
|
|
3013
3306
|
});
|
|
3014
3307
|
logger.trace(`${tableName} table loaded`);
|
|
3015
3308
|
|
|
3016
3309
|
return Table as TableResourceType;
|
|
3017
|
-
//
|
|
3018
|
-
//
|
|
3310
|
+
// A migrated catalog can retain a named primary descriptor beside a bare table tombstone, so a
|
|
3311
|
+
// drop in flight has to be checked on both representations.
|
|
3019
3312
|
function tableIsDropping(descriptor: any, descriptorKey: string) {
|
|
3020
3313
|
if (descriptor?.dropping) return true;
|
|
3021
3314
|
return descriptorKey !== tableName + '/' && attributesDbi.getSync(tableName + '/')?.dropping;
|
|
@@ -3023,12 +3316,7 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
3023
3316
|
// The catalog row initStores() reads a table's settings from: the primary key's own row when it
|
|
3024
3317
|
// has one, and the bare table row otherwise.
|
|
3025
3318
|
function primaryDescriptorKey() {
|
|
3026
|
-
|
|
3027
|
-
if (declaredPrimaryKey) {
|
|
3028
|
-
const attributeKey = tableName + '/' + declaredPrimaryKey;
|
|
3029
|
-
if (attributesDbi.getSync(attributeKey)) return attributeKey;
|
|
3030
|
-
}
|
|
3031
|
-
return tableName + '/';
|
|
3319
|
+
return persistedPrimaryDescriptor(attributesDbi).key;
|
|
3032
3320
|
}
|
|
3033
3321
|
// The catalog of a published table stays, but a class the registration never accepted is
|
|
3034
3322
|
// unreachable, so release what makeTable() registered process-wide instead of leaving its timers
|
|
@@ -3092,70 +3380,228 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
3092
3380
|
}
|
|
3093
3381
|
}
|
|
3094
3382
|
/**
|
|
3095
|
-
*
|
|
3096
|
-
*
|
|
3097
|
-
* and config objects can supply them reordered or as numbers; without canonicalizing, such a
|
|
3098
|
-
* representation-only difference flips the structural comparison and forces a needless full rebuild
|
|
3099
|
-
* (clearing + rebuilding the index, 503-ing the attribute throughout) for a semantically identical
|
|
3100
|
-
* index. Sorts object keys and coerces numeric-looking (non-zero) string scalars to numbers.
|
|
3101
|
-
* Conservative by design: boolean-vs-object, absent-vs-present, and string-"0"-vs-number-0
|
|
3102
|
-
* differences are all preserved, so a genuine change (`true` vs `{ type: 'HNSW' }`, an added/removed
|
|
3103
|
-
* option, a changed value) still triggers a rebuild. Persistence keys off the raw form, so the stored
|
|
3104
|
-
* descriptor self-heals toward this shape over time. harper#1357
|
|
3383
|
+
* Stable structural form for deciding whether an index must be rebuilt. `coerceZero` extends numeric
|
|
3384
|
+
* coercion to zero; a truthiness-sensitive numeric option must normalize its value before using it.
|
|
3105
3385
|
*/
|
|
3106
|
-
export function canonicalizeIndexOptions(value: any): any {
|
|
3107
|
-
if (Array.isArray(value)) return value.map(canonicalizeIndexOptions);
|
|
3386
|
+
export function canonicalizeIndexOptions(value: any, coerceZero = false): any {
|
|
3387
|
+
if (Array.isArray(value)) return value.map((item) => canonicalizeIndexOptions(item, coerceZero));
|
|
3108
3388
|
if (value && typeof value === 'object') {
|
|
3109
3389
|
const canonical: Record<string, any> = {};
|
|
3110
|
-
|
|
3390
|
+
const customIndex = value.type && (CUSTOM_INDEXES as Record<string, any>)[value.type];
|
|
3391
|
+
for (const key of Object.keys(value).sort()) {
|
|
3392
|
+
if (customIndex?.truthyStructuralOptions?.has(key)) {
|
|
3393
|
+
if (value[key]) canonical[key] = true;
|
|
3394
|
+
continue;
|
|
3395
|
+
}
|
|
3396
|
+
const optionValue = customIndex?.normalizeOptionValue
|
|
3397
|
+
? customIndex.normalizeOptionValue(key, value[key])
|
|
3398
|
+
: value[key];
|
|
3399
|
+
canonical[key] = canonicalizeIndexOptions(
|
|
3400
|
+
optionValue,
|
|
3401
|
+
coerceZero || Boolean(customIndex?.numericOptions?.has(key))
|
|
3402
|
+
);
|
|
3403
|
+
}
|
|
3111
3404
|
return canonical;
|
|
3112
3405
|
}
|
|
3113
|
-
// Coerce numeric-looking strings ("16" -> 16) so string-vs-number representations of the same
|
|
3114
|
-
// option compare equal — EXCEPT zero: the string "0" is truthy while the number 0 is falsy, and
|
|
3115
|
-
// index code may branch on truthiness (e.g. HNSW `if (this.optimizeRouting)` doubles maxConnections),
|
|
3116
|
-
// so "0" and 0 build structurally different indexes and must still trigger a rebuild. Zero is the
|
|
3117
|
-
// only finite number whose string and numeric forms diverge in truthiness, so excluding it fully
|
|
3118
|
-
// closes that gap. Leave non-numeric strings, booleans, null, etc. intact.
|
|
3119
3406
|
if (typeof value === 'string' && value.trim() !== '') {
|
|
3120
3407
|
const numeric = Number(value);
|
|
3121
|
-
if (numeric !== 0 && Number.isFinite(numeric)) return numeric;
|
|
3408
|
+
if ((numeric !== 0 || coerceZero) && Number.isFinite(numeric)) return numeric;
|
|
3122
3409
|
}
|
|
3123
3410
|
return value;
|
|
3124
3411
|
}
|
|
3412
|
+
// Bumped when a change alters which keys a checkpoint may certify. A descriptor stamped by any other
|
|
3413
|
+
// version resumes as uncertified (full rebuild) rather than being trusted, and a completed index keeps
|
|
3414
|
+
// the stamp of the build that wrote it.
|
|
3415
|
+
export const CHECKPOINT_ALGORITHM = 2;
|
|
3125
3416
|
const MAX_OUTSTANDING_INDEXING = 1000;
|
|
3126
3417
|
const MIN_OUTSTANDING_INDEXING = 10;
|
|
3418
|
+
const INDEXING_YIELD_INTERVAL = 100;
|
|
3419
|
+
// A resumable checkpoint is written only after a flush (see flushIndexStores), at most once per period
|
|
3420
|
+
// and never before this many more records: the flush seals every column family in the database, so a
|
|
3421
|
+
// slow backfill must not impose the period's flush rate on unrelated tables.
|
|
3422
|
+
let indexingCheckpointPeriodMs = 5000;
|
|
3423
|
+
let indexingCheckpointMinRecords = 10000;
|
|
3424
|
+
export function setIndexingCheckpointPeriod(ms: number, minRecords = indexingCheckpointMinRecords) {
|
|
3425
|
+
const previous = { ms: indexingCheckpointPeriodMs, minRecords: indexingCheckpointMinRecords };
|
|
3426
|
+
indexingCheckpointPeriodMs = ms;
|
|
3427
|
+
indexingCheckpointMinRecords = minRecords;
|
|
3428
|
+
return previous;
|
|
3429
|
+
}
|
|
3430
|
+
const yieldEventTurn = () => new Promise((resolve) => setImmediate(resolve));
|
|
3431
|
+
// RocksDB index stores have no WAL (openRocksDatabase defaults disableWAL), so a flush is what makes the
|
|
3432
|
+
// entries a checkpoint certifies durable. A flush only covers writes issued before it started, so a caller
|
|
3433
|
+
// never joins one in flight: it joins the next one, which every backfill on that database asking meanwhile
|
|
3434
|
+
// shares — at most one in flight and one queued.
|
|
3435
|
+
const indexingFlushes = new WeakMap<object, { inFlight?: Promise<void>; queued?: Promise<void> }>();
|
|
3436
|
+
function flushIndexStores(rootStore: any): Promise<void> | undefined {
|
|
3437
|
+
if (!(rootStore instanceof RocksDatabase)) return;
|
|
3438
|
+
let flushes = indexingFlushes.get(rootStore);
|
|
3439
|
+
if (!flushes) indexingFlushes.set(rootStore, (flushes = {}));
|
|
3440
|
+
if (flushes.queued) return flushes.queued;
|
|
3441
|
+
const start = () => {
|
|
3442
|
+
flushes.queued = undefined;
|
|
3443
|
+
const flush = rootStore.flush().finally(() => {
|
|
3444
|
+
if (flushes.inFlight === flush) flushes.inFlight = undefined;
|
|
3445
|
+
});
|
|
3446
|
+
flushes.inFlight = flush;
|
|
3447
|
+
return flush;
|
|
3448
|
+
};
|
|
3449
|
+
if (!flushes.inFlight) return start();
|
|
3450
|
+
return (flushes.queued = flushes.inFlight.then(start, start));
|
|
3451
|
+
}
|
|
3452
|
+
export function resumeStartKey(attributes: { lastIndexedKey?: any }[]): any {
|
|
3453
|
+
let start: any;
|
|
3454
|
+
for (const attribute of attributes) {
|
|
3455
|
+
if (attribute.lastIndexedKey == undefined) return undefined;
|
|
3456
|
+
if (start === undefined || compareKeys(attribute.lastIndexedKey, start) < 0) start = attribute.lastIndexedKey;
|
|
3457
|
+
}
|
|
3458
|
+
return start;
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
/**
|
|
3462
|
+
* Persists the failure marker for a build that ended without running one of runIndexing's own exit
|
|
3463
|
+
* paths, so something re-triggers it. Fenced on `indexingBuildId` inside the storage engine's catalog
|
|
3464
|
+
* serialization boundary, because a replacement generation (or another thread declaring different index
|
|
3465
|
+
* options) can claim the attribute before an outgoing build's promise settles, and marking that would fail
|
|
3466
|
+
* a live build. The fence read and write stay synchronous, and nothing here may throw because
|
|
3467
|
+
* `Table.indexingOperation` reaches operations-API callers.
|
|
3468
|
+
*/
|
|
3469
|
+
async function markAbandonedIndexBuild(Table, rootStore, buildIds: Map<any, string>) {
|
|
3470
|
+
for (const [attribute, buildId] of buildIds) {
|
|
3471
|
+
try {
|
|
3472
|
+
let marked;
|
|
3473
|
+
if (buildId == null || Table.dbisDB.getSync(attribute.key)?.indexingBuildId !== buildId) continue;
|
|
3474
|
+
const markIfOwned = () => {
|
|
3475
|
+
const descriptor = Table.dbisDB.getSync(attribute.key);
|
|
3476
|
+
if (descriptor?.indexingBuildId === buildId && !descriptor.indexingFailed) {
|
|
3477
|
+
Table.dbisDB.putSync(attribute.key, { ...descriptor, indexingFailed: true });
|
|
3478
|
+
marked = true;
|
|
3479
|
+
}
|
|
3480
|
+
};
|
|
3481
|
+
if (rootStore instanceof RocksDatabase) {
|
|
3482
|
+
acquireUpdateAttributesLock(rootStore, `abandoned index build '${Table.tableName}.${attribute.name}'`);
|
|
3483
|
+
try {
|
|
3484
|
+
markIfOwned();
|
|
3485
|
+
} finally {
|
|
3486
|
+
releaseUpdateAttributesLock(rootStore);
|
|
3487
|
+
}
|
|
3488
|
+
} else {
|
|
3489
|
+
rootStore.transactionSync(markIfOwned);
|
|
3490
|
+
}
|
|
3491
|
+
if (marked)
|
|
3492
|
+
logger.warn(
|
|
3493
|
+
`Indexing of ${Table.databaseName}.${Table.tableName}.${attribute.name} ended without completing. ` +
|
|
3494
|
+
`The index stays incomplete and every query on the attribute reports it as not indexed yet; ` +
|
|
3495
|
+
`the next load of the table retries the backfill from the last checkpoint (indexingFailed=true).`
|
|
3496
|
+
);
|
|
3497
|
+
} catch (error) {
|
|
3498
|
+
// A store closed by shutdown is the common case, and it cannot be written to at all.
|
|
3499
|
+
try {
|
|
3500
|
+
logger.debug(`Could not mark the abandoned index build of ${Table.tableName}.${attribute.name}`, error);
|
|
3501
|
+
} catch {}
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3127
3505
|
async function runIndexing(Table, attributes, indicesToRemove, branchPath?: string) {
|
|
3506
|
+
let checkpointing;
|
|
3507
|
+
let hadIndexingErrors = false;
|
|
3508
|
+
const attributeErrorReported = {};
|
|
3509
|
+
const onIndexPutRejected = (property, error) => {
|
|
3510
|
+
hadIndexingErrors = true;
|
|
3511
|
+
if (attributeErrorReported[property]) return;
|
|
3512
|
+
attributeErrorReported[property] = true;
|
|
3513
|
+
logger.error(`Error indexing attribute ${property}`, error);
|
|
3514
|
+
};
|
|
3515
|
+
const putRejectionHandlers = attributes.map((attribute) => (error) => onIndexPutRejected(attribute.name, error));
|
|
3128
3516
|
try {
|
|
3129
3517
|
logger.info(`Indexing ${Table.tableName} attributes`, attributes);
|
|
3130
3518
|
await signalling.signalSchemaChange(
|
|
3131
3519
|
new SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName, undefined, branchPath)
|
|
3132
3520
|
);
|
|
3133
3521
|
let lastResolution;
|
|
3522
|
+
// The checkpoint and completion barriers have to cover every mutation still in flight: any of them
|
|
3523
|
+
// may reject after those barriers read hadIndexingErrors.
|
|
3524
|
+
const pendingMutations = new Set();
|
|
3525
|
+
let settleWaiter;
|
|
3526
|
+
const track = (result, onRejected) => {
|
|
3527
|
+
if (!result?.then) return result;
|
|
3528
|
+
const tracked = result.then(
|
|
3529
|
+
() => {
|
|
3530
|
+
pendingMutations.delete(tracked);
|
|
3531
|
+
settleWaiter?.();
|
|
3532
|
+
return false;
|
|
3533
|
+
},
|
|
3534
|
+
(error) => {
|
|
3535
|
+
pendingMutations.delete(tracked);
|
|
3536
|
+
settleWaiter?.();
|
|
3537
|
+
onRejected(error);
|
|
3538
|
+
return true;
|
|
3539
|
+
}
|
|
3540
|
+
);
|
|
3541
|
+
pendingMutations.add(tracked);
|
|
3542
|
+
return result;
|
|
3543
|
+
};
|
|
3544
|
+
// The tracked promises absorb their own rejections, so one failure never abandons its siblings.
|
|
3545
|
+
const drainMutations = async () => {
|
|
3546
|
+
if (!pendingMutations.size) return false;
|
|
3547
|
+
return (await Promise.all([...pendingMutations])).some(Boolean);
|
|
3548
|
+
};
|
|
3549
|
+
// Waiting on a chosen entry would stall behind a slow one the others have already overtaken.
|
|
3550
|
+
const nextSettlement = () =>
|
|
3551
|
+
new Promise((resolve) => {
|
|
3552
|
+
settleWaiter = () => {
|
|
3553
|
+
settleWaiter = undefined;
|
|
3554
|
+
resolve(undefined);
|
|
3555
|
+
};
|
|
3556
|
+
});
|
|
3134
3557
|
for (const index of indicesToRemove) {
|
|
3135
|
-
|
|
3558
|
+
index.customIndex?.resetDerivedStorage?.();
|
|
3559
|
+
track(index.drop(), (error) => onIndexPutRejected(index.name, error));
|
|
3136
3560
|
}
|
|
3137
3561
|
let interrupted;
|
|
3138
|
-
let hadIndexingErrors = false;
|
|
3139
|
-
const attributeErrorReported = {};
|
|
3140
3562
|
let indexed = 0;
|
|
3141
3563
|
const attributesLength = attributes.length;
|
|
3142
3564
|
await new Promise((resolve) => setImmediate(resolve)); // yield event turn, indexing should consistently take at least one event turn
|
|
3143
3565
|
if (attributesLength > 0) {
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
if (compareKeys(attribute.lastIndexedKey, start) < 0) start = attribute.lastIndexedKey;
|
|
3148
|
-
if (attribute.lastIndexedKey == undefined) {
|
|
3566
|
+
const start = resumeStartKey(attributes);
|
|
3567
|
+
if (start === undefined) {
|
|
3568
|
+
for (const attribute of attributes) {
|
|
3149
3569
|
// if we are starting from the beginning, clear out any previous index entries since we are rewriting
|
|
3570
|
+
attribute.dbi.customIndex?.resetDerivedStorage?.();
|
|
3150
3571
|
if (attribute.dbi.clearAsync) {
|
|
3151
|
-
// LMDB
|
|
3152
|
-
|
|
3572
|
+
// LMDB enqueues this ahead of the index writes, so the scan need not wait for it — but the
|
|
3573
|
+
// barriers must, or a rejected clear certifies a checkpoint over stale entries.
|
|
3574
|
+
track(attribute.dbi.clearAsync(), (error) => onIndexPutRejected(attribute.name, error));
|
|
3153
3575
|
} else {
|
|
3154
3576
|
await attribute.dbi.clear();
|
|
3155
3577
|
}
|
|
3156
3578
|
}
|
|
3157
3579
|
}
|
|
3158
|
-
|
|
3580
|
+
// A resumed scan starts at the checkpoint, so it must only name a key whose every predecessor is
|
|
3581
|
+
// durably indexed: persisted once the writes it covers have settled and flushed, frozen after any
|
|
3582
|
+
// record fails so the retry re-covers it, and stamped with its own key (see the trigger in table()).
|
|
3583
|
+
const persistCheckpoint = async (key) => {
|
|
3584
|
+
if (hadIndexingErrors) return;
|
|
3585
|
+
try {
|
|
3586
|
+
// Everything still in flight was issued for a key at or before this one: the scan has not
|
|
3587
|
+
// moved past it yet. So a failure among them is a failure this checkpoint would cover.
|
|
3588
|
+
const failed = await drainMutations();
|
|
3589
|
+
if (failed) return;
|
|
3590
|
+
await flushIndexStores(Table.primaryStore.rootStore);
|
|
3591
|
+
const puts = [];
|
|
3592
|
+
for (const attribute of attributes) {
|
|
3593
|
+
attribute.lastIndexedKey = key;
|
|
3594
|
+
attribute.checkpointCertified = key;
|
|
3595
|
+
attribute.checkpointAlgorithm = CHECKPOINT_ALGORITHM;
|
|
3596
|
+
puts.push(Table.dbisDB.put(attribute.key, attribute));
|
|
3597
|
+
}
|
|
3598
|
+
await Promise.all(puts);
|
|
3599
|
+
} catch (error) {
|
|
3600
|
+
logger.warn(`Could not persist the indexing checkpoint for ${Table.tableName}`, error);
|
|
3601
|
+
}
|
|
3602
|
+
};
|
|
3603
|
+
let nextCheckpointAt = performance.now() + indexingCheckpointPeriodMs;
|
|
3604
|
+
let nextCheckpointRecord = indexingCheckpointMinRecords;
|
|
3159
3605
|
// this means that a new attribute has been introduced that needs to be indexed
|
|
3160
3606
|
for (const { key, value: record } of Table.primaryStore.getRange({
|
|
3161
3607
|
start,
|
|
@@ -3163,93 +3609,86 @@ async function runIndexing(Table, attributes, indicesToRemove, branchPath?: stri
|
|
|
3163
3609
|
versions: true,
|
|
3164
3610
|
snapshot: false, // don't hold a read transaction this whole time
|
|
3165
3611
|
})) {
|
|
3166
|
-
|
|
3612
|
+
const atInterval = ++indexed % INDEXING_YIELD_INTERVAL === 0;
|
|
3167
3613
|
// TODO: Do we ever need to interrupt due to a schema change that was not a restart?
|
|
3168
3614
|
//if (Table.schemaVersion !== schemaVersion) return; // break out if there are any schema changes and let someone else pick it up
|
|
3169
|
-
|
|
3170
|
-
//
|
|
3171
|
-
// outstanding-based yield below never fires for them. Track that this row did synchronous
|
|
3615
|
+
// Custom indexes (e.g. HNSW) index synchronously and leave pendingMutations empty, so the
|
|
3616
|
+
// backpressure yield below never fires for them. Track that this row did synchronous
|
|
3172
3617
|
// indexing work so we can still yield the event loop after it.
|
|
3173
3618
|
let didSynchronousIndexing = false;
|
|
3174
3619
|
// every index operation needs to be guarded by the version still be the same. If it has already changed before
|
|
3175
3620
|
// we index, that's fine because indexing is idempotent, we can just put the same values again. If it changes
|
|
3176
3621
|
// during the indexing, the indexing here will fail. This is also fine because it means the other thread will have
|
|
3177
3622
|
// performed indexing and we don't need to do anything further
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
const
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3623
|
+
if (record) {
|
|
3624
|
+
for (let i = 0; i < attributesLength; i++) {
|
|
3625
|
+
const attribute = attributes[i];
|
|
3626
|
+
const property = attribute.name;
|
|
3627
|
+
const index = attribute.dbi;
|
|
3628
|
+
const onPutRejected = putRejectionHandlers[i];
|
|
3629
|
+
try {
|
|
3630
|
+
const resolver = attribute.resolve;
|
|
3631
|
+
const value = record && (resolver ? resolver(record) : record[property]);
|
|
3632
|
+
if (index.customIndex) {
|
|
3633
|
+
index.customIndex.index(key, value);
|
|
3634
|
+
didSynchronousIndexing = true;
|
|
3635
|
+
continue;
|
|
3636
|
+
}
|
|
3637
|
+
const values = getIndexedValues(value, index.indexNulls);
|
|
3638
|
+
if (values) {
|
|
3639
|
+
for (let i = 0, l = values.length; i < l; i++) {
|
|
3640
|
+
track(index.put(values[i], key), onPutRejected);
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
} catch (error) {
|
|
3644
|
+
hadIndexingErrors = true;
|
|
3645
|
+
if (!attributeErrorReported[property]) {
|
|
3646
|
+
// just report an indexing error once per attribute so we don't spam the logs.
|
|
3647
|
+
// A store closed by worker shutdown surfaces here as "Database not open"; that is
|
|
3648
|
+
// a benign interruption (the next generation re-runs the backfill), so don't log
|
|
3649
|
+
// it as an error — the outer catch returns quietly once the iterator also throws.
|
|
3650
|
+
attributeErrorReported[property] = true;
|
|
3651
|
+
if (Table.primaryStore?.rootStore?.status === 'closed')
|
|
3652
|
+
logger.debug(`Indexing attribute ${property} interrupted by store shutdown`, error);
|
|
3653
|
+
else logger.error(`Error indexing attribute ${property}`, error);
|
|
3194
3654
|
}
|
|
3195
|
-
}
|
|
3196
|
-
} catch (error) {
|
|
3197
|
-
hadIndexingErrors = true;
|
|
3198
|
-
if (!attributeErrorReported[property]) {
|
|
3199
|
-
// just report an indexing error once per attribute so we don't spam the logs.
|
|
3200
|
-
// A store closed by worker shutdown surfaces here as "Database not open"; that is
|
|
3201
|
-
// a benign interruption (the next generation re-runs the backfill), so don't log
|
|
3202
|
-
// it as an error — the outer catch returns quietly once the iterator also throws.
|
|
3203
|
-
attributeErrorReported[property] = true;
|
|
3204
|
-
if (Table.primaryStore?.rootStore?.status === 'closed')
|
|
3205
|
-
logger.debug(`Indexing attribute ${property} interrupted by store shutdown`, error);
|
|
3206
|
-
else logger.error(`Error indexing attribute ${property}`, error);
|
|
3207
3655
|
}
|
|
3208
3656
|
}
|
|
3209
3657
|
}
|
|
3210
|
-
when(
|
|
3211
|
-
lastResolution,
|
|
3212
|
-
() => outstanding--,
|
|
3213
|
-
(error) => {
|
|
3214
|
-
outstanding--;
|
|
3215
|
-
hadIndexingErrors = true;
|
|
3216
|
-
logger.error(error);
|
|
3217
|
-
}
|
|
3218
|
-
);
|
|
3219
3658
|
if (workerData && workerData.restartNumber !== manageThreads.restartNumber) {
|
|
3220
3659
|
interrupted = true;
|
|
3221
3660
|
}
|
|
3222
|
-
if (
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
}
|
|
3228
|
-
if (interrupted) return;
|
|
3661
|
+
if (interrupted) {
|
|
3662
|
+
await drainMutations();
|
|
3663
|
+
await checkpointing;
|
|
3664
|
+
await persistCheckpoint(key);
|
|
3665
|
+
return;
|
|
3229
3666
|
}
|
|
3230
|
-
if (
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3667
|
+
if (atInterval && indexed >= nextCheckpointRecord && performance.now() >= nextCheckpointAt) {
|
|
3668
|
+
nextCheckpointAt = performance.now() + indexingCheckpointPeriodMs;
|
|
3669
|
+
nextCheckpointRecord = indexed + indexingCheckpointMinRecords;
|
|
3670
|
+
await checkpointing;
|
|
3671
|
+
checkpointing = persistCheckpoint(key);
|
|
3672
|
+
}
|
|
3673
|
+
// Checked once per record, so a record's own fan-out can overshoot before the bound applies.
|
|
3674
|
+
while (pendingMutations.size > MAX_OUTSTANDING_INDEXING) await nextSettlement();
|
|
3675
|
+
if (atInterval || didSynchronousIndexing || pendingMutations.size > MIN_OUTSTANDING_INDEXING)
|
|
3676
|
+
await yieldEventTurn();
|
|
3234
3677
|
}
|
|
3235
3678
|
}
|
|
3236
|
-
|
|
3237
|
-
//
|
|
3238
|
-
|
|
3239
|
-
//
|
|
3240
|
-
//
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3679
|
+
await checkpointing;
|
|
3680
|
+
// A mutation that rejects after completion is declared has no build left to park.
|
|
3681
|
+
await drainMutations();
|
|
3682
|
+
// the tail since the last checkpoint is not durable until flushed; announcing the index complete
|
|
3683
|
+
// before that would outlive a crash that loses it
|
|
3684
|
+
if (!hadIndexingErrors) {
|
|
3685
|
+
try {
|
|
3686
|
+
await flushIndexStores(Table.primaryStore.rootStore);
|
|
3687
|
+
} catch (error) {
|
|
3688
|
+
hadIndexingErrors = true;
|
|
3689
|
+
logger.error(`Could not flush the indexes of ${Table.tableName} before marking them complete`, error);
|
|
3690
|
+
}
|
|
3248
3691
|
}
|
|
3249
|
-
// Yield one more event turn so any queued when() error callbacks (which fire as
|
|
3250
|
-
// microtasks when their tracked promise settles) have a chance to set hadIndexingErrors
|
|
3251
|
-
// before we decide whether to mark indexing as complete.
|
|
3252
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
3253
3692
|
if (hadIndexingErrors) {
|
|
3254
3693
|
// Some records failed to index. Persist the failure marker in the descriptor so
|
|
3255
3694
|
// the next call to table() (including after a restart with a fresh PID) re-triggers
|
|
@@ -3278,9 +3717,15 @@ async function runIndexing(Table, attributes, indicesToRemove, branchPath?: stri
|
|
|
3278
3717
|
// update the attributes to indicate that we are finished
|
|
3279
3718
|
for (const attribute of attributes) {
|
|
3280
3719
|
delete attribute.lastIndexedKey;
|
|
3720
|
+
delete attribute.checkpointCertified;
|
|
3721
|
+
// Survives completion, unlike the checkpoint fields: without it an index built here is
|
|
3722
|
+
// indistinguishable from one a release that could skip a failed record declared complete.
|
|
3723
|
+
attribute.checkpointAlgorithm = CHECKPOINT_ALGORITHM;
|
|
3281
3724
|
delete attribute.indexingPID;
|
|
3282
3725
|
delete attribute.indexingFailed;
|
|
3283
3726
|
delete attribute.restartNumber;
|
|
3727
|
+
delete attribute.indexingIncarnation;
|
|
3728
|
+
delete attribute.indexingBuildId;
|
|
3284
3729
|
attribute.dbi.isIndexing = false;
|
|
3285
3730
|
// Also clear isIndexing on the currently-active dbi in Table.indices, which may
|
|
3286
3731
|
// differ from attribute.dbi if a resetDatabases() call during this migration
|
|
@@ -3297,6 +3742,7 @@ async function runIndexing(Table, attributes, indicesToRemove, branchPath?: stri
|
|
|
3297
3742
|
logger.info(`Finished indexing ${Table.tableName} attributes`, attributes);
|
|
3298
3743
|
}
|
|
3299
3744
|
} catch (error) {
|
|
3745
|
+
await checkpointing;
|
|
3300
3746
|
// A worker shutting down closes its stores mid-backfill, so the range iterator or a
|
|
3301
3747
|
// put throws (e.g. "Database not open" / "Iterator not initialized"). This is an
|
|
3302
3748
|
// interruption, not a data error: the next worker generation re-runs the backfill via
|
|
@@ -3360,6 +3806,24 @@ function completeInterruptedDrop(rootStore, attributesDbi, databaseName: string,
|
|
|
3360
3806
|
} finally {
|
|
3361
3807
|
columnStore.close();
|
|
3362
3808
|
}
|
|
3809
|
+
// derived HNSW plane files live next to the store; the normal drop path removes
|
|
3810
|
+
// them through the custom index, but this recovery path drops raw column stores,
|
|
3811
|
+
// and a same-name recreate must never open a stale plane over a fresh CF
|
|
3812
|
+
try {
|
|
3813
|
+
unlinkSync(planeFilePathFor(rootStore.path, columnName));
|
|
3814
|
+
} catch (error: any) {
|
|
3815
|
+
// a stale plane left behind (e.g. Windows EBUSY while still mapped) would be
|
|
3816
|
+
// opened over a fresh same-name CF, resolving another graph's node ids
|
|
3817
|
+
// against it — tombstone it so no attach ever adopts it
|
|
3818
|
+
if (error?.code !== 'ENOENT') {
|
|
3819
|
+
logger.warn(`could not delete the HNSW plane file for ${columnName}; tombstoning it as stale`, error);
|
|
3820
|
+
try {
|
|
3821
|
+
closeSync(openSync(planeStalePathFor(planeFilePathFor(rootStore.path, columnName)), 'w'));
|
|
3822
|
+
} catch (tombstoneError) {
|
|
3823
|
+
logger.warn(`could not tombstone the stale HNSW plane file for ${columnName}`, tombstoneError);
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3363
3827
|
}
|
|
3364
3828
|
}
|
|
3365
3829
|
} else {
|