From 850540c330dceb748ccab2326344c39a65012ecd Mon Sep 17 00:00:00 2001 From: Damian Gryski Date: Wed, 28 Aug 2024 11:55:09 -0700 Subject: [PATCH] configstore: switch to config_store hostcalls --- configstore/configstore.go | 4 +- internal/abi/fastly/configstore_guest.go | 142 +++++++++++++++++++++++ internal/abi/fastly/hostcalls_noguest.go | 18 +++ internal/abi/fastly/types.go | 5 + 4 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 internal/abi/fastly/configstore_guest.go diff --git a/configstore/configstore.go b/configstore/configstore.go index c6548be..49c8ff5 100644 --- a/configstore/configstore.go +++ b/configstore/configstore.go @@ -34,13 +34,13 @@ var ( // Store is a read-only representation of a config store. type Store struct { - abiDict *fastly.Dictionary + abiDict *fastly.ConfigStore } // Open returns a config store with the given name. Names are case // sensitive. func Open(name string) (*Store, error) { - d, err := fastly.OpenDictionary(name) + d, err := fastly.OpenConfigStore(name) if err != nil { status, ok := fastly.IsFastlyError(err) switch { diff --git a/internal/abi/fastly/configstore_guest.go b/internal/abi/fastly/configstore_guest.go new file mode 100644 index 0000000..44a2efa --- /dev/null +++ b/internal/abi/fastly/configstore_guest.go @@ -0,0 +1,142 @@ +//go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls + +// Copyright 2022 Fastly, Inc. + +package fastly + +import ( + "sync" + + "github.com/fastly/compute-sdk-go/internal/abi/prim" +) + +// witx: +// +// (module $fastly_config_store +// (@interface func (export "open") +// (param $name string) +// (result $err (expected $config_store_handle (error $fastly_status))) +// ) +// +//go:wasmimport fastly_config_store open +//go:noescape +func fastlyConfigStoreOpen( + nameData prim.Pointer[prim.U8], nameLen prim.Usize, + h prim.Pointer[configstoreHandle], +) FastlyStatus + +// ConfigStore represents a Fastly config store a collection of read-only +// key/value pairs. For convenience, keys are modeled as Go strings, and values +// as byte slices. +// +// NOTE: wasm, by definition, is a single-threaded execution environment. This +// allows us to use valueBuf scratch space between the guest and host to avoid +// allocations any larger than necessary, without locking. +type ConfigStore struct { + h configstoreHandle + + mu sync.Mutex // protects valueBuf + valueBuf [configstoreMaxValueLen]byte +} + +// Dictionaries are subject to very specific limitations: 255 character keys and 8000 character values, utf-8 encoded. +// The current storage collation limits utf-8 representations to 3 bytes in length. +// https://docs.fastly.com/en/guides/about-edge-dictionaries#limitations-and-considerations +// https://dev.mysql.com/doc/refman/8.4/en/charset-unicode-utf8mb3.html +// https://en.wikipedia.org/wiki/UTF-8#Encoding +const ( + configstoreMaxKeyLen = 255 * 3 // known maximum size for config store keys: 755 bytes, for 255 3-byte utf-8 encoded characters + configstoreMaxValueLen = 8000 * 3 // known maximum size for config store values: 24,000 bytes, for 8000 3-byte utf-8 encoded characters +) + +// OpenConfigStore returns a reference to the named config store, if it exists. +func OpenConfigStore(name string) (*ConfigStore, error) { + var c ConfigStore + + nameBuffer := prim.NewReadBufferFromString(name).Wstring() + + if err := fastlyConfigStoreOpen( + nameBuffer.Data, nameBuffer.Len, + prim.ToPointer(&c.h), + ).toError(); err != nil { + return nil, err + } + return &c, nil +} + +// witx: +// +// (@interface func (export "get") +// (param $h $config_store_handle) +// (param $key string) +// (param $value (@witx pointer (@witx char8))) +// (param $value_max_len (@witx usize)) +// (param $nwritten_out (@witx pointer (@witx usize))) +// (result $err (expected (error $fastly_status))) +// ) +// +//go:wasmimport fastly_config_store get +//go:noescape +func fastlyConfigStoreGet( + h configstoreHandle, + keyData prim.Pointer[prim.U8], keyLen prim.Usize, + value prim.Pointer[prim.Char8], + valueMaxLen prim.Usize, + nWritten prim.Pointer[prim.Usize], +) FastlyStatus + +// Get the value for key, if it exists. The returned slice's backing array is +// shared between multiple calls to getBytesUnlocked. +func (c *ConfigStore) getBytesUnlocked(key string) ([]byte, error) { + keyBuffer := prim.NewReadBufferFromString(key) + if keyBuffer.Len() > configstoreMaxKeyLen { + return nil, FastlyStatusInval.toError() + } + buf := prim.NewWriteBufferFromBytes(c.valueBuf[:]) // fresh slice of backing array + keyStr := keyBuffer.Wstring() + status := fastlyConfigStoreGet( + c.h, + keyStr.Data, keyStr.Len, + prim.ToPointer(buf.Char8Pointer()), buf.Cap(), + prim.ToPointer(buf.NPointer()), + ) + if err := status.toError(); err != nil { + return nil, err + } + return buf.AsBytes(), nil +} + +// GetBytes returns a slice of newly-allocated memory for the value +// corresponding to key. +func (c *ConfigStore) GetBytes(key string) ([]byte, error) { + c.mu.Lock() + defer c.mu.Unlock() + v, err := c.getBytesUnlocked(key) + if err != nil { + return nil, err + } + p := make([]byte, len(v)) + copy(p, v) + return p, nil +} + +// Has returns true if key is found. +func (c *ConfigStore) Has(key string) (bool, error) { + keyBuffer := prim.NewReadBufferFromString(key).Wstring() + var npointer prim.Usize = 0 + + status := fastlyConfigStoreGet( + c.h, + keyBuffer.Data, keyBuffer.Len, + prim.NullChar8Pointer(), 0, + prim.ToPointer(&npointer), + ) + switch status { + case FastlyStatusOK, FastlyStatusBufLen: + return true, nil + case FastlyStatusNone: + return false, nil + default: + return false, status.toError() + } +} diff --git a/internal/abi/fastly/hostcalls_noguest.go b/internal/abi/fastly/hostcalls_noguest.go index f73e1d2..17c44d7 100644 --- a/internal/abi/fastly/hostcalls_noguest.go +++ b/internal/abi/fastly/hostcalls_noguest.go @@ -313,6 +313,24 @@ func (d *Dictionary) Has(key string) (bool, error) { return false, fmt.Errorf("not implemented") } +type ConfigStore struct{} + +func OpenConfigStore(name string) (*ConfigStore, error) { + return nil, fmt.Errorf("not implemented") +} + +func (d *ConfigStore) GetBytes(key string) ([]byte, error) { + return nil, fmt.Errorf("not implemented") +} + +func (d *ConfigStore) Get(key string) (string, error) { + return "", fmt.Errorf("not implemented") +} + +func (d *ConfigStore) Has(key string) (bool, error) { + return false, fmt.Errorf("not implemented") +} + func GeoLookup(ip net.IP) ([]byte, error) { return nil, fmt.Errorf("not implemented") } diff --git a/internal/abi/fastly/types.go b/internal/abi/fastly/types.go index 185b453..66a3a41 100644 --- a/internal/abi/fastly/types.go +++ b/internal/abi/fastly/types.go @@ -291,6 +291,11 @@ type endpointHandle handle // (typename $dictionary_handle (handle)) type dictionaryHandle handle +// witx: +// +// (typename $config_store_handle (handle)) +type configstoreHandle handle + // witx: // // (typename $multi_value_cursor u32)