d5b122d4a3
CI / lint (push) Successful in 14s
CI / quality (push) Successful in 21s
CI / security (push) Successful in 36s
CI / build (push) Successful in 36s
CI / typecheck (push) Successful in 39s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m57s
CI / integration_tests (push) Successful in 3m23s
CI / docker (push) Successful in 53s
CI / coverage (push) Successful in 5m58s
CI / benchmark-publish (push) Successful in 19m27s
111 lines
3.9 KiB
Markdown
111 lines
3.9 KiB
Markdown
# Async Resource Safety
|
|
|
|
## Overview
|
|
|
|
CleverAgents uses asynchronous resources throughout its stack — event
|
|
subscriptions, LangGraph tasks, checkpoint file I/O, and reactive stream
|
|
connections. The **async-cleanup** subsystem ensures that these resources
|
|
are tracked, closed deterministically on shutdown, and that leaks are
|
|
detected and logged.
|
|
|
|
## Core Component: `AsyncResourceTracker`
|
|
|
|
`cleveragents.core.async_cleanup.AsyncResourceTracker` is the central
|
|
registry for any resource that implements the `AsyncResource` protocol
|
|
(i.e.\ exposes an `async def close() -> None` method).
|
|
|
|
### Registration
|
|
|
|
```python
|
|
from cleveragents.core.async_cleanup import AsyncResourceTracker
|
|
|
|
tracker = AsyncResourceTracker()
|
|
tracker.register("db-pool", db_pool)
|
|
tracker.register("event-queue", event_queue)
|
|
```
|
|
|
|
- Names must be unique and non-empty.
|
|
- Duplicate registrations raise `ValueError`.
|
|
- `None` resources are rejected immediately.
|
|
|
|
### Shutdown
|
|
|
|
```python
|
|
await tracker.close_all(timeout=30.0)
|
|
```
|
|
|
|
`close_all()` iterates over every registered resource and awaits its
|
|
`close()` coroutine with `asyncio.wait_for()`. Resources that exceed
|
|
the deadline are logged as forced terminations and their names are
|
|
collected in `tracker.timed_out_resources`.
|
|
|
|
`close_all()` is **idempotent** — calling it multiple times is safe.
|
|
|
|
**After `close_all()`**, calling `register()` raises `RuntimeError`.
|
|
Resources cannot be added to a closed tracker.
|
|
|
|
### Leak Detection
|
|
|
|
If the tracker is garbage-collected without `close_all()` having been
|
|
called, the `__del__` finalizer logs a warning for each unclosed
|
|
resource **by name**. This makes it straightforward to identify leaks
|
|
during development and in CI logs.
|
|
|
|
### Async Context Manager
|
|
|
|
```python
|
|
async with AsyncResourceTracker() as tracker:
|
|
tracker.register("conn", connection)
|
|
# ... use connection ...
|
|
# connection.close() is awaited automatically
|
|
```
|
|
|
|
## Enhanced Bridge Cleanup
|
|
|
|
`cleveragents.langgraph.bridge.RxPyLangGraphBridge` now provides:
|
|
|
|
- **`cleanup_tasks_async(timeout)`** — cancels all in-flight asyncio
|
|
tasks and awaits their completion within *timeout* seconds. Tasks
|
|
that do not finish are logged as warnings.
|
|
- **`cancel_task_with_reason(task, reason)`** — cancels a specific task
|
|
and records the human-readable *reason* in
|
|
`bridge.cancellation_reasons`.
|
|
|
|
The synchronous `cleanup_tasks()` remains for best-effort cleanup in
|
|
`__del__`.
|
|
|
|
## Subscription Cleanup
|
|
|
|
`cleveragents.a2a.events.A2aEventQueue.close()` removes all local
|
|
subscriptions, clears the event buffer, and logs the count of
|
|
subscriptions that were active. After `close()`, calling `publish()`
|
|
raises `RuntimeError`. The `is_closed` property exposes the closed
|
|
state for callers to check.
|
|
|
|
## Checkpoint File Safety
|
|
|
|
`cleveragents.langgraph.state.StateManager.close()` marks the manager
|
|
as closed, completes the underlying RxPY `BehaviorSubject`, and
|
|
prevents further state updates. After `close()`, calling
|
|
`update_state()`, `reset()`, `load_checkpoint()`, or `time_travel()`
|
|
raises `RuntimeError`. Checkpoint files written via
|
|
`_save_checkpoint()` use `Path.write_text()`, which handles file-handle
|
|
closing internally.
|
|
|
|
## Thread Safety
|
|
|
|
`AsyncResourceTracker.register()` and `close_all()` are protected by a
|
|
`threading.Lock` so that resources can be registered from any thread
|
|
without races. After `close_all()` completes, `register()` raises
|
|
`RuntimeError` to prevent silently leaked resources.
|
|
|
|
## Design Decisions
|
|
|
|
| Decision | Rationale |
|
|
|---|---|
|
|
| Protocol-based `AsyncResource` | Structural typing avoids coupling to a specific base class. |
|
|
| Per-resource timeout | One slow resource should not block the entire shutdown sequence. |
|
|
| Idempotent `close_all` | Prevents double-close errors in complex shutdown paths. |
|
|
| `__del__` leak warning | Best-effort; relies on CPython deterministic GC but degrades safely. |
|
|
| Cancellation reason dict | Lightweight tracing for debugging cancelled tasks without heavy instrumentation. |
|