Skip to content

Commit

Permalink
refac(storage); simplify MemBackend (#523)
Browse files Browse the repository at this point in the history
Split MemBackend into MapBackend and SyncBackend.
Makes it easy to write test cases where a storage backend is pre-filled
with some files.
  • Loading branch information
abhinav authored Dec 21, 2024
1 parent b822347 commit cca1119
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 149 deletions.
10 changes: 5 additions & 5 deletions internal/spice/state/branch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func TestBranchStateUnmarshal(t *testing.T) {

func TestBranchTxUpsertErrors(t *testing.T) {
ctx := context.Background()
db := storage.NewDB(storage.NewMemBackend())
db := storage.NewDB(make(storage.MapBackend))
store, err := InitStore(ctx, InitStoreRequest{
DB: db,
Trunk: "main",
Expand Down Expand Up @@ -232,7 +232,7 @@ func TestBranchTxUpsertErrors(t *testing.T) {

func TestBranchTxDelete(t *testing.T) {
ctx := context.Background()
db := storage.NewDB(storage.NewMemBackend())
db := storage.NewDB(make(storage.MapBackend))
store, err := InitStore(ctx, InitStoreRequest{
DB: db,
Trunk: "main",
Expand Down Expand Up @@ -299,7 +299,7 @@ func TestBranchTxDelete(t *testing.T) {

func TestBranchTxUpsertChangeMetadataCanClear(t *testing.T) {
ctx := context.Background()
db := storage.NewDB(storage.NewMemBackend())
db := storage.NewDB(make(storage.MapBackend))
store, err := InitStore(ctx, InitStoreRequest{
DB: db,
Trunk: "main",
Expand Down Expand Up @@ -342,7 +342,7 @@ func TestBranchTxUpsertChangeMetadataCanClear(t *testing.T) {

func TestBranchTxUpsert_canClearUpstream(t *testing.T) {
ctx := context.Background()
db := storage.NewDB(storage.NewMemBackend())
db := storage.NewDB(make(storage.MapBackend))
store, err := InitStore(ctx, InitStoreRequest{
DB: db,
Trunk: "main",
Expand Down Expand Up @@ -399,7 +399,7 @@ func testBranchStateUncorruptible(t *rapid.T) {

trunk := branchNameGen.Draw(t, "trunk")
ctx := context.Background()
db := storage.NewDB(storage.NewMemBackend())
db := storage.NewDB(make(storage.MapBackend))
store, err := InitStore(ctx, InitStoreRequest{
DB: db,
Trunk: trunk,
Expand Down
4 changes: 2 additions & 2 deletions internal/spice/state/storage/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import (
)

func TestStorageBackend(t *testing.T) {
t.Run("Memory", func(t *testing.T) {
testStorageBackend(t, NewMemBackend())
t.Run("MapSync", func(t *testing.T) {
testStorageBackend(t, SyncBackend(make(MapBackend)))
})

t.Run("Git", func(t *testing.T) {
Expand Down
66 changes: 66 additions & 0 deletions internal/spice/state/storage/map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package storage

import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
)

// MapBackend is an in-memory implementation of [Backend] backed by a map.
//
// This is NOT thread safe. Use [SyncBackend] to make it so.
type MapBackend map[string][]byte

var _ Backend = (MapBackend)(nil)

// Get retrieves a value from the store.
func (m MapBackend) Get(ctx context.Context, key string, dst any) error {
v, ok := m[key]
if !ok {
return ErrNotExist
}

return json.Unmarshal(v, dst)
}

// Update applies a batch of changes to the store.
// MapBackend ignores the message associated with the update.
func (m MapBackend) Update(ctx context.Context, req UpdateRequest) error {
for i, set := range req.Sets {
v, err := json.Marshal(set.Value)
if err != nil {
return fmt.Errorf("marshal [%d]: %w", i, err)
}
m[set.Key] = v
}

for _, key := range req.Deletes {
delete(m, key)
}

return nil
}

// Clear clears all keys in the store.
func (m MapBackend) Clear(context.Context, string) error {
clear(m)
return nil
}

// Keys returns a list of keys in the store.
func (m MapBackend) Keys(ctx context.Context, dir string) ([]string, error) {
if dir != "" && !strings.HasSuffix(dir, "/") {
dir += "/"
}

keys := make([]string, 0, len(m))
for k := range m {
if rest, ok := strings.CutPrefix(k, dir); ok {
keys = append(keys, rest)
}
}
sort.Strings(keys)
return keys, nil
}
99 changes: 0 additions & 99 deletions internal/spice/state/storage/mem.go

This file was deleted.

49 changes: 49 additions & 0 deletions internal/spice/state/storage/sync.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package storage

import (
"context"
"sync"
)

// SyncBackend adds mutex-based synchronization around all operations
// of the given backend to make it thread-safe.
//
// Use it with [MapBackend].
func SyncBackend(b Backend) Backend {
return &syncBackend{b: b}
}

type syncBackend struct {
mu sync.RWMutex
b Backend
}

var _ Backend = (*syncBackend)(nil)

func (s *syncBackend) Clear(ctx context.Context, msg string) error {
s.mu.Lock()
defer s.mu.Unlock()

return s.b.Clear(ctx, msg)
}

func (s *syncBackend) Get(ctx context.Context, key string, dst any) error {
s.mu.RLock()
defer s.mu.RUnlock()

return s.b.Get(ctx, key, dst)
}

func (s *syncBackend) Keys(ctx context.Context, dir string) ([]string, error) {
s.mu.RLock()
defer s.mu.RUnlock()

return s.b.Keys(ctx, dir)
}

func (s *syncBackend) Update(ctx context.Context, req UpdateRequest) error {
s.mu.Lock()
defer s.mu.Unlock()

return s.b.Update(ctx, req)
}
20 changes: 8 additions & 12 deletions internal/spice/state/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package state_test
import (
"context"
"encoding/json"
"maps"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -15,7 +14,7 @@ import (

func TestStore(t *testing.T) {
ctx := context.Background()
db := storage.NewDB(storage.NewMemBackend())
db := storage.NewDB(make(storage.MapBackend))

_, err := state.InitStore(ctx, state.InitStoreRequest{
DB: db,
Expand Down Expand Up @@ -128,10 +127,9 @@ func TestStore(t *testing.T) {

func TestOpenStore_errors(t *testing.T) {
t.Run("VersionMismatch", func(t *testing.T) {
mem := storage.NewMemBackend()
mem.AddFiles(maps.All(map[string][]byte{
mem := storage.MapBackend{
"version": []byte("500"),
}))
}

_, err := state.OpenStore(context.Background(), storage.NewDB(mem), nil)
require.Error(t, err)
Expand All @@ -140,28 +138,26 @@ func TestOpenStore_errors(t *testing.T) {
})

t.Run("NotInitialized", func(t *testing.T) {
mem := storage.NewMemBackend()
mem := make(storage.MapBackend)
_, err := state.OpenStore(context.Background(), storage.NewDB(mem), nil)
require.Error(t, err)
assert.ErrorIs(t, err, state.ErrUninitialized)
})

t.Run("CorruptRepo/Unparseable", func(t *testing.T) {
mem := storage.NewMemBackend()
mem.AddFiles(maps.All(map[string][]byte{
mem := storage.MapBackend{
"repo": []byte(`{`),
}))
}

_, err := state.OpenStore(context.Background(), storage.NewDB(mem), nil)
require.Error(t, err)
assert.ErrorContains(t, err, "get repo state:")
})

t.Run("CorruptRepo/Incomplete", func(t *testing.T) {
mem := storage.NewMemBackend()
mem.AddFiles(maps.All(map[string][]byte{
mem := storage.MapBackend{
"repo": []byte(`{}`),
}))
}

_, err := state.OpenStore(context.Background(), storage.NewDB(mem), nil)
require.Error(t, err)
Expand Down
Loading

0 comments on commit cca1119

Please sign in to comment.