@harperfast/harper 5.3.0-alpha.1 → 5.3.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cliOperations.ts +12 -0
- package/bin/restart.ts +66 -6
- package/components/Application.ts +1134 -112
- package/components/OptionsWatcher.ts +368 -102
- package/components/Scope.ts +20 -1
- package/components/componentLoader.ts +43 -4
- package/components/deploymentOperations.ts +4 -1
- package/components/deploymentRecorder.ts +9 -2
- package/components/operations.js +284 -52
- package/components/operationsValidation.js +49 -2
- package/components/packageComponent.ts +25 -1
- package/components/requestRestart.ts +11 -0
- package/config/RootConfigWatcher.ts +191 -37
- package/config/configReadRetry.ts +62 -0
- package/config/configUtils.ts +78 -26
- package/config/parseConfigFile.ts +34 -0
- package/config/readConfigFileSync.ts +44 -0
- package/config/watcherArming.ts +59 -0
- package/config-root.schema.json +4 -0
- package/dataLayer/harperBridge/ResourceBridge.ts +28 -2
- package/dist/bin/cliOperations.js +13 -0
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/restart.js +42 -6
- package/dist/bin/restart.js.map +1 -1
- package/dist/components/Application.d.ts +104 -9
- package/dist/components/Application.js +954 -102
- package/dist/components/Application.js.map +1 -1
- package/dist/components/OptionsWatcher.d.ts +4 -1
- package/dist/components/OptionsWatcher.js +378 -104
- package/dist/components/OptionsWatcher.js.map +1 -1
- package/dist/components/Scope.js +15 -1
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/componentLoader.js +35 -3
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/deploymentOperations.js +4 -1
- package/dist/components/deploymentOperations.js.map +1 -1
- package/dist/components/deploymentRecorder.d.ts +4 -2
- package/dist/components/deploymentRecorder.js +1 -0
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/operations.d.ts +28 -0
- package/dist/components/operations.js +263 -54
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +48 -2
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/packageComponent.js +24 -0
- package/dist/components/packageComponent.js.map +1 -1
- package/dist/components/requestRestart.d.ts +1 -0
- package/dist/components/requestRestart.js +7 -0
- package/dist/components/requestRestart.js.map +1 -1
- package/dist/config/RootConfigWatcher.d.ts +2 -0
- package/dist/config/RootConfigWatcher.js +189 -35
- package/dist/config/RootConfigWatcher.js.map +1 -1
- package/dist/config/configReadRetry.d.ts +8 -0
- package/dist/config/configReadRetry.js +62 -0
- package/dist/config/configReadRetry.js.map +1 -0
- package/dist/config/configUtils.d.ts +10 -9
- package/dist/config/configUtils.js +63 -27
- package/dist/config/configUtils.js.map +1 -1
- package/dist/config/parseConfigFile.d.ts +4 -0
- package/dist/config/parseConfigFile.js +35 -0
- package/dist/config/parseConfigFile.js.map +1 -0
- package/dist/config/readConfigFileSync.d.ts +1 -0
- package/dist/config/readConfigFileSync.js +47 -0
- package/dist/config/readConfigFileSync.js.map +1 -0
- package/dist/config/watcherArming.d.ts +15 -0
- package/dist/config/watcherArming.js +59 -0
- package/dist/config/watcherArming.js.map +1 -0
- package/dist/dataLayer/harperBridge/ResourceBridge.js +21 -2
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +3 -0
- package/dist/resources/DatabaseTransaction.d.ts +25 -0
- package/dist/resources/DatabaseTransaction.js +224 -18
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +2 -1
- package/dist/resources/LMDBTransaction.js +22 -3
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.js +22 -6
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/RecordEncoder.d.ts +1 -1
- package/dist/resources/RecordEncoder.js +21 -5
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/Resource.js +97 -13
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +8 -0
- package/dist/resources/RocksIndexStore.js +2 -1
- package/dist/resources/RocksIndexStore.js.map +1 -1
- package/dist/resources/RocksTransactionLogStore.d.ts +10 -0
- package/dist/resources/RocksTransactionLogStore.js +104 -33
- package/dist/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/resources/Table.d.ts +58 -7
- package/dist/resources/Table.js +1095 -346
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +10 -3
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/auditStore.d.ts +170 -0
- package/dist/resources/auditStore.js +457 -11
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/dataLoader.js +3 -4
- package/dist/resources/dataLoader.js.map +1 -1
- package/dist/resources/databases.d.ts +16 -13
- package/dist/resources/databases.js +624 -176
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/derivedIndexRegistry.d.ts +5 -0
- package/dist/resources/derivedIndexRegistry.js +68 -0
- package/dist/resources/derivedIndexRegistry.js.map +1 -0
- package/dist/resources/derivedIndexRuntime.d.ts +215 -0
- package/dist/resources/derivedIndexRuntime.js +2027 -0
- package/dist/resources/derivedIndexRuntime.js.map +1 -0
- package/dist/resources/graphql.js +3 -2
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +102 -9
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +848 -39
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
- package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
- package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
- package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
- package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
- package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
- package/dist/resources/nodeIdMapping.d.ts +5 -0
- package/dist/resources/nodeIdMapping.js +49 -0
- package/dist/resources/nodeIdMapping.js.map +1 -1
- package/dist/resources/recordLock.d.ts +47 -4
- package/dist/resources/recordLock.js +138 -7
- package/dist/resources/recordLock.js.map +1 -1
- package/dist/resources/recordLockCoordinator.d.ts +557 -0
- package/dist/resources/recordLockCoordinator.js +2565 -0
- package/dist/resources/recordLockCoordinator.js.map +1 -0
- package/dist/resources/replayLogs.js +5 -0
- package/dist/resources/replayLogs.js.map +1 -1
- package/dist/resources/replicatedApplyFailure.d.ts +16 -0
- package/dist/resources/replicatedApplyFailure.js +63 -0
- package/dist/resources/replicatedApplyFailure.js.map +1 -0
- package/dist/resources/scheduler/scheduler.js +3 -3
- package/dist/resources/scheduler/scheduler.js.map +1 -1
- package/dist/resources/search.d.ts +10 -4
- package/dist/resources/search.js +160 -40
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/tracked.d.ts +5 -1
- package/dist/resources/tracked.js +74 -23
- package/dist/resources/tracked.js.map +1 -1
- package/dist/security/jsLoader.js +6 -4
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/server/REST.js +33 -2
- package/dist/server/REST.js.map +1 -1
- package/dist/server/http.d.ts +5 -1
- package/dist/server/http.js +34 -2
- package/dist/server/http.js.map +1 -1
- package/dist/server/serverHelpers/Headers.d.ts +2 -0
- package/dist/server/serverHelpers/Headers.js +6 -0
- package/dist/server/serverHelpers/Headers.js.map +1 -1
- package/dist/server/serverHelpers/NodeAdapterResponse.d.ts +48 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js +220 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -0
- package/dist/server/serverHelpers/Request.d.ts +5 -10
- package/dist/server/serverHelpers/Request.js +38 -136
- package/dist/server/serverHelpers/Request.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.d.ts +2 -0
- package/dist/server/serverHelpers/contentTypes.js +189 -15
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.js +96 -17
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/storageReclamation.js +1 -1
- package/dist/server/storageReclamation.js.map +1 -1
- package/dist/server/threads/isolatedApplications.d.ts +47 -0
- package/dist/server/threads/isolatedApplications.js +171 -0
- package/dist/server/threads/isolatedApplications.js.map +1 -0
- package/dist/server/threads/logRotationTransport.d.ts +1 -0
- package/dist/server/threads/logRotationTransport.js +33 -0
- package/dist/server/threads/logRotationTransport.js.map +1 -0
- package/dist/server/threads/manageThreads.d.ts +64 -6
- package/dist/server/threads/manageThreads.js +261 -12
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/socketRouter.d.ts +1 -0
- package/dist/server/threads/socketRouter.js +196 -13
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadServer.js +30 -7
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +24 -0
- package/dist/utility/errors/hdbError.js +58 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/hdbTerms.d.ts +2 -0
- package/dist/utility/hdbTerms.js +2 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/logging/harper_logger.js +217 -38
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/logging/logGenerationCoordinator.d.ts +35 -0
- package/dist/utility/logging/logGenerationCoordinator.js +184 -0
- package/dist/utility/logging/logGenerationCoordinator.js.map +1 -0
- package/dist/utility/logging/logRotation.d.ts +46 -0
- package/dist/utility/logging/logRotation.js +365 -0
- package/dist/utility/logging/logRotation.js.map +1 -0
- package/dist/utility/logging/logRotator.d.ts +1 -1
- package/dist/utility/logging/logRotator.js +172 -92
- package/dist/utility/logging/logRotator.js.map +1 -1
- package/dist/utility/npmUtilities.js +6 -4
- package/dist/utility/npmUtilities.js.map +1 -1
- package/dist/utility/watcherFallback.d.ts +0 -45
- package/dist/utility/watcherFallback.js +1 -125
- package/dist/utility/watcherFallback.js.map +1 -1
- package/dist/validation/configValidator.js +6 -3
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +6 -0
- package/json/systemSchema.json +3 -0
- package/npm-shrinkwrap.json +131 -41
- package/package.json +10 -3
- package/resources/DESIGN.md +124 -19
- package/resources/DatabaseTransaction.ts +230 -17
- package/resources/LMDBTransaction.ts +21 -3
- package/resources/PrimaryRocksDatabase.ts +20 -7
- package/resources/RecordEncoder.ts +25 -5
- package/resources/Resource.ts +97 -13
- package/resources/ResourceInterface.ts +8 -0
- package/resources/RocksIndexStore.ts +2 -1
- package/resources/RocksTransactionLogStore.ts +111 -31
- package/resources/Table.ts +1224 -393
- package/resources/analytics/write.ts +10 -3
- package/resources/auditStore.ts +460 -11
- package/resources/dataLoader.ts +3 -4
- package/resources/databases.ts +610 -146
- package/resources/derivedIndexRegistry.ts +56 -0
- package/resources/derivedIndexRuntime.ts +2292 -0
- package/resources/graphql.ts +3 -2
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +905 -46
- package/resources/indexes/hnswDerivedIndex.ts +531 -0
- package/resources/indexes/hnswPlaneBinding.ts +174 -0
- package/resources/nodeIdMapping.ts +50 -0
- package/resources/recordLock.ts +173 -7
- package/resources/recordLockCoordinator.ts +3043 -0
- package/resources/replayLogs.ts +5 -0
- package/resources/replicatedApplyFailure.ts +77 -0
- package/resources/scheduler/scheduler.ts +4 -4
- package/resources/search.ts +169 -49
- package/resources/tracked.ts +73 -22
- package/security/jsLoader.ts +6 -4
- package/server/DESIGN.md +11 -0
- package/server/REST.ts +36 -3
- package/server/http.ts +34 -2
- package/server/serverHelpers/Headers.ts +5 -1
- package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
- package/server/serverHelpers/Request.ts +33 -131
- package/server/serverHelpers/contentTypes.ts +188 -15
- package/server/serverHelpers/serverUtilities.ts +143 -24
- package/server/storageReclamation.ts +2 -2
- package/server/threads/isolatedApplications.ts +157 -0
- package/server/threads/logRotationTransport.ts +40 -0
- package/server/threads/manageThreads.js +254 -12
- package/server/threads/socketRouter.ts +217 -11
- package/server/threads/threadServer.js +30 -7
- package/studio/web/assets/{Chat-BnCBegQz.js → Chat-D3j-1yY1.js} +1 -1
- package/studio/web/assets/{FloatingChat-CoDW1ySS.js → FloatingChat-BxJGYcfB.js} +3 -3
- package/studio/web/assets/{apiToken-Bwk5BLXW.js → apiToken-CT55oWOe.js} +1 -1
- package/studio/web/assets/{applications-DHxGi7JH.js → applications-D9Ct9_vm.js} +1 -1
- package/studio/web/assets/{cssMode-s0cWI-_M.js → cssMode-DV8H7VwA.js} +1 -1
- package/studio/web/assets/{editor-DNcRHK54.js → editor-uatc0unt.js} +1 -1
- package/studio/web/assets/{html-Bdssedlg.js → html-Bm6D6paN.js} +1 -1
- package/studio/web/assets/{htmlMode-CoDlJ3fw.js → htmlMode-CEn7tpLG.js} +1 -1
- package/studio/web/assets/{index-D6sxmFLR.js → index-BIXW6Pu4.js} +5 -5
- package/studio/web/assets/{index.lazy-tmU5BS8s.js → index.lazy-UI7L-Vrk.js} +1 -1
- package/studio/web/assets/{javascript-B8meVSTH.js → javascript-CJ0G3AFZ.js} +1 -1
- package/studio/web/assets/{jsonMode-DpIPd35T.js → jsonMode-DQADAYEa.js} +1 -1
- package/studio/web/assets/{languageServices-C_5FMJzQ.js → languageServices-CAQJXWcI.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-BIzNBzPK.js → lspLanguageFeatures-CCQ8P5sY.js} +1 -1
- package/studio/web/assets/{notifications-CQf18QKb.js → notifications-BbxTU6Aw.js} +1 -1
- package/studio/web/assets/{notifications-CvZivSbh.js → notifications-Cvb3P1lB.js} +1 -1
- package/studio/web/assets/{profile-DdOwtntb.js → profile-Yyb7gsvL.js} +1 -1
- package/studio/web/assets/{regions-n69fwagr.js → regions-OgjGHlU5.js} +1 -1
- package/studio/web/assets/{register-PfWTCXWB.js → register-6qwNEOY3.js} +2 -2
- package/studio/web/assets/{setComponentFile-Bg6O7X0S.js → setComponentFile-BilDMtgB.js} +1 -1
- package/studio/web/assets/{setup-CUx_aUDl.js → setup-J6qJ7OIU.js} +2 -2
- package/studio/web/assets/{status-D7BVKqX9.js → status-0RWGcfyD.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-DBSyXzMr.js → toggleHighContrast-BIn-vErT.js} +1 -1
- package/studio/web/assets/{tsMode-BByKCjBS.js → tsMode-DgUXku4d.js} +1 -1
- package/studio/web/assets/{typescript-DDLnLpw9.js → typescript-C9orXcsM.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-31CHGaHk.js → useEntityRestURL-BEoXXbUB.js} +1 -1
- package/studio/web/assets/{workers-pR3jRY9D.js → workers-JVzSDmgx.js} +1 -1
- package/studio/web/assets/{xml-2iRnMhQO.js → xml-Cq-S8S4X.js} +1 -1
- package/studio/web/assets/{yaml-Bf92gJpd.js → yaml-sfoRdh1M.js} +1 -1
- package/studio/web/index.html +1 -1
- package/utility/errors/hdbError.ts +54 -0
- package/utility/hdbTerms.ts +2 -0
- package/utility/logging/harper_logger.ts +209 -30
- package/utility/logging/logGenerationCoordinator.ts +196 -0
- package/utility/logging/logRotation.ts +367 -0
- package/utility/logging/logRotator.ts +196 -91
- package/utility/npmUtilities.ts +6 -4
- package/utility/watcherFallback.ts +0 -122
- package/validation/configValidator.ts +6 -3
|
@@ -10,6 +10,10 @@ import * as envMgr from '../../utility/environment/environmentManager.ts';
|
|
|
10
10
|
import { CONFIG_PARAMS } from '../../utility/hdbTerms.ts';
|
|
11
11
|
import * as YAML from 'yaml';
|
|
12
12
|
import { logger } from '../../utility/logging/logger.ts';
|
|
13
|
+
import {
|
|
14
|
+
errorToString as harperErrorToString,
|
|
15
|
+
errorForLog as harperErrorForLog,
|
|
16
|
+
} from '../../utility/logging/harper_logger.ts';
|
|
13
17
|
import { Blob } from '../../resources/blob.ts';
|
|
14
18
|
// TODO: Only load this if fastify is loaded
|
|
15
19
|
import fp from 'fastify-plugin';
|
|
@@ -17,6 +21,36 @@ import { parseMultipartRequest } from './multipartParser.ts';
|
|
|
17
21
|
const SERIALIZATION_BIGINT = envMgr.get(CONFIG_PARAMS.SERIALIZATION_BIGINT) !== false;
|
|
18
22
|
const JSONStringify = SERIALIZATION_BIGINT ? stringify : JSON.stringify;
|
|
19
23
|
const JSONParse = SERIALIZATION_BIGINT ? parse : JSON.parse;
|
|
24
|
+
const streamStartup = Symbol('streamStartup');
|
|
25
|
+
const serializedStreamError = Symbol('serializedStreamError');
|
|
26
|
+
|
|
27
|
+
type StreamStartup = {
|
|
28
|
+
result: Promise<{ failed?: true; error?: unknown }>;
|
|
29
|
+
abort: () => void;
|
|
30
|
+
start: () => void;
|
|
31
|
+
cancel?: () => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function streamErrorRecord(error: any) {
|
|
35
|
+
let name = 'Error';
|
|
36
|
+
let message;
|
|
37
|
+
try {
|
|
38
|
+
const code = error?.code;
|
|
39
|
+
if (typeof code === 'string' || typeof code === 'number') name = String(code);
|
|
40
|
+
else if (typeof error?.constructor?.name === 'string') name = error.constructor.name;
|
|
41
|
+
else if (typeof error?.name === 'string') name = error.name;
|
|
42
|
+
} catch {}
|
|
43
|
+
try {
|
|
44
|
+
if (typeof error?.message === 'string') message = error.message;
|
|
45
|
+
} catch {}
|
|
46
|
+
message ??= harperErrorToString(error);
|
|
47
|
+
const record: { error: string; message: string; status?: number } = { error: name, message };
|
|
48
|
+
try {
|
|
49
|
+
const status = error?.statusCode ?? error?.status;
|
|
50
|
+
if (typeof status === 'number') record.status = status;
|
|
51
|
+
} catch {}
|
|
52
|
+
return record;
|
|
53
|
+
}
|
|
20
54
|
|
|
21
55
|
const PUBLIC_ENCODE_OPTIONS = {
|
|
22
56
|
useRecords: false,
|
|
@@ -103,9 +137,14 @@ mediaTypes.set('text/yaml', {
|
|
|
103
137
|
});
|
|
104
138
|
|
|
105
139
|
const ndjsonHandler = {
|
|
106
|
-
serializeStream(data: any) {
|
|
140
|
+
serializeStream(data: any, _response?: Response, request?: any) {
|
|
107
141
|
if (data?.[Symbol.iterator] || data?.[Symbol.asyncIterator]) {
|
|
108
|
-
return
|
|
142
|
+
return errorFrameStream(
|
|
143
|
+
data,
|
|
144
|
+
(msg: any) => JSONStringify(msg) + '\n',
|
|
145
|
+
(error) => JSONStringify({ $harperStreamError: streamErrorRecord(error) }) + '\n',
|
|
146
|
+
request?.method === 'GET'
|
|
147
|
+
);
|
|
109
148
|
}
|
|
110
149
|
return JSONStringify(data) + '\n';
|
|
111
150
|
},
|
|
@@ -132,9 +171,14 @@ function serializeSSEData(data: any) {
|
|
|
132
171
|
|
|
133
172
|
mediaTypes.set('text/event-stream', {
|
|
134
173
|
// Server-Sent Events (SSE)
|
|
135
|
-
serializeStream: function (iterable) {
|
|
174
|
+
serializeStream: function (iterable, _response?: Response, request?: any) {
|
|
136
175
|
// create a readable stream that we use to stream out events from our subscription
|
|
137
|
-
return
|
|
176
|
+
return errorFrameStream(
|
|
177
|
+
iterable,
|
|
178
|
+
this.serialize,
|
|
179
|
+
(error) => this.serialize({ event: 'harper-error', data: streamErrorRecord(error) }),
|
|
180
|
+
request?.method === 'GET'
|
|
181
|
+
);
|
|
138
182
|
},
|
|
139
183
|
serialize: function (message) {
|
|
140
184
|
if (message.acknowledge) message.acknowledge();
|
|
@@ -396,7 +440,12 @@ export function serialize(responseData, request, responseObject) {
|
|
|
396
440
|
(responseData[Symbol.iterator] || responseData[Symbol.asyncIterator]) &&
|
|
397
441
|
serializer.serializer.serializeStream
|
|
398
442
|
) {
|
|
399
|
-
if (
|
|
443
|
+
if (
|
|
444
|
+
responseData.mapError &&
|
|
445
|
+
serializer.type !== 'application/x-ndjson' &&
|
|
446
|
+
serializer.type !== 'application/ndjson' &&
|
|
447
|
+
serializer.type !== 'text/event-stream'
|
|
448
|
+
) {
|
|
400
449
|
// indicate that we want iterator errors to be returned so we can serialize them in a meaningful way, if possible
|
|
401
450
|
const getColumns = responseData.getColumns;
|
|
402
451
|
responseData = responseData.mapError((error) => {
|
|
@@ -407,9 +456,10 @@ export function serialize(responseData, request, responseObject) {
|
|
|
407
456
|
});
|
|
408
457
|
responseData.getColumns = getColumns;
|
|
409
458
|
}
|
|
410
|
-
let stream = serializer.serializer.serializeStream(responseData, responseObject);
|
|
459
|
+
let stream = serializer.serializer.serializeStream(responseData, responseObject, request);
|
|
411
460
|
if (canCompress) {
|
|
412
461
|
responseObject.headers.set('Content-Encoding', 'br');
|
|
462
|
+
const uncompressedStream = stream;
|
|
413
463
|
stream = stream.pipe(
|
|
414
464
|
createBrotliCompress({
|
|
415
465
|
params: {
|
|
@@ -421,6 +471,7 @@ export function serialize(responseData, request, responseObject) {
|
|
|
421
471
|
},
|
|
422
472
|
})
|
|
423
473
|
);
|
|
474
|
+
if (uncompressedStream[streamStartup]) stream[streamStartup] = uncompressedStream[streamStartup];
|
|
424
475
|
}
|
|
425
476
|
return stream;
|
|
426
477
|
}
|
|
@@ -623,24 +674,146 @@ function deserializerUnknownType(contentType: ContentType): Deserialize {
|
|
|
623
674
|
}
|
|
624
675
|
}
|
|
625
676
|
|
|
626
|
-
function
|
|
677
|
+
function errorFrameStream(iterable, transform, serializeError, eager: boolean) {
|
|
678
|
+
const transformed = transformIterable(iterable, transform, serializeError, eager);
|
|
679
|
+
const readable = Readable.from(transformed);
|
|
680
|
+
const startup: StreamStartup = transformed[streamStartup];
|
|
681
|
+
startup.cancel = () => readable.destroy();
|
|
682
|
+
readable[streamStartup] = startup;
|
|
683
|
+
return readable;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
export function discardSerializedStream(stream) {
|
|
687
|
+
stream?.[streamStartup]?.cancel?.();
|
|
688
|
+
stream?.destroy?.();
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export function waitForStreamStartup(stream) {
|
|
692
|
+
const startup: StreamStartup | undefined = stream?.[streamStartup];
|
|
693
|
+
if (!startup) return;
|
|
694
|
+
startup.start();
|
|
695
|
+
return startup.result.then((result) => {
|
|
696
|
+
if (result.failed) {
|
|
697
|
+
startup.abort();
|
|
698
|
+
discardSerializedStream(stream);
|
|
699
|
+
throw result.error;
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function transformIterable(iterable, transform, serializeError, eager) {
|
|
705
|
+
let iterator;
|
|
706
|
+
let terminal = false;
|
|
707
|
+
let first = true;
|
|
708
|
+
let started = false;
|
|
709
|
+
let firstStep;
|
|
710
|
+
let startupSettled = false;
|
|
711
|
+
const startupResult = Promise.withResolvers<{ failed?: true; error?: unknown }>();
|
|
712
|
+
const errorDecision = Promise.withResolvers<boolean>();
|
|
713
|
+
let startupTimer;
|
|
714
|
+
|
|
715
|
+
const commit = () => {
|
|
716
|
+
if (startupTimer) clearImmediate(startupTimer);
|
|
717
|
+
if (!startupSettled) {
|
|
718
|
+
startupSettled = true;
|
|
719
|
+
startupResult.resolve({});
|
|
720
|
+
}
|
|
721
|
+
errorDecision.resolve(true);
|
|
722
|
+
};
|
|
723
|
+
const abort = () => {
|
|
724
|
+
if (startupTimer) clearImmediate(startupTimer);
|
|
725
|
+
errorDecision.resolve(false);
|
|
726
|
+
};
|
|
727
|
+
const handleError = async (error) => {
|
|
728
|
+
terminal = true;
|
|
729
|
+
try {
|
|
730
|
+
const returned = iterator.return?.();
|
|
731
|
+
returned?.catch?.(() => {});
|
|
732
|
+
} catch {}
|
|
733
|
+
if (!startupSettled) startupResult.resolve({ failed: true, error });
|
|
734
|
+
if (!startupSettled && !(await errorDecision.promise)) return { done: true };
|
|
735
|
+
logger.warn?.('Error serializing in stream', harperErrorForLog(error));
|
|
736
|
+
return { value: serializeError(error), done: false, [serializedStreamError]: true };
|
|
737
|
+
};
|
|
738
|
+
const transformStep = (step) => {
|
|
739
|
+
if (step?.[serializedStreamError]) return step;
|
|
740
|
+
if (terminal) return { done: true };
|
|
741
|
+
if (step.done) return step;
|
|
742
|
+
try {
|
|
743
|
+
return { value: transform(step.value), done: false };
|
|
744
|
+
} catch (error) {
|
|
745
|
+
return handleError(error);
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
const getRawNext = () => {
|
|
749
|
+
if (terminal) return { done: true };
|
|
750
|
+
let step;
|
|
751
|
+
try {
|
|
752
|
+
step = iterator.next();
|
|
753
|
+
} catch (error) {
|
|
754
|
+
return handleError(error);
|
|
755
|
+
}
|
|
756
|
+
if (step.then) return step.then(undefined, handleError);
|
|
757
|
+
return step;
|
|
758
|
+
};
|
|
759
|
+
const getNext = () => {
|
|
760
|
+
if (terminal) return { done: true };
|
|
761
|
+
let step;
|
|
762
|
+
try {
|
|
763
|
+
step = iterator.next();
|
|
764
|
+
} catch (error) {
|
|
765
|
+
return handleError(error);
|
|
766
|
+
}
|
|
767
|
+
if (step.then) return step.then(transformStep, handleError);
|
|
768
|
+
return transformStep(step);
|
|
769
|
+
};
|
|
770
|
+
const start = () => {
|
|
771
|
+
if (started) return;
|
|
772
|
+
started = true;
|
|
773
|
+
iterator = iterable[Symbol.asyncIterator] ? iterable[Symbol.asyncIterator]() : iterable[Symbol.iterator]();
|
|
774
|
+
firstStep = getRawNext();
|
|
775
|
+
Promise.resolve(firstStep).then(
|
|
776
|
+
() => {
|
|
777
|
+
if (!terminal) commit();
|
|
778
|
+
},
|
|
779
|
+
() => {}
|
|
780
|
+
);
|
|
781
|
+
// After this turn, the HTTP status can no longer wait for a first item without delaying long-lived streams.
|
|
782
|
+
startupTimer = setImmediate(commit);
|
|
783
|
+
};
|
|
784
|
+
if (eager) start();
|
|
785
|
+
|
|
627
786
|
return {
|
|
787
|
+
[streamStartup]: {
|
|
788
|
+
result: startupResult.promise,
|
|
789
|
+
abort,
|
|
790
|
+
start,
|
|
791
|
+
},
|
|
628
792
|
[Symbol.asyncIterator]() {
|
|
629
|
-
const iterator = iterable[Symbol.asyncIterator] ? iterable[Symbol.asyncIterator]() : iterable[Symbol.iterator]();
|
|
630
793
|
return {
|
|
631
794
|
next() {
|
|
632
|
-
|
|
633
|
-
if (
|
|
634
|
-
|
|
635
|
-
|
|
795
|
+
if (terminal) return { done: true };
|
|
796
|
+
if (first) {
|
|
797
|
+
first = false;
|
|
798
|
+
start();
|
|
799
|
+
const step = firstStep;
|
|
800
|
+
firstStep = undefined;
|
|
801
|
+
if (step.then) return step.then(transformStep);
|
|
802
|
+
return transformStep(step);
|
|
636
803
|
}
|
|
637
|
-
return
|
|
804
|
+
return getNext();
|
|
638
805
|
},
|
|
639
806
|
return(value) {
|
|
640
|
-
|
|
807
|
+
terminal = true;
|
|
808
|
+
started = true;
|
|
809
|
+
if (!iterator)
|
|
810
|
+
iterator = iterable[Symbol.asyncIterator]
|
|
811
|
+
? iterable[Symbol.asyncIterator]()
|
|
812
|
+
: iterable[Symbol.iterator]?.();
|
|
813
|
+
return iterator?.return?.(value) ?? { value, done: true };
|
|
641
814
|
},
|
|
642
815
|
throw(error) {
|
|
643
|
-
return iterator
|
|
816
|
+
return iterator?.throw?.(error) ?? Promise.reject(error);
|
|
644
817
|
},
|
|
645
818
|
};
|
|
646
819
|
},
|
|
@@ -235,6 +235,17 @@ server.setMcpQuotaHandler = (handler) => {
|
|
|
235
235
|
setMcpQuotaHandler(handler);
|
|
236
236
|
};
|
|
237
237
|
|
|
238
|
+
// The nested `search_operation` operations dataLayer/export.ts's getRecords can actually run (its
|
|
239
|
+
// VALID_SEARCH_OPERATIONS). Validated at dispatch so an unsupported or missing nested operation is a
|
|
240
|
+
// request-time 400 rather than an async job failure, and so the nested authorization below always
|
|
241
|
+
// resolves a real handler.
|
|
242
|
+
const EXPORT_SEARCH_OPERATIONS = new Set<string>([
|
|
243
|
+
terms.OPERATIONS_ENUM.SEARCH_BY_VALUE,
|
|
244
|
+
terms.OPERATIONS_ENUM.SEARCH_BY_HASH,
|
|
245
|
+
terms.OPERATIONS_ENUM.SEARCH_BY_CONDITIONS,
|
|
246
|
+
terms.OPERATIONS_ENUM.SQL,
|
|
247
|
+
]);
|
|
248
|
+
|
|
238
249
|
export function chooseOperation(json: OperationRequestBody, bypassAuth = false) {
|
|
239
250
|
let getOpResult: OperationFunctionObject;
|
|
240
251
|
try {
|
|
@@ -255,32 +266,112 @@ export function chooseOperation(json: OperationRequestBody, bypassAuth = false)
|
|
|
255
266
|
|
|
256
267
|
const { operation_function, job_operation_function } = getOpResult;
|
|
257
268
|
|
|
258
|
-
|
|
259
|
-
//
|
|
269
|
+
const isSqlOperation = json.operation === terms.OPERATIONS_ENUM.SQL;
|
|
270
|
+
// `verifyPerms` derives the tables it checks from the object it is handed, so the caller-supplied
|
|
271
|
+
// `search_operation` may only stand in as the permission subject for the operations that actually
|
|
272
|
+
// run it (dataLayer/export.ts is its only consumer). For anything else it would check the nested
|
|
273
|
+
// tables while the handler runs against the top-level ones.
|
|
274
|
+
const nestedSearch =
|
|
275
|
+
json.operation === terms.OPERATIONS_ENUM.EXPORT_LOCAL || json.operation === terms.OPERATIONS_ENUM.EXPORT_TO_S3
|
|
276
|
+
? json.search_operation
|
|
277
|
+
: undefined;
|
|
278
|
+
if (nestedSearch !== undefined) {
|
|
279
|
+
// It becomes the permission subject below, and a primitive would throw on the principal
|
|
280
|
+
// assignment — a 400 rather than a wrapped 500.
|
|
281
|
+
if (nestedSearch === null || typeof nestedSearch !== 'object' || Array.isArray(nestedSearch)) {
|
|
282
|
+
throw handleHDBError(
|
|
283
|
+
new Error(),
|
|
284
|
+
`'search_operation' must be an object`,
|
|
285
|
+
hdbErrors.HTTP_STATUS_CODES.BAD_REQUEST,
|
|
286
|
+
undefined,
|
|
287
|
+
undefined,
|
|
288
|
+
true
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
// The object check alone lets `{}` and unsupported operations through — they would then fail
|
|
292
|
+
// asynchronously in the export worker (or dereference `search_operation.operation`). Require a
|
|
293
|
+
// supported operation at request time; the nested authorization below relies on it resolving.
|
|
294
|
+
if (typeof nestedSearch.operation !== 'string' || !EXPORT_SEARCH_OPERATIONS.has(nestedSearch.operation)) {
|
|
295
|
+
throw handleHDBError(
|
|
296
|
+
new Error(),
|
|
297
|
+
`'search_operation.operation' must be one of: ${[...EXPORT_SEARCH_OPERATIONS].join(', ')}`,
|
|
298
|
+
hdbErrors.HTTP_STATUS_CODES.BAD_REQUEST,
|
|
299
|
+
undefined,
|
|
300
|
+
undefined,
|
|
301
|
+
true
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
// Dispatch state, never client input: the export worker re-reads `parsed_sql_object` off this
|
|
305
|
+
// same object (evaluateSQL), and it carries `permissions_checked`, so a body-supplied one would
|
|
306
|
+
// execute an AST no check ever saw. Deleting it rather than replacing it with the authorized
|
|
307
|
+
// parse is deliberate — the worker then re-parses and re-runs the check itself.
|
|
308
|
+
delete nestedSearch.parsed_sql_object;
|
|
309
|
+
}
|
|
310
|
+
// The AST check below covers only the statement's tables, never the caller's right to invoke the
|
|
311
|
+
// job operation, so it is additive to verifyPerms rather than an alternative to it.
|
|
312
|
+
const hasNestedSqlSearch = nestedSearch?.operation === terms.OPERATIONS_ENUM.SQL;
|
|
313
|
+
|
|
260
314
|
try {
|
|
261
|
-
if (
|
|
315
|
+
if (isSqlOperation || hasNestedSqlSearch) {
|
|
262
316
|
const sql = require('../../sqlTranslator/index');
|
|
263
|
-
const sqlStatement =
|
|
317
|
+
const sqlStatement = isSqlOperation ? json.sql : nestedSearch.sql;
|
|
264
318
|
// Before this dispatch's own parse is assigned, so a body-supplied object cannot survive it.
|
|
265
319
|
stripSuppliedParsedSqlObject(json);
|
|
266
320
|
const parsedSqlObject = sql.convertSQLToAST(sqlStatement);
|
|
267
|
-
|
|
321
|
+
if (hasNestedSqlSearch && parsedSqlObject.variant !== terms.VALID_SQL_OPS_ENUM.SELECT) {
|
|
322
|
+
throw handleHDBError(
|
|
323
|
+
new Error(),
|
|
324
|
+
`'search_operation.sql' must be a SELECT statement`,
|
|
325
|
+
hdbErrors.HTTP_STATUS_CODES.BAD_REQUEST,
|
|
326
|
+
undefined,
|
|
327
|
+
undefined,
|
|
328
|
+
true
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
// Only the direct-SQL path consumes `json.parsed_sql_object` (evaluateSQL). A nested export
|
|
332
|
+
// re-parses off `search_operation` — whose `parsed_sql_object` was deleted above — so setting
|
|
333
|
+
// it here for a job would be inert.
|
|
334
|
+
if (isSqlOperation) {
|
|
335
|
+
json.parsed_sql_object = parsedSqlObject;
|
|
336
|
+
}
|
|
268
337
|
if (!bypassAuth) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
338
|
+
if (hasNestedSqlSearch) {
|
|
339
|
+
// A nested-SQL export's outer op (export_local/export_to_s3) is `requires_su`, which the
|
|
340
|
+
// AST table check below does not enforce. Authorize invoking it through verifyPerms
|
|
341
|
+
// exactly as the non-SQL job path does — otherwise SQL is a way around the requires_su
|
|
342
|
+
// gate that path enforces. The nested table reads are then covered additively by the AST
|
|
343
|
+
// check. verifyPerms also runs the allowlist (gate 1) and the export token scope.
|
|
344
|
+
const functionToCheck = job_operation_function === undefined ? operation_function : job_operation_function;
|
|
345
|
+
const outerDenial = opAuth.verifyPerms(json, functionToCheck, { apiOperation: json.operation });
|
|
346
|
+
if (outerDenial) {
|
|
347
|
+
operationLog.error(`${HTTP_STATUS_CODES.FORBIDDEN} from operation ${json.operation}`);
|
|
348
|
+
operationLog.warn(`User '${json.hdb_user?.username}' is not permitted to ${json.operation}`);
|
|
349
|
+
throw handleHDBError(
|
|
350
|
+
new Error(),
|
|
351
|
+
outerDenial,
|
|
352
|
+
hdbErrors.HTTP_STATUS_CODES.FORBIDDEN,
|
|
353
|
+
undefined,
|
|
354
|
+
false,
|
|
355
|
+
true
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
// Direct SQL: the invoked operation IS `sql`, so the role `operations` allowlist is the
|
|
360
|
+
// operation-invocation check — otherwise an allowlisted role reaches unlisted operations
|
|
361
|
+
// via `sql`. json.operation is already the API name.
|
|
362
|
+
const allowlistDenial = opAuth.verifyOperationsAllowlist(json, json.operation);
|
|
363
|
+
if (allowlistDenial) {
|
|
364
|
+
operationLog.error(`${HTTP_STATUS_CODES.FORBIDDEN} from operation ${json.operation}`);
|
|
365
|
+
operationLog.warn(`User '${json.hdb_user?.username}' is not permitted to ${json.operation}`);
|
|
366
|
+
throw handleHDBError(
|
|
367
|
+
new Error(),
|
|
368
|
+
allowlistDenial,
|
|
369
|
+
hdbErrors.HTTP_STATUS_CODES.FORBIDDEN,
|
|
370
|
+
undefined,
|
|
371
|
+
undefined,
|
|
372
|
+
true
|
|
373
|
+
);
|
|
374
|
+
}
|
|
284
375
|
}
|
|
285
376
|
// `json.operation` explicitly — the operation this dispatch already resolved, not a
|
|
286
377
|
// field read back off the request body.
|
|
@@ -309,10 +400,9 @@ export function chooseOperation(json: OperationRequestBody, bypassAuth = false)
|
|
|
309
400
|
json.operation !== terms.OPERATIONS_ENUM.EXCHANGE_OIDC_TOKEN
|
|
310
401
|
) {
|
|
311
402
|
const functionToCheck = job_operation_function === undefined ? operation_function : job_operation_function;
|
|
312
|
-
const operation_json =
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
403
|
+
const operation_json = nestedSearch ?? json;
|
|
404
|
+
// Authentication is the only source of the principal; a nested one is overwritten, not honored.
|
|
405
|
+
operation_json.hdb_user = json.hdb_user;
|
|
316
406
|
|
|
317
407
|
// Pass the top-level operation for the token-scope check: for an export job, operation_json
|
|
318
408
|
// is the nested search_operation, so json.operation (export_local/export_to_s3) is the op the
|
|
@@ -335,6 +425,35 @@ export function chooseOperation(json: OperationRequestBody, bypassAuth = false)
|
|
|
335
425
|
true
|
|
336
426
|
);
|
|
337
427
|
}
|
|
428
|
+
|
|
429
|
+
// The check above authorizes invoking the operation, but for a non-SQL export it returns
|
|
430
|
+
// before any table check — a requires_su export is granted the moment its `operations`
|
|
431
|
+
// allowlist lists it (verifyPerms gate 2) — and the job worker then runs `search_operation`
|
|
432
|
+
// through searchByValue/Hash/Conditions, none of which check permissions. So the nested read
|
|
433
|
+
// is only enforceable here: authorize it additively against its real search handler and the
|
|
434
|
+
// authenticated principal (assigned above via operation_json), so a role that may invoke the
|
|
435
|
+
// export but lacks read on the table is denied. A nested SQL search is covered by the AST
|
|
436
|
+
// branch above instead.
|
|
437
|
+
if (nestedSearch) {
|
|
438
|
+
const nestedFunction = getOperationFunction(nestedSearch).operation_function;
|
|
439
|
+
const nestedPermsResult = opAuth.verifyPerms(nestedSearch, nestedFunction, {
|
|
440
|
+
apiOperation: json.operation,
|
|
441
|
+
});
|
|
442
|
+
if (nestedPermsResult) {
|
|
443
|
+
operationLog.error(`${HTTP_STATUS_CODES.FORBIDDEN} from operation ${json.operation}`);
|
|
444
|
+
operationLog.warn(
|
|
445
|
+
`User '${operation_json.hdb_user?.username}' is not permitted to ${nestedSearch.operation} within ${json.operation}`
|
|
446
|
+
);
|
|
447
|
+
throw handleHDBError(
|
|
448
|
+
new Error(),
|
|
449
|
+
nestedPermsResult,
|
|
450
|
+
hdbErrors.HTTP_STATUS_CODES.FORBIDDEN,
|
|
451
|
+
undefined,
|
|
452
|
+
false,
|
|
453
|
+
true
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
338
457
|
}
|
|
339
458
|
} catch (err) {
|
|
340
459
|
throw handleHDBError(err, `There was an error when trying to choose an operation path`, 500);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, realpath } from 'node:fs/promises';
|
|
2
2
|
import { statfs } from 'node:fs/promises';
|
|
3
3
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { ownsStoreMaintenance } from '../server/threads/manageThreads.js';
|
|
5
5
|
import { logger } from '../utility/logging/logger.ts';
|
|
6
6
|
import { CONFIG_PARAMS } from '../utility/hdbTerms.ts';
|
|
7
7
|
import * as envMgr from '../utility/environment/environmentManager.ts';
|
|
@@ -106,7 +106,7 @@ export function onStorageReclamation(
|
|
|
106
106
|
handler: (priority: number) => Promise<void> | void,
|
|
107
107
|
skipThreadCheck?: boolean
|
|
108
108
|
) {
|
|
109
|
-
if (skipThreadCheck ||
|
|
109
|
+
if (skipThreadCheck || ownsStoreMaintenance(path)) {
|
|
110
110
|
// only run on one thread (last one)
|
|
111
111
|
if (!path) {
|
|
112
112
|
throw new Error('Storage reclamation path cannot be empty');
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Isolated applications: an application whose root-config entry carries `isolated: true` runs in a
|
|
3
|
+
* worker thread of its own that loads no other application (harper#642, tier 2).
|
|
4
|
+
*
|
|
5
|
+
* The invariant every helper here serves: an isolated application is loaded by exactly one thread,
|
|
6
|
+
* and that thread loads nothing else. The dedicated worker is an ordinary `http` worker by type --
|
|
7
|
+
* so restarts, overlap rules and shutdown treat it as one -- distinguished only by
|
|
8
|
+
* `workerData.isolatedApplication`. It binds none of the public ports (the kernel would hand it
|
|
9
|
+
* connections for every other application) and is reachable only through its own UDS mirror, whose
|
|
10
|
+
* metadata names the application and its hosts for the fronting proxy to route by.
|
|
11
|
+
*/
|
|
12
|
+
import { isMainThread, workerData } from 'node:worker_threads';
|
|
13
|
+
import { getWorkerIndex } from './manageThreads.js';
|
|
14
|
+
import { getConfigObj, getConfigPath } from '../../config/configUtils.ts';
|
|
15
|
+
import * as env from '../../utility/environment/environmentManager.ts';
|
|
16
|
+
import { CONFIG_PARAMS } from '../../utility/hdbTerms.ts';
|
|
17
|
+
import { isDomainSocketPathTooLong } from '../../utility/domainSocket.ts';
|
|
18
|
+
import { existsSync } from 'node:fs';
|
|
19
|
+
import { basename, join } from 'node:path';
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_MAX_ISOLATED_APPLICATIONS = 8;
|
|
22
|
+
|
|
23
|
+
/** The application this thread is dedicated to, if it is a dedicated worker. */
|
|
24
|
+
export function thisThreadsIsolatedApplication(): string | undefined {
|
|
25
|
+
return (workerData as any)?.isolatedApplication;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function thisThreadOwnsApplication(
|
|
29
|
+
appName: string | undefined,
|
|
30
|
+
owner: string | undefined = thisThreadsIsolatedApplication()
|
|
31
|
+
): boolean {
|
|
32
|
+
return owner !== undefined && appName === owner;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isIsolatedApplication(
|
|
36
|
+
appName: string,
|
|
37
|
+
config: Record<string, any> | undefined = getConfigObj()
|
|
38
|
+
): boolean {
|
|
39
|
+
const entry = config?.[appName];
|
|
40
|
+
return typeof entry === 'object' && entry !== null && entry.isolated === true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Every application the root config marks isolated, in config order. */
|
|
44
|
+
export function isolatedApplicationNames(config: Record<string, any> | undefined = getConfigObj()): string[] {
|
|
45
|
+
if (!config) return [];
|
|
46
|
+
return Object.keys(config).filter((name) => isIsolatedApplication(name, config));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function presentIsolatedApplicationNames(
|
|
50
|
+
config: Record<string, any> | undefined = getConfigObj(),
|
|
51
|
+
componentsRoot: string = getConfigPath(CONFIG_PARAMS.COMPONENTSROOT) as string,
|
|
52
|
+
installedRoot: string = join(env.get(CONFIG_PARAMS.ROOTPATH), 'components'),
|
|
53
|
+
runApplicationPath: string | undefined = process.env.RUN_HDB_APP
|
|
54
|
+
): string[] {
|
|
55
|
+
return isolatedApplicationNames(config).filter(
|
|
56
|
+
(application) =>
|
|
57
|
+
existsSync(join(componentsRoot, application)) ||
|
|
58
|
+
existsSync(join(installedRoot, application)) ||
|
|
59
|
+
(runApplicationPath && basename(runApplicationPath) === application)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function maxIsolatedApplications(): number {
|
|
64
|
+
const configured = Number(env.get(CONFIG_PARAMS.THREADS_MAXISOLATED));
|
|
65
|
+
return Number.isInteger(configured) && configured >= 0 ? configured : DEFAULT_MAX_ISOLATED_APPLICATIONS;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Why `appName` cannot claim a place in the dedicated-worker budget, if the budget is full. */
|
|
69
|
+
export function isolatedApplicationCapacityRefusal(
|
|
70
|
+
appName: string,
|
|
71
|
+
runningApplications: Iterable<string>,
|
|
72
|
+
max = maxIsolatedApplications()
|
|
73
|
+
): string | undefined {
|
|
74
|
+
const running = new Set(runningApplications);
|
|
75
|
+
if (running.has(appName) || running.size < max) return undefined;
|
|
76
|
+
return `the instance already runs ${max} isolated application(s) (threads.maxIsolated)`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Whether the calling thread is the one that loads the application `appName` (an application: a
|
|
81
|
+
* directory under componentsRoot, or a root-config entry with `package`). A dedicated worker loads
|
|
82
|
+
* only its own application; every other thread -- pool workers and the main thread alike -- loads only
|
|
83
|
+
* the applications that are not isolated. Decided before any of the application's modules are
|
|
84
|
+
* imported, so a skipped application has no side effects on the thread that skipped it.
|
|
85
|
+
*/
|
|
86
|
+
export function shouldLoadApplicationHere(
|
|
87
|
+
appName: string,
|
|
88
|
+
owner: string | undefined = thisThreadsIsolatedApplication(),
|
|
89
|
+
config: Record<string, any> | undefined = getConfigObj()
|
|
90
|
+
): boolean {
|
|
91
|
+
if (owner !== undefined) return appName === owner;
|
|
92
|
+
return !isIsolatedApplication(appName, config);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Whether this thread starts the uWS listener for one `uwsServeConfigs` entry (see listenOnPorts()).
|
|
97
|
+
* A dedicated worker starts only its own per-application UDS mirrors -- the entries that carry a
|
|
98
|
+
* `socketPath` -- and never a global port listener, which the kernel would hand connections for every
|
|
99
|
+
* other application.
|
|
100
|
+
*/
|
|
101
|
+
export function shouldStartUwsListenerHere(
|
|
102
|
+
serveConfig: { socketPath?: string },
|
|
103
|
+
owner: string | undefined = thisThreadsIsolatedApplication()
|
|
104
|
+
): boolean {
|
|
105
|
+
return owner === undefined || Boolean(serveConfig.socketPath);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Whether a dedicated worker can be reached at all: it binds only UDS mirrors of the secure port, so
|
|
110
|
+
* without `tls.unixDomainSockets` and a secure port an isolated application would load and answer
|
|
111
|
+
* nothing, silently. Refused at admission instead. Returns the reason, or undefined when reachable.
|
|
112
|
+
*/
|
|
113
|
+
export function isolatedApplicationsUnreachableReason(platform = process.platform): string | undefined {
|
|
114
|
+
if (platform === 'win32') return 'Windows does not support per-application UDS mirrors';
|
|
115
|
+
// the main thread standing in as the only worker (threads.count: 0) has no thread to give
|
|
116
|
+
if (isMainThread && getWorkerIndex() === 0) return 'threads.count is 0, so there is no worker thread to dedicate';
|
|
117
|
+
if (!env.get(CONFIG_PARAMS.HTTP_SECUREPORT)) return 'no http.securePort is configured';
|
|
118
|
+
if (!env.get(CONFIG_PARAMS.TLS_UNIXDOMAINSOCKETS)) return 'tls.unixDomainSockets is not enabled';
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Why `appName` cannot get a dedicated worker on this instance, or undefined when it can. */
|
|
123
|
+
export function isolatedApplicationRefusal(appName: string): string | undefined {
|
|
124
|
+
const unreachable = isolatedApplicationsUnreachableReason();
|
|
125
|
+
if (unreachable) return `its worker would be unreachable: ${unreachable}`;
|
|
126
|
+
const socketPath = join(
|
|
127
|
+
env.getHdbBasePath(),
|
|
128
|
+
'sockets',
|
|
129
|
+
`${applicationSocketName(appName, env.get(CONFIG_PARAMS.HTTP_SECUREPORT))}.sock`
|
|
130
|
+
);
|
|
131
|
+
if (isDomainSocketPathTooLong(socketPath)) return `its UDS mirror path ${socketPath} exceeds the platform limit`;
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The filesystem name of an isolated worker's UDS mirror for `port`: every UTF-8 byte outside
|
|
137
|
+
* `[A-Za-z0-9._]` becomes a fixed-width `%XX`, which is injective (two application names never share
|
|
138
|
+
* a socket), and the `app-` prefix keeps it apart from the pool's `<workerIndex>-<port>` names.
|
|
139
|
+
*/
|
|
140
|
+
export function applicationSocketName(appName: string, port: number | string): string {
|
|
141
|
+
let encoded = '';
|
|
142
|
+
for (const byte of Buffer.from(appName, 'utf8')) {
|
|
143
|
+
const c = String.fromCharCode(byte);
|
|
144
|
+
encoded += byte < 0x80 && /[A-Za-z0-9._]/.test(c) ? c : `%${byte.toString(16).toUpperCase().padStart(2, '0')}`;
|
|
145
|
+
}
|
|
146
|
+
return `app-${encoded}-${port}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** What a dedicated worker publishes in its mirror metadata for the proxy to route by. */
|
|
150
|
+
export function isolatedApplicationRoute(
|
|
151
|
+
appName: string | undefined = thisThreadsIsolatedApplication(),
|
|
152
|
+
config: Record<string, any> | undefined = getConfigObj()
|
|
153
|
+
): { application: string; hosts: string[] } | undefined {
|
|
154
|
+
if (!appName) return undefined;
|
|
155
|
+
const host = config?.[appName]?.host; // a string, as the mount parser requires
|
|
156
|
+
return { application: appName, hosts: typeof host === 'string' && host ? [host] : [] };
|
|
157
|
+
}
|