Skip to content

fix(context): make ApplicationContext.start() idempotent (release 26.09.02) - #162

Merged
ancongui merged 1 commit into
mainfrom
fix/context-double-start
Sep 10, 2026
Merged

fix(context): make ApplicationContext.start() idempotent (release 26.09.02)#162
ancongui merged 1 commit into
mainfrom
fix/context-double-start

Conversation

@ancongui

Copy link
Copy Markdown
Contributor

ApplicationContext already tracked _started — set at the end of start(), cleared at the end of
stop() — but nothing ever read it. A second start() therefore re-ran the entire pipeline instead
of returning.

That does not refresh the context; it builds a second one beside it. Auto-configurations register again,
@configuration classes are processed again, and a second fully-initialised set of singletons is created
and started:

  • a second Kafka consumer joins the same group and steals partitions from the first
  • a second scheduler fires every @scheduled task twice
  • a second connection pool opens

Nothing owns the duplicates, so stop() disposes one set and leaks the other.

A double start is easy to reach — an ASGI server that runs the lifespan twice, a reload, or a test
harness sharing one module-level application across files — and it failed silently, which is the
worst property a lifecycle bug can have.

Every adapter in this codebase already guards itself this way; KafkaEventBus.start() opens with
if self._started: return. The context now follows its own convention. stop() still clears the flag,
so a stopped context restarts and rebuilds normally — asserted by a second test.

Found by building a service on 26.09.01: two test modules sharing one module-level app produced two
EventPublisher instances in a single container.

Verification

Gate Result
pytest tests/ (CI's ignore set) 5000 passed, 7 skipped, 0 failed
mypy src/pyfly --strict no issues in 694 source files
ruff check / ruff format --check passed / 1274 files formatted

The context already tracked `_started` — set at the end of start(), cleared at the
end of stop() — but nothing ever read it. A second start() therefore re-ran the
entire pipeline instead of returning: auto-configurations registered again,
@configuration classes were processed again, and a second fully-initialised set of
singletons was created AND STARTED beside the first.

The duplicates are live, not inert: a second Kafka consumer joins the same group
and steals partitions from the first, a second scheduler fires every @scheduled
task twice, a second connection pool opens. Nothing owns them, so stop() disposes
one set and leaks the other.

A double start is easy to reach — an ASGI server that runs the lifespan twice, a
reload, a test harness sharing one module-level application across files — and it
failed silently. Every adapter in this codebase already guards itself the same way
(KafkaEventBus.start() opens with `if self._started: return`); the context now
follows its own convention. stop() still clears the flag, so a stopped context
restarts and rebuilds normally.

Found by building a service on 26.09.01, where two test modules sharing one
module-level app produced two EventPublishers in one container.

Release 26.09.02.
@ancongui
ancongui merged commit 6f2e345 into main Sep 10, 2026
6 checks passed
@ancongui
ancongui deleted the fix/context-double-start branch September 10, 2026 04:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant