sumdb/storage: basic storage interface and tests

Copied from golang.org/x/exp/sumdb/internal/tkv and tkvtest.

For golang/go#31761.

Change-Id: Ib5a411c9b9641d1006a2e721043e8e16bf0f6bea
Reviewed-on: https://go-review.googlesource.com/c/mod/+/176465
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/sumdb/storage/mem.go b/sumdb/storage/mem.go
new file mode 100644
index 0000000..9b7823d
--- /dev/null
+++ b/sumdb/storage/mem.go
@@ -0,0 +1,114 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package storage
+
+import (
+	"context"
+	"errors"
+	"math/rand"
+	"sync"
+)
+
+// Mem is an in-memory implementation of Storage.
+// It is meant for tests and does not store any data to persistent storage.
+//
+// The zero value is an empty Mem ready for use.
+type Mem struct {
+	mu    sync.RWMutex
+	table map[string]string
+}
+
+// A memTx is a transaction in a Mem.
+type memTx struct {
+	m      *Mem
+	writes []Write
+}
+
+// errRetry is an internal sentinel indicating that the transaction should be retried.
+// It is never returned to the caller.
+var errRetry = errors.New("retry")
+
+// ReadOnly runs f in a read-only transaction.
+func (m *Mem) ReadOnly(ctx context.Context, f func(context.Context, Transaction) error) error {
+	tx := &memTx{m: m}
+	for {
+		err := func() error {
+			m.mu.Lock()
+			defer m.mu.Unlock()
+
+			if err := f(ctx, tx); err != nil {
+				return err
+			}
+			// Spurious retry with 10% probability.
+			if rand.Intn(10) == 0 {
+				return errRetry
+			}
+			return nil
+		}()
+		if err != errRetry {
+			return err
+		}
+	}
+}
+
+// ReadWrite runs f in a read-write transaction.
+func (m *Mem) ReadWrite(ctx context.Context, f func(context.Context, Transaction) error) error {
+	tx := &memTx{m: m}
+	for {
+		err := func() error {
+			m.mu.Lock()
+			defer m.mu.Unlock()
+
+			tx.writes = []Write{}
+			if err := f(ctx, tx); err != nil {
+				return err
+			}
+			// Spurious retry with 10% probability.
+			if rand.Intn(10) == 0 {
+				return errRetry
+			}
+			if m.table == nil {
+				m.table = make(map[string]string)
+			}
+			for _, w := range tx.writes {
+				if w.Value == "" {
+					delete(m.table, w.Key)
+				} else {
+					m.table[w.Key] = w.Value
+				}
+			}
+			return nil
+		}()
+		if err != errRetry {
+			return err
+		}
+	}
+}
+
+// ReadValues returns the values associated with the given keys.
+func (tx *memTx) ReadValues(ctx context.Context, keys []string) ([]string, error) {
+	vals := make([]string, len(keys))
+	for i, key := range keys {
+		vals[i] = tx.m.table[key]
+	}
+	return vals, nil
+}
+
+// ReadValue returns the value associated with the single key.
+func (tx *memTx) ReadValue(ctx context.Context, key string) (string, error) {
+	return tx.m.table[key], nil
+}
+
+// BufferWrites buffers a list of writes to be applied
+// to the table when the transaction commits.
+// The changes are not visible to reads within the transaction.
+// The map argument is not used after the call returns.
+func (tx *memTx) BufferWrites(list []Write) error {
+	if tx.writes == nil {
+		panic("BufferWrite on read-only transaction")
+	}
+	tx.writes = append(tx.writes, list...)
+	return nil
+}
diff --git a/sumdb/storage/mem_test.go b/sumdb/storage/mem_test.go
new file mode 100644
index 0000000..57ae15e
--- /dev/null
+++ b/sumdb/storage/mem_test.go
@@ -0,0 +1,14 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package storage
+
+import (
+	"context"
+	"testing"
+)
+
+func TestMem(t *testing.T) {
+	TestStorage(t, context.Background(), new(Mem))
+}
diff --git a/sumdb/storage/storage.go b/sumdb/storage/storage.go
new file mode 100644
index 0000000..3c80ac2
--- /dev/null
+++ b/sumdb/storage/storage.go
@@ -0,0 +1,56 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package storage defines storage interfaces for and a basic implementation of a checksum database.
+package storage
+
+import "context"
+
+// A Storage is a transaction key-value storage system.
+type Storage interface {
+	// ReadOnly runs f in a read-only transaction.
+	// It is equivalent to ReadWrite except that the
+	// transaction's BufferWrite method will fail unconditionally.
+	// (The implementation may be able to optimize the
+	// transaction if it knows at the start that no writes will happen.)
+	ReadOnly(ctx context.Context, f func(context.Context, Transaction) error) error
+
+	// ReadWrite runs f in a read-write transaction.
+	// If f returns an error, the transaction aborts and returns that error.
+	// If f returns nil, the transaction attempts to commit and then then return nil.
+	// Otherwise it tries again. Note that f may be called multiple times and that
+	// the result only describes the effect of the final call to f.
+	// The caller must take care not to use any state computed during
+	// earlier calls to f, or even the last call to f when an error is returned.
+	ReadWrite(ctx context.Context, f func(context.Context, Transaction) error) error
+}
+
+// A Transaction provides read and write operations within a transaction,
+// as executed by Storage's ReadOnly or ReadWrite methods.
+type Transaction interface {
+	// ReadValue reads the value associated with a single key.
+	// If there is no value associated with that key, ReadKey returns an empty value.
+	// An error is only returned for problems accessing the storage.
+	ReadValue(ctx context.Context, key string) (value string, err error)
+
+	// ReadValues reads the values associated with the given keys.
+	// If there is no value stored for a given key, ReadValues returns an empty value for that key.
+	// An error is only returned for problems accessing the storage.
+	ReadValues(ctx context.Context, keys []string) (values []string, err error)
+
+	// BufferWrites buffers the given writes,
+	// to be applied at the end of the transaction.
+	// BufferWrites panics if this is a ReadOnly transaction.
+	// It returns an error if it detects any other problems.
+	// The behavior of multiple writes buffered using the same key
+	// is undefined: it may return an error or not.
+	BufferWrites(writes []Write) error
+}
+
+// A Write is a single change to be applied at the end of a read-write transaction.
+// A Write with an empty value deletes the value associated with the given key.
+type Write struct {
+	Key   string
+	Value string
+}
diff --git a/sumdb/storage/test.go b/sumdb/storage/test.go
new file mode 100644
index 0000000..fdf410e
--- /dev/null
+++ b/sumdb/storage/test.go
@@ -0,0 +1,75 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package storage
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"testing"
+)
+
+// TestStorage tests a Storage implementation.
+func TestStorage(t *testing.T, ctx context.Context, storage Storage) {
+	s := storage
+
+	// Insert records.
+	err := s.ReadWrite(ctx, func(ctx context.Context, tx Transaction) error {
+		for i := 0; i < 10; i++ {
+			err := tx.BufferWrites([]Write{
+				{Key: fmt.Sprint(i), Value: fmt.Sprint(-i)},
+				{Key: fmt.Sprint(1000 + i), Value: fmt.Sprint(-1000 - i)},
+			})
+			if err != nil {
+				t.Fatal(err)
+			}
+		}
+		return nil
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Read the records back.
+	testRead := func() {
+		err := s.ReadOnly(ctx, func(ctx context.Context, tx Transaction) error {
+			for i := int64(0); i < 1010; i++ {
+				if i == 10 {
+					i = 1000
+				}
+				val, err := tx.ReadValue(ctx, fmt.Sprint(i))
+				if err != nil {
+					t.Fatalf("reading %v: %v", i, err)
+				}
+				if want := fmt.Sprint(-i); val != want {
+					t.Fatalf("ReadValue %v = %q, want %v", i, val, want)
+				}
+			}
+			return nil
+		})
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+	testRead()
+
+	// Buffered writes in failed transaction should not be applied.
+	err = s.ReadWrite(ctx, func(ctx context.Context, tx Transaction) error {
+		tx.BufferWrites([]Write{
+			{Key: fmt.Sprint(0), Value: ""},          // delete
+			{Key: fmt.Sprint(1), Value: "overwrite"}, // overwrite
+		})
+		if err != nil {
+			t.Fatal(err)
+		}
+		return io.ErrUnexpectedEOF
+	})
+	if err != io.ErrUnexpectedEOF {
+		t.Fatalf("ReadWrite returned %v, want ErrUnexpectedEOF", err)
+	}
+
+	// All same values should still be there.
+	testRead()
+}