[PR #151] [MERGED] Fix stop intent races and isolate shared records #151

Closed
opened 2026-10-15 12:52:23 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/games-on-whales/LXC2Docker/pull/151
Author: @JBailes
Created: 8/9/2026
Status: ✅ Merged
Merged: 8/9/2026
Merged by: @JBailes

Base: main ← Head: fix/stop-intent-before-stop


📝 Commits (3)

  • 6abbc2c api: record stop intent before stopping, not after
  • 6b5cb39 store: isolate container records across goroutines
  • 643be48 api: isolate exec records across goroutines

📊 Changes

11 files changed (+905 additions, -176 deletions)

View changed files

📝 internal/api/containers.go (+118 -28)
📝 internal/api/exec.go (+17 -3)
📝 internal/api/exec_claim_test.go (+50 -0)
📝 internal/api/portainer_ops.go (+7 -6)
➕ internal/api/stop_intent_test.go (+210 -0)
📝 internal/api/system.go (+6 -2)
📝 internal/lxc/manager.go (+141 -115)
📝 internal/lxc/pve_tarball.go (+14 -15)
➕ internal/lxc/restart_policy_race_test.go (+72 -0)
📝 internal/store/store.go (+124 -7)
📝 internal/store/store_test.go (+146 -0)

📄 Description

Summary

  • record user stop intent before stop, kill, and force-remove lifecycle operations
  • serialize API lifecycle operations with restart-watcher decisions per container
  • return isolated container metadata snapshots and use atomic updates for concurrent writers
  • isolate exec records so background completion cannot race with inspect readers
  • honor each container's configured StopTimeout during force removal

Root cause

GetContainer, ListContainers, and the exec store returned live pointers after releasing their mutexes. API request goroutines and background watchers then read and mutated those records concurrently.

For containers using unless-stopped, stop intent was also recorded only after the workload exited. A restart-watcher tick in that shutdown window could observe an exited container without user intent and immediately start it again. The already-stopped path returned 304 without recording intent at all.

Impact

Explicitly stopping Wolf and other unless-stopped containers now keeps them stopped. Stop, kill, restart-watcher, and removal operations establish a deterministic final state, while lifecycle operations for unrelated containers remain concurrent.

Validation

  • go build ./...
  • go vet ./...
  • go test ./... -skip '^TestCheckCreateDiskPressure$' -count=1
  • go test -race ./... -skip '^TestCheckCreateDiskPressure$' -count=1
  • targeted tests cover stop-intent ordering and rollback, overlapping stops, watcher-vs-stop coordination, container-record aliasing, and exec-record aliasing

TestCheckCreateDiskPressure is excluded because it depends on host free space and fails on this development machine before and after the change. No tests were run against a live Proxmox VE host.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/games-on-whales/LXC2Docker/pull/151 **Author:** [@JBailes](https://github.com/JBailes) **Created:** 8/9/2026 **Status:** ✅ Merged **Merged:** 8/9/2026 **Merged by:** [@JBailes](https://github.com/JBailes) **Base:** `main` ← **Head:** `fix/stop-intent-before-stop` --- ### 📝 Commits (3) - [`6abbc2c`](https://github.com/games-on-whales/LXC2Docker/commit/6abbc2c3ed007306b65fe8b955802cba7b7e81ee) api: record stop intent before stopping, not after - [`6b5cb39`](https://github.com/games-on-whales/LXC2Docker/commit/6b5cb39594c85c4182065e5d9783a0dfa759a410) store: isolate container records across goroutines - [`643be48`](https://github.com/games-on-whales/LXC2Docker/commit/643be48313922a85130fbf041c28573f77e3a9c4) api: isolate exec records across goroutines ### 📊 Changes **11 files changed** (+905 additions, -176 deletions) <details> <summary>View changed files</summary> 📝 `internal/api/containers.go` (+118 -28) 📝 `internal/api/exec.go` (+17 -3) 📝 `internal/api/exec_claim_test.go` (+50 -0) 📝 `internal/api/portainer_ops.go` (+7 -6) ➕ `internal/api/stop_intent_test.go` (+210 -0) 📝 `internal/api/system.go` (+6 -2) 📝 `internal/lxc/manager.go` (+141 -115) 📝 `internal/lxc/pve_tarball.go` (+14 -15) ➕ `internal/lxc/restart_policy_race_test.go` (+72 -0) 📝 `internal/store/store.go` (+124 -7) 📝 `internal/store/store_test.go` (+146 -0) </details> ### 📄 Description ## Summary - record user stop intent before stop, kill, and force-remove lifecycle operations - serialize API lifecycle operations with restart-watcher decisions per container - return isolated container metadata snapshots and use atomic updates for concurrent writers - isolate exec records so background completion cannot race with inspect readers - honor each container's configured StopTimeout during force removal ## Root cause GetContainer, ListContainers, and the exec store returned live pointers after releasing their mutexes. API request goroutines and background watchers then read and mutated those records concurrently. For containers using unless-stopped, stop intent was also recorded only after the workload exited. A restart-watcher tick in that shutdown window could observe an exited container without user intent and immediately start it again. The already-stopped path returned 304 without recording intent at all. ## Impact Explicitly stopping Wolf and other unless-stopped containers now keeps them stopped. Stop, kill, restart-watcher, and removal operations establish a deterministic final state, while lifecycle operations for unrelated containers remain concurrent. ## Validation - go build ./... - go vet ./... - go test ./... -skip '^TestCheckCreateDiskPressure$' -count=1 - go test -race ./... -skip '^TestCheckCreateDiskPressure$' -count=1 - targeted tests cover stop-intent ordering and rollback, overlapping stops, watcher-vs-stop coordination, container-record aliasing, and exec-record aliasing TestCheckCreateDiskPressure is excluded because it depends on host free space and fails on this development machine before and after the change. No tests were run against a live Proxmox VE host. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-15 12:52:23 +00:00
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
games-on-whales/LXC2Docker#151
No description provided.