@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
|
@@ -13,6 +13,8 @@ import { PACKAGE_ROOT } from '../../utility/packageUtils.js';
|
|
|
13
13
|
import { _assignPackageExport } from '../../globals.js';
|
|
14
14
|
import { Console } from 'console';
|
|
15
15
|
import { inspect, types } from 'node:util';
|
|
16
|
+
import { createRotationGuard, INVALID_MAX_SIZE_MSG, parseMaxSize, resolveRotatedLogDir } from './logRotation.ts';
|
|
17
|
+
import { registerLogSink } from './logGenerationCoordinator.ts';
|
|
16
18
|
|
|
17
19
|
const { isNativeError } = types;
|
|
18
20
|
// store the native write function so we can call it after we write to the log file (and store it on process.stdout
|
|
@@ -47,6 +49,18 @@ const DEFAULT_CONFIG_FILE = join(PACKAGE_ROOT, 'static', hdbTerms.HDB_DEFAULT_CO
|
|
|
47
49
|
|
|
48
50
|
const CLOSE_LOG_FD_TIMEOUT = 10000;
|
|
49
51
|
|
|
52
|
+
// Above the module-scope initLogSettings() call below, which reaches getFileLogger: a const
|
|
53
|
+
// declared after it is still in its temporal dead zone when the file sink first reads one.
|
|
54
|
+
const LOG_TIME_USAGE_THRESHOLD = 100;
|
|
55
|
+
// How long to write straight to stdio after the log file refuses an append.
|
|
56
|
+
const APPEND_RETRY_COOLDOWN = 5000;
|
|
57
|
+
// Ceiling on how often a failing rotation may report itself; the failure repeats every check point.
|
|
58
|
+
const ROTATION_REPORT_INTERVAL = 60000;
|
|
59
|
+
const SET_ROTATION_POLICY = Symbol('setRotationPolicy');
|
|
60
|
+
const GET_ROTATION_POLICY = Symbol('getRotationPolicy');
|
|
61
|
+
const FILE_ROTATION_SOURCE = Symbol('fileRotationSource');
|
|
62
|
+
const SERVICE_NAME = workerData?.name?.replace(/ /g, '-') || 'main';
|
|
63
|
+
|
|
50
64
|
let logConsole;
|
|
51
65
|
let log_to_file;
|
|
52
66
|
let logToStdstreams;
|
|
@@ -109,7 +123,20 @@ export function updateLogger(logger: any, logOptions: any, name?: string, mainLo
|
|
|
109
123
|
// rotation entirely (#1877). Excluded when `logger` IS mainLoggerRef: the fallback would just
|
|
110
124
|
// reassign mainLogger.rotation to itself, making it impossible to ever clear main's rotation.
|
|
111
125
|
const inheritedRotation = logOptions.rotation ?? (logger === mainLoggerRef ? undefined : mainLoggerRef?.rotation);
|
|
112
|
-
|
|
126
|
+
const loggerPolicy = logger[GET_ROTATION_POLICY]?.();
|
|
127
|
+
const mainPolicy = mainLoggerRef?.[GET_ROTATION_POLICY]?.();
|
|
128
|
+
const inheritsRotation = logOptions.rotation == null && logger !== mainLoggerRef;
|
|
129
|
+
const rotationSource = inheritsRotation ? mainPolicy?.source : loggerPolicy?.ownSource;
|
|
130
|
+
let path = logOptions.path;
|
|
131
|
+
if (path) {
|
|
132
|
+
if (!logOptions.root) logOptions.root = pathModule.dirname(path);
|
|
133
|
+
} else if (logOptions.root) {
|
|
134
|
+
path = join(logOptions.root, logName);
|
|
135
|
+
} else {
|
|
136
|
+
path = mainLoggerRef.path;
|
|
137
|
+
if (!logOptions.root) logOptions.root = pathModule.dirname(path);
|
|
138
|
+
}
|
|
139
|
+
if (inheritedRotation && inheritedRotation === mainLoggerRef?.rotation && path !== mainLoggerRef?.path) {
|
|
113
140
|
// This logger has no rotation block of its own and is inheriting main's wholesale — but
|
|
114
141
|
// inheriting `rotation.path` too would point this logger's archives at wherever main
|
|
115
142
|
// archives to. If this logger's file lives on a different filesystem/volume than that
|
|
@@ -120,20 +147,17 @@ export function updateLogger(logger: any, logOptions: any, name?: string, mainLo
|
|
|
120
147
|
// goes through `logOptions.rotation` above, not this fallback. Clone rather than mutate:
|
|
121
148
|
// `inheritedRotation` is the literal object shared by mainLogger.rotation and every other
|
|
122
149
|
// component that inherited it.
|
|
150
|
+
//
|
|
151
|
+
// Only when the paths differ. A logger writing to main's own file shares main's rotation
|
|
152
|
+
// state — file loggers are cached per path — so stripping there discards the operator's
|
|
153
|
+
// configured rotation.path for the main log itself, which no EXDEV risk justifies.
|
|
123
154
|
const { path: _inheritedPath, ...rotationWithoutPath } = inheritedRotation;
|
|
124
|
-
|
|
125
|
-
} else {
|
|
126
|
-
logger.rotation = inheritedRotation;
|
|
127
|
-
}
|
|
128
|
-
let path = logOptions.path;
|
|
129
|
-
if (path) {
|
|
130
|
-
if (!logOptions.root) logOptions.root = pathModule.dirname(path);
|
|
131
|
-
} else if (logOptions.root) {
|
|
132
|
-
path = join(logOptions.root, logName);
|
|
155
|
+
setRotationPolicy(rotationWithoutPath);
|
|
133
156
|
} else {
|
|
134
|
-
|
|
135
|
-
if (!logOptions.root) logOptions.root = pathModule.dirname(path);
|
|
157
|
+
setRotationPolicy(inheritedRotation);
|
|
136
158
|
}
|
|
159
|
+
// After the rotation above: assigning `path` goes through the setter that rebuilds this path's
|
|
160
|
+
// file logger, and it reads `logger.rotation` as it stands at that moment.
|
|
137
161
|
if (path) logger.path = path;
|
|
138
162
|
else console.error('No path for logger', logOptions);
|
|
139
163
|
logger.level = LOG_LEVEL_HIERARCHY[logOptions.level] ?? mainLoggerRef?.level ?? LOG_LEVEL_HIERARCHY.info;
|
|
@@ -142,6 +166,11 @@ export function updateLogger(logger: any, logOptions: any, name?: string, mainLo
|
|
|
142
166
|
// if there is a configured tag or if a component is logging to default/main log path, use the component name as the tag
|
|
143
167
|
// to differentiate it
|
|
144
168
|
logger.tag = logOptions.tag ?? ((mainLoggerRef.path === logger.path || externalLogger.path === logger.path) && name);
|
|
169
|
+
|
|
170
|
+
function setRotationPolicy(rotation) {
|
|
171
|
+
if (logger[SET_ROTATION_POLICY]) logger[SET_ROTATION_POLICY](rotation, rotationSource, inheritsRotation);
|
|
172
|
+
else logger.rotation = rotation;
|
|
173
|
+
}
|
|
145
174
|
}
|
|
146
175
|
// creates a logger where the methods are only defined if they are within the log level.
|
|
147
176
|
// Using this conditional logger means that every method call must be optional like log.trace?.('message),
|
|
@@ -174,7 +203,19 @@ async function updateLogSettings() {
|
|
|
174
203
|
// TODO: Any way to differentiate changes that we can and can't handle?
|
|
175
204
|
rootConfig.on('change', updateLogSettings);
|
|
176
205
|
}
|
|
177
|
-
|
|
206
|
+
applyLogSettings(rootConfig.config);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Separate from the watcher wiring above because the watcher it builds reads a process-wide path,
|
|
210
|
+
// which leaves no way to exercise these settings against a given config.
|
|
211
|
+
function applyLogSettings(rootConfigObject: any) {
|
|
212
|
+
// `ready` settles on every terminal read outcome, including ones that carry no config at all —
|
|
213
|
+
// see DESIGN.md, "Every terminal read outcome settles the barrier". Applying that as settings
|
|
214
|
+
// would not be "the defaults": `updateLogger` reads an absent `rotation` as rotation off and an
|
|
215
|
+
// absent `console` as console off, so an unreadable config would silently disable logging.
|
|
216
|
+
// What `initLogSettings()` established at startup stands until a real config arrives, which
|
|
217
|
+
// is why the watcher settles those outcomes with no config rather than an empty one.
|
|
218
|
+
if (!rootConfigObject || typeof rootConfigObject !== 'object') return;
|
|
178
219
|
const logOptions = rootConfigObject.logging ?? {};
|
|
179
220
|
// Resolve relative paths against rootPath from the same config
|
|
180
221
|
const rootPath = rootConfigObject.rootPath;
|
|
@@ -197,7 +238,9 @@ async function updateLogSettings() {
|
|
|
197
238
|
for (const name in rootConfigObject) {
|
|
198
239
|
// we now scan each component to see if it has logging individual configured
|
|
199
240
|
const component = rootConfigObject[name];
|
|
200
|
-
|
|
241
|
+
// `myComponent:` with nothing under it parses to null, and an async listener's TypeError
|
|
242
|
+
// escapes the watcher's emit guard as an unhandled rejection.
|
|
243
|
+
if (component?.logging) {
|
|
201
244
|
updateLogger(mainLogger.forComponent(name), component.logging, name);
|
|
202
245
|
} else if (mainLogger.hasComponent(name)) {
|
|
203
246
|
const componentLogger = mainLogger.forComponent(name);
|
|
@@ -399,6 +442,8 @@ module.exports = {
|
|
|
399
442
|
// we can start using the RootConfigWatcher
|
|
400
443
|
start: updateLogSettings,
|
|
401
444
|
startOnMainThread: updateLogSettings,
|
|
445
|
+
// Test-only: applies a config without the process-wide watcher `updateLogSettings` builds.
|
|
446
|
+
_applyLogSettingsForTests: applyLogSettings,
|
|
402
447
|
errorToString,
|
|
403
448
|
errorForLog,
|
|
404
449
|
inspectForLog,
|
|
@@ -463,6 +508,14 @@ export function logsAtLevel(level: any) {
|
|
|
463
508
|
export function initLogSettings(forceInit = false) {
|
|
464
509
|
try {
|
|
465
510
|
if (hdbProperties === undefined || forceInit) {
|
|
511
|
+
if (forceInit && mainLogger?.[SET_ROTATION_POLICY]) {
|
|
512
|
+
const currentPath = mainLogger.path;
|
|
513
|
+
if (currentPath) {
|
|
514
|
+
const { ownSource } = mainLogger[GET_ROTATION_POLICY]();
|
|
515
|
+
mainLogger[SET_ROTATION_POLICY](undefined, ownSource, false);
|
|
516
|
+
mainLogger.path = currentPath;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
466
519
|
closeLogFile();
|
|
467
520
|
const bootPropsFilePath = getPropsFilePath();
|
|
468
521
|
let properties = assignCMDENVVariables(['ROOTPATH']);
|
|
@@ -643,7 +696,6 @@ export function suppressLogging(callback) {
|
|
|
643
696
|
}
|
|
644
697
|
}
|
|
645
698
|
|
|
646
|
-
const SERVICE_NAME = workerData?.name?.replace(/ /g, '-') || 'main';
|
|
647
699
|
// these are used to store information about the current service and tag so we can prepend them to the log during
|
|
648
700
|
// the writes, without having to pass the information through the Console instance
|
|
649
701
|
let currentLevel = 'info'; // default is info
|
|
@@ -658,7 +710,14 @@ export function createLogger(options: any = {} as any) {
|
|
|
658
710
|
isExternalInstance,
|
|
659
711
|
writeToLog,
|
|
660
712
|
component,
|
|
713
|
+
rotationSource,
|
|
714
|
+
rotationInherited = false,
|
|
661
715
|
}: any = options;
|
|
716
|
+
const ownRotationSource = Symbol('rotationPolicySource');
|
|
717
|
+
let currentRotationSource = rotationSource ?? ownRotationSource;
|
|
718
|
+
let previousRotationSource;
|
|
719
|
+
let currentRotationInherited = rotationInherited;
|
|
720
|
+
let rotationChangeIsAuthoritative = rotation !== undefined;
|
|
662
721
|
if (!logLevel) logLevel = 'info';
|
|
663
722
|
let level = typeof logLevel === 'number' ? logLevel : LOG_LEVEL_HIERARCHY[logLevel];
|
|
664
723
|
let logger;
|
|
@@ -702,7 +761,8 @@ export function createLogger(options: any = {} as any) {
|
|
|
702
761
|
}
|
|
703
762
|
} else if (logToStdstreams) process.stderr.write(log);
|
|
704
763
|
}
|
|
705
|
-
let logToFile = logFilePath && getFileLogger(logFilePath, rotation, isExternalInstance);
|
|
764
|
+
let logToFile = logFilePath && getFileLogger(logFilePath, rotation, isExternalInstance, rotationPolicy());
|
|
765
|
+
rotationPolicyApplied();
|
|
706
766
|
function logPrepend(write) {
|
|
707
767
|
return {
|
|
708
768
|
write(log) {
|
|
@@ -725,6 +785,14 @@ export function createLogger(options: any = {} as any) {
|
|
|
725
785
|
},
|
|
726
786
|
level
|
|
727
787
|
);
|
|
788
|
+
logger[SET_ROTATION_POLICY] = function (newRotation, source = ownRotationSource, inherited = false) {
|
|
789
|
+
previousRotationSource = currentRotationSource;
|
|
790
|
+
currentRotationSource = source ?? ownRotationSource;
|
|
791
|
+
currentRotationInherited = inherited;
|
|
792
|
+
rotationChangeIsAuthoritative = true;
|
|
793
|
+
logger.rotation = newRotation;
|
|
794
|
+
};
|
|
795
|
+
logger[GET_ROTATION_POLICY] = () => ({ ownSource: ownRotationSource, source: currentRotationSource });
|
|
728
796
|
updateConditional(logger);
|
|
729
797
|
logger.path = logFilePath;
|
|
730
798
|
Object.defineProperty(logger, 'path', {
|
|
@@ -733,7 +801,9 @@ export function createLogger(options: any = {} as any) {
|
|
|
733
801
|
},
|
|
734
802
|
set(path) {
|
|
735
803
|
logFilePath = path;
|
|
736
|
-
|
|
804
|
+
if (!logFilePath) return;
|
|
805
|
+
logToFile = getFileLogger(logFilePath, logger.rotation, isExternalInstance, rotationPolicy());
|
|
806
|
+
rotationPolicyApplied();
|
|
737
807
|
if (isExternalInstance) writeToLogFile = logToFile;
|
|
738
808
|
},
|
|
739
809
|
enumerable: true,
|
|
@@ -746,12 +816,15 @@ export function createLogger(options: any = {} as any) {
|
|
|
746
816
|
let componentLogger = components.get(name);
|
|
747
817
|
if (!componentLogger) {
|
|
748
818
|
const protoLogger = isExternal ? externalLogger : logger;
|
|
819
|
+
const protoRotationPolicy = protoLogger[GET_ROTATION_POLICY]?.();
|
|
749
820
|
componentLogger = createLogger({
|
|
750
821
|
path: protoLogger.path,
|
|
751
822
|
level: protoLogger.level,
|
|
752
823
|
stdStreams: protoLogger.logToStdstreams,
|
|
753
824
|
isExternalInstance: isExternal || name === 'external',
|
|
754
825
|
rotation: protoLogger.rotation,
|
|
826
|
+
rotationSource: protoRotationPolicy?.source,
|
|
827
|
+
rotationInherited: true,
|
|
755
828
|
writeToLog,
|
|
756
829
|
component: true,
|
|
757
830
|
});
|
|
@@ -767,34 +840,83 @@ export function createLogger(options: any = {} as any) {
|
|
|
767
840
|
logger.components = components;
|
|
768
841
|
}
|
|
769
842
|
return logger;
|
|
843
|
+
|
|
844
|
+
function rotationPolicy() {
|
|
845
|
+
return {
|
|
846
|
+
source: currentRotationSource,
|
|
847
|
+
previousSource: previousRotationSource,
|
|
848
|
+
inherited: currentRotationInherited,
|
|
849
|
+
authoritative: rotationChangeIsAuthoritative,
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function rotationPolicyApplied() {
|
|
854
|
+
previousRotationSource = currentRotationSource;
|
|
855
|
+
rotationChangeIsAuthoritative = false;
|
|
856
|
+
}
|
|
770
857
|
}
|
|
771
|
-
const LOG_TIME_USAGE_THRESHOLD = 100;
|
|
772
|
-
// How long to write straight to stdio after the log file refuses an append.
|
|
773
|
-
const APPEND_RETRY_COOLDOWN = 5000;
|
|
774
858
|
/**
|
|
775
859
|
* Get the file logger for the given path. If it doesn't exist, create it.
|
|
776
860
|
* @param path
|
|
777
861
|
* @param isExternalInstance
|
|
778
862
|
* @return {any}
|
|
779
863
|
*/
|
|
780
|
-
function getFileLogger(path, rotation, isExternalInstance) {
|
|
864
|
+
function getFileLogger(path, rotation, isExternalInstance, rotationPolicy) {
|
|
781
865
|
let logger = fileLoggers.get(path);
|
|
782
866
|
let logFD, loggedFDError, loggedAppendError, logTimer, retryAppendAfter;
|
|
783
867
|
let logBuffer;
|
|
868
|
+
let rotationProblemNotice;
|
|
784
869
|
let logTimeUsage = 0;
|
|
870
|
+
let rotationGuard,
|
|
871
|
+
logFDIdentity,
|
|
872
|
+
nextRotationReport = 0;
|
|
785
873
|
if (!logger) {
|
|
786
874
|
logger = logToFile;
|
|
787
875
|
logger.closeLogFile = closeLogFile;
|
|
876
|
+
// The guard has to be installed through the live closure: later calls for this path get the
|
|
877
|
+
// cached logger back, and their own logFD/logBuffer are dead.
|
|
878
|
+
logger.installRotationGuard = installRotationGuard;
|
|
788
879
|
logger.path = path;
|
|
789
880
|
fileLoggers.set(path, logger);
|
|
881
|
+
// Registered by the sink, not by the size guard: a thread that writes this file but has no
|
|
882
|
+
// guard (no maxSize, or rotation driven only by interval) still holds a descriptor, and an
|
|
883
|
+
// answer from it has to mean "released" rather than only "handler ran".
|
|
884
|
+
registerLogSink(path, { identity: () => logFDIdentity, close: closeLogFile });
|
|
885
|
+
}
|
|
886
|
+
const currentSource = logger[FILE_ROTATION_SOURCE];
|
|
887
|
+
const sameSource = currentSource === rotationPolicy.source;
|
|
888
|
+
const replacesPreviousSource = currentSource !== undefined && currentSource === rotationPolicy.previousSource;
|
|
889
|
+
const canInstall =
|
|
890
|
+
rotation !== undefined &&
|
|
891
|
+
(!rotationPolicy.inherited || currentSource === undefined || sameSource || replacesPreviousSource);
|
|
892
|
+
const canClear = rotation === undefined && rotationPolicy.authoritative && (sameSource || replacesPreviousSource);
|
|
893
|
+
let reconfigured = false;
|
|
894
|
+
if (canInstall) {
|
|
895
|
+
logger[FILE_ROTATION_SOURCE] = rotationPolicy.source;
|
|
896
|
+
reconfigured = JSON.stringify(rotation) !== JSON.stringify(logger.rotation);
|
|
790
897
|
}
|
|
791
|
-
if (
|
|
898
|
+
if (reconfigured) {
|
|
792
899
|
logger.rotation = rotation;
|
|
900
|
+
// Every writing thread, and synchronously: request logging is produced by the HTTP workers, and
|
|
901
|
+
// at the rates that make maxSize matter a deferred guard misses megabytes before it exists.
|
|
902
|
+
logger.installRotationGuard(rotation);
|
|
903
|
+
} else if (canClear) {
|
|
904
|
+
logger.rotation = undefined;
|
|
905
|
+
logger[FILE_ROTATION_SOURCE] = undefined;
|
|
906
|
+
logger.installRotationGuard(undefined);
|
|
907
|
+
reconfigured = true;
|
|
908
|
+
}
|
|
909
|
+
if (isMainThread && reconfigured) {
|
|
793
910
|
setTimeout(() => {
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
911
|
+
// Everything inside the try: a throw from a timer callback is unhandled, and neither
|
|
912
|
+
// require('./logRotator') (which reaches environmentManager's synchronous init) nor a
|
|
913
|
+
// rotator teardown may take the process down over log rotation (#847).
|
|
797
914
|
try {
|
|
915
|
+
const previousRotator = logger.rotator;
|
|
916
|
+
logger.rotator = undefined;
|
|
917
|
+
previousRotator?.end();
|
|
918
|
+
if (!rotation) return;
|
|
919
|
+
const { logRotator } = require('./logRotator');
|
|
798
920
|
logger.rotator = logRotator({
|
|
799
921
|
logger,
|
|
800
922
|
...rotation,
|
|
@@ -805,6 +927,46 @@ function getFileLogger(path, rotation, isExternalInstance) {
|
|
|
805
927
|
}, 100);
|
|
806
928
|
}
|
|
807
929
|
return logger;
|
|
930
|
+
function installRotationGuard(newRotation) {
|
|
931
|
+
rotationGuard = undefined;
|
|
932
|
+
if (!newRotation || newRotation.enabled === false) return;
|
|
933
|
+
const maxBytes = parseMaxSize(newRotation.maxSize);
|
|
934
|
+
if (!maxBytes) {
|
|
935
|
+
if (newRotation.maxSize != null) reportRotationProblem(`logging.rotation.maxSize: ${INVALID_MAX_SIZE_MSG}`);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
try {
|
|
939
|
+
rotationGuard = createRotationGuard({
|
|
940
|
+
logPath: path,
|
|
941
|
+
maxBytes,
|
|
942
|
+
rotatedLogDir: resolveRotatedLogDir(path, newRotation.path),
|
|
943
|
+
// From the rotation block rather than environmentManager (which logRotator still reads):
|
|
944
|
+
// environmentManager imports this module, so it cannot be reached from the write path.
|
|
945
|
+
compress: newRotation.compress,
|
|
946
|
+
getLogIdentity: () => logFDIdentity,
|
|
947
|
+
closeLogFile,
|
|
948
|
+
report: reportRotationProblem,
|
|
949
|
+
onRotated(archivePath) {
|
|
950
|
+
// Same shape as every other line in the file: written straight to the sink, this
|
|
951
|
+
// bypasses the prefix createLogger's logPrepend adds, and readLog parses by level.
|
|
952
|
+
logToFile(`[${SERVICE_NAME}/${threadId}] [notify]: hdb.log rotated, old log moved to ${archivePath}`);
|
|
953
|
+
},
|
|
954
|
+
});
|
|
955
|
+
} catch (error) {
|
|
956
|
+
reportRotationProblem(`log rotation is disabled for ${path}: ${error}`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
function reportRotationProblem(text) {
|
|
960
|
+
// Straight to stdio, never back through this sink: the sink is what rotation is failing on.
|
|
961
|
+
// Rate-limited rather than once-only, so a later, different failure is still reported.
|
|
962
|
+
if (nextRotationReport > performance.now()) return;
|
|
963
|
+
nextRotationReport = performance.now() + ROTATION_REPORT_INTERVAL;
|
|
964
|
+
const message = `Harper log rotation problem — ${text}`;
|
|
965
|
+
// Scripted services discard stdio. Preserve the warning in the active file on the next
|
|
966
|
+
// successful append as well, without recursing through the rotation guard.
|
|
967
|
+
rotationProblemNotice = `${new Date().toISOString()} [${SERVICE_NAME}/${threadId}] [error]: ${message}\n`;
|
|
968
|
+
writeToStdioDirectly(process.stderr, `${message}\n`);
|
|
969
|
+
}
|
|
808
970
|
function logToFile(log) {
|
|
809
971
|
let entry = `${new Date().toISOString()} ${log}${log.endsWith('\n') ? '' : '\n'}`;
|
|
810
972
|
if (logBuffer) {
|
|
@@ -831,18 +993,28 @@ function getFileLogger(path, rotation, isExternalInstance) {
|
|
|
831
993
|
}
|
|
832
994
|
// this is called on a timer, and will write the log buffer to the file
|
|
833
995
|
function logQueuedData(entry?: any) {
|
|
834
|
-
openLogFile(undefined);
|
|
835
996
|
const payload = logBuffer ? logBuffer.join('') : entry;
|
|
997
|
+
// Released before anything can re-enter: the rotation notice is written from inside the append
|
|
998
|
+
// below, and a re-entrant flush still holding this batch would write it twice.
|
|
999
|
+
logBuffer = null;
|
|
1000
|
+
if (payload === undefined) return;
|
|
836
1001
|
// A file that just refused a write will refuse the next one too, and every attempt costs a
|
|
837
1002
|
// failed syscall plus a thrown error on whatever path is logging - which, on a full volume,
|
|
838
1003
|
// is the request path at full rate. Go straight to stdio until the cooldown expires.
|
|
839
|
-
|
|
1004
|
+
// Opened after the guard: a guard recovering by rotation closes this descriptor first.
|
|
1005
|
+
const mayAppend = !(retryAppendAfter > performance.now()) && (rotationGuard?.beforeAppend() ?? true);
|
|
1006
|
+
openLogFile(undefined);
|
|
1007
|
+
if (logFD && mayAppend) {
|
|
840
1008
|
let startTime = performance.now();
|
|
841
1009
|
try {
|
|
842
|
-
|
|
1010
|
+
const appendPayload = rotationProblemNotice ? rotationProblemNotice + payload : payload;
|
|
1011
|
+
const appendBuffer = Buffer.from(appendPayload);
|
|
1012
|
+
fs.appendFileSync(logFD, appendBuffer);
|
|
1013
|
+
rotationProblemNotice = undefined;
|
|
843
1014
|
// Both cleared, so a volume that fills again months later reports itself again
|
|
844
1015
|
retryAppendAfter = undefined;
|
|
845
1016
|
loggedAppendError = false;
|
|
1017
|
+
rotationGuard?.recordWrite(appendBuffer.length);
|
|
846
1018
|
} catch (error) {
|
|
847
1019
|
retryAppendAfter = performance.now() + APPEND_RETRY_COOLDOWN;
|
|
848
1020
|
// A log write must never take the process down: on an exhausted volume this throws from
|
|
@@ -855,7 +1027,6 @@ function getFileLogger(path, rotation, isExternalInstance) {
|
|
|
855
1027
|
writeToStdioDirectly(process.stderr, `Harper cannot write to its log file: ${error}\n`);
|
|
856
1028
|
}
|
|
857
1029
|
writeToStdioDirectly(process.stdout, payload);
|
|
858
|
-
logBuffer = null;
|
|
859
1030
|
return;
|
|
860
1031
|
}
|
|
861
1032
|
let endTime = performance.now();
|
|
@@ -863,7 +1034,6 @@ function getFileLogger(path, rotation, isExternalInstance) {
|
|
|
863
1034
|
// will start buffering
|
|
864
1035
|
logTimeUsage = Math.max(endTime, logTimeUsage) + (endTime - startTime) * 50;
|
|
865
1036
|
} else writeToStdioDirectly(process.stdout, payload);
|
|
866
|
-
if (logBuffer) logBuffer = null;
|
|
867
1037
|
}
|
|
868
1038
|
|
|
869
1039
|
function closeLogFile(_unused?: any) {
|
|
@@ -871,6 +1041,7 @@ function getFileLogger(path, rotation, isExternalInstance) {
|
|
|
871
1041
|
fs.closeSync(logFD);
|
|
872
1042
|
} catch {}
|
|
873
1043
|
logFD = null;
|
|
1044
|
+
logFDIdentity = null;
|
|
874
1045
|
if (isExternalInstance) mainLogFd = null;
|
|
875
1046
|
}
|
|
876
1047
|
|
|
@@ -878,6 +1049,14 @@ function getFileLogger(path, rotation, isExternalInstance) {
|
|
|
878
1049
|
if (!logFD) {
|
|
879
1050
|
try {
|
|
880
1051
|
logFD = fs.openSync(path, 'a');
|
|
1052
|
+
// Which generation this descriptor belongs to, recorded once here so the size guard's
|
|
1053
|
+
// checkpoint needs a single pathname stat to tell whether the file has moved under it.
|
|
1054
|
+
try {
|
|
1055
|
+
const opened = fs.fstatSync(logFD);
|
|
1056
|
+
logFDIdentity = { ino: opened.ino, dev: opened.dev };
|
|
1057
|
+
} catch {
|
|
1058
|
+
logFDIdentity = null;
|
|
1059
|
+
}
|
|
881
1060
|
if (isExternalInstance) mainLogFd = logFD;
|
|
882
1061
|
} catch (error) {
|
|
883
1062
|
if (error.code === 'ENOENT' && !isRetry) {
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Every isolate holds its own descriptor on the shared log file, so a rename leaves the others
|
|
4
|
+
// appending to the archived inode and destroying it loses whatever they write next. No size
|
|
5
|
+
// comparison can rule out a write that has not happened yet; enumerated in-process peers must answer.
|
|
6
|
+
//
|
|
7
|
+
// The thread layer injects the transport; harper_logger must never reach manageThreads.
|
|
8
|
+
|
|
9
|
+
import { statSync } from 'node:fs';
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
import { isMainThread } from 'node:worker_threads';
|
|
12
|
+
|
|
13
|
+
export const LOG_GENERATION_ROTATED = 'log_generation_rotated';
|
|
14
|
+
export const LOG_GENERATION_CLOSED = 'log_generation_closed';
|
|
15
|
+
|
|
16
|
+
// Under manageThreads' 30s ITC default, so an unanswered generation is decided here (retain the
|
|
17
|
+
// plain archive) rather than by a transport backstop that cannot express "unproven".
|
|
18
|
+
const DEFAULT_QUIESCENCE_TIMEOUT = 15000;
|
|
19
|
+
// A tiny maxSize can rotate faster than a slow peer answers. Overflow is left as a plain archive
|
|
20
|
+
// for the audit sweep instead of retaining one timer and one mesh request per generation.
|
|
21
|
+
const MAX_PENDING_RELEASE_REQUESTS = 64;
|
|
22
|
+
|
|
23
|
+
interface RotationTransport {
|
|
24
|
+
broadcast(message: any): void;
|
|
25
|
+
sendToThread(threadId: number, message: any): void;
|
|
26
|
+
onMessage(type: string, handler: (message: any) => void): void;
|
|
27
|
+
onThreadExit(handler: (threadId: number) => void): void;
|
|
28
|
+
peerThreadIds(): Iterable<number>;
|
|
29
|
+
threadId: number;
|
|
30
|
+
quiescenceTimeout?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let transport: RotationTransport | undefined;
|
|
34
|
+
type LogSink = { identity(): any; close(): void };
|
|
35
|
+
// A set per path, not one sink: harper_logger caches its file loggers by the raw configured path, so
|
|
36
|
+
// two spellings of one file are two sinks holding two descriptors on it, and a release has to close
|
|
37
|
+
// both. The key is resolved because retention compares resolved paths.
|
|
38
|
+
const sinksByPath = new Map<string, Set<LogSink>>();
|
|
39
|
+
const pendingByRequest = new Map<
|
|
40
|
+
string,
|
|
41
|
+
{ expected: Set<number>; liveLogPaths: Set<string>; settle: (released: boolean) => void }
|
|
42
|
+
>();
|
|
43
|
+
let requestCounter = 0;
|
|
44
|
+
|
|
45
|
+
export function setRotationTransport(newTransport?: RotationTransport) {
|
|
46
|
+
transport = newTransport;
|
|
47
|
+
if (!newTransport) return;
|
|
48
|
+
newTransport.onMessage(LOG_GENERATION_ROTATED, (message) => {
|
|
49
|
+
releaseLocally(message);
|
|
50
|
+
newTransport.sendToThread(message.originator, {
|
|
51
|
+
type: LOG_GENERATION_CLOSED,
|
|
52
|
+
request: message.request,
|
|
53
|
+
threadId: newTransport.threadId,
|
|
54
|
+
// The paths this isolate writes. Components load in the workers, so a component's own log
|
|
55
|
+
// is registered only there, and the thread that runs retention would otherwise see a live
|
|
56
|
+
// file it has never heard of and delete it by age.
|
|
57
|
+
logPaths: message.stale ? [...sinksByPath.keys()] : undefined,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
newTransport.onMessage(LOG_GENERATION_CLOSED, (message) => {
|
|
61
|
+
recordPeerResponse(message.request, message.threadId, message.logPaths);
|
|
62
|
+
});
|
|
63
|
+
newTransport.onThreadExit((threadId) => {
|
|
64
|
+
// An exited thread cannot hold a descriptor, so its exit answers everything it owed.
|
|
65
|
+
for (const request of [...pendingByRequest.keys()]) recordPeerResponse(request, threadId);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Registered by the file sink, not the size guard: a thread with no guard still holds a descriptor,
|
|
71
|
+
* and its answer has to mean the descriptor is gone.
|
|
72
|
+
*/
|
|
73
|
+
export function registerLogSink(logPath: string, sink: LogSink) {
|
|
74
|
+
const key = resolve(logPath);
|
|
75
|
+
const sinks = sinksByPath.get(key) ?? new Set<LogSink>();
|
|
76
|
+
sinks.add(sink);
|
|
77
|
+
sinksByPath.set(key, sinks);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function unregisterLogSink(logPath: string, sink?: LogSink) {
|
|
81
|
+
const key = resolve(logPath);
|
|
82
|
+
if (!sink) return void sinksByPath.delete(key);
|
|
83
|
+
const sinks = sinksByPath.get(key);
|
|
84
|
+
if (!sinks?.delete(sink)) return;
|
|
85
|
+
if (sinks.size === 0) sinksByPath.delete(key);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function nextGenerationId() {
|
|
89
|
+
return `${process.pid}-${transport?.threadId ?? 0}-${requestCounter++}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Ask every in-process peer this isolate can enumerate to release the descriptors this request
|
|
94
|
+
* names. `false` means the caller must leave those archives in place — it is never safe to unlink
|
|
95
|
+
* an inode a peer may still be appending to.
|
|
96
|
+
*/
|
|
97
|
+
function requestRelease(message: any, deadline?: number): Promise<{ released: boolean; liveLogPaths: Set<string> }> {
|
|
98
|
+
releaseLocally(message);
|
|
99
|
+
const liveLogPaths = new Set<string>(sinksByPath.keys());
|
|
100
|
+
// No transport in a worker isolate means the mesh cannot be enumerated yet, not that there is
|
|
101
|
+
// nobody to ask: harper_logger installs the size guard at its own module load and manageThreads
|
|
102
|
+
// installs the transport at the end of its, so a worker rotating in that window would otherwise
|
|
103
|
+
// call an empty peer set a proof and unlink the generation its siblings are appending to. On the
|
|
104
|
+
// main thread the same state means no worker has been spawned, where zero peers is the truth.
|
|
105
|
+
if (!transport) return Promise.resolve({ released: isMainThread, liveLogPaths });
|
|
106
|
+
const expected = new Set<number>(transport.peerThreadIds());
|
|
107
|
+
if (expected.size === 0) return Promise.resolve({ released: true, liveLogPaths });
|
|
108
|
+
if (pendingByRequest.size >= MAX_PENDING_RELEASE_REQUESTS) return Promise.resolve({ released: false, liveLogPaths });
|
|
109
|
+
const quiescenceTimeout = transport.quiescenceTimeout ?? DEFAULT_QUIESCENCE_TIMEOUT;
|
|
110
|
+
if (deadline !== undefined && Date.now() + quiescenceTimeout > deadline)
|
|
111
|
+
return Promise.resolve({ released: false, liveLogPaths });
|
|
112
|
+
const request = message.request;
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
let timer;
|
|
115
|
+
const settle = (released) => {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
pendingByRequest.delete(request);
|
|
118
|
+
resolve({ released, liveLogPaths });
|
|
119
|
+
};
|
|
120
|
+
pendingByRequest.set(request, { expected, liveLogPaths, settle });
|
|
121
|
+
timer = setTimeout(() => settle(false), quiescenceTimeout);
|
|
122
|
+
timer.unref?.();
|
|
123
|
+
transport.broadcast({ ...message, originator: transport.threadId });
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** One archived generation: release any descriptor still pointing at the inode that was renamed. */
|
|
128
|
+
export async function requestGenerationClose(generation: any, deadline?: number): Promise<boolean> {
|
|
129
|
+
return (
|
|
130
|
+
await requestRelease(
|
|
131
|
+
{
|
|
132
|
+
type: LOG_GENERATION_ROTATED,
|
|
133
|
+
request: generation.generation,
|
|
134
|
+
logPath: generation.logPath,
|
|
135
|
+
ino: generation.ino,
|
|
136
|
+
dev: generation.dev,
|
|
137
|
+
},
|
|
138
|
+
deadline
|
|
139
|
+
)
|
|
140
|
+
).released;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Every generation but the live one, for every log this process writes, in a single round trip.
|
|
145
|
+
* Not scoped to one path: the archive directory holds the archives of every component and external
|
|
146
|
+
* log sharing it, and retention destroys those too.
|
|
147
|
+
*/
|
|
148
|
+
export function requestStaleDescriptorRelease(
|
|
149
|
+
deadline?: number
|
|
150
|
+
): Promise<{ released: boolean; liveLogPaths: Set<string> }> {
|
|
151
|
+
return requestRelease({ type: LOG_GENERATION_ROTATED, request: nextGenerationId(), stale: true }, deadline);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function recordPeerResponse(request: string, threadId: number, logPaths?: string[]) {
|
|
155
|
+
const pending = pendingByRequest.get(request);
|
|
156
|
+
if (!pending) return;
|
|
157
|
+
if (logPaths) for (const logPath of logPaths) pending.liveLogPaths.add(logPath);
|
|
158
|
+
pending.expected.delete(threadId);
|
|
159
|
+
if (pending.expected.size === 0) pending.settle(true);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function releaseLocally(message: any) {
|
|
163
|
+
if (message.stale) return releaseStaleDescriptors();
|
|
164
|
+
for (const sink of sinksByPath.get(resolve(message.logPath)) ?? []) {
|
|
165
|
+
const identity = sink.identity();
|
|
166
|
+
// No identity to compare (no descriptor open, or a filesystem that cannot report one) means
|
|
167
|
+
// closing is the only answer that can still be called a release.
|
|
168
|
+
if (!identity || !identity.ino || !message.ino || (identity.ino === message.ino && identity.dev === message.dev))
|
|
169
|
+
sink.close();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Release every descriptor this isolate holds that is not on the live generation of its own path.
|
|
175
|
+
* Each sink decides for itself, so nothing has to name the live inode of a log it does not own.
|
|
176
|
+
*/
|
|
177
|
+
function releaseStaleDescriptors() {
|
|
178
|
+
for (const [logPath, sinks] of sinksByPath) {
|
|
179
|
+
let live;
|
|
180
|
+
try {
|
|
181
|
+
live = statSync(logPath);
|
|
182
|
+
} catch {
|
|
183
|
+
for (const sink of sinks) sink.close();
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
for (const sink of sinks) {
|
|
187
|
+
const identity = sink.identity();
|
|
188
|
+
// No identity is not the same as no descriptor: openLogFile() leaves the descriptor open when
|
|
189
|
+
// its fstat fails. Skipping the sink and still answering "released" is what lets an archive be
|
|
190
|
+
// unlinked under it, so close it here as the per-generation path already does.
|
|
191
|
+
// `ino === 0` (some Windows filesystems) proves nothing for the same reason.
|
|
192
|
+
if (!identity || !identity.ino || !live.ino || identity.ino !== live.ino || identity.dev !== live.dev)
|
|
193
|
+
sink.close();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|