From 5593b0e4d584bba414331fae1e7acee1f9dc205d Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 19 Oct 2025 10:10:02 +0100 Subject: [PATCH 1/6] 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. --- lib/src/config/app_dependencies.dart | 51 ++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/lib/src/config/app_dependencies.dart b/lib/src/config/app_dependencies.dart index 3de4fdc..7f8435e 100644 --- a/lib/src/config/app_dependencies.dart +++ b/lib/src/config/app_dependencies.dart @@ -1,5 +1,6 @@ // ignore_for_file: public_member_api_docs +import 'dart:async'; import 'package:core/core.dart'; import 'package:data_mongodb/data_mongodb.dart'; import 'package:data_repository/data_repository.dart'; @@ -40,11 +41,12 @@ class AppDependencies { /// Provides access to the singleton instance. static AppDependencies get instance => _instance; - bool _isInitialized = false; - Object? _initializationError; - StackTrace? _initializationStackTrace; final _log = Logger('AppDependencies'); + // A Completer to manage the one-time asynchronous initialization. + // This ensures the initialization logic runs only once. + Completer? _initCompleter; + // --- Late-initialized fields for all dependencies --- // Database @@ -80,15 +82,27 @@ class AppDependencies { /// /// This method is idempotent; it will only run the initialization logic once. Future init() async { - // If initialization previously failed, re-throw the original error. - if (_initializationError != null) { - return Future.error(_initializationError!, _initializationStackTrace); + // If _initCompleter is not null, it means initialization is either in + // progress or has already completed. Return its future to allow callers + // to await the single, shared initialization process. + if (_initCompleter != null) { + return _initCompleter!.future; } - if (_isInitialized) return; + // This is the first call to init(). Create the completer and start the + // initialization process. + _initCompleter = Completer(); + _log.info('Starting application dependency initialization...'); + _initializeDependencies(); - _log.info('Initializing application dependencies...'); + // Return the future from the completer. + return _initCompleter!.future; + } + /// The core logic for initializing all dependencies. + /// This method is private and should only be called once by [init]. + Future _initializeDependencies() async { + _log.info('Initializing application dependencies...'); try { // 1. Initialize Database Connection _mongoDbConnectionManager = MongoDbConnectionManager(); @@ -271,24 +285,33 @@ class AppDependencies { cacheDuration: EnvironmentConfig.countryServiceCacheDuration, ); - _isInitialized = true; _log.info('Application dependencies initialized successfully.'); + // Signal that initialization has completed successfully. + _initCompleter!.complete(); } catch (e, s) { _log.severe('Failed to initialize application dependencies', e, s); - _initializationError = e; - _initializationStackTrace = s; + // Signal that initialization has failed. + _initCompleter!.completeError(e, s); rethrow; } } /// Disposes of resources, such as closing the database connection. Future dispose() async { - if (!_isInitialized) return; + // Only attempt to dispose if initialization has been started. + if (_initCompleter == null) { + _log.info('Dispose called, but dependencies were never initialized.'); + return; + } + + _log.info('Disposing application dependencies...'); await _mongoDbConnectionManager.close(); tokenBlacklistService.dispose(); rateLimitService.dispose(); countryQueryService.dispose(); // Dispose the new service - _isInitialized = false; - _log.info('Application dependencies disposed.'); + + // Reset the completer to allow for re-initialization (e.g., in tests). + _initCompleter = null; + _log.info('Application dependencies disposed and state reset.'); } } From 853863b5ff01bf0f699fbebd14cd1c65ad00658b Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 19 Oct 2025 10:11:42 +0100 Subject: [PATCH 2/6] chore(core): resolve unawaited_futures lint in app_dependencies Resolves the `unawaited_futures` lint warning by wrapping the call to `_initializeDependencies()` with `unawaited()`. This makes it explicit that the future is intentionally not being awaited at this call site, as the logic is correctly handled by the `_initCompleter`. --- lib/src/config/app_dependencies.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/config/app_dependencies.dart b/lib/src/config/app_dependencies.dart index 7f8435e..4f962fe 100644 --- a/lib/src/config/app_dependencies.dart +++ b/lib/src/config/app_dependencies.dart @@ -93,7 +93,9 @@ class AppDependencies { // initialization process. _initCompleter = Completer(); _log.info('Starting application dependency initialization...'); - _initializeDependencies(); + // We intentionally don't await this future here. The completer's future, + // which is returned below, is what callers will await. + unawaited(_initializeDependencies()); // Return the future from the completer. return _initCompleter!.future; From aa55e93ab7299d638eaa6d6f4ad6259c4aaa0a61 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 19 Oct 2025 10:12:04 +0100 Subject: [PATCH 3/6] fix(dependencies): prevent race condition during concurrent initialization - Add guard to ensure only one instance of dependency initialization runs at a time - Update CHANGELOG.md to reflect the upcoming fix --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e1c8b6..b963480 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Upcoming Release + +- fix: prevent race condition during concurrent dependency initialization. + ## 1.0.1 - 2025-10-17 - **chore**: A new migration ensures that existing user preference documents are updated to include the savedFilters field, initialized as an empty array. From cd29d0cfaba8c10363e1ceb3e6782ff6e092bb14 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 19 Oct 2025 10:20:59 +0100 Subject: [PATCH 4/6] fix(core): prevent race condition in dispose method Updates the `dispose` method in `AppDependencies` to await the completion of the `_initCompleter` before proceeding with resource cleanup. This change prevents a race condition where `dispose()` could be called while `_initializeDependencies()` is still executing, which would lead to unpredictable errors from tearing down resources that are actively being initialized. The `dispose` method now safely waits for initialization to finish, whether it succeeded or failed, ensuring a clean and predictable shutdown sequence. --- lib/src/config/app_dependencies.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/src/config/app_dependencies.dart b/lib/src/config/app_dependencies.dart index 4f962fe..e1e9922 100644 --- a/lib/src/config/app_dependencies.dart +++ b/lib/src/config/app_dependencies.dart @@ -306,6 +306,18 @@ class AppDependencies { return; } + // Wait for initialization to complete before disposing resources. + // This prevents a race condition if dispose() is called during init(). + try { + await _initCompleter!.future; + } catch (_) { + // Initialization may have failed, but we still proceed to dispose + // any partially initialized resources. + _log.warning( + 'Disposing dependencies after a failed initialization attempt.', + ); + } + _log.info('Disposing application dependencies...'); await _mongoDbConnectionManager.close(); tokenBlacklistService.dispose(); From 658b5a8ea69d088f8b86ab9b18cdfdff7490c50e Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 19 Oct 2025 10:22:03 +0100 Subject: [PATCH 5/6] refactor(dependencies): remove unnecessary 'async' keyword Remove 'async' from the 'init' method in AppDependencies class. The method no longer contains 'await' calls, making the 'async' keyword redundant. This change slightly optimizes the method definition without altering its functionality. --- lib/src/config/app_dependencies.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/config/app_dependencies.dart b/lib/src/config/app_dependencies.dart index e1e9922..773dd63 100644 --- a/lib/src/config/app_dependencies.dart +++ b/lib/src/config/app_dependencies.dart @@ -81,7 +81,7 @@ class AppDependencies { /// Initializes all application dependencies. /// /// This method is idempotent; it will only run the initialization logic once. - Future init() async { + Future init() { // If _initCompleter is not null, it means initialization is either in // progress or has already completed. Return its future to allow callers // to await the single, shared initialization process. From 8fc6336b9138cb1a0fb62d4af9abf3d8f6c95917 Mon Sep 17 00:00:00 2001 From: fulleni Date: Sun, 19 Oct 2025 10:22:32 +0100 Subject: [PATCH 6/6] docs(changelog): update fix description format - Change single dash to double asterisks for fix description in upcoming release section --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b963480..e0a360e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Upcoming Release -- fix: prevent race condition during concurrent dependency initialization. +- **fix**: prevent race condition during concurrent dependency initialization. ## 1.0.1 - 2025-10-17