Skip to content

Commit 5b60dd5

Browse files
committed
fix(country): prevent cache stampede by caching futures per request
- Replace single Future fields with Maps to store futures per cache key - Implement logic to atomically retrieve or create futures for specific cache keys - Ensure futures are removed from the Map after completion to prevent stale data - Refactor match stage in aggregation pipeline to handle name filter
1 parent 6c59ccc commit 5b60dd5

File tree

1 file changed

+28
-27
lines changed

1 file changed

+28
-27
lines changed

lib/src/services/country_service.dart

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ class CountryService {
5353
{};
5454

5555
// Futures to hold in-flight aggregation requests to prevent cache stampedes.
56-
Future<List<Country>>? _eventCountriesFuture;
57-
Future<List<Country>>? _headquarterCountriesFuture;
56+
final Map<String, Future<List<Country>>> _eventCountriesFutures = {};
57+
final Map<String, Future<List<Country>>> _headquarterCountriesFutures = {};
5858

5959
/// Retrieves a list of countries based on the provided filter.
6060
///
@@ -156,12 +156,14 @@ class CountryService {
156156
_log.finer('Returning cached event countries for key: $cacheKey.');
157157
return _cachedEventCountries[cacheKey]!.data;
158158
}
159-
// Atomically assign the future if no fetch is in progress,
160-
// and clear it when the future completes.
161-
_eventCountriesFuture ??= _fetchAndCacheEventCountries(
162-
nameFilter: nameFilter,
163-
).whenComplete(() => _eventCountriesFuture = null);
164-
return _eventCountriesFuture!;
159+
// Atomically retrieve or create the future for the specific cache key.
160+
var future = _eventCountriesFutures[cacheKey];
161+
if (future == null) {
162+
future = _fetchAndCacheEventCountries(nameFilter: nameFilter)
163+
.whenComplete(() => _eventCountriesFutures.remove(cacheKey));
164+
_eventCountriesFutures[cacheKey] = future;
165+
}
166+
return future!;
165167
}
166168

167169
/// Fetches a distinct list of countries that are referenced as
@@ -180,12 +182,14 @@ class CountryService {
180182
_log.finer('Returning cached headquarter countries for key: $cacheKey.');
181183
return _cachedHeadquarterCountries[cacheKey]!.data;
182184
}
183-
// Atomically assign the future if no fetch is in progress,
184-
// and clear it when the future completes.
185-
_headquarterCountriesFuture ??= _fetchAndCacheHeadquarterCountries(
186-
nameFilter: nameFilter,
187-
).whenComplete(() => _headquarterCountriesFuture = null);
188-
return _headquarterCountriesFuture!;
185+
// Atomically retrieve or create the future for the specific cache key.
186+
var future = _headquarterCountriesFutures[cacheKey];
187+
if (future == null) {
188+
future = _fetchAndCacheHeadquarterCountries(nameFilter: nameFilter)
189+
.whenComplete(() => _headquarterCountriesFutures.remove(cacheKey));
190+
_headquarterCountriesFutures[cacheKey] = future;
191+
}
192+
return future!;
189193
}
190194

191195
/// Helper method to fetch and cache distinct event countries.
@@ -280,23 +284,20 @@ class CountryService {
280284
'with nameFilter: $nameFilter.',
281285
);
282286
try {
283-
final pipeline = <Map<String, Object>>[
284-
<String, Object>{
285-
r'$match': <String, Object>{
286-
'status': ContentStatus.active.name,
287-
'$fieldName.id': <String, Object>{r'$exists': true},
288-
},
289-
},
290-
];
287+
final matchStage = <String, Object>{
288+
'status': ContentStatus.active.name,
289+
'$fieldName.id': <String, Object>{r'$exists': true},
290+
};
291291

292292
// Add name filter if provided
293293
if (nameFilter != null && nameFilter.isNotEmpty) {
294-
pipeline.add(<String, Object>{
295-
r'$match': <String, Object>{'$fieldName.name': nameFilter},
296-
});
294+
matchStage['$fieldName.name'] = nameFilter;
297295
}
298296

299-
pipeline.addAll([
297+
final pipeline = <Map<String, Object>>[
298+
<String, Object>{
299+
r'$match': matchStage,
300+
},
300301
<String, Object>{
301302
r'$group': <String, Object>{
302303
'_id': '\$$fieldName.id',
@@ -306,7 +307,7 @@ class CountryService {
306307
<String, Object>{
307308
r'$replaceRoot': <String, Object>{'newRoot': r'$country'},
308309
},
309-
]);
310+
];
310311

311312
final distinctCountriesJson = await repository.aggregate(
312313
pipeline: pipeline,

0 commit comments

Comments
 (0)