Skip to content

Commit e82c94d

Browse files
jonasfjisoos
andauthored
Introduce ExportedPackage.garbageCollect(allVersionNumbers). (#8193)
* Introduce `ExportedPackage.garbageCollect(allVersionNumbers)`. * Garbage collection of version numbers, specifically archives. * Use `tryDelete` instead of `delete` to avoid errors from racing deletion, when GCS then returns 404. * Introduce `_minGarbageAge` to avoid deleting files before they are at-least a day old. This way GC won't race against creation of new files. * Apply suggestions from code review Co-authored-by: István Soós <[email protected]> --------- Co-authored-by: István Soós <[email protected]>
1 parent f71b241 commit e82c94d

File tree

1 file changed

+41
-7
lines changed

1 file changed

+41
-7
lines changed

app/lib/package/api_export/exported_api.dart

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ import '../../shared/versions.dart'
2020

2121
final _log = Logger('api_export:exported_bucket');
2222

23+
/// Minimum age before an item can be consider garbage.
24+
///
25+
/// This ensures that we don't delete files we've just created.
26+
/// It's entirely possible that one process is writing files, while another
27+
/// process is running garbage collection.
28+
const _minGarbageAge = Duration(days: 1);
29+
2330
/// Interface for [Bucket] containing exported API that is served directly from
2431
/// Google Cloud Storage.
2532
///
@@ -94,8 +101,8 @@ final class ExportedApi {
94101
}
95102
if (!allPackageNames.contains(packageName)) {
96103
final info = await _bucket.info(item.name);
97-
if (info.updated.isBefore(clock.ago(days: 1))) {
98-
// Only delete if the item is more than one day old
104+
if (info.updated.isBefore(clock.agoBy(_minGarbageAge))) {
105+
// Only delete the item if it's older than _minGarbageAge
99106
// This avoids any races where we delete files we've just created
100107
await package(packageName).delete();
101108
}
@@ -111,8 +118,8 @@ final class ExportedApi {
111118
final packageName = item.name.without(suffix: '-').split('/').last;
112119
if (!allPackageNames.contains(packageName)) {
113120
final info = await _bucket.info(item.name);
114-
if (info.updated.isBefore(clock.ago(days: 1))) {
115-
// Only delete if the item is more than one day old
121+
if (info.updated.isBefore(clock.agoBy(_minGarbageAge))) {
122+
// Only delete the item if it's older than _minGarbageAge
116123
// This avoids any races where we delete files we've just created
117124
await package(packageName).delete();
118125
}
@@ -153,7 +160,7 @@ final class ExportedApi {
153160
await _listBucket(
154161
prefix: entry.name,
155162
delimiter: '',
156-
(entry) async => await _bucket.delete(entry.name),
163+
(entry) async => await _bucket.tryDelete(entry.name),
157164
);
158165
}
159166
}));
@@ -211,6 +218,33 @@ final class ExportedPackage {
211218
Duration(hours: 2),
212219
);
213220

221+
/// Garbage collect versions from this package not in [allVersionNumbers].
222+
///
223+
/// [allVersionNumbers] must be encoded as canonical versions.
224+
Future<void> garbageCollect(Set<String> allVersionNumbers) async {
225+
await Future.wait([
226+
..._owner._prefixes.map((prefix) async {
227+
final pfx = '/api/archives/$_package-';
228+
await _owner._listBucket(prefix: pfx, delimiter: '', (item) async {
229+
assert(item.isObject);
230+
final version = item.name.without(prefix: pfx, suffix: '.tar.gz');
231+
if (allVersionNumbers.contains(version)) {
232+
return;
233+
}
234+
if (await _owner._bucket.tryInfo(item.name) case final info?) {
235+
if (info.updated.isBefore(clock.agoBy(_minGarbageAge))) {
236+
// Only delete if the item if it's older than _minGarbageAge
237+
// This avoids any races where we delete files we've just created
238+
await _owner._bucket.tryDelete(item.name);
239+
}
240+
}
241+
// Ignore cases where tryInfo fails, assuming the object has been
242+
// deleted by another process.
243+
});
244+
}),
245+
]);
246+
}
247+
214248
/// Delete all files related to this package.
215249
Future<void> delete() async {
216250
await Future.wait([
@@ -220,7 +254,7 @@ final class ExportedPackage {
220254
await _owner._listBucket(
221255
prefix: prefix + '/api/archives/$_package-',
222256
delimiter: '',
223-
(item) async => await _owner._bucket.delete(item.name),
257+
(item) async => await _owner._bucket.tryDelete(item.name),
224258
);
225259
}),
226260
]);
@@ -237,7 +271,7 @@ sealed class ExportedObject {
237271
Future<void> delete() async {
238272
await Future.wait(_owner._prefixes.map((prefix) async {
239273
await _owner._pool.withResource(() async {
240-
await _owner._bucket.delete(prefix + _objectName);
274+
await _owner._bucket.tryDelete(prefix + _objectName);
241275
});
242276
}));
243277
}

0 commit comments

Comments
 (0)