Skip to content

Commit

Permalink
Merge pull request #135 from fastly/dgryski/config-store-hostcalls
Browse files Browse the repository at this point in the history
switch configstore to new config store hostcalls
  • Loading branch information
cee-dub authored Sep 3, 2024
2 parents fc1ebc2 + 653c0f9 commit 59c05b9
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 10 deletions.
4 changes: 2 additions & 2 deletions configstore/configstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
97 changes: 89 additions & 8 deletions edgedict/dictionary.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,119 @@
package edgedict

import (
"github.com/fastly/compute-sdk-go/configstore"
"errors"
"fmt"

"github.com/fastly/compute-sdk-go/internal/abi/fastly"
)

var (
// ErrDictionaryNotFound indicates the named dictionary doesn't exist.
ErrDictionaryNotFound = configstore.ErrStoreNotFound
ErrDictionaryNotFound = errors.New("dictionary not found")

// ErrDictionaryNameEmpty indicates the given dictionary name
// was empty.
ErrDictionaryNameEmpty = configstore.ErrStoreNameEmpty
ErrDictionaryNameEmpty = errors.New("dictionary name was empty")

// ErrDictionaryNameInvalid indicates the given dictionary name
// was invalid.
ErrDictionaryNameInvalid = configstore.ErrStoreNameInvalid
ErrDictionaryNameInvalid = errors.New("dictionary name contained invalid characters")

// ErrDictionaryNameTooLong indicates the given dictionary name
// was too long.
ErrDictionaryNameTooLong = configstore.ErrStoreNameTooLong
ErrDictionaryNameTooLong = errors.New("dictionary name too long")

// ErrKeyNotFound indicates a key isn't in a dictionary.
ErrKeyNotFound = configstore.ErrKeyNotFound
ErrKeyNotFound = errors.New("key not found")

// ErrUnexpected indicates an unexpected error occurred.
ErrUnexpected = errors.New("unexpected error")
)

// Dictionary is a read-only representation of an edge dictionary.
//
// Deprecated: Use the configstore package instead.
type Dictionary = configstore.Store
type Dictionary struct {
abiDict *fastly.Dictionary
}

// Open returns an edge dictionary with the given name. Names are case
// sensitive.
//
// Deprecated: Use configstore.Open() instead.
func Open(name string) (*Dictionary, error) {
return configstore.Open(name)
d, err := fastly.OpenDictionary(name)
if err != nil {
status, ok := fastly.IsFastlyError(err)
switch {
case ok && status == fastly.FastlyStatusBadf:
return nil, ErrDictionaryNotFound
case ok && status == fastly.FastlyStatusNone:
return nil, ErrDictionaryNameEmpty
case ok && status == fastly.FastlyStatusUnsupported:
return nil, ErrDictionaryNameTooLong
case ok && status == fastly.FastlyStatusInval:
return nil, ErrDictionaryNameInvalid
default:
return nil, err
}
}
return &Dictionary{d}, nil
}

// GetBytes returns the value in the dictionary for the given key, if it exists, as a byte slice.
func (d *Dictionary) GetBytes(key string) ([]byte, error) {
if d == nil {
return nil, ErrKeyNotFound
}

v, err := d.abiDict.GetBytes(key)
if err != nil {
status, ok := fastly.IsFastlyError(err)
switch {
case ok && status == fastly.FastlyStatusBadf:
return nil, ErrDictionaryNotFound
case ok && status == fastly.FastlyStatusNone:
return nil, ErrKeyNotFound
case ok:
return nil, fmt.Errorf("%w (%s)", ErrUnexpected, status)
default:
return nil, err
}
}
return v, nil
}

// Get returns the value in the dictionary with the given key, if it exists.
func (d *Dictionary) Get(key string) (string, error) {
buf, err := d.GetBytes(key)
if err != nil {
return "", err
}
return string(buf), nil
}

// Has returns true if the key exists in the dictionary, without allocating
// space to read a value.
func (d *Dictionary) Has(key string) (bool, error) {
if d == nil {
return false, ErrKeyNotFound
}

v, err := d.abiDict.Has(key)
if err != nil {
status, ok := fastly.IsFastlyError(err)
switch {
case ok && status == fastly.FastlyStatusBadf:
return false, ErrDictionaryNotFound
case ok && status == fastly.FastlyStatusNone:
return false, ErrKeyNotFound
case ok:
return false, fmt.Errorf("%w (%s)", ErrUnexpected, status)
default:
return false, err
}
}

return v, nil
}
17 changes: 17 additions & 0 deletions edgedict/dictionary_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2022 Fastly, Inc.

package edgedict

import "testing"

func TestDictionary(t *testing.T) {
var d *Dictionary
val, err := d.Get("xyzzy")
if err != ErrKeyNotFound {
t.Errorf("Expected get on nil dictionary to return ErrKeyNotFound")
}
// check val despite err being non-nil
if val != "" {
t.Errorf("Expected get on nil dictionary to return empty string")
}
}
142 changes: 142 additions & 0 deletions internal/abi/fastly/configstore_guest.go
Original file line number Diff line number Diff line change
@@ -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()
}
}
18 changes: 18 additions & 0 deletions internal/abi/fastly/hostcalls_noguest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
5 changes: 5 additions & 0 deletions internal/abi/fastly/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 59c05b9

Please sign in to comment.