Application Lifecycle
Understanding myfy's application lifecycle helps you manage resources, handle errors, and build reliable applications.
Lifecycle Phases
- App Creation
- Initialization
- Starting Modules
- Running
- Shutdown Signal
- Stopping Modules
- Exiting
Phase 1: Creation
from myfy.core import Application
# Create application instance
app = Application(settings_class=Settings, auto_discover=False)
app.add_module(WebModule())
app.add_module(DataModule())
What happens: - Settings class is stored - Modules are collected - No providers registered yet - No connections made yet
Phase 2: Initialize
What happens:
1. Load settings from environment
2. Validate settings with Pydantic
3. Create DI container
4. Call module.configure(container) on each module
5. Register all providers
6. Build dependency graph
7. Detect cycles
8. Validate scopes
9. Cache injection plans
When it runs:
- Explicitly via app.initialize()
- Automatically on first app.run()
- Automatically by CLI commands
Phase 3: Start Modules
What happens:
1. Call await module.start() on each module (in order)
2. Connect to databases
3. Open network connections
4. Start background tasks
5. Warm up caches
Order: Modules start in the order they were added.
Phase 4: Run
What happens: - Start all modules (if not already started) - Run the main application loop - For WebModule: Start ASGI server - Handle SIGTERM and SIGINT gracefully
Blocking: This method blocks until shutdown signal received.
Phase 5: Stop Modules
What happens:
1. Call await module.stop() on each module (reverse order)
2. Close database connections
3. Stop background tasks
4. Flush buffers
5. Release resources
Order: Modules stop in reverse order (last added stops first).
Complete Example
from myfy.core import Application, BaseModule
from myfy.web import WebModule
class DataModule(BaseModule):
def __init__(self):
super().__init__("data")
self.db = None
def configure(self, container: Container) -> None:
print("1. DataModule.configure()")
# Register providers
async def start(self) -> None:
print("3. DataModule.start()")
self.db = Database(settings.database_url)
await self.db.connect()
async def stop(self) -> None:
print("5. DataModule.stop()")
if self.db:
await self.db.disconnect()
# Create app
app = Application(settings_class=Settings, auto_discover=False)
app.add_module(DataModule()) # Added first
app.add_module(WebModule()) # Added second
# Initialize
print("Phase: Initialize")
app.initialize()
# Output:
# 1. DataModule.configure()
# 2. WebModule.configure()
# Run
print("Phase: Start & Run")
await app.run()
# Output:
# 3. DataModule.start()
# 4. WebModule.start()
# ... application runs ...
# (user presses Ctrl+C)
# 6. WebModule.stop()
# 5. DataModule.stop()
Graceful Shutdown
myfy handles shutdown signals automatically:
Signals Handled
- SIGTERM - From
killcommand or container orchestrator - SIGINT - From Ctrl+C in terminal
Shutdown Process
# Application receives signal
→ Log: "Received shutdown signal"
→ Stop accepting new requests
→ Wait for in-flight requests (max 30s)
→ Call module.stop() in reverse order
→ Exit with code 0
Custom Shutdown Logic
class MyModule(BaseModule):
async def stop(self) -> None:
print("Shutting down gracefully...")
# Finish in-flight work
await self.task_queue.wait_empty(timeout=30)
# Flush buffers
await self.metrics.flush()
# Close connections
await self.db.disconnect()
print("Shutdown complete")
Error Handling
Startup Errors
class DataModule(BaseModule):
async def start(self) -> None:
try:
await self.db.connect()
except ConnectionError as e:
print(f"Failed to connect to database: {e}")
raise # Application will exit
# If module.start() raises, app exits with error code 1
Runtime Errors
@route.get("/users")
async def list_users(db: Database) -> list[User]:
try:
return await db.get_all_users()
except DatabaseError as e:
# Handle error
raise HTTPException(status_code=500, detail="Database error")
Shutdown Errors
class MyModule(BaseModule):
async def stop(self) -> None:
try:
await self.cleanup()
except Exception as e:
# Log but don't prevent other modules from stopping
print(f"Error during cleanup: {e}")
Testing Lifecycle
Manual Control
import pytest
@pytest.fixture
async def app():
app = Application(settings_class=TestSettings, auto_discover=False)
app.add_module(DataModule())
# Initialize
app.initialize()
# Start
await app.start_modules()
yield app
# Stop
await app.stop_modules()
@pytest.mark.asyncio
async def test_user_creation(app):
service = app.container.get(UserService)
user = await service.create_user("[email protected]")
assert user.email == "[email protected]"
Best Practices
✅ DO
- Initialize connections in
module.start() - Close connections in
module.stop() - Handle errors in
start()and fail fast - Log lifecycle events
- Wait for in-flight work before stopping
- Test lifecycle hooks
❌ DON'T
- Don't open connections in
configure() - Don't skip cleanup in
stop() - Don't ignore errors in
start() - Don't block indefinitely in
stop() - Don't access container outside
configure()
Advanced Patterns
Dependent Module Startup
class CacheModule(BaseModule):
async def start(self) -> None:
# Wait for database to be ready
db = self.container.get(Database)
await db.wait_ready(timeout=30)
# Now start cache
self.cache = Cache(db)
await self.cache.warm_up()
Healthchecks
class DataModule(BaseModule):
async def is_healthy(self) -> bool:
"""Check if module is healthy."""
try:
await self.db.ping()
return True
except Exception:
return False
# In route
@route.get("/health")
async def health_check(app: Application) -> dict:
checks = {}
for module in app.modules:
if hasattr(module, 'is_healthy'):
checks[module.name] = await module.is_healthy()
return {"healthy": all(checks.values()), "checks": checks}
Periodic Tasks
class MetricsModule(BaseModule):
async def start(self) -> None:
# Start background task
self.task = asyncio.create_task(self._collect_metrics())
async def stop(self) -> None:
# Cancel background task
self.task.cancel()
try:
await self.task
except asyncio.CancelledError:
pass
async def _collect_metrics(self) -> None:
while True:
await self.collect()
await asyncio.sleep(60) # Every minute
Troubleshooting
Module Won't Start
Solution: Check that dependencies (database, redis, etc.) are running:
Slow Shutdown
Solution: Reduce timeout or fix blocking code:
async def stop(self) -> None:
# Add timeout
try:
await asyncio.wait_for(self.cleanup(), timeout=10)
except asyncio.TimeoutError:
print("Cleanup timed out, forcing shutdown")
Module Stops in Wrong Order
Solution: Add modules in correct order:
# Database should be added before Cache
app.add_module(DataModule()) # First
app.add_module(CacheModule()) # Second (depends on DataModule)
CLI Lifecycle
When using myfy run:
Lifecycle:
1. CLI discovers app.py
2. Loads Application instance
3. Calls app.initialize()
4. Calls app.start_modules()
5. Starts uvicorn with ASGI app
6. (Ctrl+C pressed)
7. Uvicorn stops accepting requests
8. Calls app.stop_modules()
9. Exits
Next Steps
- Modules - Create custom modules
- Dependency Injection - Use DI in lifecycle
- Testing Guide - Test lifecycle
- Deployment - Production lifecycle