jesseduffield / jesseduffield/lazydocker
POTENTIAL DEADLOCK: Recursive locking crash in RefreshContainersAndServices / ServiceMutex
- Dominant language
- Go
- Stars
- 52.8k
- Forks
- 1.7k
- PR merge metrics
- No merged PRs in 30d
Description
### Describe the bug
lazydocker crashes intermittently with a "POTENTIAL DEADLOCK: Recursive locking" report from `go-deadlock`, pointing at `ServiceMutex` in `RefreshContainersAndServices` (docker.go:226-227), with `ContainerMutex` (docker.go:374-375, inside the `GetContainers` call it makes) shown as a lock held by the same goroutine.
Both the "current" and "previous" lock-grab entries in the report point at the *same* call site (docker.go:227), which is the single `c.ServiceMutex.Lock()` at the top of `RefreshContainersAndServices` — that function has no recursive call into itself, so this doesn't look like a genuine self-relock in the code path itself.
My guess (not confirmed, flagging for maintainer input) is that this is a false positive triggered by goroutine-ID reuse: `gui.refresh()` in gui.go spawns a brand-new goroutine (`go func(){...}()`) on every refresh, and refresh is triggered frequently (ticker + throttled trigger). `go-deadlock`'s tracking is keyed by goroutine ID, and Go reuses goroutine IDs once a goroutine exits — so under enough refresh churn, a new goroutine can inherit the ID of a just-finished one and get flagged as "the same goroutine re-locking a mutex it already holds," even though the previous holder already unlocked and exited cleanly.
### To Reproduce
Not reliably reproducible on demand yet — happens intermittently during normal use (not rapid view-switching, unlike #585). Environment at time of crash:
- 3 containers running, default local Docker socket (no `DOCKER_HOST`, no remote context)
- Docker Engine 29.8.0
- Running inside WSL2
### Expected behavior
lazydocker should not crash from its own internal deadlock-detection instrumentation on a normal, low-load setup.
### Crash log
```
POTENTIAL DEADLOCK: Recursive locking
current goroutine 2 lock 0xc000377240
/home/runner/work/lazydocker/lazydocker/pkg/commands/docker.go:227 commands.(*DockerCommand).RefreshContainersAndServices ??? <<<<<
/home/runner/work/lazydocker/lazydocker/pkg/commands/docker.go:226 commands.(*DockerCommand).RefreshContainersAndServices ???
/home/runner/work/lazydocker/lazydocker/pkg/gui/containers_panel.go:280 gui.(*Gui).refreshContainersAndServices ???
/home/runner/work/lazydocker/lazydocker/pkg/gui/gui.go:301 gui.(*Gui).refresh.func1 ???
Previous place where the lock was grabbed (same goroutine)
/home/runner/work/lazydocker/lazydocker/pkg/commands/docker.go:227 commands.(*DockerCommand).RefreshContainersAndServices ??? <<<<<
/home/runner/work/lazydocker/lazydocker/pkg/commands/docker.go:226 commands.(*DockerCommand).RefreshContainersAndServices ???
/home/runner/work/lazydocker/lazydocker/pkg/gui/containers_panel.go:280 gui.(*Gui).refreshContainersAndServices ???
/home/runner/work/lazydocker/lazydocker/pkg/gui/gui.go:301 gui.(*Gui).refresh.func1 ???
Other goroutines holding locks:
goroutine 2 lock 0xc000377238
/home/runner/work/lazydocker/lazydocker/pkg/commands/docker.go:375 commands.(*DockerCommand).GetContainers ??? <<<<<
/home/runner/work/lazydocker/lazydocker/pkg/commands/docker.go:374 commands.(*DockerCommand).GetContainers ???
/home/runner/work/lazydocker/lazydocker/pkg/commands/docker.go:230 commands.(*DockerCommand).RefreshContainersAndServices ???
/home/runner/work/lazydocker/lazydocker/pkg/gui/containers_panel.go:280 gui.(*Gui).refreshContainersAndServices ???
/home/runner/work/lazydocker/lazydocker/pkg/gui/gui.go:301 gui.(*Gui).refresh.func1 ???
```
### Screenshots
N/A (text crash, log above)
### Which OS are you using?
- [x] Linux (WSL2, kernel 6.6.87.2-microsoft-standard-WSL2)
### Setup
- lazydocker version: 0.25.2 (commit 7e7aadc, built 2026-04-19)
- Docker Engine 29.8.0, OSType linux, default local socket
Contributor guide
Research direction
Start with pkg/commands/docker.go at RefreshContainersAndServices and GetContainers, then trace the goroutine creation in pkg/gui/gui.go and the caller in pkg/gui/containers_panel.go. Reproduce or instrument the intermittent crash and verify whether refresh goroutine reuse causes a false recursive-lock report. Done means the normal refresh path no longer crashes and the root cause is covered by a regression test or documented reproduction.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, go
- Domain
- cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100