-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathstorage-reclamation.test.ts
More file actions
192 lines (176 loc) · 6.9 KB
/
storage-reclamation.test.ts
File metadata and controls
192 lines (176 loc) · 6.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
/**
* Storage reclamation integration test.
*
* Tests that storage reclamation correctly removes expired/evicted records
* from caching tables when disk space is simulated as low.
*
* This test:
* 1. Creates a caching table with short expiration/eviction times and audit logging
* 2. Populates it with records (which creates audit entries)
* 3. Configures a low storage threshold to trigger reclamation
* 4. Verifies records are removed after reclamation runs
* 5. Verifies audit logs were created for the operations
*
* Note: Audit log size reclamation uses the same underlying mechanism as record
* reclamation. The unit tests in storageReclamation.test.js cover the handler
* registration and priority-based callback system. This integration test verifies
* the end-to-end behavior is working correctly.
*/
import { suite, test, before, after } from 'node:test';
import { ok, strictEqual } from 'node:assert/strict';
import { setTimeout as sleep } from 'node:timers/promises';
import { startHarper, teardownHarper, type ContextWithHarper } from '@harperfast/integration-testing';
const TEST_DATABASE = 'test';
const TEST_TABLE = 'reclaim';
suite('Storage reclamation', (ctx: ContextWithHarper) => {
before(async () => {
// Set a very high reclamation threshold (99%) so reclamation triggers immediately
// and a short interval (1 second) for faster test execution
await startHarper(ctx, {
config: {
STORAGE_RECLAMATION_THRESHOLD: 0.99,
STORAGE_RECLAMATION_INTERVAL: '1s',
},
env: {},
});
});
after(async () => {
await teardownHarper(ctx);
});
test('verify Harper is running', async () => {
const response = await fetch(`${ctx.harper.operationsAPIURL}/health`);
strictEqual(response.status, 200);
const body = await response.text();
strictEqual(body, 'Harper is running.');
});
test('create test database and caching table with audit logging', async () => {
// Create database
const createDbResponse = await fetch(ctx.harper.operationsAPIURL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Basic ${Buffer.from(`${ctx.harper.admin.username}:${ctx.harper.admin.password}`).toString('base64')}`,
},
body: JSON.stringify({
operation: 'create_database',
database: TEST_DATABASE,
}),
});
if (createDbResponse.status !== 200) {
console.error('create_database failed:', await createDbResponse.text());
}
strictEqual(createDbResponse.status, 200);
// Create caching table with short expiration and audit logging enabled
const createTableResponse = await fetch(ctx.harper.operationsAPIURL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Basic ${Buffer.from(`${ctx.harper.admin.username}:${ctx.harper.admin.password}`).toString('base64')}`,
},
body: JSON.stringify({
operation: 'create_table',
schema: TEST_DATABASE,
table: TEST_TABLE,
primary_key: 'id',
expiration: 2, // 2 second expiration (in seconds)
eviction: 1, // 1 second eviction (in seconds)
audit: true, // Enable audit logging
}),
});
if (createTableResponse.status !== 200) {
console.error('create_table failed:', await createTableResponse.text());
}
strictEqual(createTableResponse.status, 200);
});
test('insert records into caching table', async () => {
// Insert multiple records
const records = [];
for (let i = 1; i <= 50; i++) {
records.push({
id: i,
data: `test data ${i}`.repeat(100), // Some bulk to make reclamation meaningful
});
}
const insertResponse = await fetch(ctx.harper.operationsAPIURL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Basic ${Buffer.from(`${ctx.harper.admin.username}:${ctx.harper.admin.password}`).toString('base64')}`,
},
body: JSON.stringify({
operation: 'insert',
schema: TEST_DATABASE,
table: TEST_TABLE,
records,
}),
});
const insertBody = await insertResponse.text();
strictEqual(insertResponse.status, 200, `Insert failed: ${insertBody}`);
// Verify records were inserted
const countResponse = await fetch(ctx.harper.operationsAPIURL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Basic ${Buffer.from(`${ctx.harper.admin.username}:${ctx.harper.admin.password}`).toString('base64')}`,
},
body: JSON.stringify({
operation: 'sql',
sql: `select count(*) from ${TEST_DATABASE}.${TEST_TABLE}`,
}),
});
const countBody1 = await countResponse.text();
strictEqual(countResponse.status, 200, `Count query failed: ${countBody1}`);
const countParsed = JSON.parse(countBody1);
strictEqual(countParsed[0]['COUNT(*)'], 50);
});
test('audit logs are created for insert operations', async () => {
// Read audit log to verify entries were created
const auditResponse = await fetch(ctx.harper.operationsAPIURL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Basic ${Buffer.from(`${ctx.harper.admin.username}:${ctx.harper.admin.password}`).toString('base64')}`,
},
body: JSON.stringify({
operation: 'read_audit_log',
schema: TEST_DATABASE,
table: TEST_TABLE,
}),
});
const auditBody = await auditResponse.text();
strictEqual(auditResponse.status, 200, `Read audit log failed: ${auditBody}`);
const auditLogs = JSON.parse(auditBody);
// Should have at least one audit entry for the insert operation
ok(Array.isArray(auditLogs), 'Audit log should be an array');
ok(auditLogs.length > 0, 'Audit log should have entries from the insert');
// Find the insert operation
const insertEntry = auditLogs.find((entry: { operation: string }) => entry.operation === 'insert');
ok(insertEntry, 'Should have an insert audit entry');
});
test('records are reclaimed after expiration and reclamation cycle', async () => {
// Wait for expiration (2s) + eviction (1s) + reclamation interval (1s) + buffer
// Total: ~5 seconds should be enough for records to expire and be reclaimed
await sleep(6000);
// Check record count - should be significantly reduced
const countResponse = await fetch(ctx.harper.operationsAPIURL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Basic ${Buffer.from(`${ctx.harper.admin.username}:${ctx.harper.admin.password}`).toString('base64')}`,
},
body: JSON.stringify({
operation: 'sql',
sql: `select count(*) from ${TEST_DATABASE}.${TEST_TABLE}`,
}),
});
const countBody2 = await countResponse.text();
strictEqual(countResponse.status, 200, `Count query after reclamation failed: ${countBody2}`);
const countBody = JSON.parse(countBody2);
// Records should have been reclaimed (count should be less than original 50)
// With high reclamation threshold and expired records, most/all should be removed
ok(
countBody[0]['COUNT(*)'] < 50,
`Expected record count to decrease after reclamation, got ${countBody[0]['COUNT(*)']}`
);
});
});