@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.
Files changed (291) hide show
  1. package/bin/cliOperations.ts +12 -0
  2. package/bin/restart.ts +66 -6
  3. package/components/Application.ts +1134 -112
  4. package/components/OptionsWatcher.ts +368 -102
  5. package/components/Scope.ts +20 -1
  6. package/components/componentLoader.ts +43 -4
  7. package/components/deploymentOperations.ts +4 -1
  8. package/components/deploymentRecorder.ts +9 -2
  9. package/components/operations.js +284 -52
  10. package/components/operationsValidation.js +49 -2
  11. package/components/packageComponent.ts +25 -1
  12. package/components/requestRestart.ts +11 -0
  13. package/config/RootConfigWatcher.ts +191 -37
  14. package/config/configReadRetry.ts +62 -0
  15. package/config/configUtils.ts +78 -26
  16. package/config/parseConfigFile.ts +34 -0
  17. package/config/readConfigFileSync.ts +44 -0
  18. package/config/watcherArming.ts +59 -0
  19. package/config-root.schema.json +4 -0
  20. package/dataLayer/harperBridge/ResourceBridge.ts +28 -2
  21. package/dist/bin/cliOperations.js +13 -0
  22. package/dist/bin/cliOperations.js.map +1 -1
  23. package/dist/bin/restart.js +42 -6
  24. package/dist/bin/restart.js.map +1 -1
  25. package/dist/components/Application.d.ts +104 -9
  26. package/dist/components/Application.js +954 -102
  27. package/dist/components/Application.js.map +1 -1
  28. package/dist/components/OptionsWatcher.d.ts +4 -1
  29. package/dist/components/OptionsWatcher.js +378 -104
  30. package/dist/components/OptionsWatcher.js.map +1 -1
  31. package/dist/components/Scope.js +15 -1
  32. package/dist/components/Scope.js.map +1 -1
  33. package/dist/components/componentLoader.js +35 -3
  34. package/dist/components/componentLoader.js.map +1 -1
  35. package/dist/components/deploymentOperations.js +4 -1
  36. package/dist/components/deploymentOperations.js.map +1 -1
  37. package/dist/components/deploymentRecorder.d.ts +4 -2
  38. package/dist/components/deploymentRecorder.js +1 -0
  39. package/dist/components/deploymentRecorder.js.map +1 -1
  40. package/dist/components/operations.d.ts +28 -0
  41. package/dist/components/operations.js +263 -54
  42. package/dist/components/operations.js.map +1 -1
  43. package/dist/components/operationsValidation.js +48 -2
  44. package/dist/components/operationsValidation.js.map +1 -1
  45. package/dist/components/packageComponent.js +24 -0
  46. package/dist/components/packageComponent.js.map +1 -1
  47. package/dist/components/requestRestart.d.ts +1 -0
  48. package/dist/components/requestRestart.js +7 -0
  49. package/dist/components/requestRestart.js.map +1 -1
  50. package/dist/config/RootConfigWatcher.d.ts +2 -0
  51. package/dist/config/RootConfigWatcher.js +189 -35
  52. package/dist/config/RootConfigWatcher.js.map +1 -1
  53. package/dist/config/configReadRetry.d.ts +8 -0
  54. package/dist/config/configReadRetry.js +62 -0
  55. package/dist/config/configReadRetry.js.map +1 -0
  56. package/dist/config/configUtils.d.ts +10 -9
  57. package/dist/config/configUtils.js +63 -27
  58. package/dist/config/configUtils.js.map +1 -1
  59. package/dist/config/parseConfigFile.d.ts +4 -0
  60. package/dist/config/parseConfigFile.js +35 -0
  61. package/dist/config/parseConfigFile.js.map +1 -0
  62. package/dist/config/readConfigFileSync.d.ts +1 -0
  63. package/dist/config/readConfigFileSync.js +47 -0
  64. package/dist/config/readConfigFileSync.js.map +1 -0
  65. package/dist/config/watcherArming.d.ts +15 -0
  66. package/dist/config/watcherArming.js +59 -0
  67. package/dist/config/watcherArming.js.map +1 -0
  68. package/dist/dataLayer/harperBridge/ResourceBridge.js +21 -2
  69. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  70. package/dist/index.d.ts +1 -0
  71. package/dist/index.js +4 -1
  72. package/dist/index.js.map +1 -1
  73. package/dist/json/systemSchema.json +3 -0
  74. package/dist/resources/DatabaseTransaction.d.ts +25 -0
  75. package/dist/resources/DatabaseTransaction.js +224 -18
  76. package/dist/resources/DatabaseTransaction.js.map +1 -1
  77. package/dist/resources/LMDBTransaction.d.ts +2 -1
  78. package/dist/resources/LMDBTransaction.js +22 -3
  79. package/dist/resources/LMDBTransaction.js.map +1 -1
  80. package/dist/resources/PrimaryRocksDatabase.js +22 -6
  81. package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
  82. package/dist/resources/RecordEncoder.d.ts +1 -1
  83. package/dist/resources/RecordEncoder.js +21 -5
  84. package/dist/resources/RecordEncoder.js.map +1 -1
  85. package/dist/resources/Resource.js +97 -13
  86. package/dist/resources/Resource.js.map +1 -1
  87. package/dist/resources/ResourceInterface.d.ts +8 -0
  88. package/dist/resources/RocksIndexStore.js +2 -1
  89. package/dist/resources/RocksIndexStore.js.map +1 -1
  90. package/dist/resources/RocksTransactionLogStore.d.ts +10 -0
  91. package/dist/resources/RocksTransactionLogStore.js +104 -33
  92. package/dist/resources/RocksTransactionLogStore.js.map +1 -1
  93. package/dist/resources/Table.d.ts +58 -7
  94. package/dist/resources/Table.js +1095 -346
  95. package/dist/resources/Table.js.map +1 -1
  96. package/dist/resources/analytics/write.js +10 -3
  97. package/dist/resources/analytics/write.js.map +1 -1
  98. package/dist/resources/auditStore.d.ts +170 -0
  99. package/dist/resources/auditStore.js +457 -11
  100. package/dist/resources/auditStore.js.map +1 -1
  101. package/dist/resources/dataLoader.js +3 -4
  102. package/dist/resources/dataLoader.js.map +1 -1
  103. package/dist/resources/databases.d.ts +16 -13
  104. package/dist/resources/databases.js +624 -176
  105. package/dist/resources/databases.js.map +1 -1
  106. package/dist/resources/derivedIndexRegistry.d.ts +5 -0
  107. package/dist/resources/derivedIndexRegistry.js +68 -0
  108. package/dist/resources/derivedIndexRegistry.js.map +1 -0
  109. package/dist/resources/derivedIndexRuntime.d.ts +215 -0
  110. package/dist/resources/derivedIndexRuntime.js +2027 -0
  111. package/dist/resources/derivedIndexRuntime.js.map +1 -0
  112. package/dist/resources/graphql.js +3 -2
  113. package/dist/resources/graphql.js.map +1 -1
  114. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +102 -9
  115. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +848 -39
  116. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
  117. package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
  118. package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
  119. package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
  120. package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
  121. package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
  122. package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
  123. package/dist/resources/nodeIdMapping.d.ts +5 -0
  124. package/dist/resources/nodeIdMapping.js +49 -0
  125. package/dist/resources/nodeIdMapping.js.map +1 -1
  126. package/dist/resources/recordLock.d.ts +47 -4
  127. package/dist/resources/recordLock.js +138 -7
  128. package/dist/resources/recordLock.js.map +1 -1
  129. package/dist/resources/recordLockCoordinator.d.ts +557 -0
  130. package/dist/resources/recordLockCoordinator.js +2565 -0
  131. package/dist/resources/recordLockCoordinator.js.map +1 -0
  132. package/dist/resources/replayLogs.js +5 -0
  133. package/dist/resources/replayLogs.js.map +1 -1
  134. package/dist/resources/replicatedApplyFailure.d.ts +16 -0
  135. package/dist/resources/replicatedApplyFailure.js +63 -0
  136. package/dist/resources/replicatedApplyFailure.js.map +1 -0
  137. package/dist/resources/scheduler/scheduler.js +3 -3
  138. package/dist/resources/scheduler/scheduler.js.map +1 -1
  139. package/dist/resources/search.d.ts +10 -4
  140. package/dist/resources/search.js +160 -40
  141. package/dist/resources/search.js.map +1 -1
  142. package/dist/resources/tracked.d.ts +5 -1
  143. package/dist/resources/tracked.js +74 -23
  144. package/dist/resources/tracked.js.map +1 -1
  145. package/dist/security/jsLoader.js +6 -4
  146. package/dist/security/jsLoader.js.map +1 -1
  147. package/dist/server/REST.js +33 -2
  148. package/dist/server/REST.js.map +1 -1
  149. package/dist/server/http.d.ts +5 -1
  150. package/dist/server/http.js +34 -2
  151. package/dist/server/http.js.map +1 -1
  152. package/dist/server/serverHelpers/Headers.d.ts +2 -0
  153. package/dist/server/serverHelpers/Headers.js +6 -0
  154. package/dist/server/serverHelpers/Headers.js.map +1 -1
  155. package/dist/server/serverHelpers/NodeAdapterResponse.d.ts +48 -0
  156. package/dist/server/serverHelpers/NodeAdapterResponse.js +220 -0
  157. package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -0
  158. package/dist/server/serverHelpers/Request.d.ts +5 -10
  159. package/dist/server/serverHelpers/Request.js +38 -136
  160. package/dist/server/serverHelpers/Request.js.map +1 -1
  161. package/dist/server/serverHelpers/contentTypes.d.ts +2 -0
  162. package/dist/server/serverHelpers/contentTypes.js +189 -15
  163. package/dist/server/serverHelpers/contentTypes.js.map +1 -1
  164. package/dist/server/serverHelpers/serverUtilities.js +96 -17
  165. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  166. package/dist/server/storageReclamation.js +1 -1
  167. package/dist/server/storageReclamation.js.map +1 -1
  168. package/dist/server/threads/isolatedApplications.d.ts +47 -0
  169. package/dist/server/threads/isolatedApplications.js +171 -0
  170. package/dist/server/threads/isolatedApplications.js.map +1 -0
  171. package/dist/server/threads/logRotationTransport.d.ts +1 -0
  172. package/dist/server/threads/logRotationTransport.js +33 -0
  173. package/dist/server/threads/logRotationTransport.js.map +1 -0
  174. package/dist/server/threads/manageThreads.d.ts +64 -6
  175. package/dist/server/threads/manageThreads.js +261 -12
  176. package/dist/server/threads/manageThreads.js.map +1 -1
  177. package/dist/server/threads/socketRouter.d.ts +1 -0
  178. package/dist/server/threads/socketRouter.js +196 -13
  179. package/dist/server/threads/socketRouter.js.map +1 -1
  180. package/dist/server/threads/threadServer.js +30 -7
  181. package/dist/server/threads/threadServer.js.map +1 -1
  182. package/dist/utility/errors/hdbError.d.ts +24 -0
  183. package/dist/utility/errors/hdbError.js +58 -1
  184. package/dist/utility/errors/hdbError.js.map +1 -1
  185. package/dist/utility/hdbTerms.d.ts +2 -0
  186. package/dist/utility/hdbTerms.js +2 -0
  187. package/dist/utility/hdbTerms.js.map +1 -1
  188. package/dist/utility/logging/harper_logger.js +217 -38
  189. package/dist/utility/logging/harper_logger.js.map +1 -1
  190. package/dist/utility/logging/logGenerationCoordinator.d.ts +35 -0
  191. package/dist/utility/logging/logGenerationCoordinator.js +184 -0
  192. package/dist/utility/logging/logGenerationCoordinator.js.map +1 -0
  193. package/dist/utility/logging/logRotation.d.ts +46 -0
  194. package/dist/utility/logging/logRotation.js +365 -0
  195. package/dist/utility/logging/logRotation.js.map +1 -0
  196. package/dist/utility/logging/logRotator.d.ts +1 -1
  197. package/dist/utility/logging/logRotator.js +172 -92
  198. package/dist/utility/logging/logRotator.js.map +1 -1
  199. package/dist/utility/npmUtilities.js +6 -4
  200. package/dist/utility/npmUtilities.js.map +1 -1
  201. package/dist/utility/watcherFallback.d.ts +0 -45
  202. package/dist/utility/watcherFallback.js +1 -125
  203. package/dist/utility/watcherFallback.js.map +1 -1
  204. package/dist/validation/configValidator.js +6 -3
  205. package/dist/validation/configValidator.js.map +1 -1
  206. package/index.ts +6 -0
  207. package/json/systemSchema.json +3 -0
  208. package/npm-shrinkwrap.json +131 -41
  209. package/package.json +10 -3
  210. package/resources/DESIGN.md +124 -19
  211. package/resources/DatabaseTransaction.ts +230 -17
  212. package/resources/LMDBTransaction.ts +21 -3
  213. package/resources/PrimaryRocksDatabase.ts +20 -7
  214. package/resources/RecordEncoder.ts +25 -5
  215. package/resources/Resource.ts +97 -13
  216. package/resources/ResourceInterface.ts +8 -0
  217. package/resources/RocksIndexStore.ts +2 -1
  218. package/resources/RocksTransactionLogStore.ts +111 -31
  219. package/resources/Table.ts +1224 -393
  220. package/resources/analytics/write.ts +10 -3
  221. package/resources/auditStore.ts +460 -11
  222. package/resources/dataLoader.ts +3 -4
  223. package/resources/databases.ts +610 -146
  224. package/resources/derivedIndexRegistry.ts +56 -0
  225. package/resources/derivedIndexRuntime.ts +2292 -0
  226. package/resources/graphql.ts +3 -2
  227. package/resources/indexes/HierarchicalNavigableSmallWorld.ts +905 -46
  228. package/resources/indexes/hnswDerivedIndex.ts +531 -0
  229. package/resources/indexes/hnswPlaneBinding.ts +174 -0
  230. package/resources/nodeIdMapping.ts +50 -0
  231. package/resources/recordLock.ts +173 -7
  232. package/resources/recordLockCoordinator.ts +3043 -0
  233. package/resources/replayLogs.ts +5 -0
  234. package/resources/replicatedApplyFailure.ts +77 -0
  235. package/resources/scheduler/scheduler.ts +4 -4
  236. package/resources/search.ts +169 -49
  237. package/resources/tracked.ts +73 -22
  238. package/security/jsLoader.ts +6 -4
  239. package/server/DESIGN.md +11 -0
  240. package/server/REST.ts +36 -3
  241. package/server/http.ts +34 -2
  242. package/server/serverHelpers/Headers.ts +5 -1
  243. package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
  244. package/server/serverHelpers/Request.ts +33 -131
  245. package/server/serverHelpers/contentTypes.ts +188 -15
  246. package/server/serverHelpers/serverUtilities.ts +143 -24
  247. package/server/storageReclamation.ts +2 -2
  248. package/server/threads/isolatedApplications.ts +157 -0
  249. package/server/threads/logRotationTransport.ts +40 -0
  250. package/server/threads/manageThreads.js +254 -12
  251. package/server/threads/socketRouter.ts +217 -11
  252. package/server/threads/threadServer.js +30 -7
  253. package/studio/web/assets/{Chat-BnCBegQz.js → Chat-D3j-1yY1.js} +1 -1
  254. package/studio/web/assets/{FloatingChat-CoDW1ySS.js → FloatingChat-BxJGYcfB.js} +3 -3
  255. package/studio/web/assets/{apiToken-Bwk5BLXW.js → apiToken-CT55oWOe.js} +1 -1
  256. package/studio/web/assets/{applications-DHxGi7JH.js → applications-D9Ct9_vm.js} +1 -1
  257. package/studio/web/assets/{cssMode-s0cWI-_M.js → cssMode-DV8H7VwA.js} +1 -1
  258. package/studio/web/assets/{editor-DNcRHK54.js → editor-uatc0unt.js} +1 -1
  259. package/studio/web/assets/{html-Bdssedlg.js → html-Bm6D6paN.js} +1 -1
  260. package/studio/web/assets/{htmlMode-CoDlJ3fw.js → htmlMode-CEn7tpLG.js} +1 -1
  261. package/studio/web/assets/{index-D6sxmFLR.js → index-BIXW6Pu4.js} +5 -5
  262. package/studio/web/assets/{index.lazy-tmU5BS8s.js → index.lazy-UI7L-Vrk.js} +1 -1
  263. package/studio/web/assets/{javascript-B8meVSTH.js → javascript-CJ0G3AFZ.js} +1 -1
  264. package/studio/web/assets/{jsonMode-DpIPd35T.js → jsonMode-DQADAYEa.js} +1 -1
  265. package/studio/web/assets/{languageServices-C_5FMJzQ.js → languageServices-CAQJXWcI.js} +1 -1
  266. package/studio/web/assets/{lspLanguageFeatures-BIzNBzPK.js → lspLanguageFeatures-CCQ8P5sY.js} +1 -1
  267. package/studio/web/assets/{notifications-CQf18QKb.js → notifications-BbxTU6Aw.js} +1 -1
  268. package/studio/web/assets/{notifications-CvZivSbh.js → notifications-Cvb3P1lB.js} +1 -1
  269. package/studio/web/assets/{profile-DdOwtntb.js → profile-Yyb7gsvL.js} +1 -1
  270. package/studio/web/assets/{regions-n69fwagr.js → regions-OgjGHlU5.js} +1 -1
  271. package/studio/web/assets/{register-PfWTCXWB.js → register-6qwNEOY3.js} +2 -2
  272. package/studio/web/assets/{setComponentFile-Bg6O7X0S.js → setComponentFile-BilDMtgB.js} +1 -1
  273. package/studio/web/assets/{setup-CUx_aUDl.js → setup-J6qJ7OIU.js} +2 -2
  274. package/studio/web/assets/{status-D7BVKqX9.js → status-0RWGcfyD.js} +1 -1
  275. package/studio/web/assets/{toggleHighContrast-DBSyXzMr.js → toggleHighContrast-BIn-vErT.js} +1 -1
  276. package/studio/web/assets/{tsMode-BByKCjBS.js → tsMode-DgUXku4d.js} +1 -1
  277. package/studio/web/assets/{typescript-DDLnLpw9.js → typescript-C9orXcsM.js} +1 -1
  278. package/studio/web/assets/{useEntityRestURL-31CHGaHk.js → useEntityRestURL-BEoXXbUB.js} +1 -1
  279. package/studio/web/assets/{workers-pR3jRY9D.js → workers-JVzSDmgx.js} +1 -1
  280. package/studio/web/assets/{xml-2iRnMhQO.js → xml-Cq-S8S4X.js} +1 -1
  281. package/studio/web/assets/{yaml-Bf92gJpd.js → yaml-sfoRdh1M.js} +1 -1
  282. package/studio/web/index.html +1 -1
  283. package/utility/errors/hdbError.ts +54 -0
  284. package/utility/hdbTerms.ts +2 -0
  285. package/utility/logging/harper_logger.ts +209 -30
  286. package/utility/logging/logGenerationCoordinator.ts +196 -0
  287. package/utility/logging/logRotation.ts +367 -0
  288. package/utility/logging/logRotator.ts +196 -91
  289. package/utility/npmUtilities.ts +6 -4
  290. package/utility/watcherFallback.ts +0 -122
  291. package/validation/configValidator.ts +6 -3
