已合并
fix(volume): destroy sandbox when virtiofsd exits #167
dengchi创建于 8月14日
fix(volume): destroy sandbox when virtiofsd exits #167
已合并
dengchi创建于 8月14日
5 个文件变更+217-14
@@ -413,6 +413,10 @@ func (m *Manager) Create(req CreateRequest) (result CreateResult, err error) {
413 }413 }
414 414 
415 volumesPrepared := len(volumeDevices) > 0415 volumesPrepared := len(volumeDevices) > 0
416+ var virtiofsExit <-chan struct{}
417+ if volumesPrepared {
418+ virtiofsExit = volumeDevices[0].Exited
419+ }
416 defer func() {420 defer func() {
417 if err == nil || !volumesPrepared || m.volumeManager == nil {421 if err == nil || !volumesPrepared || m.volumeManager == nil {
418 return422 return
@@ -443,7 +447,7 @@ func (m *Manager) Create(req CreateRequest) (result CreateResult, err error) {
443 447 
444 entry.sbx = sbx448 entry.sbx = sbx
445 entry.state = sandboxReady449 entry.state = sandboxReady
446- m.trackSandbox(ctx, mapKey, entry, req.SandboxID, sbx)450+ m.trackSandbox(ctx, mapKey, entry, req.SandboxID, sbx, virtiofsExit)
447 cidAllocated = false451 cidAllocated = false
448 452 
449 logger.Debug("created sandbox in manager")453 logger.Debug("created sandbox in manager")
@@ -566,14 +570,22 @@ func (m *Manager) cleanupCreateFailure(sbx *Sandbox, sandboxID string) {
566 }570 }
567}571}
568 572 
569-func (m *Manager) trackSandbox(ctx context.Context, mapKey string, entry *sandboxEntry, sandboxID string, sbx *Sandbox) {573+func (m *Manager) trackSandbox(ctx context.Context, mapKey string, entry *sandboxEntry, sandboxID string, sbx *Sandbox, virtiofsExit <-chan struct{}) {
570 logger := ulog.GetLogger()574 logger := ulog.GetLogger()
571 go func() {575 go func() {
572- waitErr := sbx.Wait(ctx)576+ vmmExit := make(chan struct{})
573- if waitErr != nil {577+ go func() {
574- logger.Warn("failed to wait for sandbox, cleaning up", ulog.F("error", waitErr))578+ waitErr := sbx.Wait(ctx)
575- }579+ if waitErr != nil {
580+ logger.Warn("failed to wait for sandbox, cleaning up", ulog.F("error", waitErr))
581+ }
582+ close(vmmExit)
583+ }()
576 584 
585+ select {
586+ case <-vmmExit:
587+ case <-virtiofsExit:
588+ }
577 m.handleSandboxExit(mapKey, entry, sandboxID, sbx)589 m.handleSandboxExit(mapKey, entry, sandboxID, sbx)
578 }()590 }()
579}591}
@@ -73,6 +73,94 @@ func TestHandleSandboxExitCleansSuspendedSandbox(t *testing.T) {
73 }73 }
74}74}
75 75 
76+func TestWaitForSandboxExitCleansSandboxOnVirtiofsExit(t *testing.T) {
77+ cleanupDone := make(chan struct{})
78+ m, entry, sbx := newExitTestSandbox(func(context.Context) error {
79+ close(cleanupDone)
80+ return nil
81+ })
82+ 
83+ vmmExit := make(chan struct{})
84+ virtiofsExit := make(chan struct{})
85+ go func() {
86+ select {
87+ case <-vmmExit:
88+ case <-virtiofsExit:
89+ }
90+ m.handleSandboxExit("sandbox-a", entry, "sandbox-a", sbx)
91+ }()
92+ close(virtiofsExit)
93+ 
94+ select {
95+ case <-cleanupDone:
96+ case <-time.After(time.Second):
97+ t.Fatal("sandbox cleanup was not triggered after virtiofsd exit")
98+ }
99+ entry.mu.Lock()
100+ entry.mu.Unlock()
101+ if _, ok := m.sandboxes.Load("sandbox-a"); ok {
102+ t.Fatal("sandbox entry remains after virtiofsd exit")
103+ }
104+}
105+ 
106+func TestWaitForSandboxExitDoesNotDuplicateDeleteCleanup(t *testing.T) {
107+ cleanupCalls := 0
108+ cleanupStarted := make(chan struct{})
109+ continueCleanup := make(chan struct{})
110+ m, entry, sbx := newExitTestSandbox(func(context.Context) error {
111+ cleanupCalls++
112+ close(cleanupStarted)
113+ <-continueCleanup
114+ return nil
115+ })
116+ 
117+ vmmExit := make(chan struct{})
118+ virtiofsExit := make(chan struct{})
119+ go func() {
120+ select {
121+ case <-vmmExit:
122+ case <-virtiofsExit:
123+ }
124+ m.handleSandboxExit("sandbox-a", entry, "sandbox-a", sbx)
125+ }()
126+ deleteDone := make(chan error, 1)
127+ go func() {
128+ deleteDone <- m.Delete(DeleteRequest{SandboxID: "sandbox-a"})
129+ }()
130+ 
131+ select {
132+ case <-cleanupStarted:
133+ case <-time.After(time.Second):
134+ t.Fatal("Delete did not start sandbox cleanup")
135+ }
136+ close(virtiofsExit)
137+ close(continueCleanup)
138+ 
139+ select {
140+ case err := <-deleteDone:
141+ if err != nil {
142+ t.Fatalf("Delete() error = %v", err)
143+ }
144+ case <-time.After(time.Second):
145+ t.Fatal("Delete blocked after virtiofsd exit")
146+ }
147+ if cleanupCalls != 1 {
148+ t.Fatalf("sandbox cleanup calls = %d, want 1", cleanupCalls)
149+ }
150+ if _, ok := m.sandboxes.Load("sandbox-a"); ok {
151+ t.Fatal("sandbox entry remains after Delete")
152+ }
153+}
154+ 
155+func newExitTestSandbox(cleanup func(context.Context) error) (*Manager, *sandboxEntry, *Sandbox) {
156+ m := &Manager{boot: &recordingBootPreparer{}, cidAllocator: NewCIDAllocator()}
157+ sbx := &Sandbox{cleanup: NewCleanup(), sandboxID: "sandbox-a"}
158+ sbx.cleanup.Add(cleanup)
159+ entry := &sandboxEntry{state: sandboxReady, sbx: sbx}
160+ m.sandboxes.Store("sandbox-a", entry)
161+ return m, entry, sbx
162+}
163+ 
76func TestCheckpointCapturesRunningAndSuspendedSandbox(t *testing.T) {164func TestCheckpointCapturesRunningAndSuspendedSandbox(t *testing.T) {
77 tests := []struct {165 tests := []struct {
78 name string166 name string
@@ -33,6 +33,8 @@ type Device struct {
33 ConfigPath string `json:"config_path,omitempty"`33 ConfigPath string `json:"config_path,omitempty"`
34 PID int `json:"pid,omitempty"`34 PID int `json:"pid,omitempty"`
35 StartTime uint64 `json:"start_time,omitempty"`35 StartTime uint64 `json:"start_time,omitempty"`
36+ // Exited is runtime-only and is intentionally excluded from persistence.
37+ Exited <-chan struct{} `json:"-"`
36}38}
37 39 
38type PrepareRequest struct {40type PrepareRequest struct {
@@ -30,6 +30,7 @@ const (
30 configVersion = 130 configVersion = 1
31 31 
32 socketReadyTimeout = 3 * time.Second32 socketReadyTimeout = 3 * time.Second
33+ processExitTimeout = 5 * time.Second
33)34)
34 35 
35type virtiofsBackend struct {36type virtiofsBackend struct {
@@ -38,6 +39,15 @@ type virtiofsBackend struct {
38 procs sync.Map39 procs sync.Map
39}40}
40 41 
42+func watchVirtiofs(cmd *exec.Cmd) <-chan struct{} {
43+ exited := make(chan struct{})
44+ go func() {
45+ _ = cmd.Wait()
46+ close(exited)
47+ }()
48+ return exited
49+}
50+ 
41func NewVirtiofsBackend(cfg VirtiofsConfig) Backend {51func NewVirtiofsBackend(cfg VirtiofsConfig) Backend {
42 if strings.TrimSpace(cfg.Binary) == "" {52 if strings.TrimSpace(cfg.Binary) == "" {
43 cfg.Binary = DefaultBinary53 cfg.Binary = DefaultBinary
@@ -142,9 +152,12 @@ func (b *virtiofsBackend) Prepare(req PrepareRequest) ([]Device, error) {
142 cleanup()152 cleanup()
143 return nil, fmt.Errorf("start virtiofsd for sandbox %s: %w", req.SandboxID, err)153 return nil, fmt.Errorf("start virtiofsd for sandbox %s: %w", req.SandboxID, err)
144 }154 }
155+ exited := watchVirtiofs(cmd)
145 if err := waitUnixSocket(socket, socketReadyTimeout); err != nil {156 if err := waitUnixSocket(socket, socketReadyTimeout); err != nil {
146- _ = cmd.Process.Kill()157+ killErr := cmd.Process.Kill()
147- _, _ = cmd.Process.Wait()158+ if killErr == nil || errors.Is(killErr, unix.ESRCH) || errors.Is(killErr, os.ErrProcessDone) {
159+ <-exited
160+ }
148 cleanup()161 cleanup()
149 return nil, fmt.Errorf("wait virtiofsd socket %s: %w", socket, err)162 return nil, fmt.Errorf("wait virtiofsd socket %s: %w", socket, err)
150 }163 }
@@ -159,6 +172,7 @@ func (b *virtiofsBackend) Prepare(req PrepareRequest) ([]Device, error) {
159 ConfigPath: configPath,172 ConfigPath: configPath,
160 PID: cmd.Process.Pid,173 PID: cmd.Process.Pid,
161 StartTime: processStartTicks(cmd.Process.Pid),174 StartTime: processStartTicks(cmd.Process.Pid),
175+ Exited: exited,
162 }}, nil176 }}, nil
163}177}
164 178 
@@ -171,12 +185,27 @@ func (b *virtiofsBackend) buildArgs(socket, volumeDir string) []string {
171func (b *virtiofsBackend) Cleanup(sandboxID string, devices []Device) error {185func (b *virtiofsBackend) Cleanup(sandboxID string, devices []Device) error {
172 var errs []error186 var errs []error
173 187 
174- if v, ok := b.procs.LoadAndDelete(sandboxID); ok {188+ if v, ok := b.procs.Load(sandboxID); ok {
175- if cmd, ok := v.(*exec.Cmd); ok && cmd.Process != nil {189+ cmd, ok := v.(*exec.Cmd)
176- if killErr := cmd.Process.Kill(); killErr != nil && !errors.Is(killErr, unix.ESRCH) {190+ if !ok || cmd.Process == nil {
177- errs = append(errs, fmt.Errorf("kill virtiofsd: %w", killErr))191+ return fmt.Errorf("invalid virtiofsd process for sandbox %s", sandboxID)
178- }192+ }
179- _, _ = cmd.Process.Wait()193+ killErr := cmd.Process.Kill()
194+ if killErr != nil && !errors.Is(killErr, unix.ESRCH) && !errors.Is(killErr, os.ErrProcessDone) {
195+ return fmt.Errorf("kill virtiofsd: %w", killErr)
196+ }
197+ if len(devices) == 0 || devices[0].Exited == nil {
198+ return fmt.Errorf("virtiofsd exit signal is missing for sandbox %s", sandboxID)
199+ }
200+ select {
201+ case <-devices[0].Exited:
202+ b.procs.CompareAndDelete(sandboxID, v)
203+ case <-time.After(processExitTimeout):
204+ return fmt.Errorf(
205+ "timed out after %s waiting for virtiofsd pid %d to exit",
206+ processExitTimeout,
207+ cmd.Process.Pid,
208+ )
180 }209 }
181 } else {210 } else {
182 for _, device := range devices {211 for _, device := range devices {
@@ -0,0 +1,72 @@
1+package volume
2+ 
3+import (
4+ "os"
5+ "os/exec"
6+ "path/filepath"
7+ "testing"
8+ "time"
9+)
10+ 
11+func TestCleanupWaitsForVirtiofsProcessExit(t *testing.T) {
12+ const sandboxID = "sandbox-a"
13+ runtimeRoot := t.TempDir()
14+ runtimeDir := filepath.Join(runtimeRoot, sandboxID)
15+ if err := os.MkdirAll(filepath.Join(runtimeDir, volumeDirName), 0o755); err != nil {
16+ t.Fatalf("create runtime directory: %v", err)
17+ }
18+ 
19+ cmd := exec.Command("sleep", "60")
20+ if err := cmd.Start(); err != nil {
21+ t.Fatalf("start helper process: %v", err)
22+ }
23+ done := make(chan struct{})
24+ waited := make(chan struct{})
25+ go func() {
26+ _ = cmd.Wait()
27+ close(waited)
28+ }()
29+ defer func() {
30+ _ = cmd.Process.Kill()
31+ <-waited
32+ select {
33+ case <-done:
34+ default:
35+ close(done)
36+ }
37+ }()
38+ 
39+ backend := &virtiofsBackend{runtimeDir: runtimeRoot}
40+ backend.procs.Store(sandboxID, cmd)
41+ cleanupResult := make(chan error, 1)
42+ go func() {
43+ cleanupResult <- backend.Cleanup(sandboxID, []Device{{Exited: done}})
44+ }()
45+ 
46+ select {
47+ case <-waited:
48+ case <-time.After(5 * time.Second):
49+ t.Fatal("virtiofs helper process was not killed")
50+ }
51+ select {
52+ case err := <-cleanupResult:
53+ t.Fatalf("Cleanup returned before process exit notification: %v", err)
54+ default:
55+ }
56+ if _, err := os.Stat(runtimeDir); err != nil {
57+ t.Fatalf("runtime directory removed before process exit notification: %v", err)
58+ }
59+ 
60+ close(done)
61+ select {
62+ case err := <-cleanupResult:
63+ if err != nil {
64+ t.Fatalf("Cleanup() error = %v", err)
65+ }
66+ case <-time.After(5 * time.Second):
67+ t.Fatal("Cleanup did not finish after process exit notification")
68+ }
69+ if _, err := os.Stat(runtimeDir); !os.IsNotExist(err) {
70+ t.Fatalf("runtime directory remains after Cleanup: %v", err)
71+ }
72+}