diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e1c8b6..e0a360e 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. diff --git a/lib/src/config/app_dependencies.dart b/lib/src/config/app_dependencies.dart index 3de4fdc..773dd63 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 @@ -79,16 +81,30 @@ class AppDependencies { /// Initializes all application dependencies. /// /// 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); + 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. + 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...'); + // We intentionally don't await this future here. The completer's future, + // which is returned below, is what callers will await. + unawaited(_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 +287,45 @@ 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; + } + + // 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(); 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.'); } }