@@ -1,21 +1,28 @@
1
1
  'use strict';
2
2
 
3
- import { promises as fsProm, createReadStream, createWriteStream, mkdirSync } from 'fs';
4
- import { createGzip } from 'zlib';
5
- import { promisify } from 'util';
6
- import { pipeline } from 'stream';
7
- const pipe = promisify(pipeline);
3
+ import { existsSync, mkdirSync, statSync, promises as fsProm } from 'fs';
8
4
  import * as path from 'path';
9
- import { createHash } from 'crypto';
10
5
  import * as envMgr from '../environment/environmentManager.ts';
11
6
  envMgr.initSync();
12
7
  import hdbLogger from './harper_logger.ts';
13
8
  import { CONFIG_PARAMS } from '../hdbTerms.ts';
14
9
  import { convertToMS } from '../common_utils.ts';
15
10
  import { onStorageReclamation } from '../../server/storageReclamation.ts';
11
+ import { requestStaleDescriptorRelease } from './logGenerationCoordinator.ts';
12
+ import {
13
+ compressPendingArchives,
14
+ INVALID_MAX_SIZE_MSG,
15
+ isArchivePendingQuiescence,
16
+ parseMaxSize,
17
+ publishArchivedGeneration,
18
+ resolveRotatedLogDir,
19
+ retryPendingGenerations,
20
+ rotateLogFileSync,
21
+ } from './logRotation.ts';
16
22
 
