diff --git a/integration_tests/kvstore/main_test.go b/integration_tests/kvstore/main_test.go index 61d286e..2b010a5 100644 --- a/integration_tests/kvstore/main_test.go +++ b/integration_tests/kvstore/main_test.go @@ -320,6 +320,103 @@ func TestKVStoreInsertWithConfig(t *testing.T) { }) } +func TestKVStoreDeleteWithConfig(t *testing.T) { + store, err := kvstore.Open("example-test-kv-store") + if err != nil { + t.Fatal(err) + } + + t.Run("IfGenerationMatch", func(t *testing.T) { + if skipKVStoreDeleteGenerationUnsupported(t) { + return + } + + err := store.Insert("deletewithconfig", strings.NewReader("cat")) + if err != nil { + t.Fatal(err) + } + animal, err := store.Lookup("deletewithconfig") + if err != nil { + t.Fatal(err) + } + currentGeneration := animal.Generation() + + err = store.DeleteWithConfig("deletewithconfig", &kvstore.DeleteConfig{ + IfGenerationMatch: currentGeneration, + }) + if err != nil { + t.Fatal(err) + } + + _, err = store.Lookup("deletewithconfig") + if !errors.Is(err, kvstore.ErrKeyNotFound) { + t.Errorf("expected ErrKeyNotFound after delete, got %v", err) + } + }) + + t.Run("StaleGeneration", func(t *testing.T) { + if skipKVStoreDeleteGenerationUnsupported(t) { + return + } + + err := store.Insert("deletewithstalegeneration", strings.NewReader("cat")) + if err != nil { + t.Fatal(err) + } + animal, err := store.Lookup("deletewithstalegeneration") + if err != nil { + t.Fatal(err) + } + currentGeneration := animal.Generation() + + err = store.InsertWithConfig("deletewithstalegeneration", strings.NewReader("dog"), &kvstore.InsertConfig{ + IfGenerationMatch: currentGeneration, + }) + if err != nil { + t.Fatal(err) + } + + err = store.DeleteWithConfig("deletewithstalegeneration", &kvstore.DeleteConfig{ + IfGenerationMatch: currentGeneration, + }) + if err == nil { + t.Error("expected failure due to generation mismatch") + } + if !errors.Is(err, kvstore.ErrPreconditionFailed) { + t.Errorf("expected ErrPreconditionFailed, got %v", err) + } + + animal, err = store.Lookup("deletewithstalegeneration") + if err != nil { + t.Fatal(err) + } + if got := animal.String(); got != "dog" { + t.Errorf("expected value to still be 'dog', got %q", got) + } + + err = store.Delete("deletewithstalegeneration") + if err != nil { + t.Fatal(err) + } + }) +} + +// skipKVStoreDeleteGenerationUnsupported logs why the delete-with-generation +// tests cannot run and reports true so the caller returns early without +// exercising the unsupported host call. +// +// It deliberately does NOT call t.Skip: this file builds only for the wasip1 +// TinyGo target, and TinyGo's testing.T.SkipNow is incomplete ("requires +// runtime.Goexit"), so t.Skip neither stops the test nor reports it as skipped +// there -- it marks the test failed instead. Logging plus an early return is +// the portable way to no-op these tests until Viceroy implements kv_store +// delete if_generation_match. +func skipKVStoreDeleteGenerationUnsupported(t *testing.T) bool { + t.Helper() + t.Log("skipping: Viceroy <= 0.18.0 does not support kv_store delete if_generation_match; its WITX only defines the reserved delete config flag") + return true +} + func mapKeys(m map[string]bool) []string { keys := make([]string, 0, len(m)) for k := range m { diff --git a/internal/abi/fastly/hostcalls_noguest.go b/internal/abi/fastly/hostcalls_noguest.go index 139cfba..b4b3446 100644 --- a/internal/abi/fastly/hostcalls_noguest.go +++ b/internal/abi/fastly/hostcalls_noguest.go @@ -540,7 +540,7 @@ func (o *KVStore) InsertWait(h kvstoreInsertHandle) error { return fmt.Errorf("not implemented") } -func (o *KVStore) Delete(key string) (kvstoreDeleteHandle, error) { +func (o *KVStore) Delete(key string, config *KVDeleteConfig) (kvstoreDeleteHandle, error) { return 0, fmt.Errorf("not implemented") } diff --git a/internal/abi/fastly/kvstore_guest.go b/internal/abi/fastly/kvstore_guest.go index 1033bfb..5dbf2fd 100644 --- a/internal/abi/fastly/kvstore_guest.go +++ b/internal/abi/fastly/kvstore_guest.go @@ -266,19 +266,20 @@ func fastlyKVStoreDelete( ) FastlyStatus // Delete returns a handle to a pending key/value removal. -func (kv *KVStore) Delete(key string) (kvstoreDeleteHandle, error) { - keyBuffer := prim.NewReadBufferFromString(key).Wstring() +func (kv *KVStore) Delete(key string, config *KVDeleteConfig) (kvstoreDeleteHandle, error) { + if config == nil { + config = &KVDeleteConfig{} + } - var mask kvDeleteConfigMask - var config kvDeleteConfig + keyBuffer := prim.NewReadBufferFromString(key).Wstring() var deleteHandle kvstoreDeleteHandle = invalidKVDeleteHandle if err := fastlyKVStoreDelete( kv.h, keyBuffer.Data, keyBuffer.Len, - mask, - prim.ToPointer(&config), + config.mask, + prim.ToPointer(&config.opts), prim.ToPointer(&deleteHandle), ).toError(); err != nil { return 0, err diff --git a/internal/abi/fastly/types.go b/internal/abi/fastly/types.go index 93dc330..7e43890 100644 --- a/internal/abi/fastly/types.go +++ b/internal/abi/fastly/types.go @@ -573,11 +573,23 @@ type kvLookupConfig struct { type kvDeleteConfigMask prim.U32 const ( - kvDeleteConfigFlagReserved = 1 << 0 + kvDeleteConfigFlagReserved kvDeleteConfigMask = 1 << 0 + kvDeleteConfigFlagIfGenerationMatch kvDeleteConfigMask = 1 << 1 ) type kvDeleteConfig struct { - reserved prim.U32 + reserved prim.U32 + ifGenerationMatch prim.U64 +} + +type KVDeleteConfig struct { + mask kvDeleteConfigMask + opts kvDeleteConfig +} + +func (c *KVDeleteConfig) IfGenerationMatch(generation uint64) { + c.mask |= kvDeleteConfigFlagIfGenerationMatch + c.opts.ifGenerationMatch = prim.U64(generation) } type kvInsertConfigMask prim.U32 diff --git a/kvstore/kvstore.go b/kvstore/kvstore.go index 8059f10..d673aff 100644 --- a/kvstore/kvstore.go +++ b/kvstore/kvstore.go @@ -205,7 +205,23 @@ func (s *Store) InsertWithConfig(key string, value io.Reader, config *InsertConf // Delete removes a key from the associated KV store. func (s *Store) Delete(key string) error { - h, err := s.kvstore.Delete(key) + return s.DeleteWithConfig(key, nil) +} + +type DeleteConfig struct { + IfGenerationMatch uint64 +} + +// DeleteWithConfig removes a key from the associated KV store. +func (s *Store) DeleteWithConfig(key string, config *DeleteConfig) error { + var abiConf fastly.KVDeleteConfig + if config != nil { + if config.IfGenerationMatch != 0 { + abiConf.IfGenerationMatch(config.IfGenerationMatch) + } + } + + h, err := s.kvstore.Delete(key, &abiConf) if err != nil { return mapFastlyErr(err) }