Skip to content

Commit

Permalink
Download counts: cache 30-days totals in backend (#8117)
Browse files Browse the repository at this point in the history
  • Loading branch information
szakarias authored Oct 7, 2024
1 parent 2bab85a commit e74ee03
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 2 deletions.
62 changes: 61 additions & 1 deletion app/lib/service/download_counts/backend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:convert';

import 'package:gcloud/service_scope.dart' as ss;
import 'package:gcloud/storage.dart';
import 'package:googleapis/storage/v1.dart';
import 'package:pub_dev/service/download_counts/compute_30_days_total_counts.dart';
import 'package:pub_dev/service/download_counts/download_counts.dart';
import 'package:pub_dev/service/download_counts/models.dart';
import 'package:pub_dev/service/entrypoint/analyzer.dart';
import 'package:pub_dev/shared/cached_value.dart';
import 'package:pub_dev/shared/configuration.dart';
import 'package:pub_dev/shared/datastore.dart';
import 'package:pub_dev/shared/redis_cache.dart';

Expand All @@ -19,7 +27,59 @@ DownloadCountsBackend get downloadCountsBackend =>
class DownloadCountsBackend {
final DatastoreDB _db;

DownloadCountsBackend(this._db);
late CachedValue<Map<String, int>> _thirtyDaysTotals;
var _lastData = (data: <String, int>{}, etag: '');

DownloadCountsBackend(this._db) {
_thirtyDaysTotals = CachedValue(
name: 'thirtyDaysTotalDownloadCounts',
maxAge: Duration(days: 14),
interval: Duration(minutes: 30),
updateFn: _updateThirtyDaysTotals);
}

Future<Map<String, int>> _updateThirtyDaysTotals() async {
try {
final info = await storageService
.bucket(activeConfiguration.reportsBucketName!)
.info(downloadCounts30DaysTotalsFileName);

if (_lastData.etag == info.etag) {
return _lastData.data;
}
final data = (await storageService
.bucket(activeConfiguration.reportsBucketName!)
.read(downloadCounts30DaysTotalsFileName)
.transform(utf8.decoder)
.transform(json.decoder)
.single as Map<String, dynamic>)
.cast<String, int>();
_lastData = (data: data, etag: info.etag);
return data;
} on FormatException catch (e, st) {
logger.severe('Error loading 30-days total download counts:', e, st);
rethrow;
} on DetailedApiRequestError catch (e, st) {
if (e.status != 404) {
logger.severe(
'Failed to load $downloadCounts30DaysTotalsFileName, error : ',
e,
st);
}
rethrow;
}
}

Future<void> start() async {
await _thirtyDaysTotals.update();
}

Future<void> close() async {
await _thirtyDaysTotals.close();
}

int? lookup30DayTotalCounts(String package) =>
_thirtyDaysTotals.isAvailable ? _thirtyDaysTotals.value![package] : null;

Future<CountData?> lookupDownloadCountData(String pkg) async {
return (await cache.downloadCounts(pkg).get(() async {
Expand Down
2 changes: 2 additions & 0 deletions app/lib/service/entrypoint/frontend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:args/command_runner.dart';
import 'package:gcloud/service_scope.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as path;
import 'package:pub_dev/service/download_counts/backend.dart';
import 'package:pub_dev/service/services.dart';
import 'package:stream_transform/stream_transform.dart' show RateLimit;
import 'package:watcher/watcher.dart';
Expand Down Expand Up @@ -52,6 +53,7 @@ Future _main() async {
await announcementBackend.start();
await topPackages.start();
await youtubeBackend.start();
await downloadCountsBackend.start();

await runHandler(_logger, appHandler, sanitize: true);
}
Expand Down
1 change: 1 addition & 0 deletions app/lib/service/services.dart
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ Future<R> _withPubServices<R>(FutureOr<R> Function() fn) async {
registerScopeExitCallback(searchClient.close);
registerScopeExitCallback(topPackages.close);
registerScopeExitCallback(youtubeBackend.close);
registerScopeExitCallback(downloadCountsBackend.close);

// Create a zone-local flag to indicate that services setup has been completed.
return await fork(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import 'package:pub_dev/shared/configuration.dart';
import 'package:test/test.dart';

import '../../shared/test_services.dart';
import 'fake_download_counts.dart';

void main() {
group('', () {
testWithProfile('compute download counts 30 day totals', fn: () async {
testWithProfile('compute download counts 30-days totals', fn: () async {
final pkg = 'foo';
final versionsCounts = {
'1.0.1': 2,
Expand Down Expand Up @@ -98,5 +99,25 @@ void main() {

expect(data, {'foo': 70, 'bar': 105, 'baz': 140});
});

testWithProfile('cache 30-days totals', fn: () async {
await generateFake30DaysTotals({'foo': 70, 'bar': 105, 'baz': 140});
expect(downloadCountsBackend.lookup30DayTotalCounts('foo'), isNull);
expect(downloadCountsBackend.lookup30DayTotalCounts('bar'), isNull);
expect(downloadCountsBackend.lookup30DayTotalCounts('baz'), isNull);

await downloadCountsBackend.start();
expect(downloadCountsBackend.lookup30DayTotalCounts('foo'), 70);
expect(downloadCountsBackend.lookup30DayTotalCounts('bar'), 105);
expect(downloadCountsBackend.lookup30DayTotalCounts('baz'), 140);
expect(downloadCountsBackend.lookup30DayTotalCounts('bax'), isNull);

await generateFake30DaysTotals({'foo': 90, 'bar': 120, 'baz': 150});
await downloadCountsBackend.start();
expect(downloadCountsBackend.lookup30DayTotalCounts('foo'), 90);
expect(downloadCountsBackend.lookup30DayTotalCounts('bar'), 120);
expect(downloadCountsBackend.lookup30DayTotalCounts('baz'), 150);
expect(downloadCountsBackend.lookup30DayTotalCounts('bax'), isNull);
});
});
}
9 changes: 9 additions & 0 deletions app/test/service/download_counts/fake_download_counts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import 'dart:io';

import 'package:gcloud/storage.dart';
import 'package:pub_dev/service/download_counts/compute_30_days_total_counts.dart';
import 'package:pub_dev/shared/configuration.dart';
import 'package:pub_dev/shared/utils.dart';

Future<void> generateFakeDownloadCounts(
String downloadCountsFileName, String dataFilePath) async {
Expand All @@ -14,3 +16,10 @@ Future<void> generateFakeDownloadCounts(
.bucket(activeConfiguration.downloadCountsBucketName!)
.writeBytes(downloadCountsFileName, file);
}

Future<void> generateFake30DaysTotals(Map<String, int> totals) async {
await storageService
.bucket(activeConfiguration.reportsBucketName!)
.writeBytes(
downloadCounts30DaysTotalsFileName, jsonUtf8Encoder.convert(totals));
}

0 comments on commit e74ee03

Please sign in to comment.