perf: bound runtime memory and storage usage

This commit is contained in:
whyour
2026-08-16 16:29:06 +08:00
parent 31f78e4d1f
commit 276dfc2382
30 changed files with 1350 additions and 68 deletions
+28
View File
@@ -0,0 +1,28 @@
const assert = require('node:assert/strict');
const test = require('node:test');
const configPath = require.resolve('../../ecosystem.config');
function loadConfig(containerValue) {
if (containerValue === undefined) {
delete process.env.QL_CONTAINER;
} else {
process.env.QL_CONTAINER = containerValue;
}
delete require.cache[configPath];
return require(configPath).apps[0];
}
test('container logging goes to the container standard streams', () => {
const app = loadConfig('true');
assert.equal(app.out_file, '/proc/1/fd/1');
assert.equal(app.error_file, '/proc/1/fd/2');
assert.equal(app.time, false);
});
test('non-container installs keep the PM2 logging defaults', () => {
const app = loadConfig(undefined);
assert.equal(app.out_file, undefined);
assert.equal(app.error_file, undefined);
assert.equal(app.time, true);
});
+72
View File
@@ -0,0 +1,72 @@
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const test = require('node:test');
const {
DEFAULT_LOG_CHUNK_BYTES,
MAX_LOG_CHUNK_BYTES,
readLogChunk,
} = require('../../back/shared/logReader');
test('log reader defaults to a bounded tail and supports incremental reads', async (t) => {
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-log-reader-'));
const file = path.join(directory, 'task.log');
t.after(() => fs.rm(directory, { recursive: true, force: true }));
const initial = 'a'.repeat(DEFAULT_LOG_CHUNK_BYTES * 2);
await fs.writeFile(file, initial);
const tail = await readLogChunk(file);
assert.equal(tail.content.length, DEFAULT_LOG_CHUNK_BYTES);
assert.equal(tail.offset, DEFAULT_LOG_CHUNK_BYTES);
assert.equal(tail.nextOffset, initial.length);
assert.equal(tail.truncated, true);
await fs.appendFile(file, 'next');
const incremental = await readLogChunk(file, { offset: tail.nextOffset });
assert.equal(incremental.content, 'next');
assert.equal(incremental.nextOffset, initial.length + 4);
});
test('log reader enforces the maximum chunk size', async (t) => {
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-log-reader-'));
const file = path.join(directory, 'large.log');
t.after(() => fs.rm(directory, { recursive: true, force: true }));
await fs.writeFile(file, 'x'.repeat(MAX_LOG_CHUNK_BYTES + 1024));
const chunk = await readLogChunk(file, { limit: Number.MAX_SAFE_INTEGER });
assert.equal(Buffer.byteLength(chunk.content), MAX_LOG_CHUNK_BYTES);
});
test('log reader preserves UTF-8 characters across byte boundaries', async (t) => {
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-log-reader-'));
const file = path.join(directory, 'utf8.log');
t.after(() => fs.rm(directory, { recursive: true, force: true }));
await fs.writeFile(file, 'a'.repeat(5) + '中文日志');
const first = await readLogChunk(file, { offset: 0, limit: 7 });
const second = await readLogChunk(file, {
offset: first.nextOffset,
limit: 7,
});
const third = await readLogChunk(file, {
offset: second.nextOffset,
limit: 7,
});
assert.equal(first.content + second.content + third.content, 'aaaaa中文日志');
assert.equal((first.content + second.content + third.content).includes(''), false);
});
test('missing logs return an empty chunk', async () => {
const chunk = await readLogChunk('/path/that/does/not/exist.log');
assert.deepEqual(chunk, {
content: '',
offset: 0,
nextOffset: 0,
total: 0,
truncated: false,
});
});
+34
View File
@@ -0,0 +1,34 @@
const assert = require('node:assert/strict');
const test = require('node:test');
const {
MAX_METRIC_SAMPLES,
metricsService,
} = require('../../back/services/metrics');
test('metrics keep only the newest bounded samples', () => {
const sampleCount = MAX_METRIC_SAMPLES * 50;
for (let index = 0; index < sampleCount; index++) {
metricsService.record('http_request', index, {
path: `/health/${index}`,
});
}
const result = metricsService.getMetrics('http_request');
assert.equal(result.count, MAX_METRIC_SAMPLES);
assert.equal(result.metrics[0].value, sampleCount - MAX_METRIC_SAMPLES);
assert.equal(result.metrics.at(-1).value, sampleCount - 1);
});
test('empty metric queries return finite aggregates', () => {
const result = metricsService.getMetrics('missing_metric');
assert.deepEqual(
{
count: result.count,
average: result.average,
min: result.min,
max: result.max,
},
{ count: 0, average: 0, min: 0, max: 0 },
);
});
+131
View File
@@ -0,0 +1,131 @@
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const test = require('node:test');
const { Op } = require('sequelize');
const {
getDirectorySize,
normalizeRetentionPolicy,
} = require('../../back/shared/retention');
test('retention policy defaults to disabled and clamps invalid values', () => {
assert.deepEqual(normalizeRetentionPolicy({}), {
runningInstanceRetentionDays: 0,
cronStatRetentionDays: 0,
});
assert.deepEqual(
normalizeRetentionPolicy({
runningInstanceRetentionDays: -10,
cronStatRetentionDays: 99999,
}),
{
runningInstanceRetentionDays: 0,
cronStatRetentionDays: 3650,
},
);
});
test('directory preview counts files recursively and ignores symlinks', async (t) => {
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-retention-'));
const nested = path.join(directory, 'nested');
t.after(() => fs.rm(directory, { recursive: true, force: true }));
await fs.mkdir(nested);
await fs.writeFile(path.join(directory, 'one'), Buffer.alloc(7));
await fs.writeFile(path.join(nested, 'two'), Buffer.alloc(11));
await fs.symlink(path.join(nested, 'two'), path.join(directory, 'link'));
assert.equal(await getDirectorySize(directory), 18);
assert.equal(await getDirectorySize(path.join(directory, 'missing')), 0);
});
test('cleanup previews first, protects running instances, and uses explicit options', async (t) => {
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-retention-'));
const nodeCache = path.join(directory, 'node');
await fs.mkdir(nodeCache);
await fs.writeFile(path.join(nodeCache, 'cache'), Buffer.alloc(13));
const config = require('../../back/config').default;
const originalCachePath = config.dependenceCachePath;
const RunningInstanceModel = {};
const CrontabStatModel = {};
const InstanceStatus = { running: 0 };
const sequelize = {};
const stubModule = (modulePath, exports) => {
require.cache[require.resolve(modulePath)] = {
id: require.resolve(modulePath),
filename: require.resolve(modulePath),
loaded: true,
exports,
children: [],
paths: [],
};
};
stubModule('../../back/data', { sequelize });
stubModule('../../back/data/cronStats', { CrontabStatModel });
stubModule('../../back/data/runningInstance', {
InstanceStatus,
RunningInstanceModel,
});
stubModule('../../back/data/system', {
AuthDataType: { systemConfig: 'systemConfig' },
SystemModel: {},
});
delete require.cache[require.resolve('../../back/services/retention')];
const RetentionService = require('../../back/services/retention').default;
t.after(async () => {
config.dependenceCachePath = originalCachePath;
await fs.rm(directory, { recursive: true, force: true });
});
let instanceWhere;
let vacuumed = false;
let instanceCountCalls = 0;
let statCountCalls = 0;
config.dependenceCachePath = directory;
RunningInstanceModel.count = async ({ where }) => {
instanceCountCalls++;
instanceWhere = where;
return 2;
};
RunningInstanceModel.destroy = async ({ where }) => {
instanceWhere = where;
return 2;
};
CrontabStatModel.count = async () => {
statCountCalls++;
return 3;
};
CrontabStatModel.destroy = async () => 3;
sequelize.transaction = async (callback) => callback({});
sequelize.query = async (query) => {
vacuumed = query === 'VACUUM';
return [];
};
const service = new RetentionService();
const disabledPreview = await service.preview({
runningInstanceRetentionDays: 0,
cronStatRetentionDays: 0,
});
assert.equal(disabledPreview.runningInstances, 0);
assert.equal(disabledPreview.cronStats, 0);
assert.equal(instanceCountCalls, 0);
assert.equal(statCountCalls, 0);
const result = await service.cleanup({
runningInstanceRetentionDays: 30,
cronStatRetentionDays: 90,
dependenceCacheTypes: ['node'],
compactDatabase: true,
});
assert.equal(instanceWhere.status[Op.ne], InstanceStatus.running);
assert.equal(result.deleted.runningInstances, 2);
assert.equal(result.deleted.cronStats, 3);
assert.equal(result.deleted.dependenceCacheBytes, 13);
assert.equal(vacuumed, true);
await assert.rejects(fs.stat(nodeCache), { code: 'ENOENT' });
});