Replies: 4 comments 7 replies
-
Yeah that looks good 😊 As for the name, could be a longlived_service, or maybe an application_service (although that might be confusing)? |
Beta Was this translation helpful? Give feedback.
-
better name idea: |
Beta Was this translation helpful? Give feedback.
-
So, as a datapoint, here is what I ended up adding in my fastapi lifespan function, as a way to avoid initializing an engine as a side-effect of initializing the registry. @functools.cache
def get_engine() -> AsyncEngine:
return create_async_engine("...")
registry.register_factory(
AsyncEngine,
get_engine,
on_registry_close=lambda: get_engine().dispose,
) This allows me to easily replace the engine in the tests with lifespan.registry.register_value(AsyncEngine, pytest_managed_postgres_engine) |
Beta Was this translation helpful? Give feedback.
-
I wanted some services to live as long as the registry. I ended up creating a It looks like this: @contextlib.asynccontextmanager
async def setup_app(registry: svcs.Registry) -> AsyncIterator[None]:
# Create long-lived, expensive services (e.g., database connections, caches, Pydantic settings)
engine = create_async_engine(
url='sqlite+aiosqlite://',
echo=True,
echo_pool=True,
connect_args={'check_same_thread': False},
)
session_factory = async_sessionmaker(engine, expire_on_commit=False)
cache = SomeRedisCache('localhost')
# Register these "singleton" services
registry.register_value(
AsyncEngine,
engine,
on_registry_close=engine.dispose,
)
registry.register_value(SomeRedisCache, cache, on_registry_close=cache.aclose)
# Register "normal" services that are created for each request
registry.register_factory(
AsyncSession,
session_factory,
)
registry.register_factory(
CacheConnection,
connection_factory,
)
yield
# Close resources that are not managed by `svcs` (i.e., those without `on_registry_close`, etc.) If I use FastAPI: @svcs.fastapi.lifespan
async def lifespan_manager(app: FastAPI, registry: svcs.Registry) -> AsyncIterator[dict[str, Any]]:
async with setup_app(registry):
# Perform other lifespan-related tasks
yield {}
app = FastAPI(
lifespan=lifespan_manager
)
@app.get('/')
async def index(services: svcs.fastapi.DepContainer):
engine, session, cache, cache_conn = await services.aget(AsyncEngine, AsyncSession)
dt = await session.scalar(text("SELECT time('now')"))
return {
'engine_id': id(engine),
'session_id': id(session),
'sql_dt': str(dt),
} During each request, async def cli_entrypoint():
registry = svcs.Registry()
async with setup_app(registry):
async with svcs.Container(registry) as con:
await some_operation(con)
...
await registry.aclose() Does this approach seem like a good design for managing long-lived and per-request services? Would you suggest any improvements or alternative patterns? |
Beta Was this translation helpful? Give feedback.
-
This idea is based on #8 (comment) and @elfjes concerns about container lifetimes.
Currently, svcs allows to register
on_registry_close
cleanups, but that's only necessary if an object created outside of svcs. For me that's common, but maybe just because I'm used to it.The idea is to add a third kind of service, the singleton. A singleton is a factory that is only called once it is needed and then it lives as long as the registry.
So instead of:
You'd write:
This allows to late-bind base/Service and integrates the engine more seamlessly into the whole life-cycle
Is this fit your vision @elfjes?
I'm also open to a better name, because surely there's some hairsplitting to be done about the term "singleton".
Beta Was this translation helpful? Give feedback.
All reactions