Skip to content

Fix process hang on /exit due to bubbletea renderer deadlock#2269

Merged
dgageot merged 2 commits intodocker:mainfrom
aheritier:fix/exit-hang-deadlock
Mar 28, 2026
Merged

Fix process hang on /exit due to bubbletea renderer deadlock#2269
dgageot merged 2 commits intodocker:mainfrom
aheritier:fix/exit-hang-deadlock

Conversation

@aheritier
Copy link
Copy Markdown
Contributor

Fixes #2268

Problem

After typing /exit, the docker-agent process hangs indefinitely. The root
cause is a mutex deadlock inside bubbletea v2's cursedRenderer: the
renderer's ticker-driven flush goroutine holds a mutex while blocked on a
write syscall to stdout (terminal buffer full), and the event loop's final
render() call after tea.Quit blocks trying to acquire the same mutex.

See #2268 for full diagnostic including goroutine dumps and sequence diagram.

Solution

Add a safety-net goroutine in cleanupAll() that calls os.Exit(0) after a
5-second timeout. This guarantees the process terminates even when bubbletea's
renderer is deadlocked.

The timeout and exit function are exposed as package-level variables
(shutdownTimeout, exitFunc) so tests can override them.

Tests

  • TestExitDeadlock_BlockedStdout — proves the bubbletea deadlock exists
    (p.Run never returns when stdout blocks during the final render)
  • TestCleanupAll_SpawnsSafetyNet — verifies cleanupAll spawns the
    safety-net goroutine that calls exitFunc after the timeout
  • TestExitSafetyNet_BlockedStdout — end-to-end: blocked stdout + safety
    net fires
  • TestExitSafetyNet_GracefulShutdown — verifies the safety net does NOT
    fire during normal shutdown (no blocked stdout)

Add tests that demonstrate the bubbletea v2 renderer mutex deadlock
that causes the process to hang after /exit when stdout blocks.

- TestExitDeadlock_BlockedStdout: proves p.Run() hangs when the
  renderer's flush goroutine holds the mutex during a blocked stdout
  write, preventing the final render after tea.Quit.

- TestExitSafetyNet_BlockedStdout: verifies a safety-net timer can
  break the deadlock by calling an exit function.

- TestExitSafetyNet_GracefulShutdown: verifies the safety net does
  not fire during normal (non-blocked) shutdown.

- TestCleanupAll_SpawnsSafetyNet: verifies cleanupAll spawns the
  safety-net goroutine that calls exitFunc. This test currently
  FAILS because the safety net is not yet implemented.

Refs: docker#2268

Assisted-By: docker-agent
When the user types /exit, bubbletea's cursedRenderer can deadlock: its
ticker-driven flush goroutine holds a mutex while blocked writing to
stdout, and the event loop's final render() call after tea.Quit blocks
on the same mutex. p.Run() never returns and the process hangs.

Add a safety-net goroutine in cleanupAll() that calls os.Exit(0) after
5 seconds, guaranteeing the process terminates even when bubbletea's
renderer is deadlocked.

Fixes docker#2268

Assisted-By: docker-agent
@aheritier aheritier marked this pull request as ready for review March 27, 2026 22:24
@aheritier aheritier requested a review from a team as a code owner March 27, 2026 22:24
@dgageot dgageot merged commit 6ecb25b into docker:main Mar 28, 2026
8 checks passed
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.

Process hangs on /exit due to bubbletea renderer mutex deadlock when stdout blocks

2 participants