Skip to content

Multiple apps in one process

A Grelmicro app is a plain async context manager, so you can construct as many as you like. Whether two can be active at the same time depends on what they configure.

The rule

Most components keep their state on the instance: Coordination, Cache, RateLimiterComponent, CircuitBreakerComponent, and HealthChecks all hold their own backend, so two overlapping apps never touch each other. They run concurrently with no special handling, the same way a web framework lets you instantiate two App objects.

Log and Trace are different. They configure process-global state (the stdlib root logger and the OpenTelemetry tracer provider) and restore the previous value on exit. Within one app the exit stack restores in reverse order, so nesting is safe. Across two overlapping apps there is no such ordering guarantee: the first to exit would restore the global and clobber the second app's configuration.

Because they own a single global, these components are singletons: registering a second Log (or Trace) on the same app raises ComponentAlreadyRegisteredError, even under a different name. There is only one root logger to configure, so a second instance has nothing of its own to own.

So grelmicro also blocks the cross-app case that is unsafe: opening a second app that owns Log or Trace while another app that owns one is still active raises MultipleActiveAppsError. Apps without those components overlap freely.

fragment
# Fine: neither app owns process-global state.
async with Grelmicro(uses=[Coordination(redis)]):
    async with Grelmicro(uses=[Cache(redis)]):
        ...

# Raises MultipleActiveAppsError: both own the root logger.
async with Grelmicro(uses=[Log()]):
    async with Grelmicro(uses=[Log()]):
        ...

Opting out

Pass Grelmicro(allow_multiple=True) when you are sure two active apps will not fight over the same global. The guard then steps aside for that app. Run apps sequentially whenever you can: it is the simplest way to keep logging and tracing predictable.

App factories

One Grelmicro is open once at a time. install(app) opens it in the framework lifespan, and a second lifespan on the same micro raises OutOfContextError while the first is still running. So two test clients open at once, or one micro installed on two apps that run together, need one Grelmicro each.

Each test client also runs the lifespan on an event loop of its own, and connection pools and locks belong to the loop that opened them. An app factory builds everything per app, so nothing crosses loops:

from collections.abc import Iterator

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient

from grelmicro import Grelmicro
from grelmicro.providers.redis import RedisProvider


# app.py
def create_app() -> tuple[FastAPI, Grelmicro]:
    micro = Grelmicro(uses=[RedisProvider("redis://cache:6379/0")])
    app = FastAPI()
    micro.install(app)
    return app, micro


# conftest.py
@pytest.fixture
def client() -> Iterator[TestClient]:
    app, micro = create_app()
    with micro.fake(), TestClient(app) as client:
        yield client


# test_two_clients.py
def test_two_clients_at_once(client: TestClient) -> None:
    other, micro = create_app()
    with micro.fake(), TestClient(other) as second:
        assert (
            client.get("/docs").status_code == second.get("/docs").status_code
        )

Running apps one after the other needs none of this. A Grelmicro closes with the client and opens again with the next one, so a module-level micro serves a whole suite of tests that each start their own client.