Skip to content

Commit 8accb17

Browse files
authored
Merge pull request #46 from flutter-news-app-full-source-code/Refactor-Country-Service-for-Concurrency-and-Type-Safety
perf(country): prevent cache stampede by caching in-flight requests
2 parents 579c6f4 + cfdf8a5 commit 8accb17

File tree

1 file changed

+52
-22
lines changed

1 file changed

+52
-22
lines changed

lib/src/services/country_service.dart

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ class CountryService {
5151
_CacheEntry<List<Country>>? _cachedEventCountries;
5252
_CacheEntry<List<Country>>? _cachedHeadquarterCountries;
5353

54+
// Futures to hold in-flight aggregation requests to prevent cache stampedes.
55+
Future<List<Country>>? _eventCountriesFuture;
56+
Future<List<Country>>? _headquarterCountriesFuture;
57+
5458
/// Retrieves a list of countries based on the provided filter.
5559
///
5660
/// Supports filtering by 'usage' to get countries that are either
@@ -114,21 +118,34 @@ class CountryService {
114118
return _cachedEventCountries!.data;
115119
}
116120

121+
// If a fetch is already in progress, await it to prevent cache stampede.
122+
if (_eventCountriesFuture != null) {
123+
_log.finer('Awaiting in-flight event countries fetch.');
124+
return _eventCountriesFuture!;
125+
}
126+
117127
_log.finer('Fetching distinct event countries via aggregation.');
118-
final distinctCountries = await _getDistinctCountriesFromAggregation(
128+
// Start a new fetch and store the future.
129+
_eventCountriesFuture = _getDistinctCountriesFromAggregation(
119130
repository: _headlineRepository,
120131
fieldName: 'eventCountry',
121132
);
122133

123-
_cachedEventCountries = _CacheEntry(
124-
distinctCountries,
125-
DateTime.now().add(_cacheDuration),
126-
);
127-
_log.info(
128-
'Successfully fetched and cached ${distinctCountries.length} '
129-
'event countries.',
130-
);
131-
return distinctCountries;
134+
try {
135+
final distinctCountries = await _eventCountriesFuture!;
136+
_cachedEventCountries = _CacheEntry(
137+
distinctCountries,
138+
DateTime.now().add(_cacheDuration),
139+
);
140+
_log.info(
141+
'Successfully fetched and cached ${distinctCountries.length} '
142+
'event countries.',
143+
);
144+
return distinctCountries;
145+
} finally {
146+
// Clear the future once the operation is complete (success or error).
147+
_eventCountriesFuture = null;
148+
}
132149
}
133150

134151
/// Fetches a distinct list of countries that are referenced as
@@ -143,21 +160,34 @@ class CountryService {
143160
return _cachedHeadquarterCountries!.data;
144161
}
145162

163+
// If a fetch is already in progress, await it to prevent cache stampede.
164+
if (_headquarterCountriesFuture != null) {
165+
_log.finer('Awaiting in-flight headquarter countries fetch.');
166+
return _headquarterCountriesFuture!;
167+
}
168+
146169
_log.finer('Fetching distinct headquarter countries via aggregation.');
147-
final distinctCountries = await _getDistinctCountriesFromAggregation(
170+
// Start a new fetch and store the future.
171+
_headquarterCountriesFuture = _getDistinctCountriesFromAggregation(
148172
repository: _sourceRepository,
149173
fieldName: 'headquarters',
150174
);
151175

152-
_cachedHeadquarterCountries = _CacheEntry(
153-
distinctCountries,
154-
DateTime.now().add(_cacheDuration),
155-
);
156-
_log.info(
157-
'Successfully fetched and cached ${distinctCountries.length} '
158-
'headquarter countries.',
159-
);
160-
return distinctCountries;
176+
try {
177+
final distinctCountries = await _headquarterCountriesFuture!;
178+
_cachedHeadquarterCountries = _CacheEntry(
179+
distinctCountries,
180+
DateTime.now().add(_cacheDuration),
181+
);
182+
_log.info(
183+
'Successfully fetched and cached ${distinctCountries.length} '
184+
'headquarter countries.',
185+
);
186+
return distinctCountries;
187+
} finally {
188+
// Clear the future once the operation is complete (success or error).
189+
_headquarterCountriesFuture = null;
190+
}
161191
}
162192

163193
/// Helper method to fetch a distinct list of countries from a given
@@ -168,8 +198,8 @@ class CountryService {
168198
/// the country object (e.g., 'eventCountry', 'headquarters').
169199
///
170200
/// Throws [OperationFailedException] for internal errors during data fetch.
171-
Future<List<Country>> _getDistinctCountriesFromAggregation({
172-
required DataRepository<dynamic> repository,
201+
Future<List<Country>> _getDistinctCountriesFromAggregation<T extends FeedItem>({
202+
required DataRepository<T> repository,
173203
required String fieldName,
174204
}) async {
175205
_log.finer('Fetching distinct countries for field "$fieldName" via aggregation.');

0 commit comments

Comments
 (0)