Skip to content

Commit 5593b0e

Browse files
committed
fix(core): prevent race condition in dependency initialization
Refactors the `AppDependencies` singleton to use a `Completer` for managing the asynchronous initialization process. This change introduces an atomic, thread-safe mechanism that ensures the core initialization logic runs exactly once, even when multiple concurrent requests trigger the `init()` method during server startup or hot reload. The previous implementation was vulnerable to a race condition where a second request could bypass the `_isInitialized` check before the first request had completed, leading to a `LateInitializationError`. The new `Completer`-based approach solves this by having all concurrent callers `await` the same `Future`, guaranteeing a single execution.
1 parent 913659b commit 5593b0e

File tree

1 file changed

+37
-14
lines changed

1 file changed

+37
-14
lines changed

lib/src/config/app_dependencies.dart

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// ignore_for_file: public_member_api_docs
22

3+
import 'dart:async';
34
import 'package:core/core.dart';
45
import 'package:data_mongodb/data_mongodb.dart';
56
import 'package:data_repository/data_repository.dart';
@@ -40,11 +41,12 @@ class AppDependencies {
4041
/// Provides access to the singleton instance.
4142
static AppDependencies get instance => _instance;
4243

43-
bool _isInitialized = false;
44-
Object? _initializationError;
45-
StackTrace? _initializationStackTrace;
4644
final _log = Logger('AppDependencies');
4745

46+
// A Completer to manage the one-time asynchronous initialization.
47+
// This ensures the initialization logic runs only once.
48+
Completer<void>? _initCompleter;
49+
4850
// --- Late-initialized fields for all dependencies ---
4951

5052
// Database
@@ -80,15 +82,27 @@ class AppDependencies {
8082
///
8183
/// This method is idempotent; it will only run the initialization logic once.
8284
Future<void> init() async {
83-
// If initialization previously failed, re-throw the original error.
84-
if (_initializationError != null) {
85-
return Future.error(_initializationError!, _initializationStackTrace);
85+
// If _initCompleter is not null, it means initialization is either in
86+
// progress or has already completed. Return its future to allow callers
87+
// to await the single, shared initialization process.
88+
if (_initCompleter != null) {
89+
return _initCompleter!.future;
8690
}
8791

88-
if (_isInitialized) return;
92+
// This is the first call to init(). Create the completer and start the
93+
// initialization process.
94+
_initCompleter = Completer<void>();
95+
_log.info('Starting application dependency initialization...');
96+
_initializeDependencies();
8997

90-
_log.info('Initializing application dependencies...');
98+
// Return the future from the completer.
99+
return _initCompleter!.future;
100+
}
91101

102+
/// The core logic for initializing all dependencies.
103+
/// This method is private and should only be called once by [init].
104+
Future<void> _initializeDependencies() async {
105+
_log.info('Initializing application dependencies...');
92106
try {
93107
// 1. Initialize Database Connection
94108
_mongoDbConnectionManager = MongoDbConnectionManager();
@@ -271,24 +285,33 @@ class AppDependencies {
271285
cacheDuration: EnvironmentConfig.countryServiceCacheDuration,
272286
);
273287

274-
_isInitialized = true;
275288
_log.info('Application dependencies initialized successfully.');
289+
// Signal that initialization has completed successfully.
290+
_initCompleter!.complete();
276291
} catch (e, s) {
277292
_log.severe('Failed to initialize application dependencies', e, s);
278-
_initializationError = e;
279-
_initializationStackTrace = s;
293+
// Signal that initialization has failed.
294+
_initCompleter!.completeError(e, s);
280295
rethrow;
281296
}
282297
}
283298

284299
/// Disposes of resources, such as closing the database connection.
285300
Future<void> dispose() async {
286-
if (!_isInitialized) return;
301+
// Only attempt to dispose if initialization has been started.
302+
if (_initCompleter == null) {
303+
_log.info('Dispose called, but dependencies were never initialized.');
304+
return;
305+
}
306+
307+
_log.info('Disposing application dependencies...');
287308
await _mongoDbConnectionManager.close();
288309
tokenBlacklistService.dispose();
289310
rateLimitService.dispose();
290311
countryQueryService.dispose(); // Dispose the new service
291-
_isInitialized = false;
292-
_log.info('Application dependencies disposed.');
312+
313+
// Reset the completer to allow for re-initialization (e.g., in tests).
314+
_initCompleter = null;
315+
_log.info('Application dependencies disposed and state reset.');
293316
}
294317
}

0 commit comments

Comments
 (0)