Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions integration_tests/kvstore/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion internal/abi/fastly/hostcalls_noguest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand Down
13 changes: 7 additions & 6 deletions internal/abi/fastly/kvstore_guest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 14 additions & 2 deletions internal/abi/fastly/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 17 additions & 1 deletion kvstore/kvstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading