AF-289: reuse one injector in the event delivery worker
run a fleet of AI agents on your own Kubernetes
Brought to you by:
agent-barn
Originally created by: AnaniyaT
Build the event delivery worker's injector once per process so it stops opening a new database engine and pool for every message.
What's included
reset_injector() for tests.Required repo config — none.
Notes
max_connections and a TTL on the reconciler jobs are deliberately left for follow-ups.Testing
make check-apimake check-migrationsmake test-api — 1908 passed; test_email_connection_is_refused_when_the_environment_has_no_agent_email_domain fails only because the local .env sets AGENT_EMAIL_DOMAIN, and passes with it unset.
Originally posted by: dominykas-aai-labs
1.
reset_injector()is public, unused, and re-creates the leak if anyone calls it.api/domains/events/worker.py:40-44drops the injector withoutengine.dispose(), so a production caller would strand exactly the pooled connections this ticket is about. It's also the only non-underscore helper in a module of_get_injector/_processor/_repository. And it's dead code: its single call site (test_event_delivery_worker.py:105) sits one line beforeimportlib.reload(worker), which already re-executes_injector = None.Pick one mechanism, not both — I'd delete it and keep the reload-based test, since nothing in production needs to reset the injector.
2. Move the reload inside the
try.test_event_delivery_worker.py:94runsimportlib.reload(worker)beforetry:. If that reload raises, thefinallynever runs and the worker module keeps the patchedcreate_injectorfor the rest of the pytest session —monkeypatchteardown restoresapi.core.utils, not the name the module already imported. Move line 94 to the first statement inside thetry.3. Document the new handler-thread contract.
docs/features/domain-events.md"Delivery worker contract" (lines 90-104) is the authoritative doc, and AGENTS.md step 5 plus review-protocol item 2 require code, tests, and docs to move together when an invariant changes. Handlers being process-wide singletons shared across worker threads is a new rule every future handler author must follow. One sentence near the "Handlers use a formal interface" paragraph:Originally posted by: dominykas-aai-labs
4. Replace the silent handler-registry fallback with no fallback. (pre-existing, but in the function this PR edits)
api/domains/events/worker.py:50-53swallows every exception frominjector.get(EventHandlerRegistry)with no log line and substitutes an empty registry. An empty registry raisesUnknownEventHandlerError, which the processor treats as terminal — so a transient failure resolving the registry (it pullsAgentRepository→ the engine, andEmailService→EmailClient) permanently dead-letters the delivery instead of retrying it. The line directly above,injector.get(OutboxMessageRepository), correctly lets the same class of failure propagate to dramatiq's retry. That inconsistency is the bug.Originally posted by: AnaniyaT
Addressed in the second commit:
reset_injector()removed. The tests reload the module through a fixture instead.create_injector.docs/features/domain-events.mdstating handlers are process-wide singletons shared across worker threads, with no per-delivery mutable state and their own sessions per call. Both existing handlers already comply.EventHandlerRegistryfallback is gone; a resolution failure propagates so dramatiq retries. Covered bytest_processor_propagates_handler_registry_resolution_failure. The registry only depends on the two handlers, whose email dependencies raise on send rather than on construction, so resolving it can't fail on missing config.make check-api,make check-migrations,make test-api(1909 passed; same single local-.envemail-domain failure as before).