Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

abi/fastly: correct max value length and use scratch buffer in Dictionary#GetBytes #120

Merged
merged 7 commits into from
Jun 24, 2024
Merged
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
4 changes: 4 additions & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ jobs:
# Newest supported configuration
- go-version: '1.22' # pairs with TinyGo 0.31.2
tinygo-version: '0.31.2'
wasmtime-version: 'latest'
# Oldest supported configuration
- go-version: '1.21' # pairs with TinyGo 0.29.0
tinygo-version: '0.29.0'
wasmtime-version: '21.0.1' # pairs with TinyGo 0.29.0

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -44,6 +46,8 @@ jobs:
- name: Set up Wasmtime
uses: bytecodealliance/actions/wasmtime/setup@v1
with:
version: ${{ matrix.wasmtime-version }}

- name: Check our dependencies
run: |
Expand Down
10 changes: 4 additions & 6 deletions configstore/configstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ func Open(name string) (*Store, error) {
return &Store{d}, nil
}

// Has checks to see if the item exists in the config store, without allocating
// any space to read it.
// Has returns true if the key exists in the config store, without allocating
// space to read a value.
func (s *Store) Has(key string) (bool, error) {
if s == nil {
return false, ErrKeyNotFound
Expand All @@ -86,7 +86,7 @@ func (s *Store) Has(key string) (bool, error) {
return v, nil
}

// Get returns the item in the config store with the given key, as a byte slice.
// GetBytes returns the value in the config store for the given key, if it exists, as a byte slice.
func (s *Store) GetBytes(key string) ([]byte, error) {
if s == nil {
return nil, ErrKeyNotFound
Expand All @@ -106,16 +106,14 @@ func (s *Store) GetBytes(key string) ([]byte, error) {
return nil, err
}
}

return v, nil
}

// Get returns the item in the config store with the given key.
// Get returns the value in the config store with the given key, if it exists.
func (s *Store) Get(key string) (string, error) {
buf, err := s.GetBytes(key)
if err != nil {
return "", err
}

return string(buf), nil
}
11 changes: 10 additions & 1 deletion integration_tests/config_store/configstore.json

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions integration_tests/config_store/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ package main

import (
"bytes"
"strconv"
"strings"
"sync"
"testing"

"github.com/fastly/compute-sdk-go/configstore"
Expand Down Expand Up @@ -60,4 +63,56 @@ func TestConfigStore(t *testing.T) {
if got, want := twitter, "https://twitter.com/fastly"; got != want {
t.Errorf("Body = %q, want %q", got, want)
}

maxKey := "maximum-length-asciiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii-1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000-2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
max, err := d.Get(maxKey)
if err != nil {
t.Fatal(err)
}

got := max
want := "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"
if got != want {
t.Errorf("Body = %q, want %q", got, want)
}

maxUTF8, err := d.Get(strings.Repeat("ゝ", 255))
cee-dub marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
t.Fatal(err)
}

got, want = maxUTF8, strings.Repeat("ゝ", 8000)
if got != want {
t.Errorf("Body = %q, want %q", got, want)
}
}

func TestConfigStoreConcurrently(t *testing.T) {
d, err := configstore.Open("configstore")
if err != nil {
t.Fatal(err)
}
var wg sync.WaitGroup
wg.Add(5)
var keys [5]string
var values [5][]byte
for i := 0; i < 5; i++ {
go func(i int) {
defer wg.Done()
var err error
keys[i] = "concurrent " + strconv.Itoa(i)
values[i], err = d.GetBytes(keys[i])
if err != nil {
t.Errorf("%d: GetBytes() error: %v", i, err)
}
}(i)
}
wg.Wait()
for i := 0; i < 5; i++ {
got := values[i]
want := bytes.Repeat([]byte{'0' + byte(i)}, i+1)
if !bytes.Equal(got, want) {
t.Errorf("%d: got: %q want: %q", i, got, want)
}
}
}
19 changes: 8 additions & 11 deletions integration_tests/request_upstream/fastly.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,15 @@ manifest_version = 2
name = "request_upstream"
service_id = ""

[local_server]

[local_server.backends]
[local_server.backends.TheOrigin]
url = "https://compute-sdk-test-backend.edgecompute.app/"

[local_server.backends.TheOrigin]
url = "https://compute-sdk-test-backend.edgecompute.app/"
[local_server.backends.TheOrigin2]
url = "https://compute-sdk-test-backend.edgecompute.app/"

[local_server.backends.TheOrigin2]
url = "https://compute-sdk-test-backend.edgecompute.app/"
[local_server.backends.example_backend]
url = "https://example.org/"

[local_server.backends.example_backend]
url = "https://example.org/"

[local_server.backends.httpme]
url = "https://http-me.glitch.me"
[local_server.backends.httpedge]
url = "https://http.edgecompute.app/anything/"
11 changes: 6 additions & 5 deletions integration_tests/request_upstream/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ const bodySize = 64 * 1024
func TestRequestUpstreamBody(t *testing.T) {
body := make([]byte, bodySize)
for i := range body {
body[i] = byte(i)
body[i] = 'A'
}

b, err := fastly.NewHTTPBody()
Expand Down Expand Up @@ -124,15 +124,15 @@ func TestRequestUpstreamBody(t *testing.T) {
}

func requestUpstreamBody(t *testing.T, body io.Reader, size int, chunked bool) {
req, err := fsthttp.NewRequest("POST", "https://http-me.glitch.me/?anything", body)
req, err := fsthttp.NewRequest("POST", "https://http.edgecompute.app/anything/", body)
if err != nil {
t.Fatalf("NewRequest: %v", err)
}

req.Header.Set("Content-Type", "application/octet-stream")
req.CacheOptions.Pass = true

resp, err := req.Send(context.Background(), "httpme")
resp, err := req.Send(context.Background(), "httpedge")
if err != nil {
t.Fatalf("Send: %v", err)
}
Expand All @@ -142,8 +142,9 @@ func requestUpstreamBody(t *testing.T, body io.Reader, size int, chunked bool) {
Headers map[string]string `json:"headers"`
}

if err := json.NewDecoder(resp.Body).Decode(&respData); err != nil {
t.Fatalf("Decode: %v", err)
gotBody := new(bytes.Buffer)
if err := json.NewDecoder(io.TeeReader(resp.Body, gotBody)).Decode(&respData); err != nil {
t.Fatalf("Decode: %v\nBody:\n%s", err, gotBody.String())
}

var teWant, clWant string
Expand Down
77 changes: 47 additions & 30 deletions internal/abi/fastly/hostcalls_guest.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"math"
"net"
"strings"
"sync"
"time"

"github.com/fastly/compute-sdk-go/internal/abi/prim"
Expand Down Expand Up @@ -2436,10 +2437,27 @@ func fastlyDictionaryOpen(
// Dictionary represents a Fastly edge dictionary, 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
cee-dub marked this conversation as resolved.
Show resolved Hide resolved
// allows us to use valueBuf scratch space between the guest and host to avoid
// allocations any larger than necessary, without locking.
type Dictionary struct {
h dictionaryHandle

mu sync.Mutex // protects valueBuf
valueBuf [dictionaryMaxValueLen]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 (
dictionaryMaxKeyLen = 255 * 3 // known maximum size for config store keys: 755 bytes, for 255 3-byte utf-8 encoded characters
dictionaryMaxValueLen = 8000 * 3 // known maximum size for config store values: 24,000 bytes, for 8000 3-byte utf-8 encoded characters
)

// OpenDictionary returns a reference to the named dictionary, if it exists.
func OpenDictionary(name string) (*Dictionary, error) {
var d Dictionary
Expand All @@ -2452,7 +2470,6 @@ func OpenDictionary(name string) (*Dictionary, error) {
).toError(); err != nil {
return nil, err
}

return &d, nil
}

Expand All @@ -2477,42 +2494,42 @@ func fastlyDictionaryGet(
nWritten prim.Pointer[prim.Usize],
) FastlyStatus

// Get the value for key, as a byte slice, if it exists.
func (d *Dictionary) GetBytes(key string) ([]byte, error) {
keyBuffer := prim.NewReadBufferFromString(key).Wstring()
n := DefaultMediumBufLen // Longest (8192) = Config Store limit; typical values likely less than 1024
for {
buf := prim.NewWriteBuffer(n)
status := fastlyDictionaryGet(
d.h,
keyBuffer.Data, keyBuffer.Len,
prim.ToPointer(buf.Char8Pointer()), buf.Cap(),
prim.ToPointer(buf.NPointer()),
)
if status == FastlyStatusBufLen && n < dictionaryValueMaxLen {
// The Dictionary API cannot return the needed size with this error.
// Instead of perfectly adapting, we allocate the maximum length a value can have.
n = dictionaryValueMaxLen
continue
}
if err := status.toError(); err != nil {
return nil, err
}
return buf.AsBytes(), nil
// Get the value for key, if it exists. The returned slice's backing array is
// shared between multiple calls to getBytesUnlocked.
func (d *Dictionary) getBytesUnlocked(key string) ([]byte, error) {
keyBuffer := prim.NewReadBufferFromString(key)
if keyBuffer.Len() > dictionaryMaxKeyLen {
return nil, FastlyStatusInval.toError()
}
buf := prim.NewWriteBufferFromBytes(d.valueBuf[:]) // fresh slice of backing array
keyStr := keyBuffer.Wstring()
status := fastlyDictionaryGet(
d.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
cee-dub marked this conversation as resolved.
Show resolved Hide resolved
}

// Get the value for key, if it exists.
func (d *Dictionary) Get(key string) (string, error) {
buf, err := d.GetBytes(key)
// GetBytes returns a slice of newly-allocated memory for the value
// corresponding to key.
func (d *Dictionary) GetBytes(key string) ([]byte, error) {
d.mu.Lock()
defer d.mu.Unlock()
v, err := d.getBytesUnlocked(key)
if err != nil {
return "", err
return nil, err
}

return string(buf), nil
p := make([]byte, len(v))
copy(p, v)
return p, nil
}

// Has returns whether a value exists.
// Has returns true if key is found.
func (d *Dictionary) Has(key string) (bool, error) {
keyBuffer := prim.NewReadBufferFromString(key).Wstring()
var npointer prim.Usize = 0
Expand Down
7 changes: 3 additions & 4 deletions internal/abi/fastly/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,11 @@ func IsFastlyError(err error) (FastlyStatus, bool) {
}

const (
ipBufLen = 16 // known size for IP address buffers
dnsBufLen = 256 // known size for "DNS" values, enough to hold the longest possible hostname or domain name
dictionaryValueMaxLen = 8192 // known size for maximum config store value https://docs.fastly.com/en/guides/about-edge-dictionaries#limitations-and-considerations
ipBufLen = 16 // known size for IP address buffers
dnsBufLen = 256 // known size for "DNS" values, enough to hold the longest possible hostname or domain name

DefaultSmallBufLen = 128 // default size for "typically-small" values with variable sizes: HTTP methods, header names, tls protocol names, cipher suites
DefaultMediumBufLen = 1024 // default size for values between small and large with variable sizes
DefaultMediumBufLen = 1024 // default size for values between small and large, with variable sizes
DefaultLargeBufLen = 8192 // default size for "typically-large" values with variable sizes; header values, URLs.
)

Expand Down
2 changes: 1 addition & 1 deletion internal/abi/prim/prim.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func (b *WriteBuffer) NValue() Usize {

// AsBytes returns a slice of the buffer's data as a byte slice.
func (b *WriteBuffer) AsBytes() []byte {
return b.buf[:b.n]
return b.buf[:b.n:b.n]
}

// ToString returns a copy of the buffer's data as a string.
Expand Down