Skip to content

Commit cb4043f

Browse files
OTLegendOTLegend
authored andcommitted
Add test coverage for operation cache cleanup
1 parent 3609d94 commit cb4043f

File tree

3 files changed

+176
-2
lines changed

3 files changed

+176
-2
lines changed

src/commands/cleaners/operation-id-cleaner-command.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,18 @@ class OperationIdCleanerCommand extends Command {
2424
* @param command
2525
*/
2626
async execute() {
27-
const memoryBytes = this.operationIdService.getOperationIdMemoryCacheSizeBytes();
28-
const fileBytes = await this.operationIdService.getOperationIdFileCacheSizeBytes();
27+
let memoryBytes = 0;
28+
let fileBytes = 0;
29+
try {
30+
memoryBytes = this.operationIdService.getOperationIdMemoryCacheSizeBytes();
31+
} catch (error) {
32+
this.logger.warn(`Unable to read memory cache footprint: ${error.message}`);
33+
}
34+
try {
35+
fileBytes = await this.operationIdService.getOperationIdFileCacheSizeBytes();
36+
} catch (error) {
37+
this.logger.warn(`Unable to read file cache footprint: ${error.message}`);
38+
}
2939
const bytesInMegabyte = 1024 * 1024;
3040
this.logger.debug(
3141
`Operation cache footprint before cleanup: memory=${(
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { describe, it, beforeEach, afterEach } from 'mocha';
2+
import { expect } from 'chai';
3+
import sinon from 'sinon';
4+
5+
import OperationIdCleanerCommand from '../../../src/commands/cleaners/operation-id-cleaner-command.js';
6+
import {
7+
OPERATION_ID_COMMAND_CLEANUP_TIME_MILLS,
8+
OPERATION_ID_FILES_FOR_REMOVAL_MAX_NUMBER,
9+
OPERATION_ID_MEMORY_CLEANUP_TIME_MILLS,
10+
OPERATION_ID_STATUS,
11+
} from '../../../src/constants/constants.js';
12+
13+
describe('OperationIdCleanerCommand', () => {
14+
let clock;
15+
let operationIdService;
16+
let repositoryModuleManager;
17+
let logger;
18+
let command;
19+
20+
beforeEach(() => {
21+
clock = sinon.useFakeTimers(new Date('2023-01-01T00:00:00Z').getTime());
22+
23+
operationIdService = {
24+
getOperationIdMemoryCacheSizeBytes: sinon.stub().returns(1024),
25+
getOperationIdFileCacheSizeBytes: sinon.stub().resolves(2048),
26+
removeExpiredOperationIdMemoryCache: sinon.stub().resolves(512),
27+
removeExpiredOperationIdFileCache: sinon.stub().resolves(3),
28+
};
29+
30+
repositoryModuleManager = {
31+
removeOperationIdRecord: sinon.stub().resolves(),
32+
};
33+
34+
logger = {
35+
debug: sinon.spy(),
36+
info: sinon.spy(),
37+
warn: sinon.spy(),
38+
error: sinon.spy(),
39+
};
40+
41+
command = new OperationIdCleanerCommand({
42+
logger,
43+
repositoryModuleManager,
44+
operationIdService,
45+
fileService: {},
46+
});
47+
});
48+
49+
afterEach(() => {
50+
clock.restore();
51+
});
52+
53+
it('cleans memory with 1h TTL and files with 24h TTL while reporting footprint', async () => {
54+
await command.execute();
55+
56+
expect(operationIdService.getOperationIdMemoryCacheSizeBytes.calledOnce).to.be.true;
57+
expect(operationIdService.getOperationIdFileCacheSizeBytes.calledOnce).to.be.true;
58+
59+
expect(
60+
repositoryModuleManager.removeOperationIdRecord.calledWith(
61+
Date.now() - OPERATION_ID_COMMAND_CLEANUP_TIME_MILLS,
62+
[OPERATION_ID_STATUS.COMPLETED, OPERATION_ID_STATUS.FAILED],
63+
),
64+
).to.be.true;
65+
66+
expect(
67+
operationIdService.removeExpiredOperationIdMemoryCache.calledWith(
68+
OPERATION_ID_MEMORY_CLEANUP_TIME_MILLS,
69+
),
70+
).to.be.true;
71+
72+
expect(
73+
operationIdService.removeExpiredOperationIdFileCache.calledWith(
74+
OPERATION_ID_COMMAND_CLEANUP_TIME_MILLS,
75+
OPERATION_ID_FILES_FOR_REMOVAL_MAX_NUMBER,
76+
),
77+
).to.be.true;
78+
79+
expect(logger.debug.called).to.be.true;
80+
});
81+
82+
it('handles missing memory cache gracefully', async () => {
83+
operationIdService.getOperationIdMemoryCacheSizeBytes.throws(new Error('no memory cache'));
84+
await command.execute();
85+
86+
expect(
87+
repositoryModuleManager.removeOperationIdRecord.calledWith(
88+
Date.now() - OPERATION_ID_COMMAND_CLEANUP_TIME_MILLS,
89+
[OPERATION_ID_STATUS.COMPLETED, OPERATION_ID_STATUS.FAILED],
90+
),
91+
).to.be.true;
92+
93+
expect(
94+
operationIdService.removeExpiredOperationIdFileCache.calledWith(
95+
OPERATION_ID_COMMAND_CLEANUP_TIME_MILLS,
96+
OPERATION_ID_FILES_FOR_REMOVAL_MAX_NUMBER,
97+
),
98+
).to.be.true;
99+
});
100+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { describe, it, beforeEach, afterEach } from 'mocha';
2+
import { expect } from 'chai';
3+
import fs from 'fs/promises';
4+
import path from 'path';
5+
import os from 'os';
6+
import OperationIdService from '../../../src/service/operation-id-service.js';
7+
8+
describe('OperationIdService file cache cleanup', () => {
9+
let tmpDir;
10+
let service;
11+
12+
beforeEach(async () => {
13+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'opid-cache-'));
14+
const now = Date.now();
15+
16+
// Older than TTL (2 hours)
17+
const oldFile = path.join(tmpDir, 'old.json');
18+
await fs.writeFile(oldFile, '{}');
19+
await fs.utimes(
20+
oldFile,
21+
new Date(now - 2 * 60 * 60 * 1000),
22+
new Date(now - 2 * 60 * 60 * 1000),
23+
);
24+
25+
// Newer than TTL (10 minutes)
26+
const newFile = path.join(tmpDir, 'new.json');
27+
await fs.writeFile(newFile, '{}');
28+
await fs.utimes(newFile, new Date(now - 10 * 60 * 1000), new Date(now - 10 * 60 * 1000));
29+
30+
const fileService = {
31+
getOperationIdCachePath: () => tmpDir,
32+
async pathExists(p) {
33+
try {
34+
await fs.stat(p);
35+
return true;
36+
} catch {
37+
return false;
38+
}
39+
},
40+
readDirectory: (p) => fs.readdir(p),
41+
stat: (p) => fs.stat(p),
42+
removeFile: (p) => fs.rm(p, { force: true }),
43+
};
44+
45+
service = new OperationIdService({
46+
logger: { debug: () => {}, warn: () => {}, error: () => {} },
47+
fileService,
48+
repositoryModuleManager: {},
49+
eventEmitter: { emit: () => {} },
50+
});
51+
});
52+
53+
afterEach(async () => {
54+
await fs.rm(tmpDir, { recursive: true, force: true });
55+
});
56+
57+
it('removes only files older than TTL', async () => {
58+
const deleted = await service.removeExpiredOperationIdFileCache(60 * 60 * 1000, 10);
59+
const remainingFiles = await fs.readdir(tmpDir);
60+
61+
expect(deleted).to.equal(1);
62+
expect(remainingFiles).to.deep.equal(['new.json']);
63+
});
64+
});

0 commit comments

Comments
 (0)