Skip to content

Commit c8109fe

Browse files
committed
fix issues, promote timeout in long api
1 parent 9ed6b6a commit c8109fe

8 files changed

Lines changed: 74 additions & 34 deletions

File tree

cmd/vm/run.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,11 @@ func (h Handler) createVM(cmd *cobra.Command, image string) (context.Context, *t
330330
return ctx, info, hyper, nil
331331
}
332332

333+
type nicHint struct {
334+
mac, ip, gw string
335+
prefix int
336+
}
337+
333338
// tapQueues returns the number of TAP queues per NIC.
334339
// FC only supports single-queue TAPs; CH uses one queue per vCPU.
335340
func tapQueues(cpu int, useFC bool) int {
@@ -433,11 +438,6 @@ func printCloudimgNetworkHints() {
433438
fmt.Println(" cloud-init modules --mode=config && systemctl restart systemd-networkd")
434439
}
435440

436-
type nicHint struct {
437-
mac, ip, gw string
438-
prefix int
439-
}
440-
441441
func printOCINetworkHints(vm *types.VM, networkConfigs []*types.NetworkConfig) {
442442
fmt.Println()
443443
fmt.Printf(" # Set hostname\n")

cmd/vm/status.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,17 @@ func (h Handler) Status(cmd *cobra.Command, args []string) error {
8787
return nil
8888
}
8989

90+
type vmEvent struct {
91+
Event string `json:"event"`
92+
VM types.VM `json:"vm"`
93+
}
94+
95+
type vmSnapshot struct {
96+
id, name, state, ip, image string
97+
cpu int
98+
memory int64
99+
}
100+
90101
func mergeWatchChannels(ctx context.Context, hypers []hypervisor.Hypervisor) <-chan struct{} {
91102
var channels []<-chan struct{}
92103
for _, h := range hypers {
@@ -199,11 +210,6 @@ func statusEventLoop(ctx context.Context, hypers []hypervisor.Hypervisor, filter
199210
})
200211
}
201212

202-
type vmEvent struct {
203-
Event string `json:"event"`
204-
VM types.VM `json:"vm"`
205-
}
206-
207213
func statusEventLoopJSON(ctx context.Context, hypers []hypervisor.Hypervisor, filters []string, watchCh <-chan struct{}, tick <-chan time.Time) {
208214
enc := json.NewEncoder(os.Stdout)
209215
type snapshotWithVM struct {
@@ -237,12 +243,6 @@ func statusEventLoopJSON(ctx context.Context, hypers []hypervisor.Hypervisor, fi
237243
})
238244
}
239245

240-
type vmSnapshot struct {
241-
id, name, state, ip, image string
242-
cpu int
243-
memory int64
244-
}
245-
246246
func takeSnapshot(vm *types.VM) vmSnapshot {
247247
return vmSnapshot{
248248
id: vm.ID,

hypervisor/backend.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,8 +309,8 @@ func (b *Backend) CleanStalePlaceholders(_ context.Context, ids []string) error
309309
}
310310

311311
// GCCollect removes orphan VM directories and stale DB records.
312-
// Runs under the GC orchestrator's flock — uses lock-free DB access
313-
// (ReadRaw/WriteRaw) to avoid self-deadlock.
312+
// Kills leftover hypervisor processes before removing directories.
313+
// Runs under the GC orchestrator's flock — uses lock-free DB access.
314314
func (b *Backend) GCCollect(ctx context.Context, ids []string) error {
315315
var errs []error
316316
for _, id := range ids {
@@ -321,6 +321,7 @@ func (b *Backend) GCCollect(ctx context.Context, ids []string) error {
321321
}
322322
return nil
323323
})
324+
b.killOrphanProcess(ctx, runDir)
324325
if err := RemoveVMDirs(runDir, logDir); err != nil {
325326
errs = append(errs, err)
326327
}
@@ -331,6 +332,20 @@ func (b *Backend) GCCollect(ctx context.Context, ids []string) error {
331332
return errors.Join(errs...)
332333
}
333334

335+
// killOrphanProcess terminates a leftover hypervisor process if the PID
336+
// file exists and the process matches the expected binary name.
337+
func (b *Backend) killOrphanProcess(ctx context.Context, runDir string) {
338+
pid, err := utils.ReadPIDFile(b.PIDFilePath(runDir))
339+
if err != nil {
340+
return
341+
}
342+
sockPath := SocketPath(runDir)
343+
if !utils.VerifyProcessCmdline(pid, b.Conf.BinaryName(), sockPath) {
344+
return
345+
}
346+
_ = utils.TerminateProcess(ctx, pid, b.Conf.BinaryName(), sockPath, b.Conf.TerminateGracePeriod())
347+
}
348+
334349
// PIDFilePath returns the PID file path for the backend's PID file name.
335350
func (b *Backend) PIDFilePath(runDir string) string {
336351
return filepath.Join(runDir, b.Conf.PIDFileName())

hypervisor/cloudhypervisor/clone.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ func (ch *CloudHypervisor) restoreAndResumeClone(
178178
}
179179
}()
180180

181-
hc := utils.NewSocketHTTPClientWithTimeout(sockPath, hypervisor.VMMemTransferTimeout)
181+
hc := utils.NewSocketHTTPClient(sockPath)
182182

183183
if err = restoreVM(ctx, hc, runDir, opts.onDemand); err != nil {
184184
return fmt.Errorf("vm.restore: %w", err)
@@ -349,10 +349,11 @@ func hotSwapNets(ctx context.Context, hc *http.Client, oldNets []chNet, networkC
349349
}
350350
logger.Infof(ctx, "removed snapshot NIC %s (old MAC %s)", oldNet.ID, oldNet.Mac)
351351
}
352-
for _, nc := range networkConfigs {
352+
for i, nc := range networkConfigs {
353353
newNet := networkConfigToNet(nc)
354354
if err := addNetVM(ctx, hc, newNet); err != nil {
355-
return fmt.Errorf("add net device for %s: %w", nc.Mac, err)
355+
return fmt.Errorf("add net device %d/%d (MAC %s, TAP %s): %w",
356+
i+1, len(networkConfigs), nc.Mac, nc.Tap, err)
356357
}
357358
logger.Infof(ctx, "added NIC with MAC %s on TAP %s", nc.Mac, nc.Tap)
358359
}

hypervisor/cloudhypervisor/helper.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ import (
1717

1818
const (
1919
cmdlineFileName = "cmdline"
20+
21+
// chMemoryRestoreOnDemand uses userfaultfd (UFFD) to lazily page in
22+
// guest memory from the snapshot file, avoiding a full upfront copy.
23+
chMemoryRestoreOnDemand chMemoryRestoreMode = "OnDemand"
2024
)
2125

2226
var runtimeFiles = []string{hypervisor.APISocketName, "ch.pid", hypervisor.ConsoleSockName, cmdlineFileName}
@@ -68,15 +72,17 @@ func resumeVM(ctx context.Context, hc *http.Client) error {
6872
return vmAPI(ctx, hc, "vm.resume", nil)
6973
}
7074

71-
// snapshotVM and restoreVM bypass vmAPI's retry layer — see hypervisor.VMMemTransferTimeout.
72-
func snapshotVM(ctx context.Context, sockPath, destDir string) error {
75+
// snapshotVM and restoreVM temporarily extend the client timeout for
76+
// long-running memory transfers, then restore it for subsequent calls.
77+
func snapshotVM(ctx context.Context, hc *http.Client, destDir string) error {
78+
hc.Timeout = hypervisor.VMMemTransferTimeout
79+
defer func() { hc.Timeout = utils.HTTPTimeout }()
7380
body, err := json.Marshal(map[string]string{
7481
"destination_url": "file://" + destDir,
7582
})
7683
if err != nil {
7784
return fmt.Errorf("marshal snapshot request: %w", err)
7885
}
79-
hc := utils.NewSocketHTTPClientWithTimeout(sockPath, hypervisor.VMMemTransferTimeout)
8086
_, err = utils.DoAPI(ctx, hc, http.MethodPut,
8187
"http://localhost/api/v1/vm.snapshot", body, http.StatusNoContent)
8288
return err
@@ -85,18 +91,14 @@ func snapshotVM(ctx context.Context, sockPath, destDir string) error {
8591
// chMemoryRestoreMode controls how CH restores guest memory from a snapshot.
8692
type chMemoryRestoreMode string
8793

88-
const (
89-
// chMemoryRestoreOnDemand uses userfaultfd (UFFD) to lazily page in
90-
// guest memory from the snapshot file, avoiding a full upfront copy.
91-
chMemoryRestoreOnDemand chMemoryRestoreMode = "OnDemand"
92-
)
93-
9494
type chRestoreConfig struct {
9595
SourceURL string `json:"source_url"`
9696
MemoryRestoreMode chMemoryRestoreMode `json:"memory_restore_mode,omitempty"`
9797
}
9898

9999
func restoreVM(ctx context.Context, hc *http.Client, sourceDir string, onDemand bool) error {
100+
hc.Timeout = hypervisor.VMMemTransferTimeout
101+
defer func() { hc.Timeout = utils.HTTPTimeout }()
100102
cfg := chRestoreConfig{
101103
SourceURL: "file://" + sourceDir,
102104
}

hypervisor/cloudhypervisor/restore.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ func (ch *CloudHypervisor) restoreAfterExtract(ctx context.Context, vmID string,
135135
}
136136
}()
137137

138-
hc := utils.NewSocketHTTPClientWithTimeout(sockPath, hypervisor.VMMemTransferTimeout)
138+
hc := utils.NewSocketHTTPClient(sockPath)
139139

140140
if err = restoreVM(ctx, hc, rec.RunDir, vmCfg.OnDemand); err != nil {
141141
return nil, fmt.Errorf("vm.restore: %w", err)

hypervisor/cloudhypervisor/snapshot.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func (ch *CloudHypervisor) Snapshot(ctx context.Context, ref string) (*types.Sna
6868
}
6969
defer doResume()
7070

71-
if err := snapshotVM(ctx, sockPath, tmpDir); err != nil {
71+
if err := snapshotVM(ctx, hc, tmpDir); err != nil {
7272
return fmt.Errorf("snapshot: %w", err)
7373
}
7474

hypervisor/firecracker/clone.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import (
1515
"github.com/cocoonstack/cocoon/utils"
1616
)
1717

18+
const (
19+
cloneBackupSuffix = ".cocoon-clone-backup"
20+
)
21+
1822
// Clone creates a new VM from a snapshot tar stream via FC snapshot/load.
1923
func (fc *Firecracker) Clone(ctx context.Context, vmID string, vmCfg *types.VMConfig, networkConfigs []*types.NetworkConfig, snapshotConfig *types.SnapshotConfig, snapshot io.Reader) (_ *types.VM, err error) {
2024
runDir, logDir, now, cleanup, err := fc.CloneSetup(ctx, vmID, vmCfg, snapshotConfig)
@@ -190,7 +194,7 @@ func createDriveRedirects(srcConfigs, dstConfigs []*types.StorageConfig) ([]driv
190194
r := driveRedirect{symlinkPath: src.Path}
191195

192196
if _, err := os.Stat(src.Path); err == nil {
193-
backup := src.Path + ".cocoon-clone-backup"
197+
backup := src.Path + cloneBackupSuffix
194198
if renameErr := os.Rename(src.Path, backup); renameErr != nil {
195199
cleanupDriveRedirects(redirects)
196200
return nil, fmt.Errorf("backup source drive %s: %w", src.Path, renameErr)
@@ -231,15 +235,33 @@ func cleanupDriveRedirects(redirects []driveRedirect) {
231235
}
232236

233237
// withSourceCOWLocked runs fn while holding the source COW lock.
238+
// Recovers stale symlink backups from crashed clones before proceeding.
234239
func withSourceCOWLocked(srcConfigs []*types.StorageConfig, fn func() error) error {
235240
for _, sc := range srcConfigs {
236241
if !sc.RO {
237-
return withCOWPathLocked(sc.Path, fn)
242+
return withCOWPathLocked(sc.Path, func() error {
243+
recoverStaleBackup(sc.Path)
244+
return fn()
245+
})
238246
}
239247
}
240248
return fn() // no RW disk, run unlocked
241249
}
242250

251+
// recoverStaleBackup restores a backup file left by a crashed clone.
252+
// Caller must hold the COW lock.
253+
func recoverStaleBackup(cowPath string) {
254+
backup := cowPath + cloneBackupSuffix
255+
if _, err := os.Stat(backup); err != nil {
256+
return
257+
}
258+
fi, err := os.Lstat(cowPath)
259+
if err == nil && fi.Mode()&os.ModeSymlink != 0 {
260+
_ = os.Remove(cowPath)
261+
}
262+
_ = os.Rename(backup, cowPath)
263+
}
264+
243265
func buildNetworkOverrides(networkConfigs []*types.NetworkConfig) []fcNetworkOverride {
244266
var overrides []fcNetworkOverride
245267
for i, nc := range networkConfigs {

0 commit comments

Comments
 (0)