Skip to content
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
37 changes: 37 additions & 0 deletions internal/spice/state/git_store_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package state

import (
"fmt"
"testing"

"github.com/stretchr/testify/require"
"go.abhg.dev/gs/internal/git"
"go.abhg.dev/gs/internal/silog/silogtest"
"go.abhg.dev/gs/internal/spice/state/storage"
)

// benchmarkGitStore seeds sibling branches in one storage update.
// Transaction benchmarks create fresh transactions without committing, so every
// iteration starts with a cold transaction cache and the same persisted graph.
func benchmarkGitStore(b *testing.B, size int) (*Store, *storage.DB) {
b.Helper()
ctx := b.Context()
log := silogtest.New(b)
repo, _, err := git.Init(ctx, b.TempDir(), git.InitOptions{Log: log})
require.NoError(b, err)
db := storage.NewDB(storage.NewGitBackend(storage.GitConfig{
Repo: repo, Ref: "refs/spice/data",
AuthorName: "Benchmark", AuthorEmail: "benchmark@example.com", Log: log,
}))
store, err := InitStore(ctx, InitStoreRequest{DB: db, Trunk: "main", Log: log})
require.NoError(b, err)
sets := make([]storage.SetRequest, size)
for i := range sets {
sets[i] = storage.SetRequest{
Key: branchKey(fmt.Sprintf("branch-%03d", i)),
Value: &branchState{Base: branchStateBase{Name: "main", Hash: "initial"}},
}
}
require.NoError(b, db.Update(ctx, storage.UpdateRequest{Sets: sets, Message: "seed branches"}))
return store, db
}
23 changes: 14 additions & 9 deletions internal/spice/state/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"errors"
"fmt"
"slices"

"go.abhg.dev/gs/internal/silog"
"go.abhg.dev/gs/internal/spice/state/storage"
Expand Down Expand Up @@ -171,20 +172,24 @@ func OpenStore(ctx context.Context, db DB, logger *silog.Logger) (*Store, error)
logger = silog.Nop()
}

version, err := loadVersion(ctx, db)
if err != nil {
return nil, fmt.Errorf("load store version: %w", err)
version := VersionOne
var info repoInfo
err := db.GetMany(ctx, []storage.GetRequest{
{Key: _versionFile, Value: &version},
{Key: _repoJSON, Value: &info},
})
missing, ok := errors.AsType[*storage.MissingKeysError](err)
if err != nil && !ok {
return nil, fmt.Errorf("read store metadata: %w", err)
}
if err := checkVersion(version); err != nil {
return nil, fmt.Errorf("check store layout: %w", err)
}

var info repoInfo
if err := db.Get(ctx, _repoJSON, &info); err != nil {
if errors.Is(err, ErrNotExist) {
return nil, ErrUninitialized
}
return nil, fmt.Errorf("get repo state: %w", err)
// An absent version keeps the v1 default; a missing repository record
// means the store has not been initialized.
if ok && slices.Contains(missing.Indexes, 1) {
return nil, ErrUninitialized
}

if err := info.Validate(); err != nil {
Expand Down
27 changes: 27 additions & 0 deletions internal/spice/state/store_open_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package state

import (
"fmt"
"testing"

"github.com/stretchr/testify/require"
"go.abhg.dev/gs/internal/silog/silogtest"
)

func BenchmarkStoreOpen(b *testing.B) {
for _, size := range []int{1, 100} {
for _, legacy := range []bool{false, true} {
b.Run(fmt.Sprintf("Branches%d/Legacy%t", size, legacy), func(b *testing.B) {
_, db := benchmarkGitStore(b, size)
if legacy {
require.NoError(b, db.Delete(b.Context(), _versionFile, "legacy layout"))
}
for b.Loop() {
if _, err := OpenStore(b.Context(), db, silogtest.New(b)); err != nil {
b.Fatal(err)
}
}
})
}
}
}
23 changes: 21 additions & 2 deletions internal/spice/state/store_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package state_test

import (
"context"
"encoding/json/jsontext"
"io"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -262,7 +264,7 @@ func TestOpenStore_remoteMigrationMalformed(t *testing.T) {

_, err := state.OpenStore(t.Context(), storage.NewDB(mem), nil)
require.Error(t, err)
assert.ErrorContains(t, err, "get repo state:")
assert.ErrorContains(t, err, `decode JSON for "repo":`)
}

func TestInitStore_writesVersionOneForSameRemote(t *testing.T) {
Expand Down Expand Up @@ -394,7 +396,7 @@ func TestOpenStore_errors(t *testing.T) {

_, err := state.OpenStore(t.Context(), storage.NewDB(mem), nil)
require.Error(t, err)
assert.ErrorContains(t, err, "get repo state:")
assert.ErrorContains(t, err, `decode JSON for "repo":`)
})

t.Run("CorruptRepo/Incomplete", func(t *testing.T) {
Expand All @@ -407,3 +409,20 @@ func TestOpenStore_errors(t *testing.T) {
assert.ErrorContains(t, err, "corrupt state:")
})
}

func TestOpenStore_batchReadError(t *testing.T) {
db := &failedBatchDB{DB: storage.NewDB(storage.MapBackend{
"version": []byte("1"),
"repo": []byte(`{"trunk":"main"}`),
})}
_, err := state.OpenStore(t.Context(), db, nil)
assert.ErrorIs(t, err, io.ErrUnexpectedEOF)
}

type failedBatchDB struct {
*storage.DB
}

func (*failedBatchDB) GetMany(context.Context, []storage.GetRequest) error {
return io.ErrUnexpectedEOF
}
23 changes: 1 addition & 22 deletions internal/spice/state/version.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package state

import (
"context"
"errors"
"fmt"
)
import "fmt"

const _versionFile = "version"

Expand Down Expand Up @@ -41,23 +37,6 @@ func checkVersion(version Version) error {
return nil
}

// loadVersion loads the version of the storage layout used by the given [DB].
func loadVersion(ctx context.Context, db DB) (Version, error) {
var version Version

if err := db.Get(ctx, _versionFile, &version); err != nil {
if errors.Is(err, ErrNotExist) {
// Version file was added during storage version 1.
// If file does not exist, it's an old v1 store.
return VersionOne, nil
}

return 0, err
}

return version, nil
}

// VersionMismatchError indicates that the data store we attempted to open
// is using a version older than this binary knows how to handle.
type VersionMismatchError struct {
Expand Down
44 changes: 0 additions & 44 deletions internal/spice/state/version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,52 +5,8 @@ import (

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.abhg.dev/gs/internal/spice/state/storage"
)

func TestLoadVersion(t *testing.T) {
tests := []struct {
name string
files storage.MapBackend
want Version
}{
{
name: "Empty",
want: VersionOne,
},
{
name: "ExplicitV1",
files: storage.MapBackend{
"version": []byte("1"),
},
want: VersionOne,
},
{
name: "ExplicitV2",
files: storage.MapBackend{
"version": []byte("2"),
},
want: VersionTwo,
},
{
name: "FutureVersion",
files: storage.MapBackend{
"version": []byte("42"),
},
want: Version(42),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db := storage.NewDB(tt.files)
got, err := loadVersion(t.Context(), db)
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}

func TestCheckVersion(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading