π‘οΈ Resilient Handler DiscoveryΒΆ
The Neuroglia framework now includes Resilient Handler Discovery in the Mediator, designed to handle real-world scenarios where packages may have complex dependencies or mixed architectural patterns.
π― Problem SolvedΒΆ
Previously, Mediator.configure()
would fail completely if a package's __init__.py
had any import errors, even when the package contained valid handlers that could be imported individually. This blocked automatic discovery in:
- Legacy migrations from UseCase patterns to CQRS handlers
- Mixed codebases with varying dependency graphs
- Optional dependencies that may not be available in all environments
- Modular monoliths with packages containing both new and legacy patterns
ποΈ How It WorksΒΆ
The resilient discovery implements a two-stage fallback strategy:
Stage 1: Package Import (Original Behavior)ΒΆ
# Attempts to import the entire package
Mediator.configure(builder, ['application.runtime_agent.queries'])
If successful, handlers are discovered and registered normally.
Stage 2: Individual Module FallbackΒΆ
# If package import fails, falls back to:
# 1. Discover individual .py files in the package directory
# 2. Attempt to import each module individually
# 3. Register handlers from successful imports
# 4. Skip modules with import failures
# Example fallback discovery:
# application.runtime_agent.queries.get_worker_query β
SUCCESS
# application.runtime_agent.queries.list_workers_query β
SUCCESS
# application.runtime_agent.queries.broken_module β SKIPPED
π Usage ExamplesΒΆ
Basic Usage (Unchanged)ΒΆ
from neuroglia.mediation import Mediator
from neuroglia.hosting import WebApplicationBuilder
builder = WebApplicationBuilder()
# This now works even if some packages have dependency issues
Mediator.configure(builder, [
'application.commands', # May have legacy UseCase imports
'application.queries', # Clean CQRS handlers
'application.event_handlers' # Mixed dependencies
])
app = builder.build()
Mixed Legacy/Modern CodebaseΒΆ
# Your package structure:
# application/
# βββ __init__.py # β Imports missing UseCase class
# βββ legacy_use_cases.py # β Uses old patterns
# βββ queries/
# βββ __init__.py # β
Clean file
# βββ get_user_query.py # β
Valid QueryHandler
# βββ list_users_query.py # β
Valid QueryHandler
# This now works! Handlers are discovered from individual modules
Mediator.configure(builder, ['application.queries'])
Debugging Discovery IssuesΒΆ
import logging
logging.basicConfig(level=logging.DEBUG)
# Enable detailed logging to see what's discovered vs skipped
Mediator.configure(builder, ['your.package.name'])
# Sample output:
# WARNING: Package import failed for 'application.queries': UseCase not found
# INFO: Attempting fallback: scanning individual modules
# DEBUG: Discovered submodule: application.queries.get_user_query
# DEBUG: Discovered submodule: application.queries.list_users_query
# INFO: Successfully registered 2 handlers from submodule: application.queries.get_user_query
# INFO: Fallback succeeded: registered 4 handlers from individual modules
π Logging and DiagnosticsΒΆ
The resilient discovery provides comprehensive logging at different levels:
INFO Level - Summary InformationΒΆ
INFO: Successfully registered 3 handlers from package: application.commands
INFO: Fallback succeeded: registered 2 handlers from individual modules in 'application.queries'
INFO: Handler discovery completed: 5 total handlers registered from 2 module specifications
WARNING Level - Import IssuesΒΆ
WARNING: Package import failed for 'application.queries': cannot import name 'UseCase'
WARNING: No submodules discovered for package: broken.package
WARNING: Error registering handlers from module application.legacy: circular import
DEBUG Level - Detailed DiscoveryΒΆ
DEBUG: Attempting to load package: application.queries
DEBUG: Found 3 potential submodules in application.queries
DEBUG: Discovered submodule: application.queries.get_user_query
DEBUG: Successfully registered QueryHandler: GetUserQueryHandler from application.queries.get_user_query
DEBUG: Skipping submodule 'application.queries.broken_module': ImportError
π§ͺ Best PracticesΒΆ
1. Incremental Migration StrategyΒΆ
# Start with clean packages, gradually add legacy ones
modules = [
'application.commands.user', # β
Clean CQRS handlers
'application.queries.user', # β
Clean CQRS handlers
'application.legacy.commands', # β οΈ Mixed patterns - will use fallback
]
Mediator.configure(builder, modules)
2. Package OrganizationΒΆ
# Recommended: Separate clean handlers from legacy code
application/
βββ handlers/ # β
Clean CQRS handlers only
β βββ commands/
β βββ queries/
βββ legacy/ # β οΈ Old patterns with complex dependencies
βββ use_cases/
βββ services/
3. Gradual CleanupΒΆ
# As you migrate legacy code, packages will automatically
# switch from fallback discovery to normal discovery
# No changes needed in configuration!
# Before migration (uses fallback):
# WARNING: Package import failed, using fallback discovery
# After migration (normal discovery):
# INFO: Successfully registered 5 handlers from package: application.commands
π§ Advanced ConfigurationΒΆ
Individual Module SpecificationΒΆ
You can also specify individual modules instead of packages:
Mediator.configure(builder, [
'application.commands.create_user_command',
'application.commands.update_user_command',
'application.queries.get_user_query'
])
Error HandlingΒΆ
try:
Mediator.configure(builder, ['your.package'])
except Exception as e:
# Resilient discovery should prevent most exceptions,
# but you can still catch unexpected errors
logger.error(f"Handler discovery failed: {e}")
π¨ Migration from Manual RegistrationΒΆ
Before (Manual Workaround)ΒΆ
# Old approach - manual registration due to import failures
try:
from application.queries.get_user_query import GetUserQueryHandler
from application.queries.list_users_query import ListUsersQueryHandler
builder.services.add_scoped(GetUserQueryHandler)
builder.services.add_scoped(ListUsersQueryHandler)
log.debug("Manually registered query handlers")
except ImportError as e:
log.warning(f"Could not register handlers: {e}")
After (Automatic Discovery)ΒΆ
# New approach - automatic resilient discovery
Mediator.configure(builder, ['application.queries'])
# That's it! No manual registration needed
β οΈ Important NotesΒΆ
Backward CompatibilityΒΆ
- 100% backward compatible - existing code continues to work unchanged
- No breaking changes - all existing
Mediator.configure()
calls work as before - Enhanced behavior - only adds fallback capability when needed
Performance ConsiderationsΒΆ
- Package discovery first - normal path is unchanged and just as fast
- Fallback only when needed - individual module discovery only triggers on import failures
- Directory scanning - minimal filesystem operations, cached results
- Logging overhead - debug logging can be disabled in production
LimitationsΒΆ
- Directory structure dependent - requires standard Python package layout
- Search paths - looks in
src/
,./
, andapp/
directories - File system access - requires read permissions to package directories
π BenefitsΒΆ
For DevelopersΒΆ
- Reduced friction during legacy code migration
- Automatic discovery without manual registration
- Clear diagnostics about what was discovered vs skipped
- Incremental adoption of CQRS patterns
For ProjectsΒΆ
- Mixed architectural patterns supported
- Gradual modernization without breaking changes
- Complex dependency graphs handled gracefully
- Better development experience with detailed logging
For TeamsΒΆ
- Parallel development - teams can work on different parts without breaking discovery
- Easier onboarding - less manual configuration needed
- Reduced support burden - fewer "handler not found" issues
The resilient discovery makes the Neuroglia framework significantly more robust for real-world codebases with complex dependencies and mixed architectural patterns! π―