Skip to content

Commit 87ede6f

Browse files
committed
Download counts: cache 30-days totals in backend
1 parent c47dca9 commit 87ede6f

File tree

5 files changed

+95
-2
lines changed

5 files changed

+95
-2
lines changed

app/lib/service/download_counts/backend.dart

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,17 @@
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 'dart:convert';
6+
57
import 'package:gcloud/service_scope.dart' as ss;
8+
import 'package:gcloud/storage.dart';
9+
import 'package:googleapis/storage/v1.dart';
10+
import 'package:pub_dev/service/download_counts/compute_30_days_total_counts.dart';
611
import 'package:pub_dev/service/download_counts/download_counts.dart';
712
import 'package:pub_dev/service/download_counts/models.dart';
13+
import 'package:pub_dev/service/entrypoint/analyzer.dart';
14+
import 'package:pub_dev/shared/cached_value.dart';
15+
import 'package:pub_dev/shared/configuration.dart';
816
import 'package:pub_dev/shared/datastore.dart';
917
import 'package:pub_dev/shared/redis_cache.dart';
1018

@@ -19,7 +27,59 @@ DownloadCountsBackend get downloadCountsBackend =>
1927
class DownloadCountsBackend {
2028
final DatastoreDB _db;
2129

22-
DownloadCountsBackend(this._db);
30+
late CachedValue<Map<String, int>> _thirtyDaysTotals;
31+
var _lastData = (data: <String, int>{}, etag: '');
32+
33+
DownloadCountsBackend(this._db) {
34+
_thirtyDaysTotals = CachedValue(
35+
name: 'thirtyDaysTotalDownloadCounts',
36+
maxAge: Duration(days: 14),
37+
interval: Duration(minutes: 30),
38+
updateFn: _updateThirtyDaysTotals);
39+
}
40+
41+
Future<Map<String, int>> _updateThirtyDaysTotals() async {
42+
try {
43+
final info = await storageService
44+
.bucket(activeConfiguration.reportsBucketName!)
45+
.info(downloadCounts30DaysTotalsFileName);
46+
47+
if (_lastData.etag == info.etag) {
48+
return _lastData.data;
49+
}
50+
final data = (await storageService
51+
.bucket(activeConfiguration.reportsBucketName!)
52+
.read(downloadCounts30DaysTotalsFileName)
53+
.transform(utf8.decoder)
54+
.transform(json.decoder)
55+
.single as Map<String, dynamic>)
56+
.cast<String, int>();
57+
_lastData = (data: data, etag: info.etag);
58+
return data;
59+
} on FormatException catch (e, st) {
60+
logger.severe('Error loading 30-days total download counts:', e, st);
61+
rethrow;
62+
} on DetailedApiRequestError catch (e, st) {
63+
if (e.status != 404) {
64+
logger.severe(
65+
'Failed to load $downloadCounts30DaysTotalsFileName, error : ',
66+
e,
67+
st);
68+
}
69+
rethrow;
70+
}
71+
}
72+
73+
Future<void> start() async {
74+
await _thirtyDaysTotals.update();
75+
}
76+
77+
Future<void> close() async {
78+
await _thirtyDaysTotals.close();
79+
}
80+
81+
int? lookup30DayTotalCounts(String package) =>
82+
_thirtyDaysTotals.isAvailable ? _thirtyDaysTotals.value![package] : null;
2383

2484
Future<CountData?> lookupDownloadCountData(String pkg) async {
2585
return (await cache.downloadCounts(pkg).get(() async {

app/lib/service/entrypoint/frontend.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:args/command_runner.dart';
88
import 'package:gcloud/service_scope.dart';
99
import 'package:logging/logging.dart';
1010
import 'package:path/path.dart' as path;
11+
import 'package:pub_dev/service/download_counts/backend.dart';
1112
import 'package:pub_dev/service/services.dart';
1213
import 'package:stream_transform/stream_transform.dart' show RateLimit;
1314
import 'package:watcher/watcher.dart';
@@ -52,6 +53,7 @@ Future _main() async {
5253
await announcementBackend.start();
5354
await topPackages.start();
5455
await youtubeBackend.start();
56+
await downloadCountsBackend.start();
5557

5658
await runHandler(_logger, appHandler, sanitize: true);
5759
}

app/lib/service/services.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ Future<R> _withPubServices<R>(FutureOr<R> Function() fn) async {
298298
registerScopeExitCallback(searchClient.close);
299299
registerScopeExitCallback(topPackages.close);
300300
registerScopeExitCallback(youtubeBackend.close);
301+
registerScopeExitCallback(downloadCountsBackend.close);
301302

302303
// Create a zone-local flag to indicate that services setup has been completed.
303304
return await fork(

app/test/service/download_counts/compute_total_download_counts_test.dart

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import 'package:pub_dev/shared/configuration.dart';
1111
import 'package:test/test.dart';
1212

1313
import '../../shared/test_services.dart';
14+
import 'fake_download_counts.dart';
1415

1516
void main() {
1617
group('', () {
17-
testWithProfile('compute download counts 30 day totals', fn: () async {
18+
testWithProfile('compute download counts 30-days totals', fn: () async {
1819
final pkg = 'foo';
1920
final versionsCounts = {
2021
'1.0.1': 2,
@@ -98,5 +99,25 @@ void main() {
9899

99100
expect(data, {'foo': 70, 'bar': 105, 'baz': 140});
100101
});
102+
103+
testWithProfile('cache 30-days totals', fn: () async {
104+
await generateFake30DaysTotals({'foo': 70, 'bar': 105, 'baz': 140});
105+
expect(downloadCountsBackend.lookup30DayTotalCounts('foo'), isNull);
106+
expect(downloadCountsBackend.lookup30DayTotalCounts('bar'), isNull);
107+
expect(downloadCountsBackend.lookup30DayTotalCounts('baz'), isNull);
108+
109+
await downloadCountsBackend.start();
110+
expect(downloadCountsBackend.lookup30DayTotalCounts('foo'), 70);
111+
expect(downloadCountsBackend.lookup30DayTotalCounts('bar'), 105);
112+
expect(downloadCountsBackend.lookup30DayTotalCounts('baz'), 140);
113+
expect(downloadCountsBackend.lookup30DayTotalCounts('bax'), isNull);
114+
115+
await generateFake30DaysTotals({'foo': 90, 'bar': 120, 'baz': 150});
116+
await downloadCountsBackend.start();
117+
expect(downloadCountsBackend.lookup30DayTotalCounts('foo'), 90);
118+
expect(downloadCountsBackend.lookup30DayTotalCounts('bar'), 120);
119+
expect(downloadCountsBackend.lookup30DayTotalCounts('baz'), 150);
120+
expect(downloadCountsBackend.lookup30DayTotalCounts('bax'), isNull);
121+
});
101122
});
102123
}

app/test/service/download_counts/fake_download_counts.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import 'dart:io';
66

77
import 'package:gcloud/storage.dart';
8+
import 'package:pub_dev/service/download_counts/compute_30_days_total_counts.dart';
89
import 'package:pub_dev/shared/configuration.dart';
10+
import 'package:pub_dev/shared/utils.dart';
911

1012
Future<void> generateFakeDownloadCounts(
1113
String downloadCountsFileName, String dataFilePath) async {
@@ -14,3 +16,10 @@ Future<void> generateFakeDownloadCounts(
1416
.bucket(activeConfiguration.downloadCountsBucketName!)
1517
.writeBytes(downloadCountsFileName, file);
1618
}
19+
20+
Future<void> generateFake30DaysTotals(Map<String, int> totals) async {
21+
await storageService
22+
.bucket(activeConfiguration.reportsBucketName!)
23+
.writeBytes(
24+
downloadCounts30DaysTotalsFileName, jsonUtf8Encoder.convert(totals));
25+
}

0 commit comments

Comments
 (0)