diff --git a/hypervisor/cloudhypervisor/args.go b/hypervisor/cloudhypervisor/args.go index d5152d4e..46cd4902 100644 --- a/hypervisor/cloudhypervisor/args.go +++ b/hypervisor/cloudhypervisor/args.go @@ -178,10 +178,18 @@ func serialConsoleFor(directBoot bool, consoleSock string) (serial, console *chR return &chRuntimeFile{Mode: "Socket", Socket: consoleSock}, &chRuntimeFile{Mode: "Off"} } +func qcow2Overlay(sc *types.StorageConfig) bool { + return !sc.RO && filepath.Ext(sc.Path) == ".qcow2" +} + func effectiveDirectIO(sc *types.StorageConfig, noDirectIO bool) bool { if sc.DirectIO != nil { return *sc.DirectIO } + // CH applies this flag to the backing file too, so O_DIRECT would stop one page-cache copy of the shared base serving every VM. + if qcow2Overlay(sc) { + return false + } return !sc.RO && !noDirectIO } @@ -201,7 +209,7 @@ func storageConfigToDisk(storageConfig *types.StorageConfig, cpuCount, diskQueue switch { case filepath.Ext(storageConfig.Path) == ".qcow2": d.ImageType = "Qcow2" - d.BackingFiles = !storageConfig.RO + d.BackingFiles = qcow2Overlay(storageConfig) case storageConfig.RO: d.ImageType = "Raw" default: diff --git a/hypervisor/cloudhypervisor/args_test.go b/hypervisor/cloudhypervisor/args_test.go index 8283adbd..5b1d6ed1 100644 --- a/hypervisor/cloudhypervisor/args_test.go +++ b/hypervisor/cloudhypervisor/args_test.go @@ -30,3 +30,39 @@ func TestMemoryCLIArg(t *testing.T) { }) } } + +func TestEffectiveDirectIO(t *testing.T) { + tests := []struct { + name string + sc types.StorageConfig + noDirectIO bool + want bool + }{ + {name: "raw cow", sc: types.StorageConfig{Path: "/v/cow.raw", Role: types.StorageRoleCOW}, want: true}, + {name: "raw cow with no-direct-io", sc: types.StorageConfig{Path: "/v/cow.raw", Role: types.StorageRoleCOW}, noDirectIO: true}, + {name: "readonly layer", sc: types.StorageConfig{Path: "/v/base.raw", RO: true, Role: types.StorageRoleLayer}}, + {name: "qcow2 overlay stays buffered", sc: types.StorageConfig{Path: "/v/overlay.qcow2", Role: types.StorageRoleCOW}}, + {name: "readonly qcow2 has no backing chain", sc: types.StorageConfig{Path: "/v/base.qcow2", RO: true, Role: types.StorageRoleLayer}}, + {name: "explicit override wins", sc: types.StorageConfig{Path: "/v/data.raw", Role: types.StorageRoleData, DirectIO: ptr(false)}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := effectiveDirectIO(&tt.sc, tt.noDirectIO); got != tt.want { + t.Errorf("got %v, want %v", got, tt.want) + } + }) + } +} + +func TestQcow2OverlayDiskArgs(t *testing.T) { + sc := &types.StorageConfig{Path: "/v/overlay.qcow2", Role: types.StorageRoleCOW} + got := diskToCLIArg(storageConfigToDisk(sc, 1, 0, false, nil)) + if strings.Contains(got, "direct=on") { + t.Errorf("qcow2 overlay must stay buffered so the shared base keeps one page-cache copy: %s", got) + } + if !strings.Contains(got, "backing_files=on") { + t.Errorf("qcow2 overlay must keep backing_files=on: %s", got) + } +} + +func ptr[T any](v T) *T { return &v } diff --git a/meta/sqlite/store.go b/meta/sqlite/store.go index 3aab7ad5..c74dbb90 100644 --- a/meta/sqlite/store.go +++ b/meta/sqlite/store.go @@ -81,15 +81,6 @@ func OpenForRecovery(dbPath string, namespaces ...Namespace) (*Store, error) { return openStore(dbPath, namespaces) } -// RefuseManifest fails when a conversion manifest sits beside dbPath, meaning an offline conversion is unfinished. -func RefuseManifest(dbPath string) error { - manifest := filepath.Join(filepath.Dir(dbPath), ManifestName) - if utils.FileExists(manifest) { - return fmt.Errorf("%s exists: a conversion is in flight, run `cocoon meta convert` to finish it", manifest) - } - return nil -} - func openStore(dbPath string, namespaces []Namespace) (*Store, error) { // The driver creates a file on first touch; Open never creates — that is Init's job (§6) — and §4 refuses network filesystems before WAL work. if !utils.FileExists(dbPath) { @@ -381,6 +372,15 @@ func (h *txHandle) checkRead(ns string) error { return nil } +// RefuseManifest fails when a conversion manifest sits beside dbPath, meaning an offline conversion is unfinished. +func RefuseManifest(dbPath string) error { + manifest := filepath.Join(filepath.Dir(dbPath), ManifestName) + if utils.FileExists(manifest) { + return fmt.Errorf("%s exists: a conversion is in flight, run `cocoon meta convert` to finish it", manifest) + } + return nil +} + func tableName(ns, table string) string { return quoteIdent(ns + "__" + table) }