From 2bec5da63b20055f0b8d5dd90a556d5dd984b594 Mon Sep 17 00:00:00 2001 From: CMGS Date: Fri, 2 Oct 2026 13:38:01 +0800 Subject: [PATCH 1/2] fix(meta/json): a transaction creates a namespace's directory before locking it On a fresh json-backend root the first vm run failed for either backend: placement reads the peer VM namespace, and its lock lives under the peer backend's db directory, which only that backend's EnsureDirs creates. The store now ensures each namespace's lock directory before taking its flock, so a namespace that was never written reads as empty. Open still touches no files; the added cost is one stat per namespace per json transaction. --- meta/json/store.go | 6 +++++- meta/json/store_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/meta/json/store.go b/meta/json/store.go index 04c1a425..58d9437c 100644 --- a/meta/json/store.go +++ b/meta/json/store.go @@ -155,7 +155,11 @@ func (s *Store) resolve(nss []string) ([]*nsState, error) { // withLocked holds every namespace flock in sorted order; unlock errors log only, since joining them would make callers roll back an already-durable commit. func (s *Store) withLocked(ctx context.Context, states []*nsState, fn func() error) error { for i, st := range states { - if err := st.locker.Lock(ctx); err != nil { + err := utils.EnsureDirs(filepath.Dir(st.def.LockPath)) + if err == nil { + err = st.locker.Lock(ctx) + } + if err != nil { for _, held := range slices.Backward(states[:i]) { if uerr := held.locker.Unlock(ctx); uerr != nil { log.WithFunc("meta.json.withLocked").Errorf(ctx, uerr, "unlock %s", held.def.Name) diff --git a/meta/json/store_test.go b/meta/json/store_test.go index 6512d793..3a7e2e47 100644 --- a/meta/json/store_test.go +++ b/meta/json/store_test.go @@ -352,6 +352,30 @@ func TestTrailingDataFallsBackToPrev(t *testing.T) { } } +func TestPeerNamespaceWithoutDirectory(t *testing.T) { + ctx := t.Context() + dir := t.TempDir() + s, err := Open( + Namespace{Name: "alpha", FilePath: filepath.Join(dir, "alpha.json"), LockPath: filepath.Join(dir, "alpha.lock"), Codec: GenericCodec{}}, + Namespace{Name: "beta", FilePath: filepath.Join(dir, "beta", "db", "beta.json"), LockPath: filepath.Join(dir, "beta", "db", "beta.lock"), Codec: GenericCodec{}}, + ) + if err != nil { + t.Fatalf("open: %v", err) + } + t.Cleanup(func() { _ = s.Close() }) + c := meta.NewCollection[map[string]int]("alpha", "records") + v := map[string]int{"n": 1} + + if err := s.Update(ctx, meta.Scope{Write: "alpha", Read: []string{"beta"}}, meta.CommitDurable, func(w meta.Writer) error { + return c.Insert(ctx, w, "a", &v) + }); err != nil { + t.Fatalf("update reading a peer namespace whose directory does not exist yet: %v", err) + } + if rec, err := mustGet(t, s, "alpha", "a"); err != nil || rec == nil || (*rec)["n"] != 1 { + t.Fatalf("got %v, %v; want the inserted record", rec, err) + } +} + type crashPoint struct { step string err error From 5668fa677ea2a824c81c7fec95f25154aacebde4 Mon Sep 17 00:00:00 2001 From: CMGS Date: Fri, 2 Oct 2026 13:58:57 +0800 Subject: [PATCH 2/2] fix(cmd/core): create the json namespace directories once at store bootstrap Directory creation is one-time setup, so it moves out of every json transaction and into the json branch of MetaStore, which already runs once per process and declares the namespace set. The json store's lock path is back to a bare flock; the regression test moves to cmd/core and drives the placement-shaped scope (write CH, read FC) on a fresh json root. --- cmd/core/metastore.go | 9 ++++++++- cmd/core/metastore_test.go | 19 +++++++++++++++++++ meta/json/store.go | 6 +----- meta/json/store_test.go | 24 ------------------------ 4 files changed, 28 insertions(+), 30 deletions(-) diff --git a/cmd/core/metastore.go b/cmd/core/metastore.go index a0696a6b..feaba18c 100644 --- a/cmd/core/metastore.go +++ b/cmd/core/metastore.go @@ -135,7 +135,14 @@ func MetaStore(conf *config.Config) (meta.Store, error) { if conf.MetaBackend == "" { log.WithFunc("core.MetaStore").Info(ctx, "legacy json meta store in use; `cocoon meta convert` upgrades it to sqlite") } - if s, err := metajson.Open(MetaJSONNamespaces(conf)...); err != nil { + nss := MetaJSONNamespaces(conf) + for _, ns := range nss { + if err := utils.EnsureDirs(filepath.Dir(ns.LockPath)); err != nil { + metaErr = err + return + } + } + if s, err := metajson.Open(nss...); err != nil { metaErr = err } else { metaStore = s diff --git a/cmd/core/metastore_test.go b/cmd/core/metastore_test.go index 6f7f5165..c92bafa9 100644 --- a/cmd/core/metastore_test.go +++ b/cmd/core/metastore_test.go @@ -7,6 +7,8 @@ import ( "testing" "github.com/cocoonstack/cocoon/config" + "github.com/cocoonstack/cocoon/hypervisor" + "github.com/cocoonstack/cocoon/meta" "github.com/cocoonstack/cocoon/utils" ) @@ -39,6 +41,23 @@ func TestMetaStoreBootstrapsFreshRoot(t *testing.T) { resetMetaStore() } +func TestMetaStoreJSONFreshRootLocksPeerNamespace(t *testing.T) { + resetMetaStore() + conf := testConf(t) + conf.MetaBackend = config.MetaBackendJSON + s, err := MetaStore(conf) + if err != nil { + t.Fatalf("open fresh json root: %v", err) + } + ch := hypervisor.VMNamespaceName(string(config.HypervisorCloudHypervisor)) + fc := hypervisor.VMNamespaceName(string(config.HypervisorFirecracker)) + if err := s.Update(t.Context(), meta.Scope{Write: ch, Read: []string{fc}}, meta.CommitDurable, func(meta.Writer) error { return nil }); err != nil { + t.Fatalf("placement-shaped update on a fresh json root: %v", err) + } + CloseMetaStore(t.Context()) + resetMetaStore() +} + func TestMetaStoreKeepsLegacyJSON(t *testing.T) { resetMetaStore() conf := testConf(t) diff --git a/meta/json/store.go b/meta/json/store.go index 58d9437c..04c1a425 100644 --- a/meta/json/store.go +++ b/meta/json/store.go @@ -155,11 +155,7 @@ func (s *Store) resolve(nss []string) ([]*nsState, error) { // withLocked holds every namespace flock in sorted order; unlock errors log only, since joining them would make callers roll back an already-durable commit. func (s *Store) withLocked(ctx context.Context, states []*nsState, fn func() error) error { for i, st := range states { - err := utils.EnsureDirs(filepath.Dir(st.def.LockPath)) - if err == nil { - err = st.locker.Lock(ctx) - } - if err != nil { + if err := st.locker.Lock(ctx); err != nil { for _, held := range slices.Backward(states[:i]) { if uerr := held.locker.Unlock(ctx); uerr != nil { log.WithFunc("meta.json.withLocked").Errorf(ctx, uerr, "unlock %s", held.def.Name) diff --git a/meta/json/store_test.go b/meta/json/store_test.go index 3a7e2e47..6512d793 100644 --- a/meta/json/store_test.go +++ b/meta/json/store_test.go @@ -352,30 +352,6 @@ func TestTrailingDataFallsBackToPrev(t *testing.T) { } } -func TestPeerNamespaceWithoutDirectory(t *testing.T) { - ctx := t.Context() - dir := t.TempDir() - s, err := Open( - Namespace{Name: "alpha", FilePath: filepath.Join(dir, "alpha.json"), LockPath: filepath.Join(dir, "alpha.lock"), Codec: GenericCodec{}}, - Namespace{Name: "beta", FilePath: filepath.Join(dir, "beta", "db", "beta.json"), LockPath: filepath.Join(dir, "beta", "db", "beta.lock"), Codec: GenericCodec{}}, - ) - if err != nil { - t.Fatalf("open: %v", err) - } - t.Cleanup(func() { _ = s.Close() }) - c := meta.NewCollection[map[string]int]("alpha", "records") - v := map[string]int{"n": 1} - - if err := s.Update(ctx, meta.Scope{Write: "alpha", Read: []string{"beta"}}, meta.CommitDurable, func(w meta.Writer) error { - return c.Insert(ctx, w, "a", &v) - }); err != nil { - t.Fatalf("update reading a peer namespace whose directory does not exist yet: %v", err) - } - if rec, err := mustGet(t, s, "alpha", "a"); err != nil || rec == nil || (*rec)["n"] != 1 { - t.Fatalf("got %v, %v; want the inserted record", rec, err) - } -} - type crashPoint struct { step string err error