Skip to main content

CLI & Worker Lifecycle Architecture

To ensure a "No-Magic" and deterministic experience across the modular monolith, Framework M follows a strictly separated two-phase lifecycle for all entry points (Web, CLI, Workers, REPL).

The Two-Phase Lifecycle

The framework distinguishes between wiring the application and activating its connections.

1. Hydration Phase (Synchronous)

Goal: Build the "brain" of the application.

  • Action: Call hydrate_apps(container).
  • Behavior: Scans entry points, loads provider overrides, discovers background jobs, and runs all BootstrapProtocol steps.
  • Context: Mandatory for ALL entry points.
  • Side Effects: Metadata decoration, DI wiring, job registration, and configuration loading. No network connections are opened.
  • Job Discovery: During this phase, the framework scans the framework_m.jobs entry point group in pyproject.toml and imports the specified modules, triggering @job decorators to populate the global JobRegistry.

2. Activation Phase (Asynchronous)

Goal: Open the "pipes" (connections) for long-lived processing.

  • Action: Call await container.startup_hooks().
  • Behavior: Executes all async functions registered in the container's startup hook list.
  • Context: Required for Web Apps and Background Workers.
  • Side Effects: Connecting to NATS, initializing DB connection pools, starting socket listeners.

Contract for Entry Points

Web Application

The web application (create_app) handles both phases automatically:

  • Hydration: Happens during the factory call.
  • Activation: Happens inside the Litestar app_lifespan generator.

Background Workers

Workers (e.g., m worker) must manually orchestrate both phases to ensure services like NATS and DB are ready:

# Phase 1: Hydration
container = Container()
hydrate_apps(container)

async def start_worker():
# Phase 2: Activation
await container.startup_hooks()
try:
await run_processing_loop()
finally:
# Phase 3: Teardown
await container.shutdown_hooks()

CLI Commands (Lightweight vs. Full)

Standard CLI commands (like m info or m migrate) are "Lightweight" by default.

  • One-Shot Commands: Only perform Phase 1 (Hydration). If they need a database, they use the engine provided by the hydration phase, which opens a connection on-demand and closes it when the command ends.
  • Long-Lived CLI Commands: Commands that act like mini-servers (e.g., an interactive shell or a listener) should perform Phase 2 (Activation).

Why this matters?

  1. Speed: CLI commands stay fast because they don't connect to NATS or warm up heavy pools unless they specifically ask for it.
  2. Determinism: The "Wiring" is always complete before any "Execution" begins.
  3. Isolation: Prevents side effects (like NATS subscriptions) from happening when you just want to check a configuration or run a migration.