17
23
  // Interval in ms to check log file and decide if it should be rotated.
18
24
  const LOG_AUDIT_INTERVAL = 60000;
25
+ const RELEASE_PROOF_REPORT_INTERVAL = 5 * 60 * 1000;
19
26
  const INT_SIZE_UNDEFINED_MSG =
20
27
  "'interval' and 'maxSize' are both undefined, to enable logging rotation at least one of these values must be defined in harperdb-config.yaml";
21
28
 
@@ -27,7 +34,16 @@ export { logRotator };
27
34
  * If log file is within the values set in config, log file will be renamed/moved and a new empty hdb.log created.
28
35
  * @returns LogRotator
29
36
  */
30
- function logRotator({ logger, maxSize, interval, retention, enabled, path: rotatedLogDir, auditInterval }: any) {
37
+ function logRotator({
38
+ logger,
39
+ maxSize,
40
+ interval,
41
+ retention,
42
+ enabled,
43
+ compress,
44
+ path: rotatedLogDir,
45
+ auditInterval,
46
+ }: any) {
31
47
  if (enabled === false) return;
32
48
  let reclamationPriority = 0;
33
49
  onStorageReclamation(
@@ -42,21 +58,18 @@ function logRotator({ logger, maxSize, interval, retention, enabled, path: rotat
42
58
  throw new Error(INT_SIZE_UNDEFINED_MSG);
43
59
  }
44
60
 
45
- if (!rotatedLogDir) {
46
- rotatedLogDir = path.join(path.dirname(logger.path), 'rotated');
47
- }
61
+ rotatedLogDir = resolveRotatedLogDir(logger.path, rotatedLogDir);
48
62
  mkdirSync(rotatedLogDir, { recursive: true });
49
63
 
50
- // Convert maxSize param to bytes.
51
- let maxBytes;
52
- if (maxSize) {
53
- const unit = maxSize.slice(-1);
54
- const size = maxSize.slice(0, -1);
55
- if (unit === 'G') maxBytes = size * 1000000000;
56
- else if (unit === 'M') maxBytes = size * 1000000;
57
- else maxBytes = size * 1000;
64
+ const maxBytes = parseMaxSize(maxSize);
65
+ if (maxSize && !maxBytes) {
66
+ hdbLogger.error(`Ignoring logging.rotation.maxSize '${maxSize}': ${INVALID_MAX_SIZE_MSG}`);
58
67
  }
59
68
 
69
+ // The rotation block first, because that is all the write-path guard can read — environmentManager
70
+ // imports harper_logger — and two sources would mean two destruction policies for one log.
71
+ const compressArchives = compress ?? envMgr.get(CONFIG_PARAMS.LOGGING_ROTATION_COMPRESS);
72
+
60
73
  // Convert interval param to ms.
61
74
  let maxInterval;
62
75
  if (interval) {
@@ -64,127 +77,219 @@ function logRotator({ logger, maxSize, interval, retention, enabled, path: rotat
64
77
  }
65
78
 
66
79
  let lastRotatedLogPath;
67
- // convert date.now to minutes
68
80
  let lastRotationTime = Date.now();
81
+ let observedGeneration;
82
+ try {
83
+ const active = statSync(logger.path);
84
+ observedGeneration = generationIdentity(active);
85
+ if (active.birthtimeMs > 0) lastRotationTime = Math.min(lastRotationTime, active.birthtimeMs);
86
+ } catch {}
69
87
  hdbLogger.trace('Log rotate enabled, maxSize:', maxSize, 'interval:', interval);
88
+ let tickInFlight = false;
89
+ let ended = false;
90
+ let releaseProofStalled = false;
91
+ let nextReleaseProofReport = 0;
92
+ let retentionCursor = 0;
93
+ const auditIntervalMs = auditInterval ?? LOG_AUDIT_INTERVAL;
94
+ const compressionBudgetMs = Math.max(1, Math.floor(auditIntervalMs / 4));
95
+ /**
96
+ * rename() reports ENOENT for a missing destination directory as well as a missing source, and
97
+ * only the second is the benign lost race. The directory is created once at rotator start, so a
98
+ * rotation target removed under a running instance otherwise stops rotation permanently and
99
+ * silently — and with `interval` and no `maxSize` there is no write-path guard to recreate it.
100
+ */
101
+ function recoverRotationTarget(err: any) {
102
+ if (err.code !== 'ENOENT' || existsSync(rotatedLogDir)) return;
103
+ try {
104
+ mkdirSync(rotatedLogDir, { recursive: true });
105
+ hdbLogger.warn(`The log rotation directory ${rotatedLogDir} was missing and has been recreated`);
106
+ } catch (mkdirErr) {
107
+ hdbLogger.error('Could not recreate the log rotation directory', rotatedLogDir, mkdirErr);
108
+ }
109
+ }
70
110
  const setIntervalId = setInterval(async () => {
111
+ // setInterval does not await the callback, and one pass can now wait on peers; overlapping
112
+ // passes would work the same archive twice and double-compress it.
113
+ if (tickInFlight || ended) return;
114
+ tickInFlight = true;
115
+ const tickDeadline = Date.now() + auditIntervalMs;
71
116
  // The tick is async but setInterval doesn't await it, so any error that escapes this callback
72
117
  // becomes an unhandled rejection rather than surfacing anywhere useful — and since it isn't
73
118
  // caught, it also skips the retention cleanup below. Contain everything here and report via
74
119
  // the logger instead, so one bad tick (e.g. an unexpected fs error) never kills rotation for
75
120
  // the rest of the process's life.
76
121
  try {
77
- // A missing/relocated active log file only invalidates the rotation checks below — retention cleanup
78
- // must still run. So skip the individual check on ENOENT rather than returning from the whole tick.
79
122
  if (maxBytes) {
80
- let fileStats;
81
123
  try {
82
- fileStats = await fsProm.stat(logger.path);
124
+ // statSync, and the rename in the same turn: an await here lets a writing thread rotate
125
+ // the generation this tick measured and start a fresh one, which the tick would then
126
+ // archive near-empty.
127
+ const active = statSync(logger.path);
128
+ if (active.size >= maxBytes) {
129
+ lastRotatedLogPath = await moveLogFile(logger.path, rotatedLogDir, logger, compressArchives, active);
130
+ // The interval clock counts from the last rotation of any kind. Without this an
131
+ // instance whose uptime has passed `interval` archives a freshly-created log every
132
+ // interval on top of the size rotations already doing the work.
133
+ lastRotationTime = Date.now();
134
+ }
83
135
  } catch (err) {
84
- // If the log file doesn't exist, skip the size-based rotation check
136
+ // A missing or already-rotated active log only invalidates this check; retention below
137
+ // must still run, so skip the check rather than leaving the whole tick.
138
+ recoverRotationTarget(err);
85
139
  if (err.code !== 'ENOENT') throw err;
86
140
  }
87
-
88
- if (fileStats && fileStats.size >= maxBytes) {
89
- try {
90
- lastRotatedLogPath = await moveLogFile(logger.path, rotatedLogDir, logger);
91
- } catch (err) {
92
- // If the log file doesn't exist, skip rotation
93
- if (err.code !== 'ENOENT') throw err;
94
- }
95
- }
96
141
  }
97
142
 
98
143
  if (maxInterval) {
99
- const minSinceLastRotate = Date.now() - lastRotationTime;
100
- if (minSinceLastRotate >= maxInterval) {
144
+ try {
145
+ const activeGeneration = generationIdentity(statSync(logger.path));
146
+ if (activeGeneration && activeGeneration !== observedGeneration) {
147
+ observedGeneration = activeGeneration;
148
+ lastRotationTime = Date.now();
149
+ }
150
+ } catch (err) {
151
+ if (err.code !== 'ENOENT') throw err;
152
+ }
153
+ if (Date.now() - lastRotationTime >= maxInterval) {
101
154
  try {
102
- lastRotatedLogPath = await moveLogFile(logger.path, rotatedLogDir, logger);
155
+ lastRotatedLogPath = await moveLogFile(logger.path, rotatedLogDir, logger, compressArchives);
103
156
  lastRotationTime = Date.now();
104
157
  } catch (err) {
105
158
  // If the log file doesn't exist, skip rotation
159
+ recoverRotationTarget(err);
106
160
  if (err.code !== 'ENOENT') throw err;
107
161
  }
108
162
  }
109
163
  }
110
- if (retention || reclamationPriority) {
111
- // remove old logs after retention time
112
- // adjust retention time if there is a reclamation priority in place
113
- const retentionMs = convertToMS(retention ?? '1M') / (1 + reclamationPriority);
114
- reclamationPriority = 0; // reset it after use
115
- let files;
164
+ // The compression sweep and retention both destroy archives this thread did not rotate, so
165
+ // both need the same proof: every peer releases any descriptor that is not on the live
166
+ // generation, and reports the log paths it is writing. One round trip per tick serves both,
167
+ // and it is not gated on retention being configured — retention is unset by default, and a
168
+ // worker's uncompressed archive still needs finishing.
169
+ if (compressArchives || retention || reclamationPriority) {
170
+ // Enumerated BEFORE the proof, and only these are destroyed: an archive created after the
171
+ // proof was never covered by it, and a peer blocked mid-rotation may still be writing to it.
172
+ let candidates;
116
173
  try {
117
- files = await fsProm.readdir(rotatedLogDir);
174
+ candidates = await fsProm.readdir(rotatedLogDir);
118
175
  } catch (err) {
119
- // The rotated log dir may not exist yet (nothing rotated so far); nothing to clean up
120
176
  if (err.code !== 'ENOENT') hdbLogger.error('Error reading rotated log directory', rotatedLogDir, err);
121
- files = [];
177
+ candidates = [];
122
178
  }
123
- for (const file of files) {
124
- try {
125
- const fileStats = await fsProm.stat(path.join(rotatedLogDir, file));
126
- if (Date.now() - fileStats.mtimeMs > retentionMs) {
127
- await fsProm.unlink(path.join(rotatedLogDir, file));
179
+ const { released, liveLogPaths } = await requestStaleDescriptorRelease(tickDeadline);
180
+ // A peer whose event loop is blocked never answers, and the whole pass then destroys
181
+ // nothing — correct, but it is also the only thing bounding the rotated directory, so an
182
+ // operator who configured retention has to be able to see that it has stopped. Repeat at
183
+ // a low rate so a permanent stall does not disappear after one warning.
184
+ if (!released && Date.now() >= nextReleaseProofReport) {
185
+ nextReleaseProofReport = Date.now() + RELEASE_PROOF_REPORT_INTERVAL;
186
+ releaseProofStalled = true;
187
+ hdbLogger.warn(
188
+ `Log rotation could not prove every thread released its archived log descriptors; compression and retention are paused for ${rotatedLogDir}`
189
+ );
190
+ } else if (released && releaseProofStalled) {
191
+ releaseProofStalled = false;
192
+ nextReleaseProofReport = 0;
193
+ hdbLogger.notify(`Log rotation descriptor release recovered; retention resumed for ${rotatedLogDir}`);
194
+ }
195
+ const liveLogs = new Set([...liveLogPaths, logger.path].map((p) => path.resolve(p)));
196
+
197
+ if (released && (retention || reclamationPriority)) {
198
+ // remove old logs after retention time
199
+ // adjust retention time if there is a reclamation priority in place
200
+ const retentionMs = convertToMS(retention ?? '1M') / (1 + reclamationPriority);
201
+ let retentionCompleted = true;
202
+ let examined = 0;
203
+ for (; examined < candidates.length; examined++) {
204
+ if (ended || Date.now() >= tickDeadline) {
205
+ retentionCompleted = false;
206
+ break;
207
+ }
208
+ const file = candidates[(retentionCursor + examined) % candidates.length];
209
+ try {
210
+ const archivePath = path.join(rotatedLogDir, file);
211
+ // An explicitly configured rotation path may contain live component logs too.
212
+ if (liveLogs.has(path.resolve(archivePath))) continue;
213
+ // Unlinking an inode a stalled writer still holds loses whatever it writes next
214
+ // just as surely as compressing over it would.
215
+ if (isArchivePendingQuiescence(archivePath)) continue;
216
+ const fileStats = await fsProm.stat(archivePath);
217
+ if (Date.now() - fileStats.mtimeMs > retentionMs) {
218
+ await fsProm.unlink(archivePath);
219
+ }
220
+ } catch (err) {
221
+ if (err.code !== 'ENOENT') hdbLogger.error('Error trying to remove log', file, err);
128
222
  }
129
- } catch (err) {
130
- hdbLogger.error('Error trying to remove log', file, err);
131
223
  }
224
+ if (retentionCompleted) {
225
+ retentionCursor = 0;
226
+ reclamationPriority = 0;
227
+ } else if (candidates.length) {
228
+ retentionCursor = (retentionCursor + examined) % candidates.length;
229
+ }
230
+ }
231
+
232
+ if (released && compressArchives && !ended) {
233
+ const compressionFailure = await compressPendingArchives(rotatedLogDir, candidates, liveLogs, {
234
+ deadline: Math.min(tickDeadline, Date.now() + compressionBudgetMs),
235
+ shouldStop: () => ended,
236
+ });
237
+ if (compressionFailure)
238
+ hdbLogger.error('Error compressing rotated log', compressionFailure.file, compressionFailure.error);
132
239
  }
133
240
  }
134
241
  } catch (err) {
135
242
  hdbLogger.error('Error during log rotation audit tick for', logger.path, err);
243
+ } finally {
244
+ try {
245
+ await retryPendingGenerations({ deadline: tickDeadline, shouldStop: () => ended });
246
+ } catch (err) {
247
+ hdbLogger.error('Error retrying pending log generations for', logger.path, err);
248
+ }
249
+ tickInFlight = false;
136
250
  }
137
- }, auditInterval ?? LOG_AUDIT_INTERVAL).unref();
251
+ }, auditIntervalMs).unref();
138
252
  return {
139
253
  end() {
254
+ ended = true;
140
255
  clearInterval(setIntervalId);
141
256
  },
142
257
  getLastRotatedLogPath() {
143
258
  return lastRotatedLogPath;
144
259
  },
145
260
  };
261
+
262
+ function generationIdentity(stats: any) {
263
+ return stats.ino ? `${stats.dev}:${stats.ino}` : undefined;
264
+ }
146
265
  }
147
266
 
148
- // Monotonically increasing across every rotation in this process, regardless of which logger/source
149
- // triggered it — combined with the pid, this guarantees two archive names can never collide even if
150
- // two rotations for two different sources race concurrently within the same audit-interval tick.
151
- let rotationSequence = 0;
152
-
153
- async function moveLogFile(logPath: string, rotatedLogPath: string, logger?: any) {
154
- const compress = envMgr.get(CONFIG_PARAMS.LOGGING_ROTATION_COMPRESS);
155
- // Name the archive after its source log (hdb, external, a component name, ...), not a fixed
156
- // "HDB" literal — external/component loggers can now inherit rotation from the main logger
157
- // (#1877) and default to the same rotated directory as it, so distinct sources rotating in
158
- // the same audit tick must not collide on the same timestamp-only filename. A basename alone
159
- // is not enough either: two distinct source paths can share a basename (e.g. `/logs/a/hdb.log`
160
- // and `/logs/b/hdb.log`), so a hash of the full resolved source path plus a pid+sequence suffix
161
- // give every archive a name POSIX rename() can never clobber, even under a same-millisecond race.
162
- const sourceName = path.basename(logPath, path.extname(logPath)) || 'HDB';
163
- // sha256, not sha1: this only needs a stable identifier, not cryptographic strength, but a FIPS-mode
164
- // OpenSSL provider disables sha1 and throws synchronously, which would crash every rotation tick.
165
- const sourceId = createHash('sha256').update(path.resolve(logPath)).digest('hex').slice(0, 8);
166
- const uniqueSuffix = `${process.pid}-${rotationSequence++}`;
167
- let fullRotateLogPath = path.join(
267
+ async function moveLogFile(
268
+ logPath: string,
269
+ rotatedLogPath: string,
270
+ logger?: any,
271
+ compress?: boolean,
272
+ activeStats?: any
273
+ ) {
274
+ // The rename and the descriptor close must not be separated by an await: the descriptor would
275
+ // otherwise keep feeding the archived inode while the event loop runs. Closing the rotating
276
+ // logger's own descriptor (not the module-global one) is what makes the next write reopen a
277
+ // fresh log file rather than append to the moved — and, when compressing, unlinked — inode.
278
+ // `activeStats` is the caller's own stat of the live generation, when it has one: the size check
279
+ // must rename the generation it measured, and a second stat here could pick up a newer one.
280
+ const generation = rotateLogFileSync(
281
+ logPath,
168
282
  rotatedLogPath,
169
- `${sourceName}-${sourceId}-${new Date(Date.now()).toISOString().replaceAll(':', '-')}-${uniqueSuffix}.log`
283
+ logger?.closeLogFile ?? hdbLogger.closeLogFile,
284
+ activeStats
285
+ );
286
+ const publishedPath = await publishArchivedGeneration(
287
+ generation,
288
+ compress ?? envMgr.get(CONFIG_PARAMS.LOGGING_ROTATION_COMPRESS),
289
+ (error) => hdbLogger.error('Error compressing rotated log', generation.archivePath, error)
170
290
  );
171
- // Move log file to rotated log path first (if we crash
172
- // during compression, we don't want to restart the compression with a new file)
173
- await fsProm.rename(logPath, fullRotateLogPath);
174
- // Close the rotating logger's own file descriptor now that the file has moved. This must be the
175
- // logger's own closeLogFile (which resets its internal logFD), not the module-global one — otherwise
176
- // the descriptor stays open on the moved (and, when compressing, subsequently unlinked) inode until the
177
- // logger's safety timeout fires, pinning disk space and sending any writes in that window into the
178
- // rotated/deleted file. Closing it here makes the next write reopen a fresh log file immediately.
179
- (logger?.closeLogFile ?? hdbLogger.closeLogFile)();
180
- if (compress) {
181
- logPath = fullRotateLogPath;
182
- fullRotateLogPath += '.gz';
183
- await pipe(createReadStream(logPath), createGzip(), createWriteStream(fullRotateLogPath));
184
- await fsProm.unlink(logPath);
185
- }
186
291
 
187
292
  // This notify log will create a new log file after the previous one has been rotated. It's important to keep this log as notify
188
- hdbLogger.notify(`hdb.log rotated, old log moved to ${fullRotateLogPath}`);
189
- return fullRotateLogPath;
293
+ hdbLogger.notify(`hdb.log rotated, old log moved to ${publishedPath}`);
294
+ return publishedPath;
190
295
  }
@@ -31,14 +31,13 @@ export async function installModules(req: any) {
31
31
  throw handleHDBError(validation, validation.message, HTTP_STATUS_CODES.BAD_REQUEST);
32
32
  }
33
33
 
34
- const { projects, dry_run: dryRun } = validatedRequest;
34
+ const { projects, dry_run: dryRun, allowInstallScripts } = validatedRequest;
35
35
 
36
36
  const componentsRootDirPath = getConfigPath(CONFIG_PARAMS.COMPONENTSROOT);
37
37
 
38
38
  const responseObject: any = {};
39
39
 
40
- // `allowInstallScripts` is true because this operation has always run a project's install lifecycle
41
- const args = [...packageManagerInstallArguments('npm', true, true), '--json'];
40
+ const args = [...packageManagerInstallArguments('npm', allowInstallScripts, true), '--json'];
42
41
  if (dryRun) args.push('--dry-run');
43
42
 
44
43
  for (const project of projects) {
@@ -101,7 +100,10 @@ function modulesValidator(req: any) {
101
100
  const funcSchema = Joi.object({
102
101
  projects: Joi.array().min(1).items(Joi.string()).required(),
103
102
  dry_run: Joi.boolean().default(false),
104
- }).rename('dryRun', 'dry_run', { ignoreUndefined: true });
103
+ allowInstallScripts: Joi.boolean().default(true),
104
+ })
105
+ .rename('dryRun', 'dry_run', { ignoreUndefined: true })
106
+ .rename('install_allow_scripts', 'allowInstallScripts', { ignoreUndefined: true });
105
107
 
106
108
  return validator.validateAndConvertBySchema(req, funcSchema);
107
109
  }
@@ -11,7 +11,6 @@ import { loggerWithTag } from './logging/harper_logger.ts';
11
11
  // One-time process-wide warning so a thundering herd of failing watchers doesn't
12
12
  // produce hundreds of identical log lines.
13
13
  let exhaustionWarned = false;
14
- const partialReadWarned = new Set<string>();
15
14
 
16
15
  const fallbackLogger = loggerWithTag('watcher');
17
16
 
@@ -221,124 +220,3 @@ export function guardedWatch(paths: string | string[], options?: ChokidarOptions
221
220
  export function _lostNativeWatchCountForTests(): number {
222
221
  return lostNativeWatchCount;
223
222
  }
224
-
225
- /**
226
- * A config file replaced in place (truncate, then write) can be read back empty or
227
- * half-written, and chokidar may emit nothing further for that write — so a watcher that
228
- * simply drops the unusable read would serve stale config until something else touched the
229
- * file. Re-read on a later turn instead, bounded so a genuinely empty or corrupt file cannot
230
- * spin. Callers read synchronously, so the descriptor still never outlives a single turn.
231
- */
232
- const PARTIAL_READ_REREAD_DELAY_MS = 20;
233
- const PARTIAL_READ_MAX_REREADS = 10;
234
-
235
- export class PartialReadRetry {
236
- #filePath: string;
237
- #timer?: ReturnType<typeof setTimeout>;
238
- #remaining: number = PARTIAL_READ_MAX_REREADS;
239
- #closed = false;
240
-
241
- constructor(filePath: string) {
242
- this.#filePath = filePath;
243
- }
244
-
245
- /** False once the budget is spent, so the caller can fall back to its own error handling. */
246
- schedule(reread: () => void): boolean {
247
- if (this.#closed) return false;
248
- if (this.#timer) return true;
249
- if (this.#remaining <= 0) return false;
250
- this.#remaining--;
251
- this.#timer = setTimeout(() => {
252
- this.#timer = undefined;
253
- reread();
254
- }, PARTIAL_READ_REREAD_DELAY_MS);
255
- this.#timer.unref?.();
256
- return true;
257
- }
258
-
259
- /** A usable read arrived, so any re-read still armed for the previous one would duplicate it. */
260
- settled() {
261
- if (this.#timer) clearTimeout(this.#timer);
262
- this.#timer = undefined;
263
- this.#remaining = PARTIAL_READ_MAX_REREADS;
264
- // The file recovered, so the next time it breaks is a new incident and has to be reported
265
- // again rather than silenced by the warning it emitted weeks ago.
266
- partialReadWarned.delete(this.#filePath);
267
- }
268
-
269
- /**
270
- * The budget is spent. Distinct from `settled()` in that the report stands — the file has not
271
- * recovered, and it is shared with every other watcher of it. The budget itself is restored,
272
- * because the next event may be the repair, and that repair can be observed mid-write too.
273
- * Returns whether this give-up was the one reported.
274
- */
275
- gaveUp(error?: unknown): boolean {
276
- if (this.#closed) return false;
277
- this.#remaining = PARTIAL_READ_MAX_REREADS;
278
- return warnPartialReadGaveUp(this.#filePath, error);
279
- }
280
-
281
- /** Terminal: the watcher is closing, so nothing may re-arm the re-read or report on it. */
282
- cancel() {
283
- if (this.#timer) clearTimeout(this.#timer);
284
- this.#timer = undefined;
285
- this.#closed = true;
286
- }
287
- }
288
-
289
- /**
290
- * ENOENT is excluded not because it cannot be transient, but because it already has an answer:
291
- * `OptionsWatcher` routes it to `remove` (env-only fallback at boot, then removal), and
292
- * `RootConfigWatcher` keeps its last config rather than tearing down core features on a file
293
- * that may just be mid-replace. Re-reading would only delay a decision already made.
294
- */
295
- export function isPartialReadError(error: unknown): boolean {
296
- return !(typeof error === 'object' && error !== null && (error as { code?: string }).code === 'ENOENT');
297
- }
298
-
299
- /**
300
- * A watcher that exhausts its re-read budget serves stale config from then on, so the give-up
301
- * has to be visible — otherwise the only symptom is a config change that silently did nothing.
302
- * Warned once per file: every root-config scope watches the same one and would otherwise report
303
- * a single bad file once each, on every event.
304
- */
305
- export function warnPartialReadGaveUp(filePath: string, error?: unknown): boolean {
306
- if (partialReadWarned.has(filePath)) return false;
307
- partialReadWarned.add(filePath);
308
- // The cause matters to whoever has to fix it: a file that never parses is a typo to correct,
309
- // while one that reads empty is a writer that never finished. Report the kind and position
310
- // only — a YAML parse error's message quotes the offending source, and this file holds
311
- // credentials.
312
- const cause = error ? `: ${describeReadFailure(error)}` : ' that were empty or incomplete';
313
- fallbackLogger.warn(`Gave up re-reading ${filePath} after ${PARTIAL_READ_MAX_REREADS} unusable reads${cause}`);
314
- return true;
315
- }
316
-
317
- /**
318
- * The kind and position of a failed config read, never its content: a YAML parse error's message
319
- * quotes the offending source lines, and these files hold credentials.
320
- */
321
- export function describeReadFailure(error: unknown): string {
322
- if (typeof error !== 'object' || error === null) return 'unusable';
323
- const { name, code, linePos } = error as { name?: string; code?: string; linePos?: { line: number; col: number }[] };
324
- const at = linePos?.[0] ? ` at line ${linePos[0].line}, column ${linePos[0].col}` : '';
325
- return `${code ?? name ?? 'unusable'}${at}`;
326
- }
327
-
328
- /** Test-only: whether a give-up warning for this file is currently suppressed as a duplicate. */
329
- export function isPartialReadWarned(filePath: string): boolean {
330
- return partialReadWarned.has(filePath);
331
- }
332
-
333
- /** Test-only: forget that this file was reported, so a suite can start from a known state. */
334
- export function clearPartialReadWarning(filePath: string) {
335
- partialReadWarned.delete(filePath);
336
- }
337
-
338
- /**
339
- * A listener that throws while applying new config is a bug in that listener, not evidence the
340
- * file was half-written — the watcher's own state is already updated, so it keeps going.
341
- */
342
- export function warnWatcherListenerError(filePath: string, error: unknown) {
343
- fallbackLogger.warn(`Error applying a configuration change from ${filePath}`, error);
344
- }
@@ -11,6 +11,7 @@ import * as hdbUtils from '../utility/common_utils.ts';
11
11
  import * as hdbTerms from '../utility/hdbTerms.ts';
12
12
  import { getDomainSocketPathMaxBytes } from '../utility/domainSocket.ts';
13
13
  import { bareHostViolation } from '../utility/nodeIdentity.ts';
14
+ import { parseMaxSize } from '../utility/logging/logRotation.ts';
14
15
  import * as validator from './validationWrapper.ts';
15
16
 
16
17
  const DEFAULT_LOG_FOLDER = 'log';
@@ -19,7 +20,7 @@ const INVALID_SIZE_UNIT_MSG = 'Invalid logging.rotation.maxSize unit. Available
19
20
  const INVALID_INTERVAL_UNIT_MSG =
20
21
  'Invalid logging.rotation.interval unit. Available units are D (days), H (hours), M (months) or m (minutes)';
21
22
  const INVALID_MAX_SIZE_VALUE_MSG =
22
- "Invalid logging.rotation.maxSize value. Value should be a number followed by unit e.g. '10M'";
23
+ "Invalid logging.rotation.maxSize value. Value should be a positive number followed by unit e.g. '10M'";
23
24
  const INVALID_INTERVAL_VALUE_MSG =
24
25
  "Invalid logging.rotation.interval value. Value should be a number followed by unit e.g. '10D'";
25
26
  const INVALID_RETENTION_UNIT_MSG =
@@ -434,6 +435,7 @@ export function configValidator(configJson, skipFsValidation = false) {
434
435
  })
435
436
  ),
436
437
  maxHeapMemory: number.min(0).optional(),
438
+ maxIsolated: number.integer().min(0).optional(),
437
439
  preload: Joi.alternatives([string, array.items(string)])
438
440
  .allow(null)
439
441
  .optional(),
@@ -599,8 +601,9 @@ function validateRotationMaxSize(value, helpers) {
599
601
  return helpers.message(INVALID_SIZE_UNIT_MSG);
600
602
  }
601
603
 
602
- const size = value.slice(0, -1);
603
- if (isNaN(parseInt(size))) {
604
+ // The same parser the write-path size check uses, so a value accepted here can never become a
605
+ // zero, negative or NaN byte limit downstream. `parseInt` alone accepted '0K', '-1K' and '1xK'.
606
+ if (!parseMaxSize(value)) {
604
607
  return helpers.message(INVALID_MAX_SIZE_VALUE_MSG);
605
608
  }
606
609