From 743ebaf1cb37bd275a241816f75135e94b868862 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Wed, 20 May 2026 13:54:51 -0600 Subject: [PATCH 1/7] feat(storage): quota-aware reclamation via storage_quotaSize config When running containerised with per-user XFS quotas, statfs() returns full-filesystem free space rather than quota headroom. The new storage_quotaSize config parameter (e.g. "20GB") tells Harper its allocated quota; when set, reclamation priority is computed as: (quota - du usage of data path) / quota instead of statfs bavail/blocks. Operators can verify which basis is active via the free_space_basis field now present in system_information disk output, alongside quota_size_bytes and quota_used_bytes. Host manager should set storage_quotaSize = storageGb (in bytes or with a unit suffix) when provisioning a container to wire the two together. Co-Authored-By: Claude Sonnet 4.6 --- server/storageReclamation.ts | 41 +++++++++++++++- unitTests/server/storageReclamation.test.js | 53 +++++++++++++++++++++ utility/common_utils.ts | 25 ++++++++++ utility/environment/systemInformation.ts | 14 ++++++ utility/hdbTerms.ts | 1 + validation/configValidator.ts | 1 + 6 files changed, 134 insertions(+), 1 deletion(-) diff --git a/server/storageReclamation.ts b/server/storageReclamation.ts index e21796e3f..04a8f14bf 100644 --- a/server/storageReclamation.ts +++ b/server/storageReclamation.ts @@ -1,10 +1,15 @@ import { statfs } from 'node:fs/promises'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; import { getWorkerIndex, getWorkerCount } from '../server/threads/manageThreads.js'; import { logger } from '../utility/logging/logger.ts'; import { CONFIG_PARAMS } from '../utility/hdbTerms.ts'; import * as envMgr from '../utility/environment/environmentManager.ts'; -import { convertToMS } from '../utility/common_utils.ts'; +import { convertToMS, convertToBytes } from '../utility/common_utils.ts'; envMgr.initSync(); + +const execFileAsync = promisify(execFile); + const reclamationHandlers = new Map< string, { priority: number; handler: (priority: number) => Promise | void }[] @@ -12,6 +17,19 @@ const reclamationHandlers = new Map< const RECLAMATION_THRESHOLD = envMgr.get(CONFIG_PARAMS.STORAGE_RECLAMATION_THRESHOLD) ?? 0.4; // 40% remaining free space is the default const RECLAMATION_INTERVAL = convertToMS(envMgr.get(CONFIG_PARAMS.STORAGE_RECLAMATION_INTERVAL)) || 3600000; // 1 hour is the default +// let so tests can override via rewire; set once from env at startup +let QUOTA_SIZE_BYTES: number | undefined = convertToBytes(envMgr.get(CONFIG_PARAMS.STORAGE_QUOTASIZE)); + +/** + * Returns the disk block usage (bytes) for a directory path. + * Uses `du -sb` which is a GNU extension available on all Linux deployments. + * The `--` separator guards against paths that start with `-`. + */ +export async function getDirectoryUsageBytes(dirPath: string): Promise { + const { stdout } = await execFileAsync('du', ['-sb', '--', dirPath]); + return parseInt(stdout, 10); +} + /** * Register a handler to be called when storage free space is low and reclamation is needed. The callback is called * with the priority of the reclamation, which is the ratio of the threshold to the available space ratio. If space is @@ -39,7 +57,13 @@ export function onStorageReclamation( } } let reclamationTimer: NodeJS.Timeout; + +// Checked at call time so that tests can override QUOTA_SIZE_BYTES via rewire const defaultGetAvailableSpaceRatio = async (path: string): Promise => { + if (QUOTA_SIZE_BYTES) { + const usedBytes = await getDirectoryUsageBytes(path); + return Math.max(0, QUOTA_SIZE_BYTES - usedBytes) / QUOTA_SIZE_BYTES; + } const fsStats = await statfs(path); return fsStats.bavail / fsStats.blocks; }; @@ -79,3 +103,18 @@ export async function runReclamationHandlers() { export function setAvailableSpaceRatioGetter(newGetter?: (path: string) => Promise) { getAvailableSpaceRatio = newGetter ?? defaultGetAvailableSpaceRatio; } + +/** + * Returns which basis is used for free-space calculations: 'quota' when storage_quotaSize is + * configured, 'filesystem' otherwise. + */ +export function getFreeSpaceBasis(): 'quota' | 'filesystem' { + return QUOTA_SIZE_BYTES ? 'quota' : 'filesystem'; +} + +/** + * Returns quota config info when storage_quotaSize is configured, undefined otherwise. + */ +export function getQuotaInfo(): { quotaSizeBytes: number } | undefined { + return QUOTA_SIZE_BYTES ? { quotaSizeBytes: QUOTA_SIZE_BYTES } : undefined; +} diff --git a/unitTests/server/storageReclamation.test.js b/unitTests/server/storageReclamation.test.js index 2373ac11b..dd3c196b8 100644 --- a/unitTests/server/storageReclamation.test.js +++ b/unitTests/server/storageReclamation.test.js @@ -393,4 +393,57 @@ describe('storageReclamation module', function () { assert.ok(okPathHandler.calledOnce); }); }); + + describe('quota mode', function () { + const QUOTA_100GB = 100 * 1024 * 1024 * 1024; + + it('getFreeSpaceBasis returns filesystem when no quota configured', function () { + assert.equal(storageReclamation.getFreeSpaceBasis(), 'filesystem'); + }); + + it('getFreeSpaceBasis returns quota when QUOTA_SIZE_BYTES is set', function () { + storageReclamation.__set__('QUOTA_SIZE_BYTES', QUOTA_100GB); + assert.equal(storageReclamation.getFreeSpaceBasis(), 'quota'); + storageReclamation.__set__('QUOTA_SIZE_BYTES', undefined); + }); + + it('getQuotaInfo returns undefined when no quota configured', function () { + assert.equal(storageReclamation.getQuotaInfo(), undefined); + }); + + it('getQuotaInfo returns quota size when QUOTA_SIZE_BYTES is set', function () { + storageReclamation.__set__('QUOTA_SIZE_BYTES', QUOTA_100GB); + const info = storageReclamation.getQuotaInfo(); + assert.deepEqual(info, { quotaSizeBytes: QUOTA_100GB }); + storageReclamation.__set__('QUOTA_SIZE_BYTES', undefined); + }); + + it('quota-aware ratio triggers reclamation when usage exceeds threshold headroom', async function () { + // Quota: 100GB, used: 95GB → 5% available → below 40% threshold → should trigger + const customGetter = sandbox.stub().resolves(0.05); + storageReclamation.setAvailableSpaceRatioGetter(customGetter); + + const handler = sandbox.stub().returns(Promise.resolve()); + storageReclamation.onStorageReclamation('/test/path', handler, true); + + await storageReclamation.runReclamationHandlers(); + + assert.ok(handler.calledOnce); + // priority = 0.4 / 0.05 = 8 + assert.equal(handler.firstCall.args[0], 8); + }); + + it('quota-aware ratio does not trigger reclamation when headroom is healthy', async function () { + // Quota: 100GB, used: 50GB → 50% available → above 40% threshold → no reclamation + const customGetter = sandbox.stub().resolves(0.5); + storageReclamation.setAvailableSpaceRatioGetter(customGetter); + + const handler = sandbox.stub(); + storageReclamation.onStorageReclamation('/test/path', handler, true); + + await storageReclamation.runReclamationHandlers(); + + assert.ok(handler.notCalled); + }); + }); }); diff --git a/utility/common_utils.ts b/utility/common_utils.ts index 8b9b14819..e2c828551 100644 --- a/utility/common_utils.ts +++ b/utility/common_utils.ts @@ -798,6 +798,31 @@ export function transformReq(req: any) { if (req.database) req.schema = req.database; } +export function convertToBytes(size: any): number | undefined { + if (size == null) return undefined; + if (typeof size === 'number') return size; + if (typeof size === 'string') { + const num = parseFloat(size); + const suffix = size.replace(/^[\d.]+\s*/, '').toUpperCase(); + switch (suffix) { + case 'K': + case 'KB': + return Math.floor(num * 1024); + case 'M': + case 'MB': + return Math.floor(num * 1024 * 1024); + case 'G': + case 'GB': + return Math.floor(num * 1024 * 1024 * 1024); + case 'T': + case 'TB': + return Math.floor(num * 1024 * 1024 * 1024 * 1024); + default: + return Math.floor(num); + } + } + return undefined; +} export function convertToMS(interval: any) { let seconds = 0; if (typeof interval === 'number') seconds = interval; diff --git a/utility/environment/systemInformation.ts b/utility/environment/systemInformation.ts index 1070a0997..b7c463ecd 100644 --- a/utility/environment/systemInformation.ts +++ b/utility/environment/systemInformation.ts @@ -3,6 +3,7 @@ import path from 'node:path'; import si from 'systeminformation'; import logger from '../logging/harper_logger.ts'; import * as hdbTerms from '../hdbTerms.ts'; +import { getFreeSpaceBasis, getQuotaInfo, getDirectoryUsageBytes } from '../../server/storageReclamation.ts'; import { lmdbGetTableSize } from '../../dataLayer/harperBridge/lmdbBridge/lmdbUtility/lmdbGetTableSize.ts'; import { getThreadInfo } from '../../server/threads/manageThreads.js'; import * as env from './environmentManager.ts'; @@ -264,6 +265,9 @@ type DiskInfo = { io?: Pick; read_write?: Pick; size?: si.Systeminformation.FsSizeData[]; + free_space_basis?: 'quota' | 'filesystem'; + quota_size_bytes?: number; + quota_used_bytes?: number; }; /** @@ -272,6 +276,16 @@ type DiskInfo = { */ export async function getDiskInfo(): Promise { const disk: DiskInfo = {}; + disk.free_space_basis = getFreeSpaceBasis(); + const quotaInfo = getQuotaInfo(); + if (quotaInfo) { + disk.quota_size_bytes = quotaInfo.quotaSizeBytes; + try { + disk.quota_used_bytes = await getDirectoryUsageBytes(env.get(hdbTerms.CONFIG_PARAMS.ROOTPATH)); + } catch (e) { + logger.error(`error measuring quota usage: ${e}`); + } + } try { if (!env.get(hdbTerms.CONFIG_PARAMS.OPERATIONSAPI_SYSINFO_DISK)) return disk; diff --git a/utility/hdbTerms.ts b/utility/hdbTerms.ts index 0c65031b0..7c5b0b8f4 100644 --- a/utility/hdbTerms.ts +++ b/utility/hdbTerms.ts @@ -578,6 +578,7 @@ export const CONFIG_PARAMS = { STORAGE_RECLAMATION_THRESHOLD: 'storage_reclamation_threshold', STORAGE_RECLAMATION_INTERVAL: 'storage_reclamation_interval', STORAGE_RECLAMATION_EVICTIONFACTOR: 'storage_reclamation_evictionFactor', + STORAGE_QUOTASIZE: 'storage_quotaSize', STORAGE_ENGINE: 'storage_engine', STORAGE_READONLY: 'storage_readOnly', DATABASES: 'databases', diff --git a/validation/configValidator.ts b/validation/configValidator.ts index 73bcab9aa..2d0bce85a 100644 --- a/validation/configValidator.ts +++ b/validation/configValidator.ts @@ -194,6 +194,7 @@ export function configValidator(configJson, skipFsValidation = false) { prefetchWrites: boolean.optional(), maxFreeSpaceToLoad: number.optional(), maxFreeSpaceToRetain: number.optional(), + quotaSize: Joi.alternatives([number, string]).optional(), }).required(), ignoreScripts: boolean.optional(), tls: Joi.alternatives([Joi.array().items(tlsConstraints), tlsConstraints]), From 94c46fcfc8d55b10511a13a1befa639ffd95dbdb Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Wed, 20 May 2026 15:53:48 -0600 Subject: [PATCH 2/7] refactor(storage): use host-manager quota-status file instead of du for usage Replace the per-analytics-call `du -sb` (O(inodes)) with a read of quota-status.json written by host-manager every ~90s (O(1)). This file contains usedBytes, quotaBytes, and updatedAt so Harper can report accurate quota headroom without scanning the directory tree on every system_information request. Reclamation: prefers the file when fresh (< 5 min); falls back to `du` on rootPath if the file is absent or stale (covers container start race and host-manager outage). Analytics: reads usedBytes directly from the file; adds quota_status_age_seconds so operators can verify the file is being updated. Co-Authored-By: Claude Sonnet 4.6 --- server/storageReclamation.ts | 42 ++++++++++++++++++++++-- utility/environment/systemInformation.ts | 11 ++++--- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/server/storageReclamation.ts b/server/storageReclamation.ts index 04a8f14bf..45ceb6fa0 100644 --- a/server/storageReclamation.ts +++ b/server/storageReclamation.ts @@ -1,5 +1,7 @@ +import { readFile } from 'node:fs/promises'; import { statfs } from 'node:fs/promises'; import { execFile } from 'node:child_process'; +import { join } from 'node:path'; import { promisify } from 'node:util'; import { getWorkerIndex, getWorkerCount } from '../server/threads/manageThreads.js'; import { logger } from '../utility/logging/logger.ts'; @@ -20,10 +22,37 @@ const RECLAMATION_INTERVAL = convertToMS(envMgr.get(CONFIG_PARAMS.STORAGE_RECLAM // let so tests can override via rewire; set once from env at startup let QUOTA_SIZE_BYTES: number | undefined = convertToBytes(envMgr.get(CONFIG_PARAMS.STORAGE_QUOTASIZE)); +// Written by host-manager every ~90s alongside the instance's hdb root +const QUOTA_STATUS_FILE = 'quota-status.json'; +// Fall back to du if the file is older than this (host-manager outage, container start race, etc.) +const QUOTA_STATUS_MAX_AGE_MS = 5 * 60 * 1000; + +export type QuotaStatusData = { + usedBytes: number; + quotaBytes?: number; + updatedAt: number; +}; + +/** + * Reads the quota-status.json file written by host-manager. + * Returns undefined if the file is absent or malformed; does not apply age filtering. + */ +export async function getQuotaStatus(): Promise { + const rootPath = envMgr.get(CONFIG_PARAMS.ROOTPATH); + if (!rootPath) return undefined; + try { + const raw = await readFile(join(rootPath, QUOTA_STATUS_FILE), 'utf8'); + return JSON.parse(raw) as QuotaStatusData; + } catch { + return undefined; + } +} + /** * Returns the disk block usage (bytes) for a directory path. * Uses `du -sb` which is a GNU extension available on all Linux deployments. * The `--` separator guards against paths that start with `-`. + * Used as fallback when the quota-status file is absent or stale. */ export async function getDirectoryUsageBytes(dirPath: string): Promise { const { stdout } = await execFileAsync('du', ['-sb', '--', dirPath]); @@ -58,10 +87,19 @@ export function onStorageReclamation( } let reclamationTimer: NodeJS.Timeout; -// Checked at call time so that tests can override QUOTA_SIZE_BYTES via rewire +// Checked at call time so that tests can override QUOTA_SIZE_BYTES via rewire. +// In quota mode: prefer the host-manager-written quota-status file (O(1)); fall back to +// `du` on the rootPath (O(inodes)) when the file is absent or stale. +// The rootPath `du` covers ALL Harper files (logs, blobs, databases), matching how XFS +// user quotas count usage — as opposed to per-table paths registered for reclamation. const defaultGetAvailableSpaceRatio = async (path: string): Promise => { if (QUOTA_SIZE_BYTES) { - const usedBytes = await getDirectoryUsageBytes(path); + const rootPath = envMgr.get(CONFIG_PARAMS.ROOTPATH); + const status = await getQuotaStatus(); + const usedBytes = + status && Date.now() - status.updatedAt < QUOTA_STATUS_MAX_AGE_MS + ? status.usedBytes + : await getDirectoryUsageBytes(rootPath ?? path); return Math.max(0, QUOTA_SIZE_BYTES - usedBytes) / QUOTA_SIZE_BYTES; } const fsStats = await statfs(path); diff --git a/utility/environment/systemInformation.ts b/utility/environment/systemInformation.ts index b7c463ecd..eee49b055 100644 --- a/utility/environment/systemInformation.ts +++ b/utility/environment/systemInformation.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import si from 'systeminformation'; import logger from '../logging/harper_logger.ts'; import * as hdbTerms from '../hdbTerms.ts'; -import { getFreeSpaceBasis, getQuotaInfo, getDirectoryUsageBytes } from '../../server/storageReclamation.ts'; +import { getFreeSpaceBasis, getQuotaInfo, getQuotaStatus } from '../../server/storageReclamation.ts'; import { lmdbGetTableSize } from '../../dataLayer/harperBridge/lmdbBridge/lmdbUtility/lmdbGetTableSize.ts'; import { getThreadInfo } from '../../server/threads/manageThreads.js'; import * as env from './environmentManager.ts'; @@ -268,6 +268,7 @@ type DiskInfo = { free_space_basis?: 'quota' | 'filesystem'; quota_size_bytes?: number; quota_used_bytes?: number; + quota_status_age_seconds?: number; }; /** @@ -280,10 +281,10 @@ export async function getDiskInfo(): Promise { const quotaInfo = getQuotaInfo(); if (quotaInfo) { disk.quota_size_bytes = quotaInfo.quotaSizeBytes; - try { - disk.quota_used_bytes = await getDirectoryUsageBytes(env.get(hdbTerms.CONFIG_PARAMS.ROOTPATH)); - } catch (e) { - logger.error(`error measuring quota usage: ${e}`); + const status = await getQuotaStatus(); + if (status) { + disk.quota_used_bytes = status.usedBytes; + disk.quota_status_age_seconds = Math.floor((Date.now() - status.updatedAt) / 1000); } } try { From ff316afdf63200ef362b89f403c95ffd9aab2d30 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Wed, 20 May 2026 15:57:36 -0600 Subject: [PATCH 3/7] fix(test): update systemInformation test for free_space_basis field getDiskInfo() now always includes free_space_basis (set before the operationsApi_sysInfo_disk guard) so operators can always verify quota vs filesystem mode. Update the test expectations accordingly. Co-Authored-By: Claude Sonnet 4.6 --- unitTests/utility/environment/systemInformation.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/unitTests/utility/environment/systemInformation.test.js b/unitTests/utility/environment/systemInformation.test.js index fe8c16b23..5a65e291f 100644 --- a/unitTests/utility/environment/systemInformation.test.js +++ b/unitTests/utility/environment/systemInformation.test.js @@ -176,7 +176,7 @@ const EXPECTED_PROPERTIES = { 'reclaimable', 'writeback', ], - disk: ['io', 'read_write', 'size'], + disk: ['free_space_basis', 'io', 'read_write', 'size'], disk_io: ['rIO', 'wIO', 'tIO'], disk_read_write: ['rx', 'wx', 'tx'], disk_size: ['fs', 'rw', 'type', 'size', 'used', 'use', 'mount', 'available'], @@ -285,7 +285,7 @@ describe('test systemInformation module', () => { env_mgr.setProperty('operationsApi_sysInfo_disk', false); results = await system_information.getDiskInfo(); - assert.deepEqual(results, {}); + assert.deepEqual(Object.keys(results).sort(), ['free_space_basis']); }); it('test getNetworkInfo function', async () => { From 32a2f05a8e393a4a048cade2973f041c791dadf1 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Wed, 20 May 2026 16:09:29 -0600 Subject: [PATCH 4/7] fix(test): address quota-mode test blockers from PR review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add setQuotaSizeBytes() exported setter to remove rewire.__set__ calls - Add tests for getQuotaStatus() (valid file, absent, malformed JSON) - Add tests for getDirectoryUsageBytes() against a real directory - Add tests for defaultGetAvailableSpaceRatio branches: fresh file triggers/ does not trigger reclamation, over-quota clamp (ratio → 0), stale and absent file fall back to du Co-Authored-By: Claude Sonnet 4.6 --- server/storageReclamation.ts | 10 +- unitTests/server/storageReclamation.test.js | 147 +++++++++++++++++++- 2 files changed, 148 insertions(+), 9 deletions(-) diff --git a/server/storageReclamation.ts b/server/storageReclamation.ts index 45ceb6fa0..2be262e16 100644 --- a/server/storageReclamation.ts +++ b/server/storageReclamation.ts @@ -19,7 +19,6 @@ const reclamationHandlers = new Map< const RECLAMATION_THRESHOLD = envMgr.get(CONFIG_PARAMS.STORAGE_RECLAMATION_THRESHOLD) ?? 0.4; // 40% remaining free space is the default const RECLAMATION_INTERVAL = convertToMS(envMgr.get(CONFIG_PARAMS.STORAGE_RECLAMATION_INTERVAL)) || 3600000; // 1 hour is the default -// let so tests can override via rewire; set once from env at startup let QUOTA_SIZE_BYTES: number | undefined = convertToBytes(envMgr.get(CONFIG_PARAMS.STORAGE_QUOTASIZE)); // Written by host-manager every ~90s alongside the instance's hdb root @@ -87,7 +86,7 @@ export function onStorageReclamation( } let reclamationTimer: NodeJS.Timeout; -// Checked at call time so that tests can override QUOTA_SIZE_BYTES via rewire. +// Checked at call time so QUOTA_SIZE_BYTES changes (via setQuotaSizeBytes) take effect immediately. // In quota mode: prefer the host-manager-written quota-status file (O(1)); fall back to // `du` on the rootPath (O(inodes)) when the file is absent or stale. // The rootPath `du` covers ALL Harper files (logs, blobs, databases), matching how XFS @@ -142,6 +141,13 @@ export function setAvailableSpaceRatioGetter(newGetter?: (path: string) => Promi getAvailableSpaceRatio = newGetter ?? defaultGetAvailableSpaceRatio; } +/** + * Override the quota size in bytes (for testing only). + */ +export function setQuotaSizeBytes(n: number | undefined): void { + QUOTA_SIZE_BYTES = n; +} + /** * Returns which basis is used for free-space calculations: 'quota' when storage_quotaSize is * configured, 'filesystem' otherwise. diff --git a/unitTests/server/storageReclamation.test.js b/unitTests/server/storageReclamation.test.js index dd3c196b8..e2f1ac2e5 100644 --- a/unitTests/server/storageReclamation.test.js +++ b/unitTests/server/storageReclamation.test.js @@ -1,6 +1,9 @@ 'use strict'; const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); const sinon = require('sinon'); const rewire = require('rewire'); @@ -35,9 +38,10 @@ describe('storageReclamation module', function () { }); afterEach(function () { - // Reset the space ratio getter + // Reset the space ratio getter and quota size if (storageReclamation) { storageReclamation.setAvailableSpaceRatioGetter(null); + storageReclamation.setQuotaSizeBytes(undefined); } // Clear any timers @@ -396,15 +400,27 @@ describe('storageReclamation module', function () { describe('quota mode', function () { const QUOTA_100GB = 100 * 1024 * 1024 * 1024; + let tmpDir; + let quotaStatusPath; + + beforeEach(function () { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'harper-quota-test-')); + quotaStatusPath = path.join(tmpDir, 'quota-status.json'); + }); + + afterEach(function () { + try { + fs.rmSync(tmpDir, { recursive: true }); + } catch {} + }); it('getFreeSpaceBasis returns filesystem when no quota configured', function () { assert.equal(storageReclamation.getFreeSpaceBasis(), 'filesystem'); }); it('getFreeSpaceBasis returns quota when QUOTA_SIZE_BYTES is set', function () { - storageReclamation.__set__('QUOTA_SIZE_BYTES', QUOTA_100GB); + storageReclamation.setQuotaSizeBytes(QUOTA_100GB); assert.equal(storageReclamation.getFreeSpaceBasis(), 'quota'); - storageReclamation.__set__('QUOTA_SIZE_BYTES', undefined); }); it('getQuotaInfo returns undefined when no quota configured', function () { @@ -412,10 +428,127 @@ describe('storageReclamation module', function () { }); it('getQuotaInfo returns quota size when QUOTA_SIZE_BYTES is set', function () { - storageReclamation.__set__('QUOTA_SIZE_BYTES', QUOTA_100GB); - const info = storageReclamation.getQuotaInfo(); - assert.deepEqual(info, { quotaSizeBytes: QUOTA_100GB }); - storageReclamation.__set__('QUOTA_SIZE_BYTES', undefined); + storageReclamation.setQuotaSizeBytes(QUOTA_100GB); + assert.deepEqual(storageReclamation.getQuotaInfo(), { quotaSizeBytes: QUOTA_100GB }); + }); + + describe('getQuotaStatus', function () { + let originalRootPath; + + beforeEach(function () { + originalRootPath = env.get('rootPath'); + env.setProperty('rootPath', tmpDir); + }); + + afterEach(function () { + env.setProperty('rootPath', originalRootPath); + }); + + it('returns parsed object when file is present and valid', async function () { + const data = { usedBytes: 50_000_000_000, quotaBytes: QUOTA_100GB, updatedAt: Date.now() }; + fs.writeFileSync(quotaStatusPath, JSON.stringify(data)); + assert.deepEqual(await storageReclamation.getQuotaStatus(), data); + }); + + it('returns undefined when file is absent', async function () { + assert.equal(await storageReclamation.getQuotaStatus(), undefined); + }); + + it('returns undefined when file contains malformed JSON', async function () { + fs.writeFileSync(quotaStatusPath, 'not-valid-json{'); + assert.equal(await storageReclamation.getQuotaStatus(), undefined); + }); + }); + + describe('getDirectoryUsageBytes', function () { + it('returns a non-negative integer for a real directory', async function () { + const bytes = await storageReclamation.getDirectoryUsageBytes(tmpDir); + assert.ok(Number.isInteger(bytes)); + assert.ok(bytes >= 0); + }); + }); + + describe('defaultGetAvailableSpaceRatio', function () { + let originalRootPath; + + beforeEach(function () { + originalRootPath = env.get('rootPath'); + env.setProperty('rootPath', tmpDir); + storageReclamation.setQuotaSizeBytes(QUOTA_100GB); + storageReclamation.setAvailableSpaceRatioGetter(undefined); // use real default + }); + + afterEach(function () { + env.setProperty('rootPath', originalRootPath); + }); + + it('uses fresh quota-status file and triggers reclamation when headroom is low', async function () { + const usedBytes = 65 * 1024 * 1024 * 1024; // 65 GB → 35% remaining → below 40% threshold + fs.writeFileSync( + quotaStatusPath, + JSON.stringify({ usedBytes, quotaBytes: QUOTA_100GB, updatedAt: Date.now() }) + ); + + const handler = sandbox.stub().returns(Promise.resolve()); + storageReclamation.onStorageReclamation(tmpDir, handler, true); + await storageReclamation.runReclamationHandlers(); + + assert.ok(handler.calledOnce); + assert.ok(handler.firstCall.args[0] > 1); // priority = 0.4 / 0.35 ≈ 1.14 + }); + + it('uses fresh quota-status file and does not trigger when headroom is sufficient', async function () { + const usedBytes = 50 * 1024 * 1024 * 1024; // 50% used → 50% remaining → above threshold + fs.writeFileSync( + quotaStatusPath, + JSON.stringify({ usedBytes, quotaBytes: QUOTA_100GB, updatedAt: Date.now() }) + ); + + const handler = sandbox.stub(); + storageReclamation.onStorageReclamation(tmpDir, handler, true); + await storageReclamation.runReclamationHandlers(); + + assert.ok(handler.notCalled); + }); + + it('clamps ratio to 0 and triggers reclamation when usage exceeds quota', async function () { + const usedBytes = 110 * 1024 * 1024 * 1024; // 10 GB over quota + fs.writeFileSync( + quotaStatusPath, + JSON.stringify({ usedBytes, quotaBytes: QUOTA_100GB, updatedAt: Date.now() }) + ); + + const handler = sandbox.stub().returns(Promise.resolve()); + storageReclamation.onStorageReclamation(tmpDir, handler, true); + await storageReclamation.runReclamationHandlers(); + + // Ratio clamped to 0 → priority = Infinity → handler called + assert.ok(handler.calledOnce); + }); + + it('falls back to du when quota-status file is absent', async function () { + // No quota-status.json; du reports actual tmpDir usage which is far below 100 GB + const handler = sandbox.stub(); + storageReclamation.onStorageReclamation(tmpDir, handler, true); + await storageReclamation.runReclamationHandlers(); + + assert.ok(handler.notCalled); + }); + + it('falls back to du when quota-status file is stale', async function () { + const staleTimestamp = Date.now() - 10 * 60 * 1000; // 10 minutes old + fs.writeFileSync( + quotaStatusPath, + JSON.stringify({ usedBytes: 65 * 1024 * 1024 * 1024, quotaBytes: QUOTA_100GB, updatedAt: staleTimestamp }) + ); + + // Despite the stale "65 GB" reading, du reports actual tmpDir usage (far below 100 GB) + const handler = sandbox.stub(); + storageReclamation.onStorageReclamation(tmpDir, handler, true); + await storageReclamation.runReclamationHandlers(); + + assert.ok(handler.notCalled); + }); }); it('quota-aware ratio triggers reclamation when usage exceeds threshold headroom', async function () { From 87f8160d6c5cd92e8dd1c0d3633cc24e50b29e3f Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Wed, 20 May 2026 16:15:10 -0600 Subject: [PATCH 5/7] test(common_utils): add convertToBytes unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers null/undefined → undefined, bare number passthrough, GB/G/MB/KB/TB string parsing, space-before-suffix, unrecognized suffix as raw bytes, and bare numeric string. Co-Authored-By: Claude Sonnet 4.6 --- unitTests/utility/common_utils.test.js | 42 ++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/unitTests/utility/common_utils.test.js b/unitTests/utility/common_utils.test.js index bf38779a2..e883c4581 100644 --- a/unitTests/utility/common_utils.test.js +++ b/unitTests/utility/common_utils.test.js @@ -566,4 +566,46 @@ describe('Test common_utils module', () => { const c = cu_rewire.ms_to_time(1672345634534); expect(c).to.equal('52y 27d 20h 27m 14s'); }); + + describe('convertToBytes', () => { + it('returns undefined for null', () => { + assert.equal(cu.convertToBytes(null), undefined); + }); + + it('returns undefined for undefined', () => { + assert.equal(cu.convertToBytes(undefined), undefined); + }); + + it('passes through a bare number', () => { + assert.equal(cu.convertToBytes(1024), 1024); + }); + + it('converts GB string', () => { + assert.equal(cu.convertToBytes('100GB'), 100 * 1024 * 1024 * 1024); + }); + + it('converts G string', () => { + assert.equal(cu.convertToBytes('10G'), 10 * 1024 * 1024 * 1024); + }); + + it('converts string with space before suffix', () => { + assert.equal(cu.convertToBytes('1.5 GB'), Math.floor(1.5 * 1024 * 1024 * 1024)); + }); + + it('converts KB string', () => { + assert.equal(cu.convertToBytes('512KB'), 512 * 1024); + }); + + it('converts TB string', () => { + assert.equal(cu.convertToBytes('2TB'), 2 * 1024 * 1024 * 1024 * 1024); + }); + + it('treats unrecognized suffix as raw bytes', () => { + assert.equal(cu.convertToBytes('100X'), 100); + }); + + it('converts bare numeric string', () => { + assert.equal(cu.convertToBytes('4096'), 4096); + }); + }); }); From 15c6934fa1c4ef409b88fafff51510fce625ba7b Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Wed, 20 May 2026 17:08:51 -0600 Subject: [PATCH 6/7] refactor(quota): remove storage_quotaSize config; derive quota from status file quota-status.json written by host-manager already carries quotaBytes, so there is no need for a separate Harper config param. The reclamation ratio now comes purely from the file (fresh = quota-aware, stale/absent = statfs). - Remove storage_quotaSize config param and STORAGE_QUOTASIZE hdbTerms key - Remove convertToBytes utility (no longer needed) - Remove getDirectoryUsageBytes, getFreeSpaceBasis, getQuotaInfo, setQuotaSizeBytes - Remove execFile/promisify imports now that du fallback is gone - getDiskInfo derives free_space_basis from quota-status.json presence - Update tests accordingly Co-Authored-By: Claude Sonnet 4.6 --- server/storageReclamation.ts | 59 ++----------- unitTests/server/storageReclamation.test.js | 95 ++------------------- unitTests/utility/common_utils.test.js | 41 --------- utility/common_utils.ts | 25 ------ utility/environment/systemInformation.ts | 19 ++--- utility/hdbTerms.ts | 3 +- validation/configValidator.ts | 3 +- 7 files changed, 27 insertions(+), 218 deletions(-) diff --git a/server/storageReclamation.ts b/server/storageReclamation.ts index 2be262e16..9d39fa953 100644 --- a/server/storageReclamation.ts +++ b/server/storageReclamation.ts @@ -1,17 +1,13 @@ import { readFile } from 'node:fs/promises'; import { statfs } from 'node:fs/promises'; -import { execFile } from 'node:child_process'; import { join } from 'node:path'; -import { promisify } from 'node:util'; import { getWorkerIndex, getWorkerCount } from '../server/threads/manageThreads.js'; import { logger } from '../utility/logging/logger.ts'; import { CONFIG_PARAMS } from '../utility/hdbTerms.ts'; import * as envMgr from '../utility/environment/environmentManager.ts'; -import { convertToMS, convertToBytes } from '../utility/common_utils.ts'; +import { convertToMS } from '../utility/common_utils.ts'; envMgr.initSync(); -const execFileAsync = promisify(execFile); - const reclamationHandlers = new Map< string, { priority: number; handler: (priority: number) => Promise | void }[] @@ -19,11 +15,10 @@ const reclamationHandlers = new Map< const RECLAMATION_THRESHOLD = envMgr.get(CONFIG_PARAMS.STORAGE_RECLAMATION_THRESHOLD) ?? 0.4; // 40% remaining free space is the default const RECLAMATION_INTERVAL = convertToMS(envMgr.get(CONFIG_PARAMS.STORAGE_RECLAMATION_INTERVAL)) || 3600000; // 1 hour is the default -let QUOTA_SIZE_BYTES: number | undefined = convertToBytes(envMgr.get(CONFIG_PARAMS.STORAGE_QUOTASIZE)); // Written by host-manager every ~90s alongside the instance's hdb root const QUOTA_STATUS_FILE = 'quota-status.json'; -// Fall back to du if the file is older than this (host-manager outage, container start race, etc.) +// Use statfs fallback if the file is older than this (host-manager outage, container start race, etc.) const QUOTA_STATUS_MAX_AGE_MS = 5 * 60 * 1000; export type QuotaStatusData = { @@ -47,17 +42,6 @@ export async function getQuotaStatus(): Promise { } } -/** - * Returns the disk block usage (bytes) for a directory path. - * Uses `du -sb` which is a GNU extension available on all Linux deployments. - * The `--` separator guards against paths that start with `-`. - * Used as fallback when the quota-status file is absent or stale. - */ -export async function getDirectoryUsageBytes(dirPath: string): Promise { - const { stdout } = await execFileAsync('du', ['-sb', '--', dirPath]); - return parseInt(stdout, 10); -} - /** * Register a handler to be called when storage free space is low and reclamation is needed. The callback is called * with the priority of the reclamation, which is the ratio of the threshold to the available space ratio. If space is @@ -86,20 +70,12 @@ export function onStorageReclamation( } let reclamationTimer: NodeJS.Timeout; -// Checked at call time so QUOTA_SIZE_BYTES changes (via setQuotaSizeBytes) take effect immediately. -// In quota mode: prefer the host-manager-written quota-status file (O(1)); fall back to -// `du` on the rootPath (O(inodes)) when the file is absent or stale. -// The rootPath `du` covers ALL Harper files (logs, blobs, databases), matching how XFS -// user quotas count usage — as opposed to per-table paths registered for reclamation. +// If a fresh quota-status.json exists (written by host-manager every ~90s), use quota-based ratio. +// Otherwise fall back to statfs for the registered path. const defaultGetAvailableSpaceRatio = async (path: string): Promise => { - if (QUOTA_SIZE_BYTES) { - const rootPath = envMgr.get(CONFIG_PARAMS.ROOTPATH); - const status = await getQuotaStatus(); - const usedBytes = - status && Date.now() - status.updatedAt < QUOTA_STATUS_MAX_AGE_MS - ? status.usedBytes - : await getDirectoryUsageBytes(rootPath ?? path); - return Math.max(0, QUOTA_SIZE_BYTES - usedBytes) / QUOTA_SIZE_BYTES; + const status = await getQuotaStatus(); + if (status?.quotaBytes && Date.now() - status.updatedAt < QUOTA_STATUS_MAX_AGE_MS) { + return Math.max(0, status.quotaBytes - status.usedBytes) / status.quotaBytes; } const fsStats = await statfs(path); return fsStats.bavail / fsStats.blocks; @@ -141,24 +117,3 @@ export function setAvailableSpaceRatioGetter(newGetter?: (path: string) => Promi getAvailableSpaceRatio = newGetter ?? defaultGetAvailableSpaceRatio; } -/** - * Override the quota size in bytes (for testing only). - */ -export function setQuotaSizeBytes(n: number | undefined): void { - QUOTA_SIZE_BYTES = n; -} - -/** - * Returns which basis is used for free-space calculations: 'quota' when storage_quotaSize is - * configured, 'filesystem' otherwise. - */ -export function getFreeSpaceBasis(): 'quota' | 'filesystem' { - return QUOTA_SIZE_BYTES ? 'quota' : 'filesystem'; -} - -/** - * Returns quota config info when storage_quotaSize is configured, undefined otherwise. - */ -export function getQuotaInfo(): { quotaSizeBytes: number } | undefined { - return QUOTA_SIZE_BYTES ? { quotaSizeBytes: QUOTA_SIZE_BYTES } : undefined; -} diff --git a/unitTests/server/storageReclamation.test.js b/unitTests/server/storageReclamation.test.js index e2f1ac2e5..d0356b6b3 100644 --- a/unitTests/server/storageReclamation.test.js +++ b/unitTests/server/storageReclamation.test.js @@ -38,10 +38,9 @@ describe('storageReclamation module', function () { }); afterEach(function () { - // Reset the space ratio getter and quota size + // Reset the space ratio getter if (storageReclamation) { storageReclamation.setAvailableSpaceRatioGetter(null); - storageReclamation.setQuotaSizeBytes(undefined); } // Clear any timers @@ -402,48 +401,23 @@ describe('storageReclamation module', function () { const QUOTA_100GB = 100 * 1024 * 1024 * 1024; let tmpDir; let quotaStatusPath; + let originalRootPath; beforeEach(function () { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'harper-quota-test-')); quotaStatusPath = path.join(tmpDir, 'quota-status.json'); + originalRootPath = env.get('rootPath'); + env.setProperty('rootPath', tmpDir); }); afterEach(function () { + env.setProperty('rootPath', originalRootPath); try { fs.rmSync(tmpDir, { recursive: true }); } catch {} }); - it('getFreeSpaceBasis returns filesystem when no quota configured', function () { - assert.equal(storageReclamation.getFreeSpaceBasis(), 'filesystem'); - }); - - it('getFreeSpaceBasis returns quota when QUOTA_SIZE_BYTES is set', function () { - storageReclamation.setQuotaSizeBytes(QUOTA_100GB); - assert.equal(storageReclamation.getFreeSpaceBasis(), 'quota'); - }); - - it('getQuotaInfo returns undefined when no quota configured', function () { - assert.equal(storageReclamation.getQuotaInfo(), undefined); - }); - - it('getQuotaInfo returns quota size when QUOTA_SIZE_BYTES is set', function () { - storageReclamation.setQuotaSizeBytes(QUOTA_100GB); - assert.deepEqual(storageReclamation.getQuotaInfo(), { quotaSizeBytes: QUOTA_100GB }); - }); - describe('getQuotaStatus', function () { - let originalRootPath; - - beforeEach(function () { - originalRootPath = env.get('rootPath'); - env.setProperty('rootPath', tmpDir); - }); - - afterEach(function () { - env.setProperty('rootPath', originalRootPath); - }); - it('returns parsed object when file is present and valid', async function () { const data = { usedBytes: 50_000_000_000, quotaBytes: QUOTA_100GB, updatedAt: Date.now() }; fs.writeFileSync(quotaStatusPath, JSON.stringify(data)); @@ -460,28 +434,11 @@ describe('storageReclamation module', function () { }); }); - describe('getDirectoryUsageBytes', function () { - it('returns a non-negative integer for a real directory', async function () { - const bytes = await storageReclamation.getDirectoryUsageBytes(tmpDir); - assert.ok(Number.isInteger(bytes)); - assert.ok(bytes >= 0); - }); - }); - describe('defaultGetAvailableSpaceRatio', function () { - let originalRootPath; - beforeEach(function () { - originalRootPath = env.get('rootPath'); - env.setProperty('rootPath', tmpDir); - storageReclamation.setQuotaSizeBytes(QUOTA_100GB); storageReclamation.setAvailableSpaceRatioGetter(undefined); // use real default }); - afterEach(function () { - env.setProperty('rootPath', originalRootPath); - }); - it('uses fresh quota-status file and triggers reclamation when headroom is low', async function () { const usedBytes = 65 * 1024 * 1024 * 1024; // 65 GB → 35% remaining → below 40% threshold fs.writeFileSync( @@ -526,57 +483,23 @@ describe('storageReclamation module', function () { assert.ok(handler.calledOnce); }); - it('falls back to du when quota-status file is absent', async function () { - // No quota-status.json; du reports actual tmpDir usage which is far below 100 GB + it('falls back to statfs when quota-status file is absent', async function () { const handler = sandbox.stub(); storageReclamation.onStorageReclamation(tmpDir, handler, true); - await storageReclamation.runReclamationHandlers(); - - assert.ok(handler.notCalled); + await assert.doesNotReject(storageReclamation.runReclamationHandlers()); }); - it('falls back to du when quota-status file is stale', async function () { + it('falls back to statfs when quota-status file is stale', async function () { const staleTimestamp = Date.now() - 10 * 60 * 1000; // 10 minutes old fs.writeFileSync( quotaStatusPath, JSON.stringify({ usedBytes: 65 * 1024 * 1024 * 1024, quotaBytes: QUOTA_100GB, updatedAt: staleTimestamp }) ); - // Despite the stale "65 GB" reading, du reports actual tmpDir usage (far below 100 GB) const handler = sandbox.stub(); storageReclamation.onStorageReclamation(tmpDir, handler, true); - await storageReclamation.runReclamationHandlers(); - - assert.ok(handler.notCalled); + await assert.doesNotReject(storageReclamation.runReclamationHandlers()); }); }); - - it('quota-aware ratio triggers reclamation when usage exceeds threshold headroom', async function () { - // Quota: 100GB, used: 95GB → 5% available → below 40% threshold → should trigger - const customGetter = sandbox.stub().resolves(0.05); - storageReclamation.setAvailableSpaceRatioGetter(customGetter); - - const handler = sandbox.stub().returns(Promise.resolve()); - storageReclamation.onStorageReclamation('/test/path', handler, true); - - await storageReclamation.runReclamationHandlers(); - - assert.ok(handler.calledOnce); - // priority = 0.4 / 0.05 = 8 - assert.equal(handler.firstCall.args[0], 8); - }); - - it('quota-aware ratio does not trigger reclamation when headroom is healthy', async function () { - // Quota: 100GB, used: 50GB → 50% available → above 40% threshold → no reclamation - const customGetter = sandbox.stub().resolves(0.5); - storageReclamation.setAvailableSpaceRatioGetter(customGetter); - - const handler = sandbox.stub(); - storageReclamation.onStorageReclamation('/test/path', handler, true); - - await storageReclamation.runReclamationHandlers(); - - assert.ok(handler.notCalled); - }); }); }); diff --git a/unitTests/utility/common_utils.test.js b/unitTests/utility/common_utils.test.js index e883c4581..cd093bf06 100644 --- a/unitTests/utility/common_utils.test.js +++ b/unitTests/utility/common_utils.test.js @@ -567,45 +567,4 @@ describe('Test common_utils module', () => { expect(c).to.equal('52y 27d 20h 27m 14s'); }); - describe('convertToBytes', () => { - it('returns undefined for null', () => { - assert.equal(cu.convertToBytes(null), undefined); - }); - - it('returns undefined for undefined', () => { - assert.equal(cu.convertToBytes(undefined), undefined); - }); - - it('passes through a bare number', () => { - assert.equal(cu.convertToBytes(1024), 1024); - }); - - it('converts GB string', () => { - assert.equal(cu.convertToBytes('100GB'), 100 * 1024 * 1024 * 1024); - }); - - it('converts G string', () => { - assert.equal(cu.convertToBytes('10G'), 10 * 1024 * 1024 * 1024); - }); - - it('converts string with space before suffix', () => { - assert.equal(cu.convertToBytes('1.5 GB'), Math.floor(1.5 * 1024 * 1024 * 1024)); - }); - - it('converts KB string', () => { - assert.equal(cu.convertToBytes('512KB'), 512 * 1024); - }); - - it('converts TB string', () => { - assert.equal(cu.convertToBytes('2TB'), 2 * 1024 * 1024 * 1024 * 1024); - }); - - it('treats unrecognized suffix as raw bytes', () => { - assert.equal(cu.convertToBytes('100X'), 100); - }); - - it('converts bare numeric string', () => { - assert.equal(cu.convertToBytes('4096'), 4096); - }); - }); }); diff --git a/utility/common_utils.ts b/utility/common_utils.ts index e2c828551..8b9b14819 100644 --- a/utility/common_utils.ts +++ b/utility/common_utils.ts @@ -798,31 +798,6 @@ export function transformReq(req: any) { if (req.database) req.schema = req.database; } -export function convertToBytes(size: any): number | undefined { - if (size == null) return undefined; - if (typeof size === 'number') return size; - if (typeof size === 'string') { - const num = parseFloat(size); - const suffix = size.replace(/^[\d.]+\s*/, '').toUpperCase(); - switch (suffix) { - case 'K': - case 'KB': - return Math.floor(num * 1024); - case 'M': - case 'MB': - return Math.floor(num * 1024 * 1024); - case 'G': - case 'GB': - return Math.floor(num * 1024 * 1024 * 1024); - case 'T': - case 'TB': - return Math.floor(num * 1024 * 1024 * 1024 * 1024); - default: - return Math.floor(num); - } - } - return undefined; -} export function convertToMS(interval: any) { let seconds = 0; if (typeof interval === 'number') seconds = interval; diff --git a/utility/environment/systemInformation.ts b/utility/environment/systemInformation.ts index eee49b055..657a8daa9 100644 --- a/utility/environment/systemInformation.ts +++ b/utility/environment/systemInformation.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import si from 'systeminformation'; import logger from '../logging/harper_logger.ts'; import * as hdbTerms from '../hdbTerms.ts'; -import { getFreeSpaceBasis, getQuotaInfo, getQuotaStatus } from '../../server/storageReclamation.ts'; +import { getQuotaStatus } from '../../server/storageReclamation.ts'; import { lmdbGetTableSize } from '../../dataLayer/harperBridge/lmdbBridge/lmdbUtility/lmdbGetTableSize.ts'; import { getThreadInfo } from '../../server/threads/manageThreads.js'; import * as env from './environmentManager.ts'; @@ -277,15 +277,14 @@ type DiskInfo = { */ export async function getDiskInfo(): Promise { const disk: DiskInfo = {}; - disk.free_space_basis = getFreeSpaceBasis(); - const quotaInfo = getQuotaInfo(); - if (quotaInfo) { - disk.quota_size_bytes = quotaInfo.quotaSizeBytes; - const status = await getQuotaStatus(); - if (status) { - disk.quota_used_bytes = status.usedBytes; - disk.quota_status_age_seconds = Math.floor((Date.now() - status.updatedAt) / 1000); - } + const quotaStatus = await getQuotaStatus(); + if (quotaStatus?.quotaBytes) { + disk.free_space_basis = 'quota'; + disk.quota_size_bytes = quotaStatus.quotaBytes; + disk.quota_used_bytes = quotaStatus.usedBytes; + disk.quota_status_age_seconds = Math.floor((Date.now() - quotaStatus.updatedAt) / 1000); + } else { + disk.free_space_basis = 'filesystem'; } try { if (!env.get(hdbTerms.CONFIG_PARAMS.OPERATIONSAPI_SYSINFO_DISK)) return disk; diff --git a/utility/hdbTerms.ts b/utility/hdbTerms.ts index 7c5b0b8f4..e943917e0 100644 --- a/utility/hdbTerms.ts +++ b/utility/hdbTerms.ts @@ -578,8 +578,7 @@ export const CONFIG_PARAMS = { STORAGE_RECLAMATION_THRESHOLD: 'storage_reclamation_threshold', STORAGE_RECLAMATION_INTERVAL: 'storage_reclamation_interval', STORAGE_RECLAMATION_EVICTIONFACTOR: 'storage_reclamation_evictionFactor', - STORAGE_QUOTASIZE: 'storage_quotaSize', - STORAGE_ENGINE: 'storage_engine', +STORAGE_ENGINE: 'storage_engine', STORAGE_READONLY: 'storage_readOnly', DATABASES: 'databases', IGNORE_SCRIPTS: 'ignoreScripts', diff --git a/validation/configValidator.ts b/validation/configValidator.ts index 2d0bce85a..2d2b647aa 100644 --- a/validation/configValidator.ts +++ b/validation/configValidator.ts @@ -194,8 +194,7 @@ export function configValidator(configJson, skipFsValidation = false) { prefetchWrites: boolean.optional(), maxFreeSpaceToLoad: number.optional(), maxFreeSpaceToRetain: number.optional(), - quotaSize: Joi.alternatives([number, string]).optional(), - }).required(), +}).required(), ignoreScripts: boolean.optional(), tls: Joi.alternatives([Joi.array().items(tlsConstraints), tlsConstraints]), }); From c0c80d196e58a5ef61c023b2c60487e72cf63df5 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Wed, 20 May 2026 17:11:57 -0600 Subject: [PATCH 7/7] style: apply formatter --- server/storageReclamation.ts | 1 - unitTests/utility/common_utils.test.js | 1 - utility/hdbTerms.ts | 2 +- validation/configValidator.ts | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/server/storageReclamation.ts b/server/storageReclamation.ts index 9d39fa953..8a3950150 100644 --- a/server/storageReclamation.ts +++ b/server/storageReclamation.ts @@ -116,4 +116,3 @@ export async function runReclamationHandlers() { export function setAvailableSpaceRatioGetter(newGetter?: (path: string) => Promise) { getAvailableSpaceRatio = newGetter ?? defaultGetAvailableSpaceRatio; } - diff --git a/unitTests/utility/common_utils.test.js b/unitTests/utility/common_utils.test.js index cd093bf06..bf38779a2 100644 --- a/unitTests/utility/common_utils.test.js +++ b/unitTests/utility/common_utils.test.js @@ -566,5 +566,4 @@ describe('Test common_utils module', () => { const c = cu_rewire.ms_to_time(1672345634534); expect(c).to.equal('52y 27d 20h 27m 14s'); }); - }); diff --git a/utility/hdbTerms.ts b/utility/hdbTerms.ts index e943917e0..0c65031b0 100644 --- a/utility/hdbTerms.ts +++ b/utility/hdbTerms.ts @@ -578,7 +578,7 @@ export const CONFIG_PARAMS = { STORAGE_RECLAMATION_THRESHOLD: 'storage_reclamation_threshold', STORAGE_RECLAMATION_INTERVAL: 'storage_reclamation_interval', STORAGE_RECLAMATION_EVICTIONFACTOR: 'storage_reclamation_evictionFactor', -STORAGE_ENGINE: 'storage_engine', + STORAGE_ENGINE: 'storage_engine', STORAGE_READONLY: 'storage_readOnly', DATABASES: 'databases', IGNORE_SCRIPTS: 'ignoreScripts', diff --git a/validation/configValidator.ts b/validation/configValidator.ts index 2d2b647aa..73bcab9aa 100644 --- a/validation/configValidator.ts +++ b/validation/configValidator.ts @@ -194,7 +194,7 @@ export function configValidator(configJson, skipFsValidation = false) { prefetchWrites: boolean.optional(), maxFreeSpaceToLoad: number.optional(), maxFreeSpaceToRetain: number.optional(), -}).required(), + }).required(), ignoreScripts: boolean.optional(), tls: Joi.alternatives([Joi.array().items(tlsConstraints), tlsConstraints]), });