Skip to content

Commit 5823b79

Browse files
authored
cache trend scores in backend (#8748)
1 parent ebf72a9 commit 5823b79

File tree

3 files changed

+106
-19
lines changed

3 files changed

+106
-19
lines changed

app/lib/fake/backend/fake_download_counts.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,9 @@ Future<void> generateFake30DaysTotals(Map<String, int> totals) async {
4848
.writeBytesWithRetry(
4949
downloadCounts30DaysTotalsFileName, jsonUtf8Encoder.convert(totals));
5050
}
51+
52+
Future<void> generateFakeTrendScores(Map<String, int> totals) async {
53+
await storageService
54+
.bucket(activeConfiguration.reportsBucketName!)
55+
.writeBytesWithRetry(trendScoreFileName, jsonUtf8Encoder.convert(totals));
56+
}

app/lib/service/download_counts/backend.dart

Lines changed: 80 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,62 +29,123 @@ class DownloadCountsBackend {
2929
final DatastoreDB _db;
3030

3131
late CachedValue<Map<String, int>> _thirtyDaysTotals;
32-
var _lastData = (data: <String, int>{}, etag: '');
32+
var _lastDownloadsData = (data: <String, int>{}, etag: '');
33+
34+
late CachedValue<Map<String, int>> _trendScores;
35+
var _lastTrendData = (data: <String, int>{}, etag: '');
3336

3437
DownloadCountsBackend(this._db) {
3538
_thirtyDaysTotals = CachedValue(
3639
name: 'thirtyDaysTotalDownloadCounts',
3740
maxAge: Duration(days: 14),
3841
interval: Duration(minutes: 30),
3942
updateFn: _updateThirtyDaysTotals);
43+
_trendScores = CachedValue(
44+
name: 'trendScores',
45+
maxAge: Duration(days: 14),
46+
interval: Duration(minutes: 30),
47+
updateFn: _updateTrendScores);
4048
}
4149

4250
Future<Map<String, int>> _updateThirtyDaysTotals() async {
51+
return _fetchAndUpdateCachedData(
52+
fileName: downloadCounts30DaysTotalsFileName,
53+
currentCachedData: _lastDownloadsData,
54+
updateCache: (data) => _lastDownloadsData = data,
55+
errorContext: '30-days total download counts');
56+
}
57+
58+
Future<Map<String, int>> _updateTrendScores() async {
59+
return _fetchAndUpdateCachedData(
60+
fileName: trendScoreFileName,
61+
currentCachedData: _lastTrendData,
62+
updateCache: (data) => _lastTrendData = data,
63+
errorContext: 'trend scores');
64+
}
65+
66+
Future<Map<String, int>> _fetchAndUpdateCachedData({
67+
required String fileName,
68+
required ({Map<String, int> data, String etag}) currentCachedData,
69+
required void Function(({Map<String, int> data, String etag}) newData)
70+
updateCache,
71+
required String errorContext,
72+
}) async {
4373
try {
4474
final info = await storageService
4575
.bucket(activeConfiguration.reportsBucketName!)
46-
.infoWithRetry(downloadCounts30DaysTotalsFileName);
76+
.infoWithRetry(fileName);
4777

48-
if (_lastData.etag == info.etag) {
49-
return _lastData.data;
78+
if (currentCachedData.etag == info.etag) {
79+
return currentCachedData.data;
5080
}
51-
final data = (await storageService
52-
.bucket(activeConfiguration.reportsBucketName!)
53-
.readWithRetry(
54-
downloadCounts30DaysTotalsFileName,
55-
(input) async => await input
56-
.transform(utf8.decoder)
57-
.transform(json.decoder)
58-
.single as Map<String, dynamic>,
59-
))
60-
.cast<String, int>();
61-
_lastData = (data: data, etag: info.etag);
81+
82+
final rawData = await storageService
83+
.bucket(activeConfiguration.reportsBucketName!)
84+
.readWithRetry(
85+
fileName,
86+
(input) async => await input
87+
.transform(utf8.decoder)
88+
.transform(json.decoder)
89+
.single,
90+
);
91+
92+
final data = _parseJsonToMapStringInt(rawData, fileName);
93+
94+
final newData = (data: data, etag: info.etag);
95+
updateCache(newData);
6296
return data;
6397
} on FormatException catch (e, st) {
64-
logger.severe('Error loading 30-days total download counts:', e, st);
98+
logger.severe('Error parsing $errorContext: $e', e, st);
6599
rethrow;
66100
} on DetailedApiRequestError catch (e, st) {
67101
if (e.status != 404) {
68102
logger.severe(
69-
'Failed to load $downloadCounts30DaysTotalsFileName, error : ',
70-
e,
71-
st);
103+
'Failed to load $fileName ($errorContext), error : $e', e, st);
72104
}
73105
rethrow;
106+
} on TypeError catch (e, st) {
107+
logger.severe('Type error during processing $errorContext: $e', e, st);
108+
rethrow;
74109
}
75110
}
76111

112+
Map<String, int> _parseJsonToMapStringInt(dynamic rawJson, String fileName) {
113+
if (rawJson is! Map) {
114+
throw FormatException(
115+
'Expected JSON for $fileName to be a Map, but got ${rawJson.runtimeType}');
116+
}
117+
118+
final Map<String, int> result = {};
119+
for (final entry in rawJson.entries) {
120+
if (entry.key is! String) {
121+
throw FormatException(
122+
'Expected map keys for $fileName to be String, but found ${entry.key.runtimeType}');
123+
}
124+
if (entry.value is! int) {
125+
throw FormatException(
126+
'Expected map value for key "${entry.key}" in $fileName to be int, but got ${entry.value.runtimeType}');
127+
}
128+
result[entry.key as String] = entry.value as int;
129+
}
130+
return result;
131+
}
132+
77133
Future<void> start() async {
78134
await _thirtyDaysTotals.update();
135+
await _trendScores.update();
79136
}
80137

81138
Future<void> close() async {
82139
await _thirtyDaysTotals.close();
140+
await _trendScores.close();
83141
}
84142

85143
int? lookup30DaysTotalCounts(String package) =>
86144
_thirtyDaysTotals.isAvailable ? _thirtyDaysTotals.value![package] : null;
87145

146+
int? lookupTrendScore(String package) =>
147+
_trendScores.isAvailable ? _trendScores.value![package] : null;
148+
88149
Future<CountData?> lookupDownloadCountData(String pkg) async {
89150
return (await cache.downloadCounts(pkg).get(() async {
90151
final key = _db.emptyKey.append(DownloadCounts, id: pkg);

app/test/service/download_counts/computations_test.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,4 +337,24 @@ void main() {
337337
expect(data, trends);
338338
});
339339
});
340+
341+
testWithProfile('cache package trend scores', fn: () async {
342+
await generateFakeTrendScores({'foo': 3, 'bar': 1, 'baz': 2});
343+
expect(downloadCountsBackend.lookupTrendScore('foo'), isNull);
344+
expect(downloadCountsBackend.lookupTrendScore('bar'), isNull);
345+
expect(downloadCountsBackend.lookupTrendScore('baz'), isNull);
346+
347+
await downloadCountsBackend.start();
348+
expect(downloadCountsBackend.lookupTrendScore('foo'), 3);
349+
expect(downloadCountsBackend.lookupTrendScore('bar'), 1);
350+
expect(downloadCountsBackend.lookupTrendScore('baz'), 2);
351+
expect(downloadCountsBackend.lookupTrendScore('bax'), isNull);
352+
353+
await generateFakeTrendScores({'foo': 9, 'bar': 2, 'baz': 5});
354+
await downloadCountsBackend.start();
355+
expect(downloadCountsBackend.lookupTrendScore('foo'), 9);
356+
expect(downloadCountsBackend.lookupTrendScore('bar'), 2);
357+
expect(downloadCountsBackend.lookupTrendScore('baz'), 5);
358+
expect(downloadCountsBackend.lookupTrendScore('bax'), isNull);
359+
});
340360
}

0 commit comments

Comments
 (0)