Skip to content

Commit c67a79f

Browse files
authored
Fix deletion cron so that it can handle large bucket sizes (#45)
1 parent f83cab7 commit c67a79f

File tree

2 files changed

+41
-12
lines changed

2 files changed

+41
-12
lines changed

src/crons/deleteOldCache.test.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { MockedFunction, afterEach, beforeEach, expect, test, vi } from 'vitest';
22
import { Env } from '../';
33
import { isDateOlderThan } from '../utils/date';
4-
import { deleteOldCache } from './deleteOldCache';
4+
import { CURSOR_SIZE, deleteOldCache } from './deleteOldCache';
55

66
vi.mock('../utils/date', async (importActual) => {
77
const actual = await importActual<typeof import('../utils/date')>();
@@ -48,7 +48,21 @@ describe('R2 delete cron', () => {
4848

4949
test('should delete all artifacts when there are more than CURSOR_SIZE', async () => {
5050
isDateOlderThanMock.mockReturnValue(true);
51-
for (let i = 0; i < 1000; i++) {
51+
for (let i = 0; i < Math.round(CURSOR_SIZE * 1.5); i++) {
52+
await workerEnv.R2_STORE.put(`${teamId}/${artifactId}-${i}`, artifactContent, {
53+
customMetadata: { artifactTag },
54+
});
55+
}
56+
57+
await deleteOldCache(workerEnv);
58+
59+
const artifacts = await workerEnv.R2_STORE.list();
60+
expect(artifacts.objects.length).toEqual(0);
61+
});
62+
63+
test('should delete all artifacts when there are over 1000 items ready for deletion', async () => {
64+
isDateOlderThanMock.mockReturnValue(true);
65+
for (let i = 0; i < 1001; i++) {
5266
await workerEnv.R2_STORE.put(`${teamId}/${artifactId}-${i}`, artifactContent, {
5367
customMetadata: { artifactTag },
5468
});

src/crons/deleteOldCache.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,46 @@
11
import { Env } from '..';
22
import { isDateOlderThan } from '../utils/date';
33

4-
const CURSOR_SIZE = 500;
4+
// Cursor size should be kept below 1000 to avoid limits on bulk operations
5+
export const CURSOR_SIZE = 500;
56

6-
export async function deleteOldCache(env: Env) {
7+
class R2KeysForDeletion {
8+
keys: string[] = [];
9+
add(key: string) {
10+
this.keys.push(key);
11+
}
12+
}
13+
14+
export async function deleteOldCache(env: Env): Promise<void> {
715
const BUCKET_CUTOFF_HOURS = env.BUCKET_OBJECT_EXPIRATION_HOURS;
816
let truncated = false;
917
let cursor: string | undefined;
1018
let list: R2Objects;
11-
const keysMarkedForDeletion: string[] = [];
19+
const keysMarkedForDeletion: R2KeysForDeletion[] = [];
1220

1321
do {
1422
list = await env.R2_STORE.list({ limit: CURSOR_SIZE, cursor });
1523
truncated = list.truncated;
1624
cursor = list.truncated ? list.cursor : undefined;
1725

26+
/**
27+
* Deleting keys while iterating over the list can sometimes cause the list to be truncated.
28+
* So we mark the keys for deletion and delete after at least one additional iteration.
29+
*/
30+
const keysAvailableForDeletion = keysMarkedForDeletion.shift();
31+
if (keysAvailableForDeletion) {
32+
await env.R2_STORE.delete(keysAvailableForDeletion.keys);
33+
}
34+
35+
const keysForDeletion = new R2KeysForDeletion();
1836
for (const object of list.objects) {
1937
if (isDateOlderThan(object.uploaded, BUCKET_CUTOFF_HOURS)) {
20-
keysMarkedForDeletion.push(object.key);
38+
keysForDeletion.add(object.key);
2139
}
2240
}
41+
keysMarkedForDeletion.push(keysForDeletion);
2342
} while (truncated);
24-
25-
if (keysMarkedForDeletion.length > 0) {
26-
// if (env.ENVIRONMENT === 'development') {
27-
// console.log(`Deleting ${keysMarkedForDeletion.length} keys`, keysMarkedForDeletion);
28-
// }
29-
await env.R2_STORE.delete(keysMarkedForDeletion);
43+
for (const keysForDeletion of keysMarkedForDeletion) {
44+
await env.R2_STORE.delete(keysForDeletion.keys);
3045
}
3146
}

0 commit comments

Comments
 (0)