Package storage defines storage interfaces and a basic implementation for a checksum database.
import "golang.org/x/mod/sumdb/storage"The storage package provides abstract interfaces for transactional key-value storage systems used by checksum databases. It includes:
Storage interface for transaction managementTransaction interface for read/write operationsMem - an in-memory implementation for testingtype 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 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 Storage is a transaction key-value storage system.
type Transaction interface {
// ReadValue reads the value associated with a single key.
// If there is no value associated with that key, ReadValue 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 Transaction provides read and write operations within a transaction.
type Write struct {
Key string
Value string
}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 Mem struct {
// Has unexported fields.
}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.
func (m *Mem) ReadOnly(ctx context.Context, f func(context.Context, Transaction) error) errorRuns f in a read-only transaction.
func (m *Mem) ReadWrite(ctx context.Context, f func(context.Context, Transaction) error) errorRuns f in a read-write transaction.
func TestStorage(t *testing.T, ctx context.Context, storage Storage)Tests a Storage implementation.
Example:
func TestMyStorage(t *testing.T) {
storage := &MyStorage{}
ctx := context.Background()
storage.TestStorage(t, ctx, storage)
}package main
import (
"context"
"fmt"
"log"
"golang.org/x/mod/sumdb/storage"
)
func main() {
var mem storage.Mem
ctx := context.Background()
// Write some data
err := mem.ReadWrite(ctx, func(ctx context.Context, tx storage.Transaction) error {
return tx.BufferWrites([]storage.Write{
{Key: "key1", Value: "value1"},
{Key: "key2", Value: "value2"},
})
})
if err != nil {
log.Fatal(err)
}
// Read data back
err = mem.ReadOnly(ctx, func(ctx context.Context, tx storage.Transaction) error {
value, err := tx.ReadValue(ctx, "key1")
if err != nil {
return err
}
fmt.Printf("key1: %s\n", value)
return nil
})
if err != nil {
log.Fatal(err)
}
}package main
import (
"context"
"fmt"
"sync"
"golang.org/x/mod/sumdb/storage"
)
type MapStorage struct {
mu sync.RWMutex
data map[string]string
}
func NewMapStorage() *MapStorage {
return &MapStorage{
data: make(map[string]string),
}
}
func (s *MapStorage) ReadOnly(ctx context.Context, f func(context.Context, storage.Transaction) error) error {
s.mu.RLock()
defer s.mu.RUnlock()
tx := &mapTransaction{s: s, readOnly: true}
return f(ctx, tx)
}
func (s *MapStorage) ReadWrite(ctx context.Context, f func(context.Context, storage.Transaction) error) error {
s.mu.Lock()
defer s.mu.Unlock()
tx := &mapTransaction{s: s, readOnly: false}
if err := f(ctx, tx); err != nil {
return err
}
// Apply buffered writes
for _, w := range tx.writes {
if w.Value == "" {
delete(s.data, w.Key)
} else {
s.data[w.Key] = w.Value
}
}
return nil
}
type mapTransaction struct {
s *MapStorage
readOnly bool
writes []storage.Write
}
func (tx *mapTransaction) ReadValue(ctx context.Context, key string) (string, error) {
return tx.s.data[key], nil
}
func (tx *mapTransaction) ReadValues(ctx context.Context, keys []string) ([]string, error) {
values := make([]string, len(keys))
for i, key := range keys {
values[i] = tx.s.data[key]
}
return values, nil
}
func (tx *mapTransaction) BufferWrites(writes []storage.Write) error {
if tx.readOnly {
panic("write to read-only transaction")
}
tx.writes = append(tx.writes, writes...)
return nil
}
func main() {
s := NewMapStorage()
ctx := context.Background()
// Test the storage
err := s.ReadWrite(ctx, func(ctx context.Context, tx storage.Transaction) error {
return tx.BufferWrites([]storage.Write{
{Key: "test", Value: "data"},
})
})
if err != nil {
panic(err)
}
err = s.ReadOnly(ctx, func(ctx context.Context, tx storage.Transaction) error {
val, err := tx.ReadValue(ctx, "test")
fmt.Printf("Value: %s\n", val)
return err
})
if err != nil {
panic(err)
}
}package main
import (
"context"
"fmt"
"log"
"golang.org/x/mod/sumdb/storage"
)
func main() {
var mem storage.Mem
ctx := context.Background()
// Batch write
err := mem.ReadWrite(ctx, func(ctx context.Context, tx storage.Transaction) error {
writes := make([]storage.Write, 100)
for i := 0; i < 100; i++ {
writes[i] = storage.Write{
Key: fmt.Sprintf("key%d", i),
Value: fmt.Sprintf("value%d", i),
}
}
return tx.BufferWrites(writes)
})
if err != nil {
log.Fatal(err)
}
// Batch read
err = mem.ReadOnly(ctx, func(ctx context.Context, tx storage.Transaction) error {
keys := make([]string, 100)
for i := 0; i < 100; i++ {
keys[i] = fmt.Sprintf("key%d", i)
}
values, err := tx.ReadValues(ctx, keys)
if err != nil {
return err
}
fmt.Printf("Read %d values\n", len(values))
return nil
})
if err != nil {
log.Fatal(err)
}
}package main
import (
"context"
"fmt"
"golang.org/x/mod/sumdb/storage"
)
func main() {
var mem storage.Mem
ctx := context.Background()
// Write and then delete
mem.ReadWrite(ctx, func(ctx context.Context, tx storage.Transaction) error {
return tx.BufferWrites([]storage.Write{
{Key: "temp", Value: "data"},
})
})
mem.ReadWrite(ctx, func(ctx context.Context, tx storage.Transaction) error {
// Empty value deletes the key
return tx.BufferWrites([]storage.Write{
{Key: "temp", Value: ""},
})
})
mem.ReadOnly(ctx, func(ctx context.Context, tx storage.Transaction) error {
val, _ := tx.ReadValue(ctx, "temp")
fmt.Printf("Value after delete: '%s' (empty)\n", val)
return nil
})
}BufferWrites will panicf may be called multiple timesTransactions provide atomicity:
err := storage.ReadWrite(ctx, func(ctx context.Context, tx storage.Transaction) error {
// All writes succeed or all fail
tx.BufferWrites([]storage.Write{
{Key: "key1", Value: "value1"},
{Key: "key2", Value: "value2"},
})
// If we return error, no writes are applied
if someCondition {
return errors.New("abort transaction")
}
return nil // Commits all writes
})package mypackage
import (
"context"
"testing"
"golang.org/x/mod/sumdb/storage"
)
func TestMyStorage(t *testing.T) {
s := NewMyStorage()
ctx := context.Background()
storage.TestStorage(t, ctx, s)
}