@harperfast/harper 5.3.0-beta.3 → 5.3.0-beta.4
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 +25 -3
- package/bin/run.ts +18 -0
- package/components/Application.ts +52 -78
- package/components/DESIGN.md +76 -3
- package/components/Scope.ts +10 -4
- package/components/anthropic/index.ts +10 -1
- package/components/bedrock/index.ts +10 -1
- package/components/componentLoader.ts +23 -14
- package/components/mcp/tools/operations.ts +3 -1
- package/components/ollama/index.ts +10 -1
- package/components/openai/index.ts +265 -18
- package/components/operations.js +40 -1
- package/config/configUtils.ts +3 -19
- package/dataLayer/DESIGN.md +29 -5
- package/dataLayer/harperBridge/ResourceBridge.ts +39 -3
- package/dataLayer/restoreMarker.ts +250 -14
- package/dataLayer/schema.ts +2 -7
- package/dataLayer/schemaDescribe.ts +10 -0
- package/dist/bin/cliOperations.js +28 -3
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/run.js +19 -0
- package/dist/bin/run.js.map +1 -1
- package/dist/components/Application.d.ts +8 -2
- package/dist/components/Application.js +43 -69
- package/dist/components/Application.js.map +1 -1
- package/dist/components/Scope.d.ts +1 -1
- package/dist/components/Scope.js +9 -4
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/anthropic/index.d.ts +0 -10
- package/dist/components/anthropic/index.js +9 -1
- package/dist/components/anthropic/index.js.map +1 -1
- package/dist/components/bedrock/index.d.ts +0 -12
- package/dist/components/bedrock/index.js +9 -1
- package/dist/components/bedrock/index.js.map +1 -1
- package/dist/components/componentLoader.js +23 -14
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/mcp/tools/operations.js +1 -1
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/ollama/index.d.ts +0 -13
- package/dist/components/ollama/index.js +9 -1
- package/dist/components/ollama/index.js.map +1 -1
- package/dist/components/openai/index.d.ts +11 -15
- package/dist/components/openai/index.js +224 -12
- package/dist/components/openai/index.js.map +1 -1
- package/dist/components/operations.d.ts +9 -0
- package/dist/components/operations.js +32 -2
- package/dist/components/operations.js.map +1 -1
- package/dist/config/configUtils.d.ts +1 -2
- package/dist/config/configUtils.js +7 -22
- package/dist/config/configUtils.js.map +1 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.js +29 -2
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/restoreMarker.d.ts +25 -3
- package/dist/dataLayer/restoreMarker.js +213 -13
- package/dist/dataLayer/restoreMarker.js.map +1 -1
- package/dist/dataLayer/schema.js +1 -4
- package/dist/dataLayer/schema.js.map +1 -1
- package/dist/dataLayer/schemaDescribe.js +10 -0
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +28 -0
- package/dist/resources/DatabaseTransaction.d.ts +21 -1
- package/dist/resources/DatabaseTransaction.js +147 -8
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.js +1 -1
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/Table.d.ts +31 -3
- package/dist/resources/Table.js +397 -79
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/auditStore.js +16 -10
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/blob.d.ts +1 -0
- package/dist/resources/blob.js +14 -5
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/branchDatabase.js +3 -3
- package/dist/resources/branchDatabase.js.map +1 -1
- package/dist/resources/databaseDropPreparation.d.ts +20 -0
- package/dist/resources/databaseDropPreparation.js +106 -0
- package/dist/resources/databaseDropPreparation.js.map +1 -0
- package/dist/resources/databases.d.ts +26 -21
- package/dist/resources/databases.js +1040 -204
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/derivedIndexRegistry.d.ts +23 -0
- package/dist/resources/derivedIndexRegistry.js +92 -0
- package/dist/resources/derivedIndexRegistry.js.map +1 -1
- package/dist/resources/derivedIndexRuntime.d.ts +10 -0
- package/dist/resources/derivedIndexRuntime.js +136 -25
- package/dist/resources/derivedIndexRuntime.js.map +1 -1
- package/dist/resources/derivedIndexes.d.ts +39 -0
- package/dist/resources/derivedIndexes.js +733 -0
- package/dist/resources/derivedIndexes.js.map +1 -0
- package/dist/resources/fullTextSchema.d.ts +8 -0
- package/dist/resources/fullTextSchema.js +42 -0
- package/dist/resources/fullTextSchema.js.map +1 -1
- package/dist/resources/fullTextSchemaLifecycle.d.ts +0 -5
- package/dist/resources/fullTextSchemaLifecycle.js +0 -9
- package/dist/resources/fullTextSchemaLifecycle.js.map +1 -1
- package/dist/resources/graphql.js +101 -9
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/hnswDerivedIndex.d.ts +0 -10
- package/dist/resources/indexes/hnswDerivedIndex.js +0 -230
- package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -1
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.d.ts +3 -0
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js +57 -8
- package/dist/resources/indexes/nativeFullTextDerivedIndexLifecycle.js.map +1 -1
- package/dist/resources/models/Models.d.ts +26 -3
- package/dist/resources/models/Models.js +256 -13
- package/dist/resources/models/Models.js.map +1 -1
- package/dist/resources/models/TestBackend.d.ts +3 -1
- package/dist/resources/models/TestBackend.js +35 -1
- package/dist/resources/models/TestBackend.js.map +1 -1
- package/dist/resources/models/analyticsTable.d.ts +2 -2
- package/dist/resources/models/analyticsTable.js +4 -2
- package/dist/resources/models/analyticsTable.js.map +1 -1
- package/dist/resources/models/backendHelpers.d.ts +24 -0
- package/dist/resources/models/backendHelpers.js +18 -1
- package/dist/resources/models/backendHelpers.js.map +1 -1
- package/dist/resources/models/backendRegistry.d.ts +10 -21
- package/dist/resources/models/backendRegistry.js +74 -18
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/bootstrap.d.ts +7 -0
- package/dist/resources/models/bootstrap.js +36 -4
- package/dist/resources/models/bootstrap.js.map +1 -1
- package/dist/resources/models/decideHook.d.ts +33 -0
- package/dist/resources/models/decideHook.js +105 -0
- package/dist/resources/models/decideHook.js.map +1 -0
- package/dist/resources/models/decision.d.ts +64 -0
- package/dist/resources/models/decision.js +507 -0
- package/dist/resources/models/decision.js.map +1 -0
- package/dist/resources/models/decisionStore.d.ts +134 -0
- package/dist/resources/models/decisionStore.js +350 -0
- package/dist/resources/models/decisionStore.js.map +1 -0
- package/dist/resources/models/embedHook.d.ts +66 -8
- package/dist/resources/models/embedHook.js +228 -61
- package/dist/resources/models/embedHook.js.map +1 -1
- package/dist/resources/models/generativeDecision.d.ts +58 -0
- package/dist/resources/models/generativeDecision.js +315 -0
- package/dist/resources/models/generativeDecision.js.map +1 -0
- package/dist/resources/models/types.d.ts +232 -4
- package/dist/security/authn/oidc/tokenExchange.js +3 -33
- package/dist/security/authn/oidc/tokenExchange.js.map +1 -1
- package/dist/security/authn/oidc/tokenUseTable.d.ts +9 -0
- package/dist/security/authn/oidc/tokenUseTable.js +28 -0
- package/dist/security/authn/oidc/tokenUseTable.js.map +1 -0
- package/dist/security/certificateVerification/certificateVerificationSource.d.ts +0 -1
- package/dist/security/certificateVerification/certificateVerificationSource.js +3 -3
- package/dist/security/certificateVerification/certificateVerificationSource.js.map +1 -1
- package/dist/security/certificateVerification/crlVerification.js +77 -165
- package/dist/security/certificateVerification/crlVerification.js.map +1 -1
- package/dist/security/certificateVerification/types.d.ts +0 -3
- package/dist/security/certificateVerification/verificationTables.d.ts +19 -0
- package/dist/security/certificateVerification/verificationTables.js +125 -0
- package/dist/security/certificateVerification/verificationTables.js.map +1 -0
- package/dist/security/certificateVerification/verificationUtils.d.ts +1 -1
- package/dist/security/certificateVerification/verificationUtils.js +6 -31
- package/dist/security/certificateVerification/verificationUtils.js.map +1 -1
- package/dist/security/jsLoader.d.ts +5 -0
- package/dist/security/jsLoader.js +8 -0
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/server/Server.d.ts +1 -0
- package/dist/server/Server.js.map +1 -1
- package/dist/server/http.js +2 -2
- package/dist/server/http.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +21 -2
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/jobs/jobOwnership.d.ts +25 -0
- package/dist/server/jobs/jobOwnership.js +127 -0
- package/dist/server/jobs/jobOwnership.js.map +1 -0
- package/dist/server/jobs/jobProcess.js +9 -2
- package/dist/server/jobs/jobProcess.js.map +1 -1
- package/dist/server/jobs/jobs.js +18 -5
- package/dist/server/jobs/jobs.js.map +1 -1
- package/dist/server/loadRootComponents.js +10 -0
- package/dist/server/loadRootComponents.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.js +11 -1
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/serverHandlers.js +12 -2
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/threads/itc.d.ts +4 -1
- package/dist/server/threads/itc.js +29 -6
- package/dist/server/threads/itc.js.map +1 -1
- package/dist/server/threads/manageThreads.d.ts +12 -4
- package/dist/server/threads/manageThreads.js +241 -119
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/threadServer.d.ts +1 -0
- package/dist/server/threads/threadServer.js +15 -1
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/server/threads/windowsProcessTree.d.ts +91 -0
- package/dist/server/threads/windowsProcessTree.js +361 -0
- package/dist/server/threads/windowsProcessTree.js.map +1 -0
- package/dist/server/throttle.d.ts +1 -1
- package/dist/server/throttle.js +8 -1
- package/dist/server/throttle.js.map +1 -1
- package/dist/upgrade/directives/5-3-0.js +53 -46
- package/dist/upgrade/directives/5-3-0.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +10 -1
- package/dist/utility/errors/hdbError.js +55 -11
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/fsync.d.ts +2 -0
- package/dist/utility/fsync.js +20 -0
- package/dist/utility/fsync.js.map +1 -0
- package/dist/utility/globalSchema.d.ts +18 -0
- package/dist/utility/hdbTerms.d.ts +3 -0
- package/dist/utility/hdbTerms.js +3 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/signalling.d.ts +7 -1
- package/dist/utility/signalling.js +27 -3
- package/dist/utility/signalling.js.map +1 -1
- package/dist/utility/when.d.ts +1 -0
- package/dist/utility/when.js +21 -0
- package/dist/utility/when.js.map +1 -1
- package/dist/validation/configValidator.js +30 -3
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +13 -0
- package/json/systemSchema.json +28 -0
- package/npm-shrinkwrap.json +151 -55
- package/package.json +14 -13
- package/resources/DESIGN.md +80 -100
- package/resources/DatabaseTransaction.ts +157 -10
- package/resources/PrimaryRocksDatabase.ts +2 -2
- package/resources/Table.ts +494 -101
- package/resources/auditStore.ts +18 -10
- package/resources/blob.ts +13 -6
- package/resources/branchDatabase.ts +3 -3
- package/resources/databaseDropPreparation.ts +120 -0
- package/resources/databases.ts +1155 -205
- package/resources/derivedIndexRegistry.ts +122 -0
- package/resources/derivedIndexRuntime.ts +157 -30
- package/resources/derivedIndexes.ts +856 -0
- package/resources/fullTextSchema.ts +55 -0
- package/resources/fullTextSchemaLifecycle.ts +0 -15
- package/resources/graphql.ts +107 -12
- package/resources/indexes/DESIGN.md +7 -0
- package/resources/indexes/hnswDerivedIndex.ts +0 -260
- package/resources/indexes/nativeFullTextDerivedIndexLifecycle.ts +72 -9
- package/resources/models/DESIGN.md +39 -0
- package/resources/models/Models.ts +361 -18
- package/resources/models/TestBackend.ts +56 -1
- package/resources/models/analyticsTable.ts +6 -4
- package/resources/models/backendHelpers.ts +17 -0
- package/resources/models/backendRegistry.ts +76 -21
- package/resources/models/bootstrap.ts +57 -7
- package/resources/models/decideHook.ts +135 -0
- package/resources/models/decision.ts +505 -0
- package/resources/models/decisionStore.ts +419 -0
- package/resources/models/embedHook.ts +272 -55
- package/resources/models/generativeDecision.ts +459 -0
- package/resources/models/types.ts +231 -4
- package/security/DESIGN.md +22 -2
- package/security/authn/oidc/tokenExchange.ts +6 -34
- package/security/authn/oidc/tokenUseTable.ts +27 -0
- package/security/certificateVerification/certificateVerificationSource.ts +2 -3
- package/security/certificateVerification/crlVerification.ts +96 -174
- package/security/certificateVerification/types.ts +0 -3
- package/security/certificateVerification/verificationTables.ts +124 -0
- package/security/certificateVerification/verificationUtils.ts +8 -32
- package/security/jsLoader.ts +9 -1
- package/server/DESIGN.md +27 -1
- package/server/Server.ts +2 -1
- package/server/http.ts +4 -2
- package/server/itc/serverHandlers.js +29 -1
- package/server/jobs/jobOwnership.ts +89 -0
- package/server/jobs/jobProcess.ts +8 -2
- package/server/jobs/jobs.ts +15 -4
- package/server/loadRootComponents.js +11 -0
- package/server/serverHelpers/contentTypes.ts +13 -1
- package/server/serverHelpers/serverHandlers.js +11 -2
- package/server/threads/itc.js +32 -6
- package/server/threads/manageThreads.js +268 -122
- package/server/threads/threadServer.js +13 -1
- package/server/threads/windowsProcessTree.ts +409 -0
- package/server/throttle.ts +11 -1
- package/studio/web/assets/{Chat-CveGbxe6.js → Chat-Bq60AO-g.js} +1 -1
- package/studio/web/assets/{FloatingChat-IpCFgYWL.js → FloatingChat-D4560T0p.js} +3 -3
- package/studio/web/assets/{apiToken-CS8fFTgL.js → apiToken-BEvbFPMc.js} +1 -1
- package/studio/web/assets/{applications-D7F4Jopl.js → applications-CSUVTH4E.js} +1 -1
- package/studio/web/assets/{cssMode-Daw_oLPq.js → cssMode-DJyc9mHj.js} +1 -1
- package/studio/web/assets/{editor-B3Bs1zWX.js → editor-DDIsQfj6.js} +1 -1
- package/studio/web/assets/{html-CDaQwWiE.js → html-g1BJHxLy.js} +1 -1
- package/studio/web/assets/{htmlMode-Cb6emO4U.js → htmlMode-Vh5IglCQ.js} +1 -1
- package/studio/web/assets/{index-B4g2Fu_2.js → index-DGRAzvO4.js} +5 -5
- package/studio/web/assets/{index.lazy-NwhvF1q1.js → index.lazy-vgSic7et.js} +1 -1
- package/studio/web/assets/{javascript-2_86gB9Q.js → javascript-5VIolqf7.js} +1 -1
- package/studio/web/assets/{jsonMode-DX-i86uQ.js → jsonMode-DG4zQraI.js} +1 -1
- package/studio/web/assets/{languageServices-B85WN-Ej.js → languageServices-t5FBpX-i.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-JCWHIsKg.js → lspLanguageFeatures-DgVnNPh-.js} +1 -1
- package/studio/web/assets/{notifications-D9ejzt52.js → notifications-BeWcX04m.js} +1 -1
- package/studio/web/assets/{notifications-B7hjKhai.js → notifications-DUGWrWoO.js} +1 -1
- package/studio/web/assets/{profile-D8B6No3F.js → profile-BvUEDBzW.js} +1 -1
- package/studio/web/assets/{regions-DFazIfTK.js → regions-CB-_2m7M.js} +1 -1
- package/studio/web/assets/{register-CwFLviLF.js → register-1w1Zr3b2.js} +2 -2
- package/studio/web/assets/{setComponentFile-VnJ80SSJ.js → setComponentFile-z4-qJCbA.js} +1 -1
- package/studio/web/assets/{setup-BYQxFy-C.js → setup-CS50uJ4x.js} +2 -2
- package/studio/web/assets/{status-Cn5XjmsL.js → status-CI0wOdOE.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-g9xIh6TX.js → toggleHighContrast-BsAE4jPe.js} +1 -1
- package/studio/web/assets/{tsMode-EC-n0Wqc.js → tsMode-D83Gg3cq.js} +1 -1
- package/studio/web/assets/{typescript-DHmM9JCO.js → typescript-Dfp-GrNq.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-BUdnVSVv.js → useEntityRestURL-BKetlINf.js} +1 -1
- package/studio/web/assets/{workers-BVl9bBB-.js → workers-CCvmDLH3.js} +1 -1
- package/studio/web/assets/{xml-C4_sqOkX.js → xml-06pZp00U.js} +1 -1
- package/studio/web/assets/{yaml-CVkbDqtW.js → yaml-iSnWMVlV.js} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/directives/5-3-0.ts +61 -54
- package/utility/DESIGN.md +5 -1
- package/utility/errors/hdbError.ts +56 -8
- package/utility/fsync.ts +16 -0
- package/utility/hdbTerms.ts +3 -0
- package/utility/signalling.ts +33 -4
- package/utility/when.ts +22 -0
- package/validation/configValidator.ts +36 -7
package/resources/databases.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { randomBytes, randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { initSync, getHdbBasePath, get as envGet } from '../utility/environment/environmentManager.ts';
|
|
4
4
|
import { INTERNAL_DBIS_NAME } from '../utility/lmdb/terms.ts';
|
|
5
5
|
import { open, compareKeys, type Database, type RootDatabase } from 'lmdb';
|
|
6
|
-
import { join, extname, basename } from 'node:path';
|
|
6
|
+
import { join, extname, basename, dirname, resolve } from 'node:path';
|
|
7
7
|
import {
|
|
8
8
|
closeSync,
|
|
9
9
|
existsSync,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
realpathSync,
|
|
15
15
|
unlinkSync,
|
|
16
16
|
} from 'node:fs';
|
|
17
|
-
import { unlink } from 'node:fs/promises';
|
|
17
|
+
import { rm, unlink } from 'node:fs/promises';
|
|
18
18
|
import {
|
|
19
19
|
getBaseSchemaPath,
|
|
20
20
|
getTransactionAuditStoreBasePath,
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
acquireUpdateAttributesLock,
|
|
26
26
|
releaseUpdateAttributesLock,
|
|
27
27
|
tryUpdateAttributesLock,
|
|
28
|
+
withUpdateAttributesLockNonBlocking,
|
|
28
29
|
} from './Table.ts';
|
|
29
30
|
import OpenEnvironmentObject from '../utility/lmdb/OpenEnvironmentObject.ts';
|
|
30
31
|
import {
|
|
@@ -35,7 +36,7 @@ import {
|
|
|
35
36
|
RESERVED_DATABASE_NAMES,
|
|
36
37
|
} from '../utility/hdbTerms.ts';
|
|
37
38
|
import { getConfigPath } from '../config/configUtils.ts';
|
|
38
|
-
import { ClientError } from '../utility/errors/hdbError.ts';
|
|
39
|
+
import { ClientError, DatabaseClosingError } from '../utility/errors/hdbError.ts';
|
|
39
40
|
import { _assignPackageExport } from '../globals.js';
|
|
40
41
|
import { getIndexedValues } from '../utility/lmdb/commonUtility.ts';
|
|
41
42
|
import * as signalling from '../utility/signalling.ts';
|
|
@@ -55,43 +56,78 @@ import {
|
|
|
55
56
|
import { handleLocalTimeForGets } from './RecordEncoder.ts';
|
|
56
57
|
import {
|
|
57
58
|
databasePaths,
|
|
59
|
+
deleteBlobPaths,
|
|
58
60
|
deleteBlobAndWait,
|
|
59
61
|
deleteBlobsInObject,
|
|
60
|
-
deleteRootBlobPathsForDB,
|
|
61
62
|
findBlobsInObject,
|
|
63
|
+
getBlobPathsForDatabaseName,
|
|
64
|
+
getRootBlobPathsForDB,
|
|
62
65
|
} from './blob.ts';
|
|
63
66
|
import { removeStorageReclamation } from '../server/storageReclamation.ts';
|
|
64
67
|
import { commonValidators, schemaRegex } from '../validation/common_validators.ts';
|
|
65
68
|
import { CUSTOM_INDEXES } from './indexes/customIndexes.ts';
|
|
66
69
|
import { planeFilePathFor, planeStalePathFor } from './indexes/hnswPlaneBinding.ts';
|
|
67
70
|
import { OpenDBIObject } from '../utility/lmdb/OpenDBIObject.ts';
|
|
68
|
-
import { RocksDatabase, supportedCompression, type RocksDatabaseOptions } from '@harperfast/rocksdb-js';
|
|
71
|
+
import { registryStatus, RocksDatabase, supportedCompression, type RocksDatabaseOptions } from '@harperfast/rocksdb-js';
|
|
69
72
|
import { PrimaryRocksDatabase } from './PrimaryRocksDatabase.ts';
|
|
73
|
+
import {
|
|
74
|
+
databaseCommitsSuspended,
|
|
75
|
+
getDatabaseCommitDrainTimeoutMilliseconds,
|
|
76
|
+
permanentlySuspendDatabaseCommits,
|
|
77
|
+
suspendDatabaseCommits,
|
|
78
|
+
} from './DatabaseTransaction.ts';
|
|
70
79
|
import { replayLogs } from './replayLogs.ts';
|
|
71
|
-
import {
|
|
80
|
+
import {
|
|
81
|
+
assertFullTextActivationSupported,
|
|
82
|
+
permanentlySuspendDerivedIndexActivation,
|
|
83
|
+
refreshDerivedIndexes,
|
|
84
|
+
retireFullTextIndexes,
|
|
85
|
+
suspendDerivedIndexActivation,
|
|
86
|
+
} from './derivedIndexes.ts';
|
|
87
|
+
import {
|
|
88
|
+
acquireFullTextRetirementFence,
|
|
89
|
+
fullTextClearInProgress,
|
|
90
|
+
fullTextRetirementInProgress,
|
|
91
|
+
} from './derivedIndexRegistry.ts';
|
|
72
92
|
import { totalmem } from 'node:os';
|
|
73
93
|
import { RocksIndexStore } from './RocksIndexStore.ts';
|
|
74
94
|
import { resolveRocksMemoryConfig } from '../utility/rocksMemoryConfig.ts';
|
|
75
95
|
import { isProcessRunning } from '../utility/processManagement/processManagement.js';
|
|
76
|
-
import {
|
|
96
|
+
import {
|
|
97
|
+
compileFullTextDefinitions,
|
|
98
|
+
persistedFullTextIndexNames,
|
|
99
|
+
reconcileFullTextIndexGenerations,
|
|
100
|
+
type FullTextDefinition,
|
|
101
|
+
type FullTextIndexGenerations,
|
|
102
|
+
} from './fullTextSchema.ts';
|
|
77
103
|
import { projectAttributesToProperties } from './jsonSchemaTypes.ts';
|
|
78
104
|
import {
|
|
79
105
|
definitionsEqual,
|
|
80
106
|
mergePeerFullTextDefinitions,
|
|
81
107
|
readPersistedFullTextDefinitions,
|
|
82
108
|
retainFullTextDefinitions,
|
|
83
|
-
safeInterimFullTextDefinitions,
|
|
84
109
|
serializeFullTextState,
|
|
85
110
|
} from './fullTextSchemaLifecycle.ts';
|
|
111
|
+
import { assertDerivedFieldOwnership } from './models/embedHook.ts';
|
|
86
112
|
import {
|
|
87
|
-
|
|
113
|
+
abandonDatabaseDrop,
|
|
114
|
+
beginDatabaseDrop,
|
|
115
|
+
cancelDatabaseDrop,
|
|
88
116
|
checkRestoreState,
|
|
89
|
-
|
|
90
|
-
|
|
117
|
+
completeDatabaseDrop,
|
|
118
|
+
databaseDropMarkerPresent,
|
|
119
|
+
scanBlockedDatabaseDrops,
|
|
91
120
|
scanBlockedRestores,
|
|
92
121
|
RESTORE_META_DIR,
|
|
93
|
-
type
|
|
122
|
+
type DatabaseDropLock,
|
|
94
123
|
} from '../dataLayer/restoreMarker.ts';
|
|
124
|
+
import {
|
|
125
|
+
claimDatabaseDropPreparations,
|
|
126
|
+
databaseDropPrepared,
|
|
127
|
+
databaseDropPreparedWithin,
|
|
128
|
+
releaseDatabaseDropPreparations,
|
|
129
|
+
trackDatabaseDropPreparationTask,
|
|
130
|
+
} from './databaseDropPreparation.ts';
|
|
95
131
|
|
|
96
132
|
/**
|
|
97
133
|
* Check if Harper is running in read-only mode.
|
|
@@ -101,6 +137,10 @@ import {
|
|
|
101
137
|
* - storage.readOnly config setting
|
|
102
138
|
*/
|
|
103
139
|
let _isReadOnlyMode: boolean | undefined;
|
|
140
|
+
export function __setReadOnlyModeForTest(value: boolean | undefined): void {
|
|
141
|
+
_isReadOnlyMode = value;
|
|
142
|
+
}
|
|
143
|
+
|
|
104
144
|
export function isReadOnlyMode(): boolean {
|
|
105
145
|
if (_isReadOnlyMode !== undefined) return _isReadOnlyMode;
|
|
106
146
|
// Check environment variable
|
|
@@ -454,6 +494,20 @@ export function openRocksDatabase(path: string, options: RocksDatabaseOptions &
|
|
|
454
494
|
|
|
455
495
|
const lmdbDatabaseEnvs = new Map<string, LMDBRootDatabase>();
|
|
456
496
|
const rocksdbDatabaseEnvs = new Map<string, RocksRootDatabase>();
|
|
497
|
+
type IncompleteDatabaseClose = {
|
|
498
|
+
databaseNames: Set<string>;
|
|
499
|
+
rootPaths: string[];
|
|
500
|
+
retry: () => Promise<void>;
|
|
501
|
+
task?: Promise<void>;
|
|
502
|
+
};
|
|
503
|
+
const incompleteDatabaseCloses = new Map<string, IncompleteDatabaseClose>();
|
|
504
|
+
|
|
505
|
+
function databaseRootUnavailable(rootPath: string): boolean {
|
|
506
|
+
return (
|
|
507
|
+
databaseDropPrepared(rootPath) ||
|
|
508
|
+
(incompleteDatabaseCloses.size > 0 && incompleteDatabaseCloses.has(resolve(rootPath)))
|
|
509
|
+
);
|
|
510
|
+
}
|
|
457
511
|
|
|
458
512
|
// set the following in both global and exports
|
|
459
513
|
_assignPackageExport('databases', databases);
|
|
@@ -474,6 +528,7 @@ const PEER_REDEFINABLE_FIELDS = [
|
|
|
474
528
|
'elements',
|
|
475
529
|
'properties',
|
|
476
530
|
'embed',
|
|
531
|
+
'decide',
|
|
477
532
|
];
|
|
478
533
|
// `indexNulls` is derived from the durable descriptor, never sent by a peer, so naming it in the
|
|
479
534
|
// discard warn would blame the peer for a field it did not write.
|
|
@@ -488,6 +543,24 @@ function applyDurableDeclaration(attribute: any, descriptor: any) {
|
|
|
488
543
|
}
|
|
489
544
|
}
|
|
490
545
|
|
|
546
|
+
function persistedFullTextIndexGenerations(
|
|
547
|
+
value: unknown,
|
|
548
|
+
definitions: readonly FullTextDefinition[]
|
|
549
|
+
): FullTextIndexGenerations {
|
|
550
|
+
const persisted =
|
|
551
|
+
value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
|
552
|
+
const generations: FullTextIndexGenerations = Object.create(null);
|
|
553
|
+
for (const { name } of definitions) {
|
|
554
|
+
const generation = persisted[name];
|
|
555
|
+
if (typeof generation === 'string' && generation.length > 0) generations[name] = generation;
|
|
556
|
+
}
|
|
557
|
+
return generations;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function createFullTextIndexGeneration(): string {
|
|
561
|
+
return randomBytes(16).toString('hex');
|
|
562
|
+
}
|
|
563
|
+
|
|
491
564
|
/**
|
|
492
565
|
* True when a descriptor claims an index build no live operation in this process can own. The PID and
|
|
493
566
|
* worker generation cannot answer that alone: a container reuses PID 1 and starts the in-memory
|
|
@@ -667,6 +740,11 @@ export function getDatabases(): Databases {
|
|
|
667
740
|
// TODO: Load any databases defined with explicit storage paths from the config
|
|
668
741
|
const entries = readdirSync(databasePath, { withFileTypes: true });
|
|
669
742
|
const blockedByRestore = databasesBlockedByRestore(databasePath);
|
|
743
|
+
const blockedByDrop = databaseRootsBlockedByDrop(databasePath);
|
|
744
|
+
const configuredDropCoversDirectory = Object.entries(schemaConfigs).some(
|
|
745
|
+
([name, config]: [string, any]) =>
|
|
746
|
+
config?.path && resolve(config.path) === resolve(databasePath) && blockedByDrop.databaseNames.has(name)
|
|
747
|
+
);
|
|
670
748
|
for (const databaseEntry of entries) {
|
|
671
749
|
// in-progress migration staging dirs are not databases until atomically renamed into place
|
|
672
750
|
if (databaseEntry.name.endsWith(MIGRATING_DIR_SUFFIX)) continue;
|
|
@@ -678,6 +756,13 @@ export function getDatabases(): Databases {
|
|
|
678
756
|
if (databaseEntry.name === BRANCH_ROOT_DIR) continue;
|
|
679
757
|
const dbName = basename(databaseEntry.name, '.mdb');
|
|
680
758
|
const dbPath = join(databasePath, databaseEntry.name);
|
|
759
|
+
if (databaseRootUnavailable(dbPath)) continue;
|
|
760
|
+
if (
|
|
761
|
+
configuredDropCoversDirectory ||
|
|
762
|
+
blockedByDrop.rootPaths.has(dbPath) ||
|
|
763
|
+
blockedByDrop.databaseNames.has(dbName)
|
|
764
|
+
)
|
|
765
|
+
continue;
|
|
681
766
|
if (blockedByRestore.has(dbName)) continue;
|
|
682
767
|
if (isOpenBranchPath(dbPath)) continue;
|
|
683
768
|
|
|
@@ -715,16 +800,14 @@ export function getDatabases(): Databases {
|
|
|
715
800
|
if (!schemaEntry.isFile()) {
|
|
716
801
|
const schemaPath = join(baseSchemaPath, schemaEntry.name);
|
|
717
802
|
const schemaAuditPath = join(getTransactionAuditStoreBasePath(), schemaEntry.name);
|
|
803
|
+
const blockedByDrop = databaseRootsBlockedByDrop(schemaPath);
|
|
718
804
|
for (const tableEntry of readdirSync(schemaPath, { withFileTypes: true })) {
|
|
719
805
|
if (tableEntry.isFile() && extname(tableEntry.name).toLowerCase() === '.mdb') {
|
|
806
|
+
const tablePath = join(schemaPath, tableEntry.name);
|
|
807
|
+
if (databaseRootUnavailable(tablePath)) continue;
|
|
808
|
+
if (blockedByDrop.rootPaths.has(tablePath) || blockedByDrop.databaseNames.has(schemaEntry.name)) continue;
|
|
720
809
|
const auditPath = join(schemaAuditPath, tableEntry.name);
|
|
721
|
-
readMetaDb(
|
|
722
|
-
join(schemaPath, tableEntry.name),
|
|
723
|
-
basename(tableEntry.name, '.mdb'),
|
|
724
|
-
schemaEntry.name,
|
|
725
|
-
auditPath,
|
|
726
|
-
true
|
|
727
|
-
);
|
|
810
|
+
readMetaDb(tablePath, basename(tableEntry.name, '.mdb'), schemaEntry.name, auditPath, true);
|
|
728
811
|
}
|
|
729
812
|
}
|
|
730
813
|
}
|
|
@@ -734,21 +817,25 @@ export function getDatabases(): Databases {
|
|
|
734
817
|
if (schemaConfigs) {
|
|
735
818
|
for (const dbName in schemaConfigs) {
|
|
736
819
|
const schemaConfig = schemaConfigs[dbName];
|
|
820
|
+
if (databaseDropRecoveryPending(dbName)) continue;
|
|
737
821
|
const databasePath = schemaConfig.path;
|
|
738
822
|
if (existsSync(databasePath)) {
|
|
739
823
|
const entries = readdirSync(databasePath, { withFileTypes: true });
|
|
740
824
|
const blockedByRestore = databasesBlockedByRestore(databasePath);
|
|
825
|
+
const blockedByDrop = databaseRootsBlockedByDrop(databasePath);
|
|
741
826
|
for (const databaseEntry of entries) {
|
|
742
827
|
if (databaseEntry.name.endsWith(MIGRATING_DIR_SUFFIX)) continue; // migration staging dir
|
|
743
828
|
if (databaseEntry.name === RESTORE_META_DIR) continue; // reserved restore-metadata dir
|
|
744
829
|
if (databaseEntry.name === BRANCH_ROOT_DIR) continue; // reserved branch root
|
|
745
830
|
if (blockedByRestore.has(basename(databaseEntry.name, '.mdb'))) continue;
|
|
746
|
-
|
|
831
|
+
const dbPath = join(databasePath, databaseEntry.name);
|
|
832
|
+
if (databaseRootUnavailable(dbPath)) continue;
|
|
833
|
+
if (blockedByDrop.rootPaths.has(dbPath) || blockedByDrop.databaseNames.has(dbName)) continue;
|
|
834
|
+
if (isOpenBranchPath(dbPath)) continue;
|
|
747
835
|
if (databaseEntry.isFile() && extname(databaseEntry.name).toLowerCase() === '.mdb') {
|
|
748
|
-
readMetaDb(
|
|
836
|
+
readMetaDb(dbPath, basename(databaseEntry.name, '.mdb'), dbName);
|
|
749
837
|
} else {
|
|
750
838
|
try {
|
|
751
|
-
const dbPath = join(databasePath, databaseEntry.name);
|
|
752
839
|
const files = readdirSync(dbPath, { withFileTypes: true });
|
|
753
840
|
if (
|
|
754
841
|
files.find((file) => file.name === 'CURRENT')?.isFile() &&
|
|
@@ -770,7 +857,7 @@ export function getDatabases(): Databases {
|
|
|
770
857
|
for (const tableName in tableConfigs) {
|
|
771
858
|
const tableConfig = tableConfigs[tableName];
|
|
772
859
|
const tablePath = join(tableConfig.path, basename(tableName + '.mdb'));
|
|
773
|
-
if (existsSync(tablePath)) {
|
|
860
|
+
if (!databaseRootUnavailable(tablePath) && !databaseDropMarkerPresent(tablePath) && existsSync(tablePath)) {
|
|
774
861
|
readMetaDb(tablePath, tableName, dbName, null, true);
|
|
775
862
|
}
|
|
776
863
|
}
|
|
@@ -780,6 +867,7 @@ export function getDatabases(): Databases {
|
|
|
780
867
|
}
|
|
781
868
|
// now remove any databases or tables that have been removed
|
|
782
869
|
for (const dbName in databases) {
|
|
870
|
+
if (databaseUsesPreparedRoot(dbName)) continue;
|
|
783
871
|
const definedTables = definedDatabases.get(dbName);
|
|
784
872
|
if (definedTables) {
|
|
785
873
|
const tables = databases[dbName];
|
|
@@ -1010,6 +1098,42 @@ function databasesBlockedByRestore(databasePath: string): Set<string> {
|
|
|
1010
1098
|
return blocked;
|
|
1011
1099
|
}
|
|
1012
1100
|
|
|
1101
|
+
const loggedDropMarkersByDirectory = new Map<string, Set<string>>();
|
|
1102
|
+
|
|
1103
|
+
function databaseRootsBlockedByDrop(databasePath: string): {
|
|
1104
|
+
rootPaths: Set<string>;
|
|
1105
|
+
databaseNames: Set<string>;
|
|
1106
|
+
} {
|
|
1107
|
+
const rootPaths = new Set<string>();
|
|
1108
|
+
const databaseNames = new Set<string>();
|
|
1109
|
+
const directoryKey = resolve(databasePath);
|
|
1110
|
+
const previouslyLogged = loggedDropMarkersByDirectory.get(directoryKey) ?? new Set<string>();
|
|
1111
|
+
const currentMarkers = new Set<string>();
|
|
1112
|
+
for (const drop of scanBlockedDatabaseDrops(databasePath)) {
|
|
1113
|
+
currentMarkers.add(drop.markerPath);
|
|
1114
|
+
const message = drop.databaseName
|
|
1115
|
+
? `Incomplete drop of database '${drop.databaseName}' detected; not loading ${drop.rootPath} — retry drop_database to recover (${drop.markerPath})`
|
|
1116
|
+
: `Incomplete database drop with invalid marker metadata detected; not loading ${drop.rootPath} — inspect the root and marker before manual recovery (${drop.markerPath})`;
|
|
1117
|
+
if (previouslyLogged.has(drop.markerPath)) logger.trace(message);
|
|
1118
|
+
else logger.error(message);
|
|
1119
|
+
rootPaths.add(drop.rootPath);
|
|
1120
|
+
if (drop.databaseName) databaseNames.add(drop.databaseName);
|
|
1121
|
+
}
|
|
1122
|
+
if (currentMarkers.size > 0) loggedDropMarkersByDirectory.set(directoryKey, currentMarkers);
|
|
1123
|
+
else loggedDropMarkersByDirectory.delete(directoryKey);
|
|
1124
|
+
return { rootPaths, databaseNames };
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function configuredDropPendingWithin(databasePath: string, schemaConfigs: Record<string, any>): boolean {
|
|
1128
|
+
const normalizedPath = resolve(databasePath);
|
|
1129
|
+
return Object.entries(schemaConfigs).some(
|
|
1130
|
+
([name, config]: [string, any]) =>
|
|
1131
|
+
config?.path &&
|
|
1132
|
+
resolve(config.path) === normalizedPath &&
|
|
1133
|
+
scanBlockedDatabaseDrops(config.path).some((drop) => drop.databaseName === name)
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1013
1137
|
/**
|
|
1014
1138
|
* This is responsible for reading the internal dbi of a single database file to get a list of all the tables and
|
|
1015
1139
|
* their indexed or registered attributes
|
|
@@ -1220,14 +1344,14 @@ function initStores(
|
|
|
1220
1344
|
}
|
|
1221
1345
|
if (failedAttempts < MAX_INTERRUPTED_DROP_ATTEMPTS) {
|
|
1222
1346
|
try {
|
|
1223
|
-
completeInterruptedDrop(rootStore, attributesDbi, databaseName, tableName);
|
|
1347
|
+
const completed = completeInterruptedDrop(rootStore, attributesDbi, databaseName, tableName);
|
|
1224
1348
|
// Sweep every generation this worker has ever tracked for this table, not
|
|
1225
1349
|
// just the one just resolved: if a prior generation was exhausted here,
|
|
1226
1350
|
// then resolved+recreated+re-dropped by another worker as this generation
|
|
1227
1351
|
// without this worker ever observing a live row in between (the only other
|
|
1228
1352
|
// place that sweeps), the prior generation's entry would otherwise never
|
|
1229
1353
|
// be cleared.
|
|
1230
|
-
clearInterruptedDropEntries(path, tableName);
|
|
1354
|
+
if (completed) clearInterruptedDropEntries(path, tableName);
|
|
1231
1355
|
definedTables?.delete(tableName);
|
|
1232
1356
|
} catch (error) {
|
|
1233
1357
|
const attempt = failedAttempts + 1;
|
|
@@ -1319,9 +1443,16 @@ function initStores(
|
|
|
1319
1443
|
for (const warning of warnings) logger.warn(warning);
|
|
1320
1444
|
} else if (warnings.length === 0) warnedFullTextStates.delete(warningKey);
|
|
1321
1445
|
}
|
|
1446
|
+
const fullTextIndexGenerations = persistedFullTextIndexGenerations(
|
|
1447
|
+
primaryAttribute.fullTextIndexGenerations,
|
|
1448
|
+
fullTextIndexes
|
|
1449
|
+
);
|
|
1450
|
+
const fullTextIndexRetirements = persistedFullTextIndexNames(primaryAttribute.fullTextIndexRetirements);
|
|
1322
1451
|
if (table && !recreateTable) {
|
|
1323
1452
|
if (primaryAttribute.audit === true && table.audit !== true) table.enableAuditing();
|
|
1324
1453
|
table.fullTextIndexes = fullTextIndexes;
|
|
1454
|
+
table.fullTextIndexGenerations = fullTextIndexGenerations;
|
|
1455
|
+
table.fullTextIndexRetirements = fullTextIndexRetirements;
|
|
1325
1456
|
indices = table.indices;
|
|
1326
1457
|
existingAttributes = table.attributes;
|
|
1327
1458
|
table.schemaVersion++;
|
|
@@ -1474,6 +1605,8 @@ function initStores(
|
|
|
1474
1605
|
indices,
|
|
1475
1606
|
attributes,
|
|
1476
1607
|
fullTextIndexes,
|
|
1608
|
+
fullTextIndexGenerations,
|
|
1609
|
+
fullTextIndexRetirements,
|
|
1477
1610
|
schemaDefined: primaryAttribute.schemaDefined,
|
|
1478
1611
|
dbisDB: attributesDbi,
|
|
1479
1612
|
})
|
|
@@ -1481,8 +1614,7 @@ function initStores(
|
|
|
1481
1614
|
table.schemaVersion = 1;
|
|
1482
1615
|
if (!destination) databaseEventsEmitter.emit('updateTable', table);
|
|
1483
1616
|
}
|
|
1484
|
-
|
|
1485
|
-
table.derivedIndexRuntime = attachDerivedIndexes(table);
|
|
1617
|
+
refreshDerivedIndexes(table);
|
|
1486
1618
|
if (Array.isArray(primaryAttribute.relationships)) {
|
|
1487
1619
|
relationshipsToHydrate.push({ table, databaseName, tableName, definitions: primaryAttribute.relationships });
|
|
1488
1620
|
} else if (primaryAttribute.relationships !== undefined) {
|
|
@@ -1557,7 +1689,7 @@ export interface BranchDatabase {
|
|
|
1557
1689
|
* a relationship whose target the application also branched against that branch.
|
|
1558
1690
|
*/
|
|
1559
1691
|
relatedBranches?: Map<string, BranchDatabase>;
|
|
1560
|
-
close(): void
|
|
1692
|
+
close(): Promise<void>;
|
|
1561
1693
|
}
|
|
1562
1694
|
|
|
1563
1695
|
/** `undefined` marks a path reserved by an open still in flight, which owns it just as firmly. */
|
|
@@ -1833,10 +1965,28 @@ export function openBranchDatabase(
|
|
|
1833
1965
|
releaseBranchIdentity(storeName);
|
|
1834
1966
|
const stranded = rocksdbDatabaseEnvs.get(path);
|
|
1835
1967
|
rocksdbDatabaseEnvs.delete(path);
|
|
1968
|
+
const auditCleanup = (stranded as any)?.auditStore?.stopAuditCleanup?.();
|
|
1969
|
+
auditCleanup?.catch((cleanupError) =>
|
|
1970
|
+
logger.warn(`Error retiring audit cleanup for branch database at ${path}`, cleanupError)
|
|
1971
|
+
);
|
|
1836
1972
|
closeBranchHandles(path, stranded, openedStores, tables);
|
|
1837
1973
|
throw error;
|
|
1838
1974
|
}
|
|
1839
|
-
let
|
|
1975
|
+
let closing: Promise<void> | undefined;
|
|
1976
|
+
let handleTeardownStarted = false;
|
|
1977
|
+
const unregisterBranch = () => {
|
|
1978
|
+
if (openBranches.get(path) === branch) openBranches.delete(path);
|
|
1979
|
+
releaseBranchIdentity(storeName);
|
|
1980
|
+
rocksdbDatabaseEnvs.delete(path);
|
|
1981
|
+
manageThreads.markBranchStorePath(path, false);
|
|
1982
|
+
};
|
|
1983
|
+
const closeRemainingHandles = () => {
|
|
1984
|
+
const closeFailures = closeBranchHandles(path, rootStore, openedStores, tables);
|
|
1985
|
+
if (closeFailures.length) {
|
|
1986
|
+
throw new AggregateError(closeFailures, `Could not close branch database '${databaseName}'`);
|
|
1987
|
+
}
|
|
1988
|
+
unregisterBranch();
|
|
1989
|
+
};
|
|
1840
1990
|
const branch: BranchDatabase = {
|
|
1841
1991
|
tables,
|
|
1842
1992
|
rootStore,
|
|
@@ -1848,13 +1998,54 @@ export function openBranchDatabase(
|
|
|
1848
1998
|
close() {
|
|
1849
1999
|
// guard on the handle, not on the registrations: those are keyed by path, and a closed
|
|
1850
2000
|
// branch frees its path, so a stale handle would otherwise tear down its successor
|
|
1851
|
-
if (
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2001
|
+
if (closing) return closing;
|
|
2002
|
+
if (handleTeardownStarted) {
|
|
2003
|
+
const operation = Promise.resolve().then(closeRemainingHandles);
|
|
2004
|
+
const retryable = operation.catch((error) => {
|
|
2005
|
+
if (closing === retryable) closing = undefined;
|
|
2006
|
+
throw error;
|
|
2007
|
+
});
|
|
2008
|
+
closing = retryable;
|
|
2009
|
+
closing.catch(() => {});
|
|
2010
|
+
return closing;
|
|
2011
|
+
}
|
|
2012
|
+
const releaseActivation = suspendDerivedIndexActivation(rootStore);
|
|
2013
|
+
const commitSuspension = suspendDatabaseCommits([rootStore]);
|
|
2014
|
+
const deadline = Date.now() + getDatabaseCommitDrainTimeoutMilliseconds();
|
|
2015
|
+
let closed = false;
|
|
2016
|
+
const operation = commitSuspension
|
|
2017
|
+
.waitForDrain({ deadline, databaseName })
|
|
2018
|
+
.then(() => settleTableMaintenance(tables, deadline))
|
|
2019
|
+
.then(() => settleBranchDerivedIndexes(tables))
|
|
2020
|
+
.then(() => (rootStore as any).auditStore?.stopAuditCleanup?.())
|
|
2021
|
+
.then(() => {
|
|
2022
|
+
handleTeardownStarted = true;
|
|
2023
|
+
closeRemainingHandles();
|
|
2024
|
+
commitSuspension.release();
|
|
2025
|
+
releaseActivation();
|
|
2026
|
+
closed = true;
|
|
2027
|
+
})
|
|
2028
|
+
.finally(() => {
|
|
2029
|
+
if (!closed && !handleTeardownStarted) {
|
|
2030
|
+
commitSuspension.release();
|
|
2031
|
+
releaseActivation();
|
|
2032
|
+
resumeTableMaintenance(tables);
|
|
2033
|
+
for (const table of Object.values(tables)) refreshDerivedIndexes(table);
|
|
2034
|
+
} else if (!closed) {
|
|
2035
|
+
// Native handle teardown started, so this wrapper graph cannot safely return to service.
|
|
2036
|
+
permanentlySuspendDatabaseCommits([rootStore]);
|
|
2037
|
+
permanentlySuspendDerivedIndexActivation(rootStore);
|
|
2038
|
+
commitSuspension.release();
|
|
2039
|
+
releaseActivation();
|
|
2040
|
+
}
|
|
2041
|
+
});
|
|
2042
|
+
const retryable = operation.catch((error) => {
|
|
2043
|
+
if (closing === retryable) closing = undefined;
|
|
2044
|
+
throw error;
|
|
2045
|
+
});
|
|
2046
|
+
closing = retryable;
|
|
2047
|
+
closing.catch(() => {});
|
|
2048
|
+
return closing;
|
|
1858
2049
|
},
|
|
1859
2050
|
};
|
|
1860
2051
|
openBranches.set(path, branch);
|
|
@@ -1875,9 +2066,9 @@ function closeBranchHandles(
|
|
|
1875
2066
|
rootStore?: RootDatabaseKind,
|
|
1876
2067
|
openedStores: any[] = [],
|
|
1877
2068
|
tables: Tables = {}
|
|
1878
|
-
):
|
|
2069
|
+
): unknown[] {
|
|
1879
2070
|
const reclamationPaths = new Set<string>([path]);
|
|
1880
|
-
|
|
2071
|
+
const closeFailures: unknown[] = [];
|
|
1881
2072
|
const closeStore = (store: any, description: string) => {
|
|
1882
2073
|
if (!store || store.status === 'closed') return;
|
|
1883
2074
|
if (store.path) reclamationPaths.add(store.path);
|
|
@@ -1885,6 +2076,7 @@ function closeBranchHandles(
|
|
|
1885
2076
|
store.close?.();
|
|
1886
2077
|
} catch (error) {
|
|
1887
2078
|
logger.warn(`Error closing ${description} for branch database at ${path}`, error);
|
|
2079
|
+
closeFailures.push(error);
|
|
1888
2080
|
}
|
|
1889
2081
|
};
|
|
1890
2082
|
// the class, before its stores: an expiration timer or a reclamation handler on a closed store
|
|
@@ -1902,11 +2094,34 @@ function closeBranchHandles(
|
|
|
1902
2094
|
closeStore(rootStore, 'root store');
|
|
1903
2095
|
if (rootStore) databasePaths.delete(rootStore as RootDatabase);
|
|
1904
2096
|
for (const reclamationPath of reclamationPaths) removeStorageReclamation(reclamationPath);
|
|
2097
|
+
return closeFailures;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
async function settleBranchDerivedIndexes(tables: Tables): Promise<void> {
|
|
2101
|
+
const attachments = new Set<any>();
|
|
2102
|
+
for (const tableName in tables) {
|
|
2103
|
+
const attachment = tables[tableName]?.derivedIndexRuntime;
|
|
2104
|
+
if (attachment) attachments.add(attachment);
|
|
2105
|
+
}
|
|
2106
|
+
await Promise.all([...attachments].map((attachment) => attachment.close()));
|
|
1905
2107
|
}
|
|
1906
2108
|
|
|
1907
2109
|
/** Branches are process-local, so this is shutdown, not a data operation. */
|
|
1908
|
-
export function
|
|
1909
|
-
|
|
2110
|
+
export function branchDatabasesHaveWork(): boolean {
|
|
2111
|
+
return openBranches.size > 0;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
export async function closeBranchDatabases(requireClosed = false): Promise<void> {
|
|
2115
|
+
const results = await Promise.allSettled([...openBranches.values()].map((branch) => branch?.close()));
|
|
2116
|
+
const failures: unknown[] = [];
|
|
2117
|
+
for (const result of results) {
|
|
2118
|
+
if (result.status === 'rejected') {
|
|
2119
|
+
logger.warn('Error closing branch database during worker teardown', result.reason);
|
|
2120
|
+
failures.push(result.reason);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
if (requireClosed && failures.length > 0)
|
|
2124
|
+
throw new AggregateError(failures, 'Could not close every branch database during worker teardown');
|
|
1910
2125
|
}
|
|
1911
2126
|
|
|
1912
2127
|
export function resetDatabases() {
|
|
@@ -2037,12 +2252,24 @@ export function resolveDatabasePath(databaseName: string): string {
|
|
|
2037
2252
|
* @returns
|
|
2038
2253
|
*/
|
|
2039
2254
|
export function database({ database: databaseName, table: tableName }) {
|
|
2255
|
+
return openDatabaseRoot({ database: databaseName, table: tableName });
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
/**
|
|
2259
|
+
* Open a database root. Destructive DDL uses the internal bypass only after it owns the database's
|
|
2260
|
+
* drop-preparation fence; public callers must continue to fail while that fence is held.
|
|
2261
|
+
*/
|
|
2262
|
+
function openDatabaseRoot(
|
|
2263
|
+
{ database: databaseName, table: tableName }: { database: string; table?: string },
|
|
2264
|
+
{ allowPreparedDrop = false }: { allowPreparedDrop?: boolean } = {}
|
|
2265
|
+
) {
|
|
2040
2266
|
if (!databaseName) databaseName = DEFAULT_DATABASE_NAME;
|
|
2041
2267
|
getDatabases();
|
|
2042
|
-
|
|
2043
|
-
const definedDatabase = definedDatabases.get(databaseName);
|
|
2268
|
+
let definedDatabase = definedDatabases.get(databaseName);
|
|
2044
2269
|
if ((definedDatabase as any)?.rootStore) {
|
|
2045
|
-
|
|
2270
|
+
const rootStore = (definedDatabase as any).rootStore;
|
|
2271
|
+
if (!allowPreparedDrop && databaseRootUnavailable(rootStore.path)) throw new DatabaseClosingError(databaseName);
|
|
2272
|
+
return rootStore;
|
|
2046
2273
|
}
|
|
2047
2274
|
const databaseConfig = envGet(CONFIG_PARAMS.DATABASES) || {};
|
|
2048
2275
|
if (process.env.SCHEMAS_DATA_PATH) {
|
|
@@ -2050,11 +2277,39 @@ export function database({ database: databaseName, table: tableName }) {
|
|
|
2050
2277
|
}
|
|
2051
2278
|
const tablePath = tableName && databaseConfig[databaseName]?.tables?.[tableName]?.path;
|
|
2052
2279
|
const databasePath = resolveDatabaseStorageRoot(databaseName, tableName);
|
|
2280
|
+
if (!allowPreparedDrop && configuredDropPendingWithin(databasePath, databaseConfig))
|
|
2281
|
+
throw new DatabaseClosingError(databaseName);
|
|
2282
|
+
// A configured path is a scan root, not an exact database path, so an unresolved alias can
|
|
2283
|
+
// target any child store. Keep first-open fenced until the destructive operation completes.
|
|
2284
|
+
if (!allowPreparedDrop && databaseConfig[databaseName]?.path) {
|
|
2285
|
+
const blockedRestores = scanBlockedRestores(databasePath);
|
|
2286
|
+
if (blockedRestores.length > 0) {
|
|
2287
|
+
const error: any = new Error(
|
|
2288
|
+
`Database '${databaseName}' shares a configured path with an ${blockedRestores[0][1]} restore of '${blockedRestores[0][0]}'; complete that restore before reopening it`
|
|
2289
|
+
);
|
|
2290
|
+
error.statusCode = 409;
|
|
2291
|
+
throw error;
|
|
2292
|
+
}
|
|
2293
|
+
if (databaseDropPreparedWithin(databasePath)) throw new DatabaseClosingError(databaseName);
|
|
2294
|
+
if (scanBlockedDatabaseDrops(databasePath).length > 0) {
|
|
2295
|
+
const error: any = new Error(
|
|
2296
|
+
`Database '${databaseName}' shares a configured path with an incomplete drop; retry drop_database to recover it`
|
|
2297
|
+
);
|
|
2298
|
+
error.statusCode = 409;
|
|
2299
|
+
throw error;
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2053
2302
|
|
|
2054
2303
|
let rootStore: RootDatabaseKind;
|
|
2055
2304
|
const useRocksdb = (process.env.HARPER_STORAGE_ENGINE || envGet(CONFIG_PARAMS.STORAGE_ENGINE)) !== 'lmdb';
|
|
2305
|
+
const path = useRocksdb
|
|
2306
|
+
? join(databasePath, tablePath ? tableName : databaseName)
|
|
2307
|
+
: join(databasePath, `${tablePath ? tableName : databaseName}.mdb`);
|
|
2308
|
+
if (!allowPreparedDrop && databaseRootUnavailable(path)) throw new DatabaseClosingError(databaseName);
|
|
2309
|
+
throwIfBlockedByRestore(path, databaseName);
|
|
2310
|
+
ensureDB(databaseName);
|
|
2311
|
+
definedDatabase = definedDatabases.get(databaseName);
|
|
2056
2312
|
if (useRocksdb) {
|
|
2057
|
-
const path = join(databasePath, tablePath ? tableName : databaseName);
|
|
2058
2313
|
// the scan is not the only way to reach a branch's directory: a branch leaves its store in
|
|
2059
2314
|
// `rocksdbDatabaseEnvs`, so without this an on-demand open would staple it onto
|
|
2060
2315
|
// `definedDatabases` and the next `closeDatabase` would close it under the live handle
|
|
@@ -2068,7 +2323,6 @@ export function database({ database: databaseName, table: tableName }) {
|
|
|
2068
2323
|
// this on-demand open (create_table/create_database and friends) must not resurrect a
|
|
2069
2324
|
// database that a restore is rewriting (or left half-purged) — the scan-time restore
|
|
2070
2325
|
// checks don't cover this path
|
|
2071
|
-
throwIfBlockedByRestore(path, databaseName);
|
|
2072
2326
|
rootStore = openRocksDatabase(path, {
|
|
2073
2327
|
disableWAL: false,
|
|
2074
2328
|
enableStats: true,
|
|
@@ -2076,7 +2330,6 @@ export function database({ database: databaseName, table: tableName }) {
|
|
|
2076
2330
|
rocksdbDatabaseEnvs.set(path, rootStore as any);
|
|
2077
2331
|
}
|
|
2078
2332
|
} else {
|
|
2079
|
-
const path = join(databasePath, `${tablePath ? tableName : databaseName}.mdb`);
|
|
2080
2333
|
rootStore = lmdbDatabaseEnvs.get(path);
|
|
2081
2334
|
if (!rootStore || rootStore.status === 'closed') {
|
|
2082
2335
|
// TODO: validate database name
|
|
@@ -2092,6 +2345,13 @@ export function database({ database: databaseName, table: tableName }) {
|
|
|
2092
2345
|
return rootStore;
|
|
2093
2346
|
}
|
|
2094
2347
|
function throwIfBlockedByRestore(dbPath: string, databaseName: string): void {
|
|
2348
|
+
if (databaseDropMarkerPresent(dbPath)) {
|
|
2349
|
+
const error: any = new Error(
|
|
2350
|
+
`Database '${databaseName}' has an incomplete drop; retry drop_database to recover it`
|
|
2351
|
+
);
|
|
2352
|
+
error.statusCode = 409;
|
|
2353
|
+
throw error;
|
|
2354
|
+
}
|
|
2095
2355
|
const restoreState = checkRestoreState(dbPath);
|
|
2096
2356
|
if (restoreState !== 'clear') {
|
|
2097
2357
|
const error: any = new Error(
|
|
@@ -2104,189 +2364,612 @@ function throwIfBlockedByRestore(dbPath: string, databaseName: string): void {
|
|
|
2104
2364
|
}
|
|
2105
2365
|
}
|
|
2106
2366
|
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
*/
|
|
2116
|
-
function lockDatabaseForDrop(dbPath: string, databaseName: string, held: RestoreLock[]): void {
|
|
2117
|
-
if (held.some((h) => h.dbPath === dbPath)) return;
|
|
2118
|
-
let lock: RestoreLock;
|
|
2367
|
+
function lockDatabaseForDrop(
|
|
2368
|
+
dbPath: string,
|
|
2369
|
+
databaseName: string,
|
|
2370
|
+
held: DatabaseDropLock[],
|
|
2371
|
+
blobDatabaseName = databaseName,
|
|
2372
|
+
blobPaths = getBlobPathsForDatabaseName(blobDatabaseName)
|
|
2373
|
+
): void {
|
|
2374
|
+
if (held.some((lock) => lock.dbPath === dbPath)) return;
|
|
2119
2375
|
try {
|
|
2120
|
-
|
|
2376
|
+
held.push(beginDatabaseDrop(dbPath, databaseName, blobDatabaseName, blobPaths));
|
|
2121
2377
|
} catch (error) {
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
);
|
|
2133
|
-
error.statusCode = 409;
|
|
2378
|
+
const cleanupFailures = [];
|
|
2379
|
+
for (const lock of held.splice(0)) {
|
|
2380
|
+
try {
|
|
2381
|
+
cancelDatabaseDrop(lock);
|
|
2382
|
+
} catch (cleanupError) {
|
|
2383
|
+
cleanupFailures.push(cleanupError);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
if (cleanupFailures.length > 0)
|
|
2387
|
+
throw new AggregateError([error, ...cleanupFailures], `Could not cancel database drop '${databaseName}'`);
|
|
2134
2388
|
throw error;
|
|
2135
2389
|
}
|
|
2136
|
-
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
function releaseDatabaseDropLocks(locks: DatabaseDropLock[], retainMarkers: boolean): void {
|
|
2393
|
+
const failures = [];
|
|
2394
|
+
while (locks.length > 0) {
|
|
2395
|
+
try {
|
|
2396
|
+
const lock = locks.shift()!;
|
|
2397
|
+
if (retainMarkers) abandonDatabaseDrop(lock);
|
|
2398
|
+
else cancelDatabaseDrop(lock);
|
|
2399
|
+
} catch (error) {
|
|
2400
|
+
failures.push(error);
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
if (failures.length > 0) throw new AggregateError(failures, 'Could not release database drop locks');
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
function inferDropBlobDatabaseName(databaseName: string, rootPath: string): string {
|
|
2407
|
+
const schemaConfigs = envGet(CONFIG_PARAMS.DATABASES) || {};
|
|
2408
|
+
const databaseConfig = schemaConfigs[databaseName];
|
|
2409
|
+
const normalizedRootPath = resolve(rootPath);
|
|
2410
|
+
for (const [tableName, tableConfig] of Object.entries(databaseConfig?.tables || {}) as [string, any][]) {
|
|
2411
|
+
if (!tableConfig?.path) continue;
|
|
2412
|
+
if (
|
|
2413
|
+
[resolve(tableConfig.path, tableName), resolve(tableConfig.path, `${tableName}.mdb`)].includes(normalizedRootPath)
|
|
2414
|
+
)
|
|
2415
|
+
return databaseName;
|
|
2416
|
+
}
|
|
2417
|
+
if (resolve(dirname(rootPath)) === resolve(getBaseSchemaPath(), databaseName)) return databaseName;
|
|
2418
|
+
|
|
2419
|
+
const physicalName = basename(rootPath).replace(/\.mdb$/, '');
|
|
2420
|
+
const rootDirectory = resolve(dirname(rootPath));
|
|
2421
|
+
if (databaseConfig?.path && resolve(databaseConfig.path) === rootDirectory) {
|
|
2422
|
+
if (!schemaConfigs[physicalName]?.path) return physicalName;
|
|
2423
|
+
for (const [configuredName, configured] of Object.entries(schemaConfigs) as [string, any][]) {
|
|
2424
|
+
if (configured?.path && resolve(configured.path) === rootDirectory) return configuredName;
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
try {
|
|
2428
|
+
const storageRoot = resolveDatabaseStorageRoot(databaseName);
|
|
2429
|
+
if ([resolve(storageRoot, databaseName), resolve(storageRoot, `${databaseName}.mdb`)].includes(normalizedRootPath))
|
|
2430
|
+
return databaseName;
|
|
2431
|
+
} catch {}
|
|
2432
|
+
return physicalName;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
const incompleteDatabaseDropStores = new Map<string, RootDatabaseKind>();
|
|
2436
|
+
|
|
2437
|
+
function rememberIncompleteDatabaseClose(
|
|
2438
|
+
databaseNames: Iterable<string>,
|
|
2439
|
+
rootPaths: Iterable<string>,
|
|
2440
|
+
retry: () => Promise<void>
|
|
2441
|
+
): void {
|
|
2442
|
+
const names = new Set(databaseNames);
|
|
2443
|
+
const normalizedPaths = [...new Set([...rootPaths].map((rootPath) => resolve(rootPath)))];
|
|
2444
|
+
const incompleteClose: IncompleteDatabaseClose = { databaseNames: names, rootPaths: normalizedPaths, retry };
|
|
2445
|
+
for (const rootPath of normalizedPaths) incompleteDatabaseCloses.set(rootPath, incompleteClose);
|
|
2446
|
+
logger.warn(
|
|
2447
|
+
`Database '${[...names].join("', '")}' handles remain partially closed on this worker; the affected roots stay unavailable until drop_database is retried or the worker restarts`,
|
|
2448
|
+
normalizedPaths
|
|
2449
|
+
);
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
function incompleteDatabaseClosePaths(databaseName: string): string[] {
|
|
2453
|
+
const rootPaths = new Set<string>();
|
|
2454
|
+
for (const incompleteClose of new Set(incompleteDatabaseCloses.values())) {
|
|
2455
|
+
if (!incompleteClose.databaseNames.has(databaseName)) continue;
|
|
2456
|
+
for (const rootPath of incompleteClose.rootPaths) rootPaths.add(rootPath);
|
|
2457
|
+
}
|
|
2458
|
+
return [...rootPaths];
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
async function retryIncompleteDatabaseClose(rootPath: string): Promise<void> {
|
|
2462
|
+
const incompleteClose = incompleteDatabaseCloses.get(resolve(rootPath));
|
|
2463
|
+
if (!incompleteClose) return;
|
|
2464
|
+
if (!incompleteClose.task) {
|
|
2465
|
+
const operation = incompleteClose.retry().then(() => {
|
|
2466
|
+
for (const path of incompleteClose.rootPaths) {
|
|
2467
|
+
if (incompleteDatabaseCloses.get(path) === incompleteClose) incompleteDatabaseCloses.delete(path);
|
|
2468
|
+
}
|
|
2469
|
+
});
|
|
2470
|
+
let retryable: Promise<void>;
|
|
2471
|
+
retryable = operation.catch((error) => {
|
|
2472
|
+
if (incompleteClose.task === retryable) incompleteClose.task = undefined;
|
|
2473
|
+
throw error;
|
|
2474
|
+
});
|
|
2475
|
+
incompleteClose.task = retryable;
|
|
2476
|
+
}
|
|
2477
|
+
await incompleteClose.task;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
async function destroyIncompleteDatabaseRoot(rootPath: string): Promise<void> {
|
|
2481
|
+
const rootStore = incompleteDatabaseDropStores.get(rootPath);
|
|
2482
|
+
if (!rootStore || !existsSync(rootPath)) {
|
|
2483
|
+
incompleteDatabaseDropStores.delete(rootPath);
|
|
2484
|
+
await rm(rootPath, { recursive: true, force: true });
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
if (rootStore instanceof RocksDatabase) {
|
|
2488
|
+
if (rootStore.status === 'open') {
|
|
2489
|
+
await (rootStore as any).dbisDb?.close?.();
|
|
2490
|
+
await rootStore.close();
|
|
2491
|
+
}
|
|
2492
|
+
await rootStore.destroy();
|
|
2493
|
+
} else {
|
|
2494
|
+
if (rootStore.status === 'open') await rootStore.close();
|
|
2495
|
+
await rm(rootPath, { force: true });
|
|
2496
|
+
}
|
|
2497
|
+
incompleteDatabaseDropStores.delete(rootPath);
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
async function resumeIncompleteDatabaseDrop(databaseName: string, rootPaths: Iterable<string>): Promise<void> {
|
|
2501
|
+
const dropLocks: DatabaseDropLock[] = [];
|
|
2502
|
+
let destructiveWorkStarted = false;
|
|
2503
|
+
try {
|
|
2504
|
+
const paths = new Set(rootPaths);
|
|
2505
|
+
for (const rootPath of paths) await retryIncompleteDatabaseClose(rootPath);
|
|
2506
|
+
if (
|
|
2507
|
+
registryStatus().some(
|
|
2508
|
+
(entry) => paths.has(entry.path) && entry.refCount > 0 && !incompleteDatabaseDropStores.has(entry.path)
|
|
2509
|
+
)
|
|
2510
|
+
)
|
|
2511
|
+
throw new DatabaseClosingError(databaseName);
|
|
2512
|
+
for (const rootPath of paths)
|
|
2513
|
+
lockDatabaseForDrop(rootPath, databaseName, dropLocks, inferDropBlobDatabaseName(databaseName, rootPath));
|
|
2514
|
+
destructiveWorkStarted = true;
|
|
2515
|
+
for (const lock of dropLocks) await destroyIncompleteDatabaseRoot(lock.dbPath);
|
|
2516
|
+
await deleteBlobPaths(dropLocks.flatMap((lock) => lock.blobPaths));
|
|
2517
|
+
while (dropLocks.length > 0) completeDatabaseDrop(dropLocks.shift()!);
|
|
2518
|
+
} finally {
|
|
2519
|
+
releaseDatabaseDropLocks(dropLocks, destructiveWorkStarted);
|
|
2520
|
+
}
|
|
2137
2521
|
}
|
|
2138
2522
|
|
|
2139
2523
|
/**
|
|
2140
2524
|
* Delete the database
|
|
2141
2525
|
* @param databaseName
|
|
2142
2526
|
*/
|
|
2143
|
-
export async function dropDatabase(databaseName) {
|
|
2144
|
-
if (!databases[databaseName])
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2527
|
+
export async function dropDatabase(databaseName, requestedRootPaths: Iterable<string> = []) {
|
|
2528
|
+
if (!databases[databaseName]) {
|
|
2529
|
+
const rootPaths = new Set(requestedRootPaths);
|
|
2530
|
+
if (rootPaths.size === 0) throw new Error('Database does not exist');
|
|
2531
|
+
return resumeIncompleteDatabaseDrop(databaseName, rootPaths);
|
|
2532
|
+
}
|
|
2533
|
+
const { databaseNames, dbTables, rootStores } = collectDatabaseGraph(databaseName);
|
|
2534
|
+
|
|
2535
|
+
const dropLocks: DatabaseDropLock[] = [];
|
|
2536
|
+
let releaseDerivedIndexActivation: (() => void) | undefined;
|
|
2537
|
+
let destructiveWorkStarted = false;
|
|
2538
|
+
let dropCompleted = false;
|
|
2154
2539
|
try {
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2540
|
+
if (rootStores.size === 0) {
|
|
2541
|
+
rootStores.add(openDatabaseRoot({ database: databaseName }, { allowPreparedDrop: true }));
|
|
2542
|
+
}
|
|
2543
|
+
const blobStorageByRootPath = new Map(
|
|
2544
|
+
[...rootStores].map((rootStore) => [
|
|
2545
|
+
rootStore.path,
|
|
2546
|
+
{
|
|
2547
|
+
databaseName: rootStore.databaseName ?? databaseName,
|
|
2548
|
+
paths: getRootBlobPathsForDB(rootStore as RootDatabase),
|
|
2549
|
+
},
|
|
2550
|
+
])
|
|
2551
|
+
);
|
|
2552
|
+
for (const rootPath of new Set([...requestedRootPaths, ...blobStorageByRootPath.keys()])) {
|
|
2553
|
+
const blobStorage = blobStorageByRootPath.get(rootPath);
|
|
2554
|
+
const blobDatabaseName = blobStorage?.databaseName ?? inferDropBlobDatabaseName(databaseName, rootPath);
|
|
2555
|
+
lockDatabaseForDrop(
|
|
2556
|
+
rootPath,
|
|
2557
|
+
databaseName,
|
|
2558
|
+
dropLocks,
|
|
2559
|
+
blobDatabaseName,
|
|
2560
|
+
blobStorage?.paths ?? getBlobPathsForDatabaseName(blobDatabaseName)
|
|
2561
|
+
);
|
|
2562
|
+
}
|
|
2563
|
+
const openedRootPaths = new Set([...rootStores].map((rootStore) => rootStore.path));
|
|
2564
|
+
const detachedRootPaths = dropLocks.map((lock) => lock.dbPath).filter((rootPath) => !openedRootPaths.has(rootPath));
|
|
2565
|
+
const detachedRootPathSet = new Set(detachedRootPaths);
|
|
2566
|
+
const detachedRootHasForeignHandle = () =>
|
|
2567
|
+
registryStatus().some(
|
|
2568
|
+
(entry) =>
|
|
2569
|
+
detachedRootPathSet.has(entry.path) && entry.refCount > 0 && !incompleteDatabaseDropStores.has(entry.path)
|
|
2570
|
+
);
|
|
2571
|
+
if (detachedRootHasForeignHandle()) throw new DatabaseClosingError(databaseName);
|
|
2572
|
+
releaseDerivedIndexActivation = await settleDatabaseDerivedIndexes(dbTables, true, rootStores);
|
|
2573
|
+
destructiveWorkStarted = true;
|
|
2574
|
+
for (const rootStore of rootStores) {
|
|
2159
2575
|
lmdbDatabaseEnvs.delete(rootStore.path);
|
|
2160
2576
|
rocksdbDatabaseEnvs.delete(rootStore.path);
|
|
2161
2577
|
}
|
|
2162
2578
|
|
|
2163
|
-
for (const tableName
|
|
2164
|
-
databaseEventsEmitter.emit('dropTable', tableName, databaseName);
|
|
2579
|
+
for (const [tableName, table] of Object.entries(dbTables)) {
|
|
2580
|
+
databaseEventsEmitter.emit('dropTable', (table as any).tableName ?? tableName, (table as any).databaseName);
|
|
2165
2581
|
}
|
|
2166
2582
|
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2583
|
+
for (const name of databaseNames) {
|
|
2584
|
+
if (name === 'data') {
|
|
2585
|
+
for (const tableName in tables) {
|
|
2586
|
+
delete tables[tableName];
|
|
2587
|
+
}
|
|
2588
|
+
delete tables[DEFINED_TABLES];
|
|
2170
2589
|
}
|
|
2171
|
-
delete
|
|
2590
|
+
delete databases[name];
|
|
2591
|
+
definedDatabases?.delete(name);
|
|
2592
|
+
databaseEventsEmitter.emit('dropDatabase', name);
|
|
2172
2593
|
}
|
|
2173
|
-
delete databases[databaseName];
|
|
2174
2594
|
|
|
2175
|
-
|
|
2595
|
+
const closedStores = new Set<any>();
|
|
2596
|
+
for (const table of Object.values(dbTables) as any[]) {
|
|
2597
|
+
if (!(table?.primaryStore?.rootStore instanceof RocksDatabase)) continue;
|
|
2598
|
+
for (const store of [...Object.values(table.indices || {}), table.primaryStore] as any[]) {
|
|
2599
|
+
if (!store || closedStores.has(store)) continue;
|
|
2600
|
+
closedStores.add(store);
|
|
2601
|
+
await store.close?.();
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2176
2604
|
|
|
2177
|
-
|
|
2178
|
-
// awaited: retirement stops the loop admitting work, the barrier is what says the pass that was
|
|
2179
|
-
// already running has released the stores this is about to close and unlink
|
|
2605
|
+
for (const rootStore of rootStores) {
|
|
2180
2606
|
await rootStore.auditStore?.stopAuditCleanup?.();
|
|
2181
2607
|
removeStorageReclamation(rootStore.path);
|
|
2182
2608
|
if (rootStore.status === 'open') {
|
|
2183
2609
|
if (rootStore instanceof RocksDatabase) {
|
|
2184
|
-
rootStore.close();
|
|
2185
|
-
rootStore.
|
|
2610
|
+
await (rootStore as any).dbisDb?.close?.();
|
|
2611
|
+
await rootStore.close();
|
|
2612
|
+
await rootStore.destroy();
|
|
2186
2613
|
} else {
|
|
2187
2614
|
await rootStore.close();
|
|
2188
2615
|
await unlink(rootStore.path);
|
|
2189
2616
|
}
|
|
2190
2617
|
}
|
|
2191
|
-
} else {
|
|
2192
|
-
rootStore = database({ database: databaseName, table: null });
|
|
2193
|
-
// a tableless database resolves its root store here rather than in the loop above, so take
|
|
2194
|
-
// the drop lock now (still before any destructive step)
|
|
2195
|
-
if (rootStore instanceof RocksDatabase) lockDatabaseForDrop(rootStore.path, databaseName, restoreLocks);
|
|
2196
|
-
await rootStore.auditStore?.stopAuditCleanup?.();
|
|
2197
|
-
removeStorageReclamation(rootStore.path);
|
|
2198
|
-
if (rootStore instanceof RocksDatabase) {
|
|
2199
|
-
rootStore.close();
|
|
2200
|
-
rootStore.destroy();
|
|
2201
|
-
} else if (rootStore.status === 'open') {
|
|
2202
|
-
await rootStore.close();
|
|
2203
|
-
await unlink(rootStore.path);
|
|
2204
|
-
}
|
|
2205
2618
|
}
|
|
2619
|
+
if (detachedRootHasForeignHandle()) throw new DatabaseClosingError(databaseName);
|
|
2620
|
+
for (const rootPath of detachedRootPaths) await rm(rootPath, { recursive: true, force: true });
|
|
2206
2621
|
|
|
2207
|
-
await
|
|
2622
|
+
await deleteBlobPaths(dropLocks.flatMap((lock) => lock.blobPaths));
|
|
2623
|
+
while (dropLocks.length > 0) completeDatabaseDrop(dropLocks.shift()!);
|
|
2624
|
+
dropCompleted = true;
|
|
2208
2625
|
} finally {
|
|
2209
|
-
|
|
2626
|
+
if (destructiveWorkStarted) {
|
|
2627
|
+
for (const rootStore of rootStores) databasePaths.delete(rootStore as RootDatabase);
|
|
2628
|
+
for (const rootStore of rootStores) {
|
|
2629
|
+
if (databaseDropMarkerPresent(rootStore.path)) incompleteDatabaseDropStores.set(rootStore.path, rootStore);
|
|
2630
|
+
}
|
|
2631
|
+
if (!dropCompleted) {
|
|
2632
|
+
permanentlySuspendDatabaseCommits(rootStores);
|
|
2633
|
+
for (const rootStore of rootStores) permanentlySuspendDerivedIndexActivation(rootStore);
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
releaseDerivedIndexActivation?.();
|
|
2637
|
+
releaseDatabaseDropLocks(dropLocks, destructiveWorkStarted);
|
|
2210
2638
|
}
|
|
2211
2639
|
}
|
|
2212
2640
|
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
const rootStores = new Set<any>();
|
|
2227
|
-
const closeStore = (store: any, description: string) => {
|
|
2641
|
+
const databaseDropPreparationTasks = new Map<string, { id: string; task: Promise<void>; rootPaths: string[] }>();
|
|
2642
|
+
|
|
2643
|
+
function addPhysicalDatabaseRoots(directory: string, rootPaths: Set<string>): void {
|
|
2644
|
+
if (!existsSync(directory)) return;
|
|
2645
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
2646
|
+
if (entry.name === RESTORE_META_DIR || entry.name === BRANCH_ROOT_DIR || entry.name.endsWith(MIGRATING_DIR_SUFFIX))
|
|
2647
|
+
continue;
|
|
2648
|
+
const rootPath = join(directory, entry.name);
|
|
2649
|
+
if (entry.isFile()) {
|
|
2650
|
+
if (extname(entry.name).toLowerCase() === '.mdb') rootPaths.add(rootPath);
|
|
2651
|
+
continue;
|
|
2652
|
+
}
|
|
2653
|
+
if (!entry.isDirectory()) continue;
|
|
2228
2654
|
try {
|
|
2229
|
-
|
|
2655
|
+
const files = readdirSync(rootPath, { withFileTypes: true });
|
|
2656
|
+
if (
|
|
2657
|
+
files.find((file) => file.name === 'CURRENT')?.isFile() &&
|
|
2658
|
+
files.some((file) => file.name.startsWith('MANIFEST-'))
|
|
2659
|
+
)
|
|
2660
|
+
rootPaths.add(rootPath);
|
|
2230
2661
|
} catch (error) {
|
|
2231
|
-
|
|
2662
|
+
if (!('code' in error && error.code === 'ENOENT')) throw error;
|
|
2232
2663
|
}
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
function databaseDropRecoveryGraphPaths(databaseName: string, incompleteRootPaths: string[]): string[] {
|
|
2668
|
+
const rootPaths = new Set(incompleteRootPaths);
|
|
2669
|
+
if (incompleteRootPaths.length === 0) return [];
|
|
2670
|
+
const databaseConfig = (envGet(CONFIG_PARAMS.DATABASES) || {})[databaseName];
|
|
2671
|
+
if (databaseConfig?.path) addPhysicalDatabaseRoots(databaseConfig.path, rootPaths);
|
|
2672
|
+
addPhysicalDatabaseRoots(join(getBaseSchemaPath(), databaseName), rootPaths);
|
|
2673
|
+
for (const [tableName, tableConfig] of Object.entries(databaseConfig?.tables || {}) as [string, any][]) {
|
|
2674
|
+
if (!tableConfig?.path) continue;
|
|
2675
|
+
for (const rootPath of [join(tableConfig.path, tableName), join(tableConfig.path, `${tableName}.mdb`)]) {
|
|
2676
|
+
if (existsSync(rootPath)) rootPaths.add(rootPath);
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
try {
|
|
2680
|
+
const storageRoot = resolveDatabaseStorageRoot(databaseName);
|
|
2681
|
+
for (const rootPath of [join(storageRoot, databaseName), join(storageRoot, `${databaseName}.mdb`)]) {
|
|
2682
|
+
if (existsSync(rootPath)) rootPaths.add(rootPath);
|
|
2683
|
+
}
|
|
2684
|
+
} catch {}
|
|
2685
|
+
return [...rootPaths];
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
function incompleteDatabaseDropPaths(databaseName: string): string[] {
|
|
2689
|
+
const directories = new Set<string>();
|
|
2690
|
+
try {
|
|
2691
|
+
directories.add(resolveDatabaseStorageRoot(databaseName));
|
|
2692
|
+
} catch {}
|
|
2693
|
+
directories.add(join(getBaseSchemaPath(), databaseName));
|
|
2694
|
+
const databaseConfig = (envGet(CONFIG_PARAMS.DATABASES) || {})[databaseName];
|
|
2695
|
+
if (databaseConfig?.path) directories.add(databaseConfig.path);
|
|
2696
|
+
for (const tableConfig of Object.values(databaseConfig?.tables || {}) as any[]) {
|
|
2697
|
+
if (tableConfig?.path) directories.add(tableConfig.path);
|
|
2698
|
+
}
|
|
2699
|
+
const rootPaths = new Set<string>();
|
|
2700
|
+
for (const directory of directories) {
|
|
2701
|
+
for (const drop of scanBlockedDatabaseDrops(directory)) {
|
|
2702
|
+
if (drop.databaseName === databaseName) rootPaths.add(drop.rootPath);
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
return [...rootPaths];
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
export function databaseDropRecoveryPending(databaseName: string): boolean {
|
|
2709
|
+
return incompleteDatabaseDropPaths(databaseName).length > 0 || incompleteDatabaseClosePaths(databaseName).length > 0;
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
export function databaseDropPreparationTargets(databaseName: string): {
|
|
2713
|
+
rootPaths: string[];
|
|
2714
|
+
} {
|
|
2715
|
+
getDatabases();
|
|
2716
|
+
const incompleteRootPaths = [
|
|
2717
|
+
...new Set([...incompleteDatabaseDropPaths(databaseName), ...incompleteDatabaseClosePaths(databaseName)]),
|
|
2718
|
+
];
|
|
2719
|
+
const recoveryRootPaths = databaseDropRecoveryGraphPaths(databaseName, incompleteRootPaths);
|
|
2720
|
+
if (!databases[databaseName]) {
|
|
2721
|
+
if (recoveryRootPaths.length > 0) return { rootPaths: recoveryRootPaths };
|
|
2722
|
+
throw new Error(`Database '${databaseName}' does not exist`);
|
|
2723
|
+
}
|
|
2724
|
+
const graph = collectDatabaseGraph(databaseName);
|
|
2725
|
+
if (graph.rootStores.size === 0) graph.rootStores.add(openDatabaseRoot({ database: databaseName }));
|
|
2726
|
+
return {
|
|
2727
|
+
rootPaths: [...new Set([...recoveryRootPaths, ...[...graph.rootStores].map((rootStore) => rootStore.path)])],
|
|
2233
2728
|
};
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
export function prepareDatabaseDrop(
|
|
2732
|
+
databaseName: string,
|
|
2733
|
+
preparationId: string,
|
|
2734
|
+
ownerThreadId: number,
|
|
2735
|
+
requestedRootPaths: Iterable<string> = []
|
|
2736
|
+
): Promise<void> {
|
|
2737
|
+
const existing = databaseDropPreparationTasks.get(databaseName);
|
|
2738
|
+
if (existing?.id === preparationId) return existing.task;
|
|
2739
|
+
const rootPaths = new Set(requestedRootPaths);
|
|
2740
|
+
if (databases[databaseName]) {
|
|
2741
|
+
const graph = collectDatabaseGraph(databaseName);
|
|
2742
|
+
for (const rootStore of graph.rootStores) rootPaths.add(rootStore.path);
|
|
2238
2743
|
}
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2744
|
+
const names: string[] = [];
|
|
2745
|
+
for (const name of Object.keys(databases)) {
|
|
2746
|
+
const definedRoot = (definedDatabases?.get(name) as any)?.rootStore;
|
|
2747
|
+
if (definedRoot && rootPaths.has(definedRoot.path)) {
|
|
2748
|
+
names.push(name);
|
|
2749
|
+
continue;
|
|
2750
|
+
}
|
|
2751
|
+
const tables = databases[name];
|
|
2752
|
+
if (
|
|
2753
|
+
Object.values(tables).some((table: any) => {
|
|
2754
|
+
const rootStore = table?.primaryStore?.rootStore;
|
|
2755
|
+
return rootStore && rootPaths.has(rootStore.path);
|
|
2756
|
+
})
|
|
2757
|
+
)
|
|
2758
|
+
names.push(name);
|
|
2759
|
+
}
|
|
2760
|
+
const paths = [...rootPaths];
|
|
2761
|
+
claimDatabaseDropPreparations(paths, preparationId, ownerThreadId, databaseName);
|
|
2762
|
+
const task = (async () => {
|
|
2763
|
+
const failures: unknown[] = [];
|
|
2764
|
+
for (const rootPath of paths) {
|
|
2765
|
+
try {
|
|
2766
|
+
await retryIncompleteDatabaseClose(rootPath);
|
|
2767
|
+
} catch (error) {
|
|
2768
|
+
failures.push(error);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
for (const name of names) {
|
|
2772
|
+
try {
|
|
2773
|
+
await closeDatabase(name, { requireClosed: true });
|
|
2774
|
+
} catch (error) {
|
|
2775
|
+
failures.push(error);
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
for (const rootPath of paths) {
|
|
2779
|
+
if (!incompleteDatabaseDropStores.has(rootPath)) continue;
|
|
2780
|
+
try {
|
|
2781
|
+
await destroyIncompleteDatabaseRoot(rootPath);
|
|
2782
|
+
} catch (error) {
|
|
2783
|
+
failures.push(error);
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
if (failures.length > 0)
|
|
2787
|
+
throw new AggregateError(failures, `Could not prepare database '${databaseName}' for drop`);
|
|
2788
|
+
})();
|
|
2789
|
+
databaseDropPreparationTasks.set(databaseName, { id: preparationId, task, rootPaths: paths });
|
|
2790
|
+
for (const rootPath of paths) trackDatabaseDropPreparationTask(rootPath, preparationId, task);
|
|
2791
|
+
return task;
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
export async function completeDatabaseDropPreparation(
|
|
2795
|
+
databaseName: string,
|
|
2796
|
+
preparationId: string,
|
|
2797
|
+
requestedRootPaths: Iterable<string> = []
|
|
2798
|
+
): Promise<void> {
|
|
2799
|
+
const existing = databaseDropPreparationTasks.get(databaseName);
|
|
2800
|
+
if (existing?.id === preparationId) {
|
|
2801
|
+
try {
|
|
2802
|
+
await existing.task;
|
|
2803
|
+
} catch {
|
|
2804
|
+
// Preparation already reported this failure to the strict broadcaster.
|
|
2805
|
+
}
|
|
2806
|
+
databaseDropPreparationTasks.delete(databaseName);
|
|
2807
|
+
}
|
|
2808
|
+
releaseDatabaseDropPreparations(existing?.rootPaths ?? requestedRootPaths, preparationId);
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
async function closeDatabaseStores(
|
|
2812
|
+
databaseName: string,
|
|
2813
|
+
dbTables: Record<string, any>,
|
|
2814
|
+
rootStores: Set<any>,
|
|
2815
|
+
requireClosed: boolean
|
|
2816
|
+
): Promise<unknown[]> {
|
|
2817
|
+
const rootStorePaths: string[] = [];
|
|
2818
|
+
const closeFailures: unknown[] = [];
|
|
2819
|
+
const closeStore = async (store: any, description: string) => {
|
|
2820
|
+
if (!store || store.status === 'closed') return true;
|
|
2821
|
+
try {
|
|
2822
|
+
await store.close?.();
|
|
2823
|
+
return true;
|
|
2824
|
+
} catch (error) {
|
|
2825
|
+
logger.warn(`Error closing ${description} while closing database ${databaseName}:`, error);
|
|
2826
|
+
closeFailures.push(error);
|
|
2827
|
+
return false;
|
|
2828
|
+
}
|
|
2829
|
+
};
|
|
2249
2830
|
const lmdbRootStores = new Set([...rootStores].filter((rootStore) => !(rootStore instanceof RocksDatabase)));
|
|
2250
2831
|
for (const tableName in dbTables) {
|
|
2251
2832
|
const table: any = dbTables[tableName];
|
|
2252
|
-
// a dbi closed natively after its environment closed (through another alias sharing it) runs
|
|
2253
|
-
// mdb_dbi_close against the freed environment
|
|
2254
2833
|
if (!table?.primaryStore || lmdbRootStores.has(table.primaryStore.rootStore)) continue;
|
|
2834
|
+
const tableIdentity = `${table.databaseName ?? databaseName}.${table.tableName ?? tableName}`;
|
|
2255
2835
|
for (const indexName in table.indices || {}) {
|
|
2256
|
-
closeStore(table.indices[indexName], `index ${
|
|
2836
|
+
await closeStore(table.indices[indexName], `index ${tableIdentity}.${indexName}`);
|
|
2257
2837
|
}
|
|
2258
|
-
closeStore(table.primaryStore, `table ${
|
|
2838
|
+
await closeStore(table.primaryStore, `table ${tableIdentity}`);
|
|
2259
2839
|
}
|
|
2260
2840
|
for (const rootStore of rootStores) {
|
|
2261
2841
|
removeStorageReclamation(rootStore.path);
|
|
2842
|
+
databasePaths.delete(rootStore as RootDatabase);
|
|
2843
|
+
let rootClosed = rootStore.status !== 'open';
|
|
2262
2844
|
if (!lmdbRootStores.has(rootStore)) {
|
|
2263
|
-
closeStore(rootStore.dbisDb, 'attributes store');
|
|
2264
|
-
closeStore(rootStore, 'root store');
|
|
2265
|
-
} else if (
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2845
|
+
await closeStore(rootStore.dbisDb, 'attributes store');
|
|
2846
|
+
rootClosed = await closeStore(rootStore, 'root store');
|
|
2847
|
+
} else if (!rootClosed) rootClosed = await closeStore(rootStore, 'root store');
|
|
2848
|
+
if (rootClosed || !requireClosed) rootStorePaths.push(rootStore.path);
|
|
2849
|
+
}
|
|
2850
|
+
for (const path of rootStorePaths) {
|
|
2851
|
+
lmdbDatabaseEnvs.delete(path);
|
|
2852
|
+
rocksdbDatabaseEnvs.delete(path);
|
|
2853
|
+
}
|
|
2854
|
+
return closeFailures;
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
/**
|
|
2858
|
+
* Close a database's store handles on this thread and unregister every alias sharing those roots,
|
|
2859
|
+
* without touching files. Restore uses this before rewriting the closed directory. With
|
|
2860
|
+
* `requireClosed`, any handle failure rejects and leaves the affected wrappers permanently fenced
|
|
2861
|
+
* for an explicit retry. Closing an LMDB environment closes every DBI and alias that shares it.
|
|
2862
|
+
*/
|
|
2863
|
+
export async function closeDatabase(
|
|
2864
|
+
databaseName: string,
|
|
2865
|
+
{ requireClosed = false }: { requireClosed?: boolean } = {}
|
|
2866
|
+
): Promise<boolean> {
|
|
2867
|
+
if (!databases[databaseName]) return false;
|
|
2868
|
+
const { databaseNames, dbTables, rootStores } = collectDatabaseGraph(databaseName);
|
|
2869
|
+
const releaseDerivedIndexActivation = await settleDatabaseDerivedIndexes(dbTables, false, rootStores);
|
|
2870
|
+
let databaseClosed = false;
|
|
2871
|
+
let handleCloseStarted = false;
|
|
2872
|
+
try {
|
|
2873
|
+
await Promise.all([...rootStores].map((rootStore) => rootStore.auditStore?.stopAuditCleanup?.()));
|
|
2874
|
+
handleCloseStarted = true;
|
|
2875
|
+
const closeFailures = await closeDatabaseStores(databaseName, dbTables, rootStores, requireClosed);
|
|
2876
|
+
if (requireClosed && closeFailures.length > 0) {
|
|
2877
|
+
for (const rootStore of rootStores) {
|
|
2878
|
+
lmdbDatabaseEnvs.delete(rootStore.path);
|
|
2879
|
+
rocksdbDatabaseEnvs.delete(rootStore.path);
|
|
2880
|
+
}
|
|
2881
|
+
for (const name of databaseNames) unregisterDatabase(name);
|
|
2882
|
+
rememberIncompleteDatabaseClose(
|
|
2883
|
+
databaseNames,
|
|
2884
|
+
[...rootStores].map((rootStore) => rootStore.path),
|
|
2885
|
+
async () => {
|
|
2886
|
+
const retryFailures = await closeDatabaseStores(databaseName, dbTables, rootStores, true);
|
|
2887
|
+
if (retryFailures.length > 0)
|
|
2888
|
+
throw new AggregateError(retryFailures, `Could not finish closing database graph '${databaseName}'`);
|
|
2889
|
+
}
|
|
2890
|
+
);
|
|
2891
|
+
throw new AggregateError(
|
|
2892
|
+
closeFailures,
|
|
2893
|
+
`Could not close database graph '${[...databaseNames].join("', '")}' for destructive DDL`
|
|
2894
|
+
);
|
|
2895
|
+
}
|
|
2896
|
+
for (const name of databaseNames) unregisterDatabase(name);
|
|
2897
|
+
databaseClosed = true;
|
|
2898
|
+
return true;
|
|
2899
|
+
} finally {
|
|
2900
|
+
if (handleCloseStarted && !databaseClosed) {
|
|
2901
|
+
permanentlySuspendDatabaseCommits(rootStores);
|
|
2902
|
+
for (const rootStore of rootStores) permanentlySuspendDerivedIndexActivation(rootStore);
|
|
2903
|
+
} else if (!databaseClosed && !handleCloseStarted) {
|
|
2904
|
+
resumeTableMaintenance(dbTables);
|
|
2905
|
+
for (const table of Object.values(dbTables)) refreshDerivedIndexes(table);
|
|
2906
|
+
}
|
|
2907
|
+
releaseDerivedIndexActivation();
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
function collectDatabaseGraph(databaseName: string): {
|
|
2912
|
+
databaseNames: Set<string>;
|
|
2913
|
+
dbTables: Record<string, any>;
|
|
2914
|
+
rootStores: Set<any>;
|
|
2915
|
+
} {
|
|
2916
|
+
const databaseNames = new Set([databaseName]);
|
|
2917
|
+
const processedNames = new Set<string>();
|
|
2918
|
+
const rootStores = new Set<any>();
|
|
2919
|
+
const tableSet = new Set<any>();
|
|
2920
|
+
for (;;) {
|
|
2921
|
+
for (const name of databaseNames) {
|
|
2922
|
+
if (processedNames.has(name)) continue;
|
|
2923
|
+
processedNames.add(name);
|
|
2924
|
+
const tables = databases[name];
|
|
2925
|
+
for (const tableName in tables) {
|
|
2926
|
+
const table = tables[tableName];
|
|
2927
|
+
tableSet.add(table);
|
|
2928
|
+
if (table?.primaryStore?.rootStore) rootStores.add(table.primaryStore.rootStore);
|
|
2929
|
+
}
|
|
2930
|
+
const definedRoot = (definedDatabases?.get(name) as any)?.rootStore;
|
|
2931
|
+
if (definedRoot) rootStores.add(definedRoot);
|
|
2932
|
+
}
|
|
2933
|
+
let foundAlias = false;
|
|
2934
|
+
for (const aliasName of Object.keys(databases)) {
|
|
2935
|
+
if (databaseNames.has(aliasName)) continue;
|
|
2936
|
+
for (const rootStore of rootStores) {
|
|
2937
|
+
if (!databaseUsesRootStore(aliasName, rootStore)) continue;
|
|
2938
|
+
databaseNames.add(aliasName);
|
|
2939
|
+
foundAlias = true;
|
|
2274
2940
|
break;
|
|
2275
2941
|
}
|
|
2276
2942
|
}
|
|
2943
|
+
if (!foundAlias) break;
|
|
2277
2944
|
}
|
|
2278
|
-
return
|
|
2945
|
+
return {
|
|
2946
|
+
databaseNames,
|
|
2947
|
+
dbTables: Object.fromEntries([...tableSet].map((table, index) => [index, table])),
|
|
2948
|
+
rootStores,
|
|
2949
|
+
};
|
|
2279
2950
|
}
|
|
2280
2951
|
|
|
2281
2952
|
function databaseUsesRootStore(databaseName: string, rootStore: any): boolean {
|
|
2282
2953
|
if ((definedDatabases?.get(databaseName) as any)?.rootStore === rootStore) return true;
|
|
2283
2954
|
const dbTables = databases[databaseName];
|
|
2955
|
+
if (!dbTables) return false;
|
|
2284
2956
|
for (const tableName in dbTables) {
|
|
2285
2957
|
if ((dbTables[tableName] as any)?.primaryStore?.rootStore === rootStore) return true;
|
|
2286
2958
|
}
|
|
2287
2959
|
return false;
|
|
2288
2960
|
}
|
|
2289
2961
|
|
|
2962
|
+
function databaseUsesPreparedRoot(databaseName: string): boolean {
|
|
2963
|
+
const definedRoot = (definedDatabases?.get(databaseName) as any)?.rootStore;
|
|
2964
|
+
if (definedRoot && databaseDropPrepared(definedRoot.path)) return true;
|
|
2965
|
+
const dbTables = databases[databaseName];
|
|
2966
|
+
for (const tableName in dbTables) {
|
|
2967
|
+
const rootStore = (dbTables[tableName] as any)?.primaryStore?.rootStore;
|
|
2968
|
+
if (rootStore && databaseDropPrepared(rootStore.path)) return true;
|
|
2969
|
+
}
|
|
2970
|
+
return false;
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2290
2973
|
function unregisterDatabase(databaseName: string): void {
|
|
2291
2974
|
const definedDatabase = definedDatabases?.get(databaseName);
|
|
2292
2975
|
if (definedDatabase) (definedDatabase as any).rootStore = undefined;
|
|
@@ -2300,41 +2983,105 @@ function unregisterDatabase(databaseName: string): void {
|
|
|
2300
2983
|
}
|
|
2301
2984
|
|
|
2302
2985
|
/**
|
|
2303
|
-
* Close every
|
|
2986
|
+
* Close every user database this thread has open, releasing its native handles.
|
|
2304
2987
|
*
|
|
2305
|
-
*
|
|
2306
|
-
*
|
|
2307
|
-
*
|
|
2308
|
-
*
|
|
2309
|
-
* `getDatabases()` and exits when the job finishes — must close its handles explicitly, or those
|
|
2310
|
-
* handles linger process-wide (and, e.g., block an online `restore_backup` from confirming the
|
|
2311
|
-
* database is closed). The `system` database is intentionally left open: it is non-enumerable here
|
|
2988
|
+
* A job worker opens the database graph and must prove every handle is closed before its exit can
|
|
2989
|
+
* satisfy destructive-DDL preparation. RocksDB handles otherwise leak in the process-global
|
|
2990
|
+
* registry; an open LMDB handle can likewise prevent deletion on Windows. The `system` database is
|
|
2991
|
+
* intentionally left open: it is non-enumerable here
|
|
2312
2992
|
* (skipped by the loop), is never restored online, and the exiting worker may still touch the job
|
|
2313
2993
|
* table during teardown. Best-effort: closing failures are swallowed inside `closeDatabase`.
|
|
2314
2994
|
*
|
|
2315
2995
|
* Branches are invisible to the loop below but hold handles from the same registry, so this — the
|
|
2316
2996
|
* thread's one teardown entry point — closes them too.
|
|
2317
2997
|
*/
|
|
2318
|
-
export function closeLoadedDatabases(): void {
|
|
2319
|
-
|
|
2998
|
+
export async function closeLoadedDatabases({ requireClosed = false }: { requireClosed?: boolean } = {}): Promise<void> {
|
|
2999
|
+
const closeFailures: unknown[] = [];
|
|
3000
|
+
try {
|
|
3001
|
+
await closeBranchDatabases(requireClosed);
|
|
3002
|
+
} catch (error) {
|
|
3003
|
+
closeFailures.push(error);
|
|
3004
|
+
}
|
|
2320
3005
|
// snapshot the names first: closeDatabase() deletes from `databases` as it goes
|
|
2321
3006
|
for (const databaseName of Object.keys(databases)) {
|
|
2322
3007
|
const dbTables = databases[databaseName];
|
|
2323
3008
|
if (!dbTables) continue;
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
}
|
|
3009
|
+
try {
|
|
3010
|
+
await closeDatabase(databaseName, { requireClosed });
|
|
3011
|
+
} catch (error) {
|
|
3012
|
+
logger.warn(`Error closing database ${databaseName} during worker teardown`, error);
|
|
3013
|
+
closeFailures.push(error);
|
|
2330
3014
|
}
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
3015
|
+
}
|
|
3016
|
+
if (requireClosed && closeFailures.length > 0)
|
|
3017
|
+
throw new AggregateError(closeFailures, 'Could not close every database during worker teardown');
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
async function settleTableMaintenance(dbTables: Record<string, any>, deadline: number): Promise<void> {
|
|
3021
|
+
await Promise.all(Object.values(dbTables).map((table: any) => table?.closeMaintenance?.(deadline)));
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
function resumeTableMaintenance(dbTables: Record<string, any>): void {
|
|
3025
|
+
for (const table of Object.values(dbTables)) table?.resumeMaintenance?.();
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
async function settleDatabaseDerivedIndexes(
|
|
3029
|
+
dbTables: Record<string, any>,
|
|
3030
|
+
dropping: boolean,
|
|
3031
|
+
additionalRootStores: Iterable<object> = []
|
|
3032
|
+
): Promise<() => void> {
|
|
3033
|
+
const rootStores = new Set<object>(additionalRootStores);
|
|
3034
|
+
for (const tableName in dbTables) {
|
|
3035
|
+
const rootStore = dbTables[tableName]?.primaryStore?.rootStore;
|
|
3036
|
+
if (rootStore) rootStores.add(rootStore);
|
|
3037
|
+
}
|
|
3038
|
+
const activationReleases = [...rootStores].map(suspendDerivedIndexActivation);
|
|
3039
|
+
const commitSuspension = suspendDatabaseCommits(rootStores);
|
|
3040
|
+
const deadline = Date.now() + getDatabaseCommitDrainTimeoutMilliseconds();
|
|
3041
|
+
const databaseName = ([...rootStores][0] as any)?.databaseName ?? 'unknown';
|
|
3042
|
+
let lifecycleReleased = false;
|
|
3043
|
+
const releaseLifecycle = () => {
|
|
3044
|
+
if (lifecycleReleased) return;
|
|
3045
|
+
lifecycleReleased = true;
|
|
3046
|
+
commitSuspension.release();
|
|
3047
|
+
for (const release of activationReleases) release();
|
|
3048
|
+
};
|
|
3049
|
+
const attachments = new Map<any, any[]>();
|
|
3050
|
+
for (const tableName in dbTables) {
|
|
3051
|
+
const table = dbTables[tableName];
|
|
3052
|
+
const attachment = table?.derivedIndexRuntime;
|
|
3053
|
+
if (!attachment) continue;
|
|
3054
|
+
let tables = attachments.get(attachment);
|
|
3055
|
+
if (!tables) attachments.set(attachment, (tables = []));
|
|
3056
|
+
tables.push(table);
|
|
3057
|
+
}
|
|
3058
|
+
const entries = [...attachments];
|
|
3059
|
+
try {
|
|
3060
|
+
await commitSuspension.waitForDrain({ deadline, databaseName });
|
|
3061
|
+
await settleInterruptedDropRetirements(rootStores);
|
|
3062
|
+
await settleTableMaintenance(dbTables, deadline);
|
|
3063
|
+
if (attachments.size === 0) return releaseLifecycle;
|
|
3064
|
+
const results = await Promise.allSettled(entries.map(async ([attachment]) => attachment.close(dropping)));
|
|
3065
|
+
const failures = results
|
|
3066
|
+
.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
|
3067
|
+
.map((result) => result.reason);
|
|
3068
|
+
if (failures.length === 1) throw failures[0];
|
|
3069
|
+
if (failures.length)
|
|
3070
|
+
throw new AggregateError(failures, 'Could not settle derived indexes before closing the database');
|
|
3071
|
+
} catch (error) {
|
|
3072
|
+
resumeTableMaintenance(dbTables);
|
|
3073
|
+
releaseLifecycle();
|
|
3074
|
+
for (const [attachment, tables] of entries) {
|
|
3075
|
+
if (dropping) attachment.completeDrop?.(false);
|
|
3076
|
+
for (const table of tables) refreshDerivedIndexes(table);
|
|
2335
3077
|
}
|
|
2336
|
-
|
|
3078
|
+
throw error;
|
|
2337
3079
|
}
|
|
3080
|
+
for (const [attachment, tables] of entries) {
|
|
3081
|
+
if (dropping) attachment.completeDrop?.();
|
|
3082
|
+
for (const table of tables) if (table.derivedIndexRuntime === attachment) table.derivedIndexRuntime = undefined;
|
|
3083
|
+
}
|
|
3084
|
+
return releaseLifecycle;
|
|
2338
3085
|
}
|
|
2339
3086
|
// HNSW_NO_AUTOVERSION kill-switch: when set, a NEW index initializes as legacy rather than
|
|
2340
3087
|
// versioned. process.env values are strings, so a bare truthiness check would treat "0"/"false"
|
|
@@ -2809,6 +3556,8 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
2809
3556
|
let fullTextValuesForPersistence: unknown;
|
|
2810
3557
|
let fullTextPersistencePending = false;
|
|
2811
3558
|
let activeFullTextIndexes: FullTextDefinition[] | undefined;
|
|
3559
|
+
let fullTextIndexGenerationMap: FullTextIndexGenerations = Object.create(null);
|
|
3560
|
+
let fullTextIndexRetirementNames: string[] | undefined;
|
|
2812
3561
|
let restoreFullTextLiveState: (() => void) | undefined;
|
|
2813
3562
|
let armFullTextLiveStateRestore: (() => void) | undefined;
|
|
2814
3563
|
const attributesToIndex = [];
|
|
@@ -2857,8 +3606,10 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
2857
3606
|
let persistedPrimary = persistedPrimaryDescriptor(Table.dbisDB);
|
|
2858
3607
|
let persistedFullTextValues = persistedPrimary.descriptor?.fullTextIndexes;
|
|
2859
3608
|
const incomingFullTextValues = fullTextIndexesExplicit ? fullTextIndexes : [];
|
|
3609
|
+
let fullTextValidationAttributes: any[] | undefined;
|
|
2860
3610
|
if (
|
|
2861
3611
|
(persistedFullTextValues !== undefined && (rootStore instanceof RocksDatabase || fullTextIndexesExplicit)) ||
|
|
3612
|
+
persistedPrimary.descriptor?.fullTextIndexRetirements !== undefined ||
|
|
2862
3613
|
Table.fullTextIndexes?.length > 0 ||
|
|
2863
3614
|
(fullTextIndexesExplicit && (!Array.isArray(incomingFullTextValues) || incomingFullTextValues.length > 0))
|
|
2864
3615
|
) {
|
|
@@ -2868,6 +3619,9 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
2868
3619
|
persistedFullTextValues = persistedPrimary.descriptor?.fullTextIndexes;
|
|
2869
3620
|
}
|
|
2870
3621
|
const originalAttributes = Table.attributes.slice();
|
|
3622
|
+
fullTextIndexRetirementNames = persistedFullTextIndexNames(
|
|
3623
|
+
persistedPrimary.descriptor?.fullTextIndexRetirements
|
|
3624
|
+
);
|
|
2871
3625
|
const relationshipAttributes = originalAttributes.filter((attribute: any) => attribute.relationship);
|
|
2872
3626
|
const originalMetadata = {
|
|
2873
3627
|
description: Table.description,
|
|
@@ -2886,14 +3640,19 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
2886
3640
|
Object.assign(Table, originalMetadata);
|
|
2887
3641
|
Table.properties = projectAttributesToProperties(restored);
|
|
2888
3642
|
const restoredPrimary = persistedPrimaryDescriptor(Table.dbisDB).descriptor;
|
|
2889
|
-
|
|
3643
|
+
const restoredFullTextIndexes =
|
|
2890
3644
|
rootStore instanceof RocksDatabase && restoredPrimary?.audit === true
|
|
2891
3645
|
? readPersistedFullTextDefinitions(restoredPrimary?.fullTextIndexes, restored, fullTextWarning)
|
|
2892
3646
|
: [];
|
|
3647
|
+
Table.fullTextIndexes = restoredFullTextIndexes;
|
|
3648
|
+
Table.fullTextIndexGenerations = persistedFullTextIndexGenerations(
|
|
3649
|
+
restoredPrimary?.fullTextIndexGenerations,
|
|
3650
|
+
restoredFullTextIndexes
|
|
3651
|
+
);
|
|
3652
|
+
Table.fullTextIndexRetirements = persistedFullTextIndexNames(restoredPrimary?.fullTextIndexRetirements);
|
|
2893
3653
|
Table.schemaVersion++;
|
|
2894
3654
|
Table.updatedAttributes();
|
|
2895
|
-
|
|
2896
|
-
Table.derivedIndexRuntime = attachDerivedIndexes(Table);
|
|
3655
|
+
refreshDerivedIndexes(Table);
|
|
2897
3656
|
};
|
|
2898
3657
|
};
|
|
2899
3658
|
const durableAttributes = catalogAttributes(Table.dbisDB);
|
|
@@ -2909,6 +3668,10 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
2909
3668
|
if (durablePrimary) validationAttributes.unshift(durablePrimary);
|
|
2910
3669
|
}
|
|
2911
3670
|
}
|
|
3671
|
+
fullTextValidationAttributes = validationAttributes;
|
|
3672
|
+
// Before the full-text branches can write an interim descriptor; the check on the merged list
|
|
3673
|
+
// below stays authoritative.
|
|
3674
|
+
assertDerivedFieldOwnership(validationAttributes as any[]);
|
|
2912
3675
|
|
|
2913
3676
|
const persistedAudit = persistedPrimary.descriptor?.audit;
|
|
2914
3677
|
const durableAudit = persistedAudit === true;
|
|
@@ -2947,21 +3710,59 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
2947
3710
|
`Table '${databaseName}.${tableName}' must enable audit logging before using @fullText because its transaction log is the derived-index recovery source`,
|
|
2948
3711
|
400
|
|
2949
3712
|
);
|
|
2950
|
-
const interim = safeInterimFullTextDefinitions(persistedFullTextValues, validationAttributes, compiled);
|
|
2951
3713
|
const pinAudit = compiled.length > 0 && persistedPrimary.descriptor?.audit !== true;
|
|
2952
|
-
|
|
3714
|
+
const requestedNames = new Set(compiled.map(({ name }) => name));
|
|
3715
|
+
const durableDefinitions = readPersistedFullTextDefinitions(
|
|
3716
|
+
persistedFullTextValues,
|
|
3717
|
+
durableAttributes,
|
|
3718
|
+
fullTextWarning
|
|
3719
|
+
);
|
|
3720
|
+
const removedNames = durableDefinitions.map(({ name }) => name).filter((name) => !requestedNames.has(name));
|
|
3721
|
+
const invalidatedNames = durableDefinitions
|
|
3722
|
+
.filter(({ name }) => requestedNames.has(name))
|
|
3723
|
+
.filter((definition) => {
|
|
3724
|
+
try {
|
|
3725
|
+
compileFullTextDefinitions([definition], validationAttributes);
|
|
3726
|
+
return false;
|
|
3727
|
+
} catch (error) {
|
|
3728
|
+
if (error instanceof ClientError) return true;
|
|
3729
|
+
throw error;
|
|
3730
|
+
}
|
|
3731
|
+
})
|
|
3732
|
+
.map(({ name }) => name);
|
|
3733
|
+
const transitionRetirements = new Set([...removedNames, ...invalidatedNames]);
|
|
3734
|
+
if (pinAudit || transitionRetirements.size > 0) {
|
|
2953
3735
|
const interimPrimary = { ...persistedPrimary.descriptor };
|
|
2954
3736
|
if (pinAudit) interimPrimary.audit = true;
|
|
2955
|
-
if (
|
|
2956
|
-
|
|
3737
|
+
if (transitionRetirements.size > 0) {
|
|
3738
|
+
const interimDefinitions = durableDefinitions.filter(({ name }) => requestedNames.has(name));
|
|
3739
|
+
if (interimDefinitions.length > 0) {
|
|
3740
|
+
interimPrimary.fullTextIndexes = interimDefinitions;
|
|
3741
|
+
interimPrimary.fullTextIndexGenerations = persistedFullTextIndexGenerations(
|
|
3742
|
+
persistedPrimary.descriptor?.fullTextIndexGenerations,
|
|
3743
|
+
interimDefinitions
|
|
3744
|
+
);
|
|
3745
|
+
} else {
|
|
3746
|
+
delete interimPrimary.fullTextIndexes;
|
|
3747
|
+
delete interimPrimary.fullTextIndexGenerations;
|
|
3748
|
+
}
|
|
3749
|
+
const retirements = new Set(
|
|
3750
|
+
persistedFullTextIndexNames(persistedPrimary.descriptor?.fullTextIndexRetirements)
|
|
3751
|
+
);
|
|
3752
|
+
for (const name of transitionRetirements) retirements.add(name);
|
|
3753
|
+
for (const name of requestedNames) if (!transitionRetirements.has(name)) retirements.delete(name);
|
|
3754
|
+
fullTextIndexRetirementNames = [...retirements].sort();
|
|
3755
|
+
if (fullTextIndexRetirementNames.length > 0)
|
|
3756
|
+
interimPrimary.fullTextIndexRetirements = fullTextIndexRetirementNames.map((name) => ({ name }));
|
|
3757
|
+
else delete interimPrimary.fullTextIndexRetirements;
|
|
3758
|
+
}
|
|
2957
3759
|
armFullTextLiveStateRestore?.();
|
|
2958
3760
|
Table.dbisDB.put(persistedPrimary.key, interimPrimary);
|
|
2959
3761
|
if (pinAudit && Table.audit !== true) Table.enableAuditing();
|
|
2960
|
-
Table.fullTextIndexes = rootStore instanceof RocksDatabase && finalAudit ? interim : [];
|
|
2961
3762
|
hasChanges = true;
|
|
2962
3763
|
}
|
|
2963
3764
|
fullTextValuesForPersistence = compiled;
|
|
2964
|
-
fullTextPersistencePending = !definitionsEqual(compiled,
|
|
3765
|
+
fullTextPersistencePending = !definitionsEqual(compiled, persistedFullTextValues);
|
|
2965
3766
|
activeFullTextIndexes = compiled;
|
|
2966
3767
|
} else {
|
|
2967
3768
|
const retained = retainFullTextDefinitions(
|
|
@@ -2979,6 +3780,34 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
2979
3780
|
activeFullTextIndexes = rootStore instanceof RocksDatabase && finalAudit ? retained : [];
|
|
2980
3781
|
}
|
|
2981
3782
|
}
|
|
3783
|
+
if (activeFullTextIndexes !== undefined) {
|
|
3784
|
+
const durableGenerationDefinitions = readPersistedFullTextDefinitions(
|
|
3785
|
+
persistedFullTextValues,
|
|
3786
|
+
fullTextValidationAttributes ?? Table.attributes,
|
|
3787
|
+
fullTextWarning
|
|
3788
|
+
);
|
|
3789
|
+
const finalGenerationDefinitions = readPersistedFullTextDefinitions(
|
|
3790
|
+
fullTextValuesForPersistence ?? persistedFullTextValues,
|
|
3791
|
+
fullTextValidationAttributes ?? Table.attributes,
|
|
3792
|
+
fullTextWarning
|
|
3793
|
+
);
|
|
3794
|
+
fullTextIndexGenerationMap = reconcileFullTextIndexGenerations(
|
|
3795
|
+
durableGenerationDefinitions,
|
|
3796
|
+
persistedPrimary.descriptor?.fullTextIndexGenerations,
|
|
3797
|
+
finalGenerationDefinitions,
|
|
3798
|
+
createFullTextIndexGeneration
|
|
3799
|
+
);
|
|
3800
|
+
assertFullTextActivationSupported(rootStore, databaseName, tableName, activeFullTextIndexes);
|
|
3801
|
+
if (
|
|
3802
|
+
activeFullTextIndexes.length > 0 &&
|
|
3803
|
+
rootStore instanceof RocksDatabase &&
|
|
3804
|
+
fullTextClearInProgress(rootStore, Table.tableId)
|
|
3805
|
+
)
|
|
3806
|
+
throw new ClientError(
|
|
3807
|
+
`Cannot activate @fullText on '${databaseName}.${tableName}' while Table.clear() is in progress`,
|
|
3808
|
+
409
|
|
3809
|
+
);
|
|
3810
|
+
}
|
|
2982
3811
|
// it table already exists, get the split segments setting
|
|
2983
3812
|
if (splitSegments == undefined) splitSegments = Table.splitSegments;
|
|
2984
3813
|
if (origin === 'cluster') {
|
|
@@ -3016,6 +3845,9 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
3016
3845
|
);
|
|
3017
3846
|
if (existingPrimary) attributes = [existingPrimary, ...attributes];
|
|
3018
3847
|
}
|
|
3848
|
+
// On the complete list (a peer's fields merged, an omitted primary key inherited) and before
|
|
3849
|
+
// it replaces the live one or reaches the catalog, so a refused declaration changes nothing.
|
|
3850
|
+
assertDerivedFieldOwnership(attributes as any[]);
|
|
3019
3851
|
armFullTextLiveStateRestore?.();
|
|
3020
3852
|
Table.attributes.splice(0, Table.attributes.length, ...attributes);
|
|
3021
3853
|
// Re-assert from the live declaration so a stale value on disk (replicated event,
|
|
@@ -3031,6 +3863,7 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
3031
3863
|
// undefined means a non-schema caller (add_attribute, cluster schema events) — don't clobber
|
|
3032
3864
|
if (cacheControl !== undefined) Table.cacheControl = cacheControl;
|
|
3033
3865
|
} else {
|
|
3866
|
+
if (Array.isArray(attributes)) assertDerivedFieldOwnership(attributes as any[]);
|
|
3034
3867
|
if (fullTextIndexesExplicit) {
|
|
3035
3868
|
if (origin === 'cluster') {
|
|
3036
3869
|
const merged = mergePeerFullTextDefinitions([], fullTextIndexes, attributes, fullTextWarning);
|
|
@@ -3064,8 +3897,21 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
3064
3897
|
primaryKeyAttribute.schemaDefined = schemaDefined;
|
|
3065
3898
|
// Old readers treat every attribute row as live schema, so relationships stay on the ignored primary descriptor.
|
|
3066
3899
|
if (relationshipDefinitions) primaryKeyAttribute.relationships = relationshipDefinitions;
|
|
3067
|
-
if (Array.isArray(fullTextValuesForPersistence) && fullTextValuesForPersistence.length > 0)
|
|
3900
|
+
if (Array.isArray(fullTextValuesForPersistence) && fullTextValuesForPersistence.length > 0) {
|
|
3901
|
+
const durableFullTextIndexes = readPersistedFullTextDefinitions(
|
|
3902
|
+
fullTextValuesForPersistence,
|
|
3903
|
+
attributes,
|
|
3904
|
+
fullTextWarning
|
|
3905
|
+
);
|
|
3906
|
+
fullTextIndexGenerationMap = reconcileFullTextIndexGenerations(
|
|
3907
|
+
[],
|
|
3908
|
+
undefined,
|
|
3909
|
+
durableFullTextIndexes,
|
|
3910
|
+
createFullTextIndexGeneration
|
|
3911
|
+
);
|
|
3068
3912
|
primaryKeyAttribute.fullTextIndexes = fullTextValuesForPersistence;
|
|
3913
|
+
primaryKeyAttribute.fullTextIndexGenerations = fullTextIndexGenerationMap;
|
|
3914
|
+
}
|
|
3069
3915
|
// can't change compression after the fact (except threshold), so save only when we create the table
|
|
3070
3916
|
primaryKeyAttribute.compression = getDefaultCompression();
|
|
3071
3917
|
if (trackDeletes) primaryKeyAttribute.trackDeletes = true;
|
|
@@ -3117,6 +3963,11 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
3117
3963
|
markInternalDbiNonVersioned(attributesDbi);
|
|
3118
3964
|
|
|
3119
3965
|
exclusiveLock(); // get an exclusive lock on the database so we can verify that we are the only thread creating the table (and assigning the table id)
|
|
3966
|
+
if (rootStore instanceof RocksDatabase && fullTextRetirementInProgress(rootStore, tableName))
|
|
3967
|
+
throw new ClientError(
|
|
3968
|
+
`Cannot create '${databaseName}.${tableName}' while its previous full-text storage is being retired`,
|
|
3969
|
+
409
|
|
3970
|
+
);
|
|
3120
3971
|
const existingTableMeta = (attributesDbi as any).getSync(dbiName);
|
|
3121
3972
|
if (existingTableMeta && !existingTableMeta.dropping) {
|
|
3122
3973
|
// table was created while we were setting up; the lock is not reentrant, so release
|
|
@@ -3133,7 +3984,11 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
3133
3984
|
// create below starts from a clean slate; treating the tombstoned
|
|
3134
3985
|
// entry as an existing table would recurse forever on the stale
|
|
3135
3986
|
// catalog row.
|
|
3136
|
-
completeInterruptedDrop(rootStore, attributesDbi, databaseName, tableName)
|
|
3987
|
+
if (!completeInterruptedDrop(rootStore, attributesDbi, databaseName, tableName))
|
|
3988
|
+
throw new ClientError(
|
|
3989
|
+
`Cannot create '${databaseName}.${tableName}' while its interrupted full-text drop is being retired`,
|
|
3990
|
+
409
|
|
3991
|
+
);
|
|
3137
3992
|
// This resolves the drop without ever going through the schema-load
|
|
3138
3993
|
// reconcile below, which is the only other place that returns a spent
|
|
3139
3994
|
// budget. Without clearing it here too, a table that gets dropped again
|
|
@@ -3188,6 +4043,8 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
3188
4043
|
indices: {},
|
|
3189
4044
|
attributes,
|
|
3190
4045
|
fullTextIndexes: activeFullTextIndexes ?? [],
|
|
4046
|
+
fullTextIndexGenerations: fullTextIndexGenerationMap,
|
|
4047
|
+
fullTextIndexRetirements: fullTextIndexRetirementNames ?? [],
|
|
3191
4048
|
schemaDefined,
|
|
3192
4049
|
dbisDB: attributesDbi,
|
|
3193
4050
|
description,
|
|
@@ -3433,9 +4290,13 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
3433
4290
|
JSON.stringify(attributeDescriptor.elements) !== JSON.stringify(attribute.elements) ||
|
|
3434
4291
|
// Include `embed` so a source/model change refreshes the embed registry.
|
|
3435
4292
|
JSON.stringify(attributeDescriptor.embed) !== JSON.stringify(attribute.embed);
|
|
3436
|
-
// any metadata difference (drives persistence)
|
|
4293
|
+
// any metadata difference (drives persistence). `decide` is compared here and not in
|
|
4294
|
+
// `commonChanged`: a changed directive refreshes the decide registry, and a stored decision
|
|
4295
|
+
// does not depend on the model, so an indexed decision attribute must not rebuild its index.
|
|
3437
4296
|
const changed =
|
|
3438
|
-
commonChanged ||
|
|
4297
|
+
commonChanged ||
|
|
4298
|
+
JSON.stringify(attributeDescriptor?.indexed) !== JSON.stringify(attribute.indexed) ||
|
|
4299
|
+
JSON.stringify(attributeDescriptor?.decide) !== JSON.stringify(attribute.decide);
|
|
3439
4300
|
// structure-affecting difference (drives reindex) — ignores search-only option changes and
|
|
3440
4301
|
// representation-only differences (key order, string-vs-number) via canonicalIndexKey
|
|
3441
4302
|
const indexOptionsStructurallyChanged =
|
|
@@ -3599,14 +4460,39 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
3599
4460
|
attributesDbi.put(dbiKey, attribute);
|
|
3600
4461
|
}
|
|
3601
4462
|
}
|
|
3602
|
-
if (
|
|
4463
|
+
if (
|
|
4464
|
+
!deferredPrimaryRow &&
|
|
4465
|
+
(fullTextPersistencePending ||
|
|
4466
|
+
(rootStore instanceof RocksDatabase &&
|
|
4467
|
+
activeFullTextIndexes !== undefined &&
|
|
4468
|
+
Array.isArray(fullTextValuesForPersistence)))
|
|
4469
|
+
) {
|
|
3603
4470
|
const { key, descriptor } = persistedPrimaryDescriptor(attributesDbi);
|
|
3604
4471
|
if (descriptor && !tableIsDropping(descriptor, key)) {
|
|
3605
4472
|
const updatedPrimary = { ...descriptor };
|
|
3606
|
-
if (Array.isArray(fullTextValuesForPersistence) && fullTextValuesForPersistence.length > 0)
|
|
4473
|
+
if (Array.isArray(fullTextValuesForPersistence) && fullTextValuesForPersistence.length > 0) {
|
|
3607
4474
|
updatedPrimary.fullTextIndexes = fullTextValuesForPersistence;
|
|
3608
|
-
|
|
3609
|
-
|
|
4475
|
+
updatedPrimary.fullTextIndexGenerations = fullTextIndexGenerationMap;
|
|
4476
|
+
} else {
|
|
4477
|
+
delete updatedPrimary.fullTextIndexes;
|
|
4478
|
+
delete updatedPrimary.fullTextIndexGenerations;
|
|
4479
|
+
}
|
|
4480
|
+
const activeNames = new Set(persistedFullTextIndexNames(updatedPrimary.fullTextIndexes));
|
|
4481
|
+
const retirements = new Set(persistedFullTextIndexNames(descriptor.fullTextIndexRetirements));
|
|
4482
|
+
for (const name of persistedFullTextIndexNames(descriptor.fullTextIndexes))
|
|
4483
|
+
if (!activeNames.has(name)) retirements.add(name);
|
|
4484
|
+
for (const name of activeNames) retirements.delete(name);
|
|
4485
|
+
fullTextIndexRetirementNames = [...retirements].sort();
|
|
4486
|
+
if (fullTextIndexRetirementNames.length > 0)
|
|
4487
|
+
updatedPrimary.fullTextIndexRetirements = fullTextIndexRetirementNames.map((name) => ({ name }));
|
|
4488
|
+
else delete updatedPrimary.fullTextIndexRetirements;
|
|
4489
|
+
if (
|
|
4490
|
+
!definitionsEqual(descriptor.fullTextIndexes, updatedPrimary.fullTextIndexes) ||
|
|
4491
|
+
JSON.stringify(descriptor.fullTextIndexGenerations ?? {}) !==
|
|
4492
|
+
JSON.stringify(updatedPrimary.fullTextIndexGenerations ?? {}) ||
|
|
4493
|
+
JSON.stringify(descriptor.fullTextIndexRetirements ?? []) !==
|
|
4494
|
+
JSON.stringify(updatedPrimary.fullTextIndexRetirements ?? [])
|
|
4495
|
+
) {
|
|
3610
4496
|
exclusiveLock();
|
|
3611
4497
|
attributesDbi.put(key, updatedPrimary);
|
|
3612
4498
|
hasChanges = true;
|
|
@@ -3674,7 +4560,11 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
3674
4560
|
} finally {
|
|
3675
4561
|
releaseLock();
|
|
3676
4562
|
}
|
|
3677
|
-
if (activeFullTextIndexes !== undefined)
|
|
4563
|
+
if (activeFullTextIndexes !== undefined) {
|
|
4564
|
+
Table.fullTextIndexes = activeFullTextIndexes;
|
|
4565
|
+
Table.fullTextIndexGenerations = fullTextIndexGenerationMap;
|
|
4566
|
+
Table.fullTextIndexRetirements = fullTextIndexRetirementNames ?? [];
|
|
4567
|
+
}
|
|
3678
4568
|
if (hasChanges || refreshRelationshipAttributes) Table.schemaVersion++;
|
|
3679
4569
|
if (hasChanges || refreshRelationshipAttributes || refreshedLiveAttributes) Table.updatedAttributes();
|
|
3680
4570
|
logger.trace(`${tableName} table loading, running index`);
|
|
@@ -3691,8 +4581,7 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
3691
4581
|
signalling.signalSchemaChange(
|
|
3692
4582
|
new SchemaEventMsg(process.pid, 'schema-change', Table.databaseName, Table.tableName, undefined, branchPath)
|
|
3693
4583
|
);
|
|
3694
|
-
|
|
3695
|
-
Table.derivedIndexRuntime = attachDerivedIndexes(Table);
|
|
4584
|
+
refreshDerivedIndexes(Table);
|
|
3696
4585
|
|
|
3697
4586
|
Table.origin = origin;
|
|
3698
4587
|
// scope-private: replication and other global subscribers must not learn of a branch class
|
|
@@ -4537,8 +5426,34 @@ function scheduleGenerationReclaim(rootStore: RocksDatabase, attributesDbi, data
|
|
|
4537
5426
|
state.delay = Math.min(state.delay * 2, 60_000);
|
|
4538
5427
|
}
|
|
4539
5428
|
|
|
5429
|
+
const interruptedDropRetirements = new WeakMap<object, Set<Promise<void>>>();
|
|
5430
|
+
|
|
5431
|
+
function trackInterruptedDropRetirement(rootStore: object, task: Promise<void>): void {
|
|
5432
|
+
let tasks = interruptedDropRetirements.get(rootStore);
|
|
5433
|
+
if (!tasks) interruptedDropRetirements.set(rootStore, (tasks = new Set()));
|
|
5434
|
+
tasks.add(task);
|
|
5435
|
+
const remove = () => {
|
|
5436
|
+
tasks.delete(task);
|
|
5437
|
+
};
|
|
5438
|
+
task.then(remove, remove);
|
|
5439
|
+
}
|
|
5440
|
+
|
|
5441
|
+
async function settleInterruptedDropRetirements(rootStores: Iterable<object>): Promise<void> {
|
|
5442
|
+
for (;;) {
|
|
5443
|
+
const tasks = [...rootStores].flatMap((rootStore) => [...(interruptedDropRetirements.get(rootStore) ?? [])]);
|
|
5444
|
+
if (tasks.length === 0) return;
|
|
5445
|
+
await Promise.allSettled(tasks);
|
|
5446
|
+
}
|
|
5447
|
+
}
|
|
5448
|
+
|
|
4540
5449
|
/** Finishes the durable logical state of a drop that stopped after writing its tombstone. */
|
|
4541
|
-
function completeInterruptedDrop(
|
|
5450
|
+
function completeInterruptedDrop(
|
|
5451
|
+
rootStore,
|
|
5452
|
+
attributesDbi,
|
|
5453
|
+
databaseName: string,
|
|
5454
|
+
tableName: string,
|
|
5455
|
+
fullTextRetired = false
|
|
5456
|
+
): boolean {
|
|
4542
5457
|
logger.debug(`Completing interrupted drop of table ${databaseName}.${tableName}`);
|
|
4543
5458
|
const catalogRows = [...attributesDbi.getRange({ start: tableName + '/', end: tableName + '0' })] as Array<{
|
|
4544
5459
|
key: string;
|
|
@@ -4548,6 +5463,40 @@ function completeInterruptedDrop(rootStore, attributesDbi, databaseName: string,
|
|
|
4548
5463
|
const primaryEntry =
|
|
4549
5464
|
(bareEntry?.value?.isPrimaryKey ? bareEntry : undefined) ?? catalogRows.find(({ value }) => value?.isPrimaryKey);
|
|
4550
5465
|
const tombstoneEntry = bareEntry ?? primaryEntry;
|
|
5466
|
+
if (rootStore instanceof RocksDatabase && !fullTextRetired) {
|
|
5467
|
+
const names = [
|
|
5468
|
+
...new Set([
|
|
5469
|
+
...persistedFullTextIndexNames(primaryEntry?.value?.fullTextIndexes),
|
|
5470
|
+
...persistedFullTextIndexNames(primaryEntry?.value?.fullTextIndexRetirements),
|
|
5471
|
+
]),
|
|
5472
|
+
];
|
|
5473
|
+
if (names.length > 0) {
|
|
5474
|
+
const releaseRetirement = acquireFullTextRetirementFence(rootStore, tableName);
|
|
5475
|
+
if (releaseRetirement) {
|
|
5476
|
+
const retirement = (async () => {
|
|
5477
|
+
try {
|
|
5478
|
+
const retired = await retireFullTextIndexes(
|
|
5479
|
+
{ databaseName, tableName, primaryStore: { rootStore } },
|
|
5480
|
+
names.map((name) => ({ name })),
|
|
5481
|
+
() => rootStore.status === 'open' && !databaseCommitsSuspended(rootStore)
|
|
5482
|
+
);
|
|
5483
|
+
if (!retired || rootStore.status === 'closed' || databaseCommitsSuspended(rootStore)) return;
|
|
5484
|
+
await withUpdateAttributesLockNonBlocking(
|
|
5485
|
+
rootStore,
|
|
5486
|
+
`complete interrupted drop of '${databaseName}.${tableName}'`,
|
|
5487
|
+
() => completeInterruptedDrop(rootStore, attributesDbi, databaseName, tableName, true)
|
|
5488
|
+
);
|
|
5489
|
+
} catch (error) {
|
|
5490
|
+
logger.warn(`Could not retire full-text storage for interrupted drop ${databaseName}.${tableName}`, error);
|
|
5491
|
+
} finally {
|
|
5492
|
+
releaseRetirement();
|
|
5493
|
+
}
|
|
5494
|
+
})();
|
|
5495
|
+
trackInterruptedDropRetirement(rootStore, retirement);
|
|
5496
|
+
}
|
|
5497
|
+
return false;
|
|
5498
|
+
}
|
|
5499
|
+
}
|
|
4551
5500
|
if (rootStore instanceof RocksDatabase) {
|
|
4552
5501
|
const tombstone = tombstoneEntry?.value;
|
|
4553
5502
|
const generation = tombstone?.generation ?? primaryEntry?.value?.generation;
|
|
@@ -4598,6 +5547,7 @@ function completeInterruptedDrop(rootStore, attributesDbi, databaseName: string,
|
|
|
4598
5547
|
(attributesDbi as any).removeSync(key);
|
|
4599
5548
|
}
|
|
4600
5549
|
if (tombstoneEntry) (attributesDbi as any).removeSync(tombstoneEntry.key);
|
|
5550
|
+
return true;
|
|
4601
5551
|
}
|
|
4602
5552
|
|
|
4603
5553
|
export function dropTableMeta({ table: tableName, database: databaseName }) {
|