or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

dirhash.mdgosumcheck.mdindex.mdmodfile.mdmodule.mdnote.mdsemver.mdstorage.mdsumdb.mdtlog.mdzip.md
tile.json

storage.mddocs/

storage - Storage Interfaces for Checksum Database

Package storage defines storage interfaces and a basic implementation for a checksum database.

Import

import "golang.org/x/mod/sumdb/storage"

Overview

The storage package provides abstract interfaces for transactional key-value storage systems used by checksum databases. It includes:

  • Storage interface for transaction management
  • Transaction interface for read/write operations
  • Mem - an in-memory implementation for testing

Types

Storage

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 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.

Transaction

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.

Write

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.

Mem

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.

Methods

func (m *Mem) ReadOnly(ctx context.Context, f func(context.Context, Transaction) error) error

Runs f in a read-only transaction.

func (m *Mem) ReadWrite(ctx context.Context, f func(context.Context, Transaction) error) error

Runs f in a read-write transaction.

Functions

TestStorage

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)
}

Usage Examples

Using In-Memory 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)
    }
}

Implementing Custom Storage

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)
    }
}

Batch Operations

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)
    }
}

Deletion Operations

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
    })
}

Transaction Semantics

Read-Only Transactions

  • Cannot write data
  • May be optimized by implementation
  • BufferWrites will panic

Read-Write Transactions

  • Can read and write
  • Changes buffered until commit
  • May be retried if commit fails
  • Function f may be called multiple times

Atomicity

Transactions 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
})

Testing Storage Implementations

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)
}

See Also

  • sumdb - Uses storage for checksum database
  • tlog - Transparent log implementation