Skip to content

Commit e0b6895

Browse files
authored
Admin action for forcing synchronization of exported API (#8263)
1 parent ba5f72e commit e0b6895

File tree

4 files changed

+126
-26
lines changed

4 files changed

+126
-26
lines changed

app/lib/admin/actions/actions.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'package:pub_dev/admin/actions/exported-api-sync.dart';
6+
57
import '../../shared/exceptions.dart';
68
import 'download_counts_backfill.dart';
79
import 'download_counts_delete.dart';
@@ -92,6 +94,7 @@ final class AdminAction {
9294
downloadCountsBackfill,
9395
downloadCountsDelete,
9496
emailSend,
97+
exportedApiSync,
9598
mergeModeratedPackageIntoExisting,
9699
moderatePackage,
97100
moderatePackageVersion,
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:pub_dev/package/api_export/api_exporter.dart';
6+
7+
import 'actions.dart';
8+
9+
final exportedApiSync = AdminAction(
10+
name: 'exported-api-sync',
11+
summary: 'Synchronize exported API bucket',
12+
description: '''
13+
This command will trigger synchronization of exported API bucket.
14+
15+
This is the bucket from which API responses are served.
16+
17+
This synchronize packages specified with:
18+
--packages="foo bar"
19+
20+
Synchronize all packages with:
21+
--packages=ALL
22+
23+
Optionally, write rewrite of all files using:
24+
--force-write=true
25+
''',
26+
options: {
27+
'packages': 'Space separated list of packages, use "ALL" for all!',
28+
'force-write': 'true/false if writes should be forced (default: false)',
29+
},
30+
invoke: (options) async {
31+
final forceWriteString = options['force-write'] ?? 'false';
32+
InvalidInputException.checkAnyOf(
33+
forceWriteString,
34+
'force-write',
35+
['true', 'false'],
36+
);
37+
final forceWrite = forceWriteString == 'true';
38+
39+
final packagesOpt = options['packages'] ?? '';
40+
final syncAll = packagesOpt == 'ALL';
41+
if (syncAll) {
42+
await apiExporter!.synchronizeExportedApi(forceWrite: forceWrite);
43+
return {
44+
'packages': 'ALL',
45+
'forceWrite': forceWrite,
46+
};
47+
} else {
48+
final packages = packagesOpt.split(' ').map((p) => p.trim()).toList();
49+
for (final p in packages) {
50+
InvalidInputException.checkPackageName(p);
51+
}
52+
for (final p in packages) {
53+
await apiExporter!.synchronizePackage(p, forceWrite: forceWrite);
54+
}
55+
return {
56+
'packages': packages,
57+
'forceWrite': forceWrite,
58+
};
59+
}
60+
},
61+
);

app/lib/package/api_export/api_exporter.dart

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -122,16 +122,19 @@ class ApiExporter {
122122
}
123123

124124
/// Gets and uploads the package name completion data.
125-
Future<void> synchronizePackageNameCompletionData() async {
125+
Future<void> synchronizePackageNameCompletionData({
126+
bool forceWrite = false,
127+
}) async {
126128
await _api.packageNameCompletionData.write(
127129
await searchBackend.getPackageNameCompletionData(),
130+
forceWrite: forceWrite,
128131
);
129132
}
130133

131134
/// Synchronize all exported API.
132135
///
133136
/// This is intended to be scheduled from a daily background task.
134-
Future<void> synchronizeExportedApi() async {
137+
Future<void> synchronizeExportedApi({bool forceWrite = false}) async {
135138
final allPackageNames = <String>{};
136139
final packageQuery = dbService.query<Package>();
137140
var errCount = 0;
@@ -143,13 +146,13 @@ class ApiExporter {
143146
allPackageNames.add(name);
144147

145148
// TODO: Consider retries around all this logic
146-
await synchronizePackage(name);
149+
await synchronizePackage(name, forceWrite: forceWrite);
147150
}, onError: (e, st) {
148151
_log.warning('synchronizePackage() failed', e, st);
149152
errCount++;
150153
});
151154

152-
await synchronizePackageNameCompletionData();
155+
await synchronizePackageNameCompletionData(forceWrite: forceWrite);
153156

154157
await _api.notFound.write({
155158
'error': {
@@ -158,7 +161,7 @@ class ApiExporter {
158161
},
159162
'code': 'NotFound',
160163
'message': 'Package or version requested could not be found.',
161-
});
164+
}, forceWrite: forceWrite);
162165

163166
await _api.garbageCollect(allPackageNames);
164167

@@ -182,7 +185,10 @@ class ApiExporter {
182185
/// * Running a full background synchronization.
183186
/// * When a change in [Package.updated] is detected.
184187
/// * A package is moderated, or other admin action is applied.
185-
Future<void> synchronizePackage(String package) async {
188+
Future<void> synchronizePackage(
189+
String package, {
190+
bool forceWrite = false,
191+
}) async {
186192
_log.info('synchronizePackage("$package")');
187193

188194
final PackageData versionListing;
@@ -212,9 +218,18 @@ class ApiExporter {
212218
(version, _) => !versionListing.versions.any((v) => v.version == version),
213219
);
214220

215-
await _api.package(package).synchronizeTarballs(versions);
216-
await _api.package(package).advisories.write(advisories);
217-
await _api.package(package).versions.write(versionListing);
221+
await _api.package(package).synchronizeTarballs(
222+
versions,
223+
forceWrite: forceWrite,
224+
);
225+
await _api.package(package).advisories.write(
226+
advisories,
227+
forceWrite: forceWrite,
228+
);
229+
await _api.package(package).versions.write(
230+
versionListing,
231+
forceWrite: forceWrite,
232+
);
218233
}
219234

220235
/// Scan for updates from packages until [abort] is resolved, or [claim]

app/lib/package/api_export/exported_api.dart

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -284,13 +284,16 @@ final class ExportedPackage {
284284
///
285285
/// This method will copy GCS objects, when necessary, relying on
286286
/// [SourceObjectInfo.md5Hash] to avoid copying objects that haven't changed.
287+
/// If [forceWrite] is given all files will be rewritten ignoring their
288+
/// previous state.
287289
///
288290
/// [versions] **must** have an entry for each version that exists.
289291
/// This will **delete** tarballs for versions that do not exist in
290292
/// [versions].
291293
Future<void> synchronizeTarballs(
292-
Map<String, SourceObjectInfo> versions,
293-
) async {
294+
Map<String, SourceObjectInfo> versions, {
295+
bool forceWrite = false,
296+
}) async {
294297
await Future.wait([
295298
..._owner._prefixes.map((prefix) async {
296299
final pfx = '$prefix/api/archives/$_package-';
@@ -314,7 +317,7 @@ final class ExportedPackage {
314317
);
315318

316319
final info = versions[version];
317-
if (info != null) {
320+
if (info != null && !forceWrite) {
318321
await tarball(version)._copyToPrefixFromIfNotContentEquals(
319322
prefix,
320323
info,
@@ -342,6 +345,7 @@ final class ExportedPackage {
342345
prefix,
343346
versions[v]!,
344347
null,
348+
forceWrite: forceWrite,
345349
);
346350
});
347351
}));
@@ -495,7 +499,10 @@ final class ExportedJsonFile<T> extends ExportedObject {
495499
}
496500

497501
/// Write [data] as gzipped JSON in UTF-8 format.
498-
Future<void> write(T data) async {
502+
///
503+
/// This will only write of `Content-Length` and `md5Hash` doesn't match the
504+
/// existing file, or if [forceWrite] is given.
505+
Future<void> write(T data, {bool forceWrite = false}) async {
499506
final gzipped = _jsonGzip.encode(data);
500507
final metadata = _metadata();
501508

@@ -505,6 +512,7 @@ final class ExportedJsonFile<T> extends ExportedObject {
505512
prefix + _objectName,
506513
gzipped,
507514
metadata,
515+
forceWrite: forceWrite,
508516
);
509517
});
510518
}));
@@ -542,14 +550,18 @@ final class ExportedBlob extends ExportedObject {
542550
}
543551

544552
/// Write binary blob to this file.
545-
Future<void> write(List<int> data) async {
553+
///
554+
/// This will only write of `Content-Length` and `md5Hash` doesn't match the
555+
/// existing file, or if [forceWrite] is given.
556+
Future<void> write(List<int> data, {bool forceWrite = false}) async {
546557
final metadata = _metadata();
547558
await Future.wait(_owner._prefixes.map((prefix) async {
548559
await _owner._pool.withResource(() async {
549560
await _owner._bucket.writeBytesIfDifferent(
550561
prefix + _objectName,
551562
data,
552563
metadata,
564+
forceWrite: forceWrite,
553565
);
554566
});
555567
}));
@@ -563,7 +575,11 @@ final class ExportedBlob extends ExportedObject {
563575
/// [source] is required to be [SourceObjectInfo] for the source object.
564576
/// This method will use [ObjectInfo.length] and [ObjectInfo.md5Hash] to
565577
/// determine if it's necessary to copy the object.
566-
Future<void> copyFrom(SourceObjectInfo source) async {
578+
/// If [forceWrite] is given, this method will always copy the object.
579+
Future<void> copyFrom(
580+
SourceObjectInfo source, {
581+
bool forceWrite = false,
582+
}) async {
567583
await Future.wait(_owner._prefixes.map((prefix) async {
568584
await _owner._pool.withResource(() async {
569585
final dst = prefix + _objectName;
@@ -572,6 +588,7 @@ final class ExportedBlob extends ExportedObject {
572588
prefix,
573589
source,
574590
await _owner._bucket.tryInfo(dst),
591+
forceWrite: forceWrite,
575592
);
576593
});
577594
}));
@@ -592,12 +609,13 @@ final class ExportedBlob extends ExportedObject {
592609
Future<void> _copyToPrefixFromIfNotContentEquals(
593610
String prefix,
594611
SourceObjectInfo source,
595-
ObjectInfo? destinationInfo,
596-
) async {
612+
ObjectInfo? destinationInfo, {
613+
bool forceWrite = false,
614+
}) async {
597615
final dst = prefix + _objectName;
598616

599617
// Check if the dst already exists
600-
if (destinationInfo != null) {
618+
if (destinationInfo != null && !forceWrite) {
601619
if (destinationInfo.name != dst) {
602620
throw ArgumentError.value(
603621
destinationInfo,
@@ -633,15 +651,18 @@ extension on Bucket {
633651
Future<void> writeBytesIfDifferent(
634652
String name,
635653
List<int> bytes,
636-
ObjectMetadata metadata,
637-
) async {
638-
if (await tryInfo(name) case final info?) {
639-
if (info.isSameContent(bytes)) {
640-
if (info.metadata.validated
641-
.isBefore(clock.agoBy(_updateValidatedAfter))) {
642-
await updateMetadata(name, metadata);
654+
ObjectMetadata metadata, {
655+
bool forceWrite = false,
656+
}) async {
657+
if (!forceWrite) {
658+
if (await tryInfo(name) case final info?) {
659+
if (info.isSameContent(bytes)) {
660+
if (info.metadata.validated
661+
.isBefore(clock.agoBy(_updateValidatedAfter))) {
662+
await updateMetadata(name, metadata);
663+
}
664+
return;
643665
}
644-
return;
645666
}
646667
}
647668

0 commit comments

Comments
 (0)