mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-17 07:40:06 +08:00
perf: bound runtime memory and storage usage
This commit is contained in:
@@ -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);
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
@@ -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 },
|
||||
);
|
||||
});
|
||||
@@ -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' });
|
||||
});
|
||||
Reference in New Issue
Block a user