Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/12879.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed ``GunicornWebWorker`` endlessly reloading when app fails during startup -- by :user:`Dreamsorcerer`.
24 changes: 18 additions & 6 deletions aiohttp/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,20 @@ def init_process(self) -> None:
super().init_process()

def run(self) -> None:
self._task = self.loop.create_task(self._run())
# base.Worker.init_process() sets self.booted = True before
# invoking run(), but for the aiohttp worker the real boot work
# (factory call, runner setup, binding sockets) happens here.
# Reset until _run() reaches the serve loop so that the arbiter
# can tell a startup failure from a normal worker exit and
# halt instead of endlessly respawning workers.
self.booted = False

try: # ignore all finalization problems
self._task = self.loop.create_task(self._run())
try:
self.loop.run_until_complete(self._task)
except Exception:
self.log.exception("Exception in gunicorn worker")
self.loop.run_until_complete(self.loop.shutdown_asyncgens())
self.loop.close()
finally:
self.loop.run_until_complete(self.loop.shutdown_asyncgens())
self.loop.close()

sys.exit(self.exit_code)

Expand Down Expand Up @@ -118,6 +124,12 @@ async def _run(self) -> None:
)
await site.start()

# Sockets are bound; tell the arbiter the worker is ready to
# accept requests. Any failure before this point propagates out
# of run() with self.booted=False so the arbiter exits with
# WORKER_BOOT_ERROR instead of treating this as a clean exit.
self.booted = True

# If our parent changed then we shut down.
pid = os.getpid()
try:
Expand Down
28 changes: 26 additions & 2 deletions tests/test_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,33 @@ def test_run_not_app(
worker.loop = loop
worker.wsgi = "not-app"
worker.alive = False
with pytest.raises(SystemExit):
with pytest.raises(RuntimeError, match="wsgi app should be"):
worker.run()
assert not worker.booted
assert loop.is_closed()


def test_run_on_startup_raises(
worker: base_worker.GunicornWebWorker, loop: asyncio.AbstractEventLoop
) -> None:
worker.log = mock.Mock()
worker.cfg = mock.Mock()
worker.cfg.access_log_format = ACCEPTABLE_LOG_FORMAT
worker.cfg.is_ssl = False
worker.cfg.graceful_timeout = 100
worker.sockets = []

app = web.Application()

async def boom(app: web.Application) -> None:
raise RuntimeError("boom during startup")

app.on_startup.append(boom)
worker.wsgi = app
worker.loop = loop
with pytest.raises(RuntimeError, match="boom during startup"):
worker.run()
worker.log.exception.assert_called_with("Exception in gunicorn worker")
assert not worker.booted
assert loop.is_closed()


Expand Down
Loading