Skip to content

Commit 0afe071

Browse files
committed
merge
2 parents cfe3e26 + 95cca41 commit 0afe071

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1881
-647
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ Important changes to data models, configuration, and migrations between each
22
AppEngine version, listed here to ease deployment and troubleshooting.
33

44
## Next Release (replace with git tag when deployed)
5+
* Note: `search` increased `description` weight from `0.9` to `1.0`.
6+
7+
## `20241107t132800-all`
8+
* `search` uses the `IndexedScore` to reduce memory allocations.
59

610
## `20241031t095600-all`
711
* Bumped runtimeVersion to `2024.10.29`.

app/lib/dartdoc/dartdoc_page.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,11 +285,11 @@ extension DartDocPageRender on DartDocPage {
285285
final dataBaseHref = p.relative('', from: p.dirname(options.path));
286286
return d.element('body', classes: [
287287
'light-theme',
288-
if (activeConfiguration.isStaging) 'staging-banner',
289288
], attributes: {
290289
'data-base-href':
291290
baseHref ?? (dataBaseHref == '.' ? '' : '$dataBaseHref/'),
292291
'data-using-base-href': usingBaseHref ?? 'false',
292+
if (activeConfiguration.isStaging) 'data-staging': '1',
293293
}, children: [
294294
if (activeConfiguration.isStaging)
295295
d.div(classes: ['staging-ribbon'], text: 'staging'),

app/lib/frontend/templates/views/shared/layout.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,10 @@ d.Node pageLayoutNode({
166166
requestContext.experimentalFlags.isDarkModeDefault
167167
? 'dark-theme'
168168
: 'light-theme',
169-
if (activeConfiguration.isStaging) 'staging-banner',
170169
],
170+
attributes: {
171+
if (activeConfiguration.isStaging) 'data-staging': '1',
172+
},
171173
children: [
172174
// The initialization of dark theme must be in a synchronous, blocking
173175
// script execution, as otherwise users may see flash of unstyled content
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
// Copyright (c) 2023, 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 'dart:async';
6+
7+
import 'package:_pub_shared/data/package_api.dart';
8+
import 'package:clock/clock.dart';
9+
import 'package:gcloud/service_scope.dart' as ss;
10+
import 'package:gcloud/storage.dart';
11+
import 'package:logging/logging.dart';
12+
import 'package:pub_dev/service/security_advisories/backend.dart';
13+
import 'package:pub_dev/shared/exceptions.dart';
14+
import 'package:pub_dev/shared/parallel_foreach.dart';
15+
16+
import '../../search/backend.dart';
17+
import '../../shared/datastore.dart';
18+
import '../../shared/versions.dart';
19+
import '../../task/global_lock.dart';
20+
import '../backend.dart';
21+
import '../models.dart';
22+
import 'exported_api.dart';
23+
24+
final Logger _log = Logger('api_export.api_exporter');
25+
26+
/// Sets the API Exporter service.
27+
void registerApiExporter(ApiExporter value) =>
28+
ss.register(#_apiExporter, value);
29+
30+
/// The active API Exporter service or null if it hasn't been initialized.
31+
ApiExporter? get apiExporter => ss.lookup(#_apiExporter) as ApiExporter?;
32+
33+
const _concurrency = 30;
34+
35+
class ApiExporter {
36+
final ExportedApi _api;
37+
38+
/// If [stop] has been called to stop background processes.
39+
///
40+
/// `null` when not started yet, or we have been fully stopped.
41+
Completer<void>? _aborted;
42+
43+
/// If background processes created by [start] have stopped.
44+
///
45+
/// This won't be resolved if [start] has not been called!
46+
/// `null` when not started yet.
47+
Completer<void>? _stopped;
48+
49+
ApiExporter({
50+
required Bucket bucket,
51+
}) : _api = ExportedApi(storageService, bucket);
52+
53+
/// Start continuous background processes for scheduling of tasks.
54+
///
55+
/// Calling [start] without first calling [stop] is an error.
56+
Future<void> start() async {
57+
if (_aborted != null) {
58+
throw StateError('ApiExporter.start() has already been called!');
59+
}
60+
// Note: During testing we call [start] and [stop] in a [FakeAsync.run],
61+
// this only works because the completers are created here.
62+
// If we create the completers in the constructor which gets called
63+
// outside [FakeAsync.run], then this won't work.
64+
// In the future we hopefully support running the entire service using
65+
// FakeAsync, but this point we rely on completers being created when
66+
// [start] is called -- and not in the [ApiExporter] constructor.
67+
final aborted = _aborted = Completer();
68+
final stopped = _stopped = Completer();
69+
70+
// Start scanning for packages to be tracked
71+
scheduleMicrotask(() async {
72+
try {
73+
// Create a lock for task scheduling, so tasks
74+
final lock = GlobalLock.create(
75+
'$runtimeVersion/package/scan-sync-export-api',
76+
expiration: Duration(minutes: 25),
77+
);
78+
79+
while (!aborted.isCompleted) {
80+
// Acquire the global lock and scan for package changes while lock is
81+
// valid.
82+
try {
83+
await lock.withClaim((claim) async {
84+
await _scanForPackageUpdates(claim, abort: aborted);
85+
}, abort: aborted);
86+
} catch (e, st) {
87+
// Log this as very bad, and then move on. Nothing good can come
88+
// from straight up stopping.
89+
_log.shout(
90+
'scanning failed (will retry when lock becomes free)',
91+
e,
92+
st,
93+
);
94+
// Sleep 5 minutes to reduce risk of degenerate behavior
95+
await Future.delayed(Duration(minutes: 5));
96+
}
97+
}
98+
} catch (e, st) {
99+
_log.severe('scanning loop crashed', e, st);
100+
} finally {
101+
_log.info('scanning loop stopped');
102+
// Report background processes as stopped
103+
stopped.complete();
104+
}
105+
});
106+
}
107+
108+
/// Stop any background process that may be running.
109+
///
110+
/// Calling this method is always safe.
111+
Future<void> stop() async {
112+
final aborted = _aborted;
113+
if (aborted == null) {
114+
return;
115+
}
116+
if (!aborted.isCompleted) {
117+
aborted.complete();
118+
}
119+
await _stopped!.future;
120+
_aborted = null;
121+
_stopped = null;
122+
}
123+
124+
/// Gets and uploads the package name completion data.
125+
Future<void> synchronizePackageNameCompletionData() async {
126+
await _api.packageNameCompletionData.write(
127+
await searchBackend.getPackageNameCompletionData(),
128+
);
129+
}
130+
131+
/// Synchronize all exported API.
132+
///
133+
/// This is intended to be scheduled from a daily background task.
134+
Future<void> synchronizeExportedApi() async {
135+
final allPackageNames = <String>{};
136+
final packageQuery = dbService.query<Package>();
137+
await packageQuery.run().parallelForEach(_concurrency, (pkg) async {
138+
final name = pkg.name!;
139+
if (pkg.isNotVisible) {
140+
return;
141+
}
142+
allPackageNames.add(name);
143+
144+
// TODO: Consider retries around all this logic
145+
await synchronizePackage(name);
146+
});
147+
148+
await synchronizePackageNameCompletionData();
149+
150+
await _api.garbageCollect(allPackageNames);
151+
}
152+
153+
/// Sync package and into [ExportedApi], this will synchronize package into
154+
/// [ExportedApi].
155+
///
156+
/// This method will update [ExportedApi] ensuring:
157+
/// * Version listing for [package] is up-to-date,
158+
/// * Advisories for [package] is up-to-date,
159+
/// * Tarballs for each version of [package] is up-to-date,
160+
/// * Delete tarballs from old versions that no-longer exist.
161+
///
162+
/// This is intended when:
163+
/// * Running a full background synchronization.
164+
/// * When a change in [Package.updated] is detected.
165+
/// * A package is moderated, or other admin action is applied.
166+
Future<void> synchronizePackage(String package) async {
167+
_log.info('synchronizePackage("$package")');
168+
169+
final PackageData versionListing;
170+
try {
171+
versionListing = await packageBackend.listVersions(package);
172+
} on NotFoundException {
173+
// Handle the case where package is moderated.
174+
final pkg = await packageBackend.lookupPackage(package);
175+
if (pkg != null && pkg.isNotVisible) {
176+
// We only delete the package if it is explicitly not visible.
177+
// If we can't find it, then it's safer to assume that it's a bug.
178+
await _api.package(package).delete();
179+
}
180+
return;
181+
}
182+
183+
final advisories = await securityAdvisoryBackend.listAdvisoriesResponse(
184+
package,
185+
skipCache: true, // Skipping the cache when fetching security advisories
186+
);
187+
188+
final versions = await packageBackend.tarballStorage
189+
.listVersionsInCanonicalBucket(package);
190+
191+
// Remove versions that are not exposed in the public API.
192+
versions.removeWhere(
193+
(version, _) => !versionListing.versions.any((v) => v.version == version),
194+
);
195+
196+
await _api.package(package).synchronizeTarballs(versions);
197+
await _api.package(package).advisories.write(advisories);
198+
await _api.package(package).versions.write(versionListing);
199+
}
200+
201+
/// Scan for updates from packages until [abort] is resolved, or [claim]
202+
/// is lost.
203+
Future<void> _scanForPackageUpdates(
204+
GlobalLockClaim claim, {
205+
Completer<void>? abort,
206+
}) async {
207+
abort ??= Completer<void>();
208+
209+
// Map from package to updated that has been seen.
210+
final seen = <String, DateTime>{};
211+
212+
// We will schedule longer overlaps every 6 hours.
213+
var nextLongScan = clock.fromNow(hours: 6);
214+
215+
// In theory 30 minutes overlap should be enough. In practice we should
216+
// allow an ample room for missed windows, and 3 days seems to be large enough.
217+
var since = clock.ago(days: 3);
218+
while (claim.valid && !abort.isCompleted) {
219+
// Look at all packages changed in [since]
220+
final q = dbService.query<Package>()
221+
..filter('updated >', since)
222+
..order('-updated');
223+
224+
if (clock.now().isAfter(nextLongScan)) {
225+
// Next time we'll do a longer scan
226+
since = clock.ago(days: 1);
227+
nextLongScan = clock.fromNow(hours: 6);
228+
} else {
229+
// Next time we'll only consider changes since now - 30 minutes
230+
since = clock.ago(minutes: 30);
231+
}
232+
233+
// Look at all packages that has changed
234+
await for (final p in q.run()) {
235+
// Abort, if claim is invalid or abort has been resolved!
236+
if (!claim.valid || abort.isCompleted) {
237+
return;
238+
}
239+
240+
// Check if the [updated] timestamp has been seen before.
241+
// If so, we skip checking it!
242+
final lastSeen = seen[p.name!];
243+
if (lastSeen != null && lastSeen.toUtc() == p.updated!.toUtc()) {
244+
continue;
245+
}
246+
// Remember the updated time for this package, so we don't check it
247+
// again...
248+
seen[p.name!] = p.updated!;
249+
250+
// Check the package
251+
await synchronizePackage(p.name!);
252+
}
253+
254+
// Cleanup the [seen] map for anything older than [since], as this won't
255+
// be relevant to the next iteration.
256+
seen.removeWhere((_, updated) => updated.isBefore(since));
257+
258+
// Wait until aborted or 10 minutes before scanning again!
259+
await abort.future.timeout(Duration(minutes: 10), onTimeout: () => null);
260+
}
261+
}
262+
}

0 commit comments

Comments
 (0)