Refactor replica system
This commit is contained in:
380
file/replica_client.go
Normal file
380
file/replica_client.go
Normal file
@@ -0,0 +1,380 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/benbjohnson/litestream"
|
||||
"github.com/benbjohnson/litestream/internal"
|
||||
)
|
||||
|
||||
var _ litestream.ReplicaClient = (*ReplicaClient)(nil)
|
||||
|
||||
// ReplicaClient is a client for writing snapshots & WAL segments to disk.
|
||||
type ReplicaClient struct {
|
||||
path string // destination path
|
||||
|
||||
Replica *litestream.Replica
|
||||
}
|
||||
|
||||
// NewReplicaClient returns a new instance of ReplicaClient.
|
||||
func NewReplicaClient(path string) *ReplicaClient {
|
||||
return &ReplicaClient{
|
||||
path: path,
|
||||
}
|
||||
}
|
||||
|
||||
// db returns the database, if available.
|
||||
func (c *ReplicaClient) db() *litestream.DB{
|
||||
if c.Replica == nil {
|
||||
return nil
|
||||
}
|
||||
return c.Replica.DB()
|
||||
}
|
||||
|
||||
// Type returns "file" as the client type.
|
||||
func (c *ReplicaClient) Type() string {
|
||||
return "file"
|
||||
}
|
||||
|
||||
// Path returns the destination path to replicate the database to.
|
||||
func (c *ReplicaClient) Path() string {
|
||||
return c.path
|
||||
}
|
||||
|
||||
// GenerationsDir returns the path to a generation root directory.
|
||||
func (c *ReplicaClient) GenerationsDir() (string, error) {
|
||||
if c.path == "" {
|
||||
return "", fmt.Errorf("file replica path required")
|
||||
}
|
||||
return filepath.Join(c.path, "generations"), nil
|
||||
}
|
||||
|
||||
// GenerationDir returns the path to a generation's root directory.
|
||||
func (c *ReplicaClient) GenerationDir(generation string) (string, error) {
|
||||
dir, err := c.GenerationsDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if generation == "" {
|
||||
return "", fmt.Errorf("generation required")
|
||||
}
|
||||
return filepath.Join(dir, generation), nil
|
||||
}
|
||||
|
||||
// SnapshotsDir returns the path to a generation's snapshot directory.
|
||||
func (c *ReplicaClient) SnapshotsDir(generation string) (string, error) {
|
||||
dir, err := c.GenerationDir(generation)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dir, "snapshots"), nil
|
||||
}
|
||||
|
||||
// SnapshotPath returns the path to an uncompressed snapshot file.
|
||||
func (c *ReplicaClient) SnapshotPath(generation string, index int) (string, error) {
|
||||
dir, err := c.SnapshotsDir(generation)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dir, litestream.FormatSnapshotPath(index)), nil
|
||||
}
|
||||
|
||||
// WALDir returns the path to a generation's WAL directory
|
||||
func (c *ReplicaClient) WALDir(generation string) (string, error) {
|
||||
dir, err := c.GenerationDir(generation)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dir, "wal"), nil
|
||||
}
|
||||
|
||||
// WALSegmentPath returns the path to a WAL segment file.
|
||||
func (c *ReplicaClient) WALSegmentPath(generation string, index int, offset int64) (string, error) {
|
||||
dir, err := c.WALDir(generation)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dir, litestream.FormatWALSegmentPath(index, offset)), nil
|
||||
}
|
||||
|
||||
// Generations returns a list of available generation names.
|
||||
func (c *ReplicaClient) Generations(ctx context.Context) ([]string, error) {
|
||||
root, err := c.GenerationsDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot determine generations path: %w", err)
|
||||
}
|
||||
|
||||
fis, err := ioutil.ReadDir(root)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var generations []string
|
||||
for _, fi := range fis {
|
||||
if !litestream.IsGenerationName(fi.Name()) {
|
||||
continue
|
||||
} else if !fi.IsDir() {
|
||||
continue
|
||||
}
|
||||
generations = append(generations, fi.Name())
|
||||
}
|
||||
return generations, nil
|
||||
}
|
||||
|
||||
// DeleteGeneration deletes all snapshots & WAL segments within a generation.
|
||||
func (c *ReplicaClient) DeleteGeneration(ctx context.Context, generation string) error {
|
||||
dir, err := c.GenerationDir(generation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot determine generation directory: %w", err)
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(dir); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Snapshots returns an iterator over all available snapshots for a generation.
|
||||
func (c *ReplicaClient) Snapshots(ctx context.Context, generation string) (litestream.SnapshotIterator, error) {
|
||||
dir, err := c.SnapshotsDir(generation)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot determine snapshot directory: %w", err)
|
||||
}
|
||||
|
||||
f, err := os.Open(dir)
|
||||
if os.IsNotExist(err) {
|
||||
return litestream.NewSnapshotInfoSliceIterator(nil), nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fis, err := f.Readdir(-1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Iterate over every file and convert to metadata.
|
||||
infos := make([]litestream.SnapshotInfo, 0, len(fis))
|
||||
for _, fi := range fis {
|
||||
// Parse index from filename.
|
||||
index, err := litestream.ParseSnapshotPath(fi.Name())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
infos = append(infos, litestream.SnapshotInfo{
|
||||
Generation: generation,
|
||||
Index: index,
|
||||
Size: fi.Size(),
|
||||
CreatedAt: fi.ModTime().UTC(),
|
||||
})
|
||||
}
|
||||
|
||||
sort.Sort(litestream.SnapshotInfoSlice(infos))
|
||||
|
||||
return litestream.NewSnapshotInfoSliceIterator(infos), nil
|
||||
}
|
||||
|
||||
// WriteSnapshot writes LZ4 compressed data from rd into a file on disk.
|
||||
func (c *ReplicaClient) WriteSnapshot(ctx context.Context, generation string, index int, rd io.Reader) (info litestream.SnapshotInfo, err error) {
|
||||
filename, err := c.SnapshotPath(generation, index)
|
||||
if err != nil {
|
||||
return info, fmt.Errorf("cannot determine snapshot path: %w", err)
|
||||
}
|
||||
|
||||
var fileInfo, dirInfo os.FileInfo
|
||||
if db := c.db(); db != nil {
|
||||
fileInfo, dirInfo = db.FileInfo(), db.DirInfo()
|
||||
}
|
||||
|
||||
|
||||
// Ensure parent directory exists.
|
||||
if err := internal.MkdirAll(filepath.Dir(filename), dirInfo); err != nil {
|
||||
return info, err
|
||||
}
|
||||
|
||||
// Write snapshot to temporary file next to destination path.
|
||||
f, err := internal.CreateFile(filename+".tmp", fileInfo)
|
||||
if err != nil {
|
||||
return info, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := io.Copy(f, rd); err != nil {
|
||||
return info, err
|
||||
} else if err := f.Sync(); err != nil {
|
||||
return info, err
|
||||
} else if err := f.Close(); err != nil {
|
||||
return info, err
|
||||
}
|
||||
|
||||
// Build metadata.
|
||||
fi, err := os.Stat(filename + ".tmp")
|
||||
if err != nil {
|
||||
return info, err
|
||||
}
|
||||
info = litestream.SnapshotInfo{
|
||||
Generation: generation,
|
||||
Index: index,
|
||||
Size: fi.Size(),
|
||||
CreatedAt: fi.ModTime().UTC(),
|
||||
}
|
||||
|
||||
// Move snapshot to final path when it has been fully written & synced to disk.
|
||||
if err := os.Rename(filename+".tmp", filename); err != nil {
|
||||
return info, err
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// SnapshotReader returns a reader for snapshot data at the given generation/index.
|
||||
// Returns os.ErrNotExist if no matching index is found.
|
||||
func (c *ReplicaClient) SnapshotReader(ctx context.Context, generation string, index int) (io.ReadCloser, error) {
|
||||
filename, err := c.SnapshotPath(generation, index)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot determine snapshot path: %w", err)
|
||||
}
|
||||
return os.Open(filename)
|
||||
}
|
||||
|
||||
// DeleteSnapshot deletes a snapshot with the given generation & index.
|
||||
func (c *ReplicaClient) DeleteSnapshot(ctx context.Context, generation string, index int) error {
|
||||
filename, err := c.SnapshotPath(generation, index)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot determine snapshot path: %w", err)
|
||||
}
|
||||
if err := os.Remove(filename); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WALSegments returns an iterator over all available WAL files for a generation.
|
||||
func (c *ReplicaClient) WALSegments(ctx context.Context, generation string) (litestream.WALSegmentIterator, error) {
|
||||
dir, err := c.WALDir(generation)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot determine wal directory: %w", err)
|
||||
}
|
||||
|
||||
f, err := os.Open(dir)
|
||||
if os.IsNotExist(err) {
|
||||
return litestream.NewWALSegmentInfoSliceIterator(nil), nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fis, err := f.Readdir(-1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Iterate over every file and convert to metadata.
|
||||
infos := make([]litestream.WALSegmentInfo, 0, len(fis))
|
||||
for _, fi := range fis {
|
||||
// Parse index from filename.
|
||||
index, offset, err := litestream.ParseWALSegmentPath(fi.Name())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
infos = append(infos, litestream.WALSegmentInfo{
|
||||
Generation: generation,
|
||||
Index: index,
|
||||
Offset: offset,
|
||||
Size: fi.Size(),
|
||||
CreatedAt: fi.ModTime().UTC(),
|
||||
})
|
||||
}
|
||||
|
||||
sort.Sort(litestream.WALSegmentInfoSlice(infos))
|
||||
|
||||
return litestream.NewWALSegmentInfoSliceIterator(infos), nil
|
||||
}
|
||||
|
||||
// WriteWALSegment writes LZ4 compressed data from rd into a file on disk.
|
||||
func (c *ReplicaClient) WriteWALSegment(ctx context.Context, pos litestream.Pos, rd io.Reader) (info litestream.WALSegmentInfo, err error) {
|
||||
filename, err := c.WALSegmentPath(pos.Generation, pos.Index, pos.Offset)
|
||||
if err != nil {
|
||||
return info, fmt.Errorf("cannot determine wal segment path: %w", err)
|
||||
}
|
||||
|
||||
var fileInfo, dirInfo os.FileInfo
|
||||
if db := c.db(); db != nil {
|
||||
fileInfo, dirInfo = db.FileInfo(), db.DirInfo()
|
||||
}
|
||||
|
||||
// Ensure parent directory exists.
|
||||
if err := internal.MkdirAll(filepath.Dir(filename), dirInfo); err != nil {
|
||||
return info, err
|
||||
}
|
||||
|
||||
// Write WAL segment to temporary file next to destination path.
|
||||
f, err := internal.CreateFile(filename+".tmp", fileInfo)
|
||||
if err != nil {
|
||||
return info, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := io.Copy(f, rd); err != nil {
|
||||
return info, err
|
||||
} else if err := f.Sync(); err != nil {
|
||||
return info, err
|
||||
} else if err := f.Close(); err != nil {
|
||||
return info, err
|
||||
}
|
||||
|
||||
// Build metadata.
|
||||
fi, err := os.Stat(filename + ".tmp")
|
||||
if err != nil {
|
||||
return info, err
|
||||
}
|
||||
info = litestream.WALSegmentInfo{
|
||||
Generation: pos.Generation,
|
||||
Index: pos.Index,
|
||||
Offset: pos.Offset,
|
||||
Size: fi.Size(),
|
||||
CreatedAt: fi.ModTime().UTC(),
|
||||
}
|
||||
|
||||
// Move WAL segment to final path when it has been written & synced to disk.
|
||||
if err := os.Rename(filename+".tmp", filename); err != nil {
|
||||
return info, err
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// WALSegmentReader returns a reader for a section of WAL data at the given position.
|
||||
// Returns os.ErrNotExist if no matching index/offset is found.
|
||||
func (c *ReplicaClient) WALSegmentReader(ctx context.Context, pos litestream.Pos) (io.ReadCloser, error) {
|
||||
filename, err := c.WALSegmentPath(pos.Generation, pos.Index, pos.Offset)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot determine wal segment path: %w", err)
|
||||
}
|
||||
return os.Open(filename)
|
||||
}
|
||||
|
||||
// DeleteWALSegments deletes WAL segments at the given positions.
|
||||
func (c *ReplicaClient) DeleteWALSegments(ctx context.Context, a []litestream.Pos) error {
|
||||
for _, pos := range a {
|
||||
filename, err := c.WALSegmentPath(pos.Generation, pos.Index, pos.Offset)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot determine wal segment path: %w", err)
|
||||
}
|
||||
if err := os.Remove(filename); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
680
file/replica_client_test.go
Normal file
680
file/replica_client_test.go
Normal file
@@ -0,0 +1,680 @@
|
||||
package file_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/benbjohnson/litestream"
|
||||
"github.com/benbjohnson/litestream/file"
|
||||
"github.com/pierrec/lz4/v4"
|
||||
)
|
||||
|
||||
func TestReplicaClient_Path(t *testing.T) {
|
||||
c := file.NewReplicaClient("/foo/bar")
|
||||
if got, want := c.Path(), "/foo/bar"; got != want {
|
||||
t.Fatalf("Path()=%v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplicaClient_Type(t *testing.T) {
|
||||
if got, want := file.NewReplicaClient("").Type(), "file"; got != want {
|
||||
t.Fatalf("Type()=%v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplicaClient_GenerationsDir(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
if got, err := file.NewReplicaClient("/foo").GenerationsDir(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if want := "/foo/generations"; got != want {
|
||||
t.Fatalf("GenerationsDir()=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
t.Run("ErrNoPath", func(t *testing.T) {
|
||||
if _, err := file.NewReplicaClient("").GenerationsDir(); err == nil || err.Error() != `file replica path required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplicaClient_GenerationDir(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
if got, err := file.NewReplicaClient("/foo").GenerationDir("0123456701234567"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if want := "/foo/generations/0123456701234567"; got != want {
|
||||
t.Fatalf("GenerationDir()=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
t.Run("ErrNoPath", func(t *testing.T) {
|
||||
if _, err := file.NewReplicaClient("").GenerationDir("0123456701234567"); err == nil || err.Error() != `file replica path required` {
|
||||
t.Fatalf("expected error: %v", err)
|
||||
}
|
||||
})
|
||||
t.Run("ErrNoGeneration", func(t *testing.T) {
|
||||
if _, err := file.NewReplicaClient("/foo").GenerationDir(""); err == nil || err.Error() != `generation required` {
|
||||
t.Fatalf("expected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplicaClient_SnapshotsDir(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
if got, err := file.NewReplicaClient("/foo").SnapshotsDir("0123456701234567"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if want := "/foo/generations/0123456701234567/snapshots"; got != want {
|
||||
t.Fatalf("SnapshotsDir()=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
t.Run("ErrNoPath", func(t *testing.T) {
|
||||
if _, err := file.NewReplicaClient("").SnapshotsDir("0123456701234567"); err == nil || err.Error() != `file replica path required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
t.Run("ErrNoGeneration", func(t *testing.T) {
|
||||
if _, err := file.NewReplicaClient("/foo").SnapshotsDir(""); err == nil || err.Error() != `generation required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplicaClient_SnapshotPath(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
if got, err := file.NewReplicaClient("/foo").SnapshotPath("0123456701234567", 1000); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if want := "/foo/generations/0123456701234567/snapshots/000003e8.snapshot.lz4"; got != want {
|
||||
t.Fatalf("SnapshotPath()=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
t.Run("ErrNoPath", func(t *testing.T) {
|
||||
if _, err := file.NewReplicaClient("").SnapshotPath("0123456701234567", 1000); err == nil || err.Error() != `file replica path required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
t.Run("ErrNoGeneration", func(t *testing.T) {
|
||||
if _, err := file.NewReplicaClient("/foo").SnapshotPath("", 1000); err == nil || err.Error() != `generation required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplicaClient_WALDir(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
if got, err := file.NewReplicaClient("/foo").WALDir("0123456701234567"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if want := "/foo/generations/0123456701234567/wal"; got != want {
|
||||
t.Fatalf("WALDir()=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
t.Run("ErrNoPath", func(t *testing.T) {
|
||||
if _, err := file.NewReplicaClient("").WALDir("0123456701234567"); err == nil || err.Error() != `file replica path required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
t.Run("ErrNoGeneration", func(t *testing.T) {
|
||||
if _, err := file.NewReplicaClient("/foo").WALDir(""); err == nil || err.Error() != `generation required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplicaClient_WALSegmentPath(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
if got, err := file.NewReplicaClient("/foo").WALSegmentPath("0123456701234567", 1000, 1001); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if want := "/foo/generations/0123456701234567/wal/000003e8_000003e9.wal.lz4"; got != want {
|
||||
t.Fatalf("WALPath()=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
t.Run("ErrNoPath", func(t *testing.T) {
|
||||
if _, err := file.NewReplicaClient("").WALSegmentPath("0123456701234567", 1000, 0); err == nil || err.Error() != `file replica path required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
t.Run("ErrNoGeneration", func(t *testing.T) {
|
||||
if _, err := file.NewReplicaClient("/foo").WALSegmentPath("", 1000, 0); err == nil || err.Error() != `generation required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplicaClient_Generations(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(dir, "generations", "5efbd8d042012dca"), 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := os.MkdirAll(filepath.Join(dir, "generations", "b16ddcf5c697540f"), 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := os.MkdirAll(filepath.Join(dir, "generations", "155fe292f8333c72"), 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c := file.NewReplicaClient(dir)
|
||||
if got, err := c.Generations(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if want := []string{"155fe292f8333c72", "5efbd8d042012dca", "b16ddcf5c697540f"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("Generations()=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("WithInvalidEntries", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(dir, "generations", "5efbd8d042012dca"), 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := os.MkdirAll(filepath.Join(dir, "generations", "not_a_generation"), 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := ioutil.WriteFile(filepath.Join(dir, "generations", "0000000000000000"), nil, 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c := file.NewReplicaClient(dir)
|
||||
if got, err := c.Generations(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if want := []string{"5efbd8d042012dca"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("Generations()=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NoGenerationsDir", func(t *testing.T) {
|
||||
c := file.NewReplicaClient(t.TempDir())
|
||||
if generations, err := c.Generations(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if got, want := len(generations), 0; got != want {
|
||||
t.Fatalf("len(Generations())=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ErrNoPath", func(t *testing.T) {
|
||||
if _, err := file.NewReplicaClient("").Generations(context.Background()); err == nil || err.Error() != `cannot determine generations path: file replica path required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplicaClient_Snapshots(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(dir, "generations", "5efbd8d042012dca", "snapshots"), 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := ioutil.WriteFile(filepath.Join(dir, "generations", "5efbd8d042012dca", "snapshots", "00000001.snapshot.lz4"), nil, 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(dir, "generations", "b16ddcf5c697540f", "snapshots"), 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := ioutil.WriteFile(filepath.Join(dir, "generations", "b16ddcf5c697540f", "snapshots", "00000005.snapshot.lz4"), []byte("x"), 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := ioutil.WriteFile(filepath.Join(dir, "generations", "b16ddcf5c697540f", "snapshots", "0000000a.snapshot.lz4"), []byte("xyz"), 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := ioutil.WriteFile(filepath.Join(dir, "generations", "b16ddcf5c697540f", "snapshots", "not_a_snapshot.snapshot.lz4"), nil, 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c := file.NewReplicaClient(dir)
|
||||
itr, err := c.Snapshots(context.Background(), "b16ddcf5c697540f")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer itr.Close()
|
||||
|
||||
// Read all snapshots into a slice so they can be sorted.
|
||||
a, err := litestream.SliceSnapshotIterator(itr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if got, want := len(a), 2; got != want {
|
||||
t.Fatalf("len=%v, want %v", got, want)
|
||||
}
|
||||
sort.Sort(litestream.SnapshotInfoSlice(a))
|
||||
|
||||
// Verify first snapshot metadata.
|
||||
if got, want := a[0].Generation, "b16ddcf5c697540f"; got != want {
|
||||
t.Fatalf("Generation=%v, want %v", got, want)
|
||||
} else if got, want := a[0].Index, 5; got != want {
|
||||
t.Fatalf("Index=%v, want %v", got, want)
|
||||
} else if got, want := a[0].Size, int64(1); got != want {
|
||||
t.Fatalf("Size=%v, want %v", got, want)
|
||||
} else if a[0].CreatedAt.IsZero() {
|
||||
t.Fatalf("expected CreatedAt")
|
||||
}
|
||||
|
||||
// Verify second snapshot metadata.
|
||||
if got, want := a[1].Generation, "b16ddcf5c697540f"; got != want {
|
||||
t.Fatalf("Generation=%v, want %v", got, want)
|
||||
} else if got, want := a[1].Index, 0xA; got != want {
|
||||
t.Fatalf("Index=%v, want %v", got, want)
|
||||
} else if got, want := a[1].Size, int64(3); got != want {
|
||||
t.Fatalf("Size=%v, want %v", got, want)
|
||||
} else if a[1].CreatedAt.IsZero() {
|
||||
t.Fatalf("expected CreatedAt")
|
||||
}
|
||||
|
||||
// Ensure close is clean.
|
||||
if err := itr.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NoGenerationDir", func(t *testing.T) {
|
||||
c := file.NewReplicaClient(t.TempDir())
|
||||
itr, err := c.Snapshots(context.Background(), "5efbd8d042012dca")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer itr.Close()
|
||||
|
||||
if itr.Next() {
|
||||
t.Fatal("expected no snapshots")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NoSnapshots", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(dir, "generations", "5efbd8d042012dca", "snapshots"), 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c := file.NewReplicaClient(dir)
|
||||
itr, err := c.Snapshots(context.Background(), "5efbd8d042012dca")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer itr.Close()
|
||||
|
||||
if itr.Next() {
|
||||
t.Fatal("expected no snapshots")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ErrNoPath", func(t *testing.T) {
|
||||
if _, err := file.NewReplicaClient("").Snapshots(context.Background(), "b16ddcf5c697540f"); err == nil || err.Error() != `cannot determine snapshot directory: file replica path required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
t.Run("ErrNoGeneration", func(t *testing.T) {
|
||||
if _, err := file.NewReplicaClient(t.TempDir()).Snapshots(context.Background(), ""); err == nil || err.Error() != `cannot determine snapshot directory: generation required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplicaClient_WriteSnapshot(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
data := []byte("foobar")
|
||||
|
||||
c := file.NewReplicaClient(t.TempDir())
|
||||
if _, err := c.WriteSnapshot(context.Background(), "b16ddcf5c697540f", 1000, bytes.NewReader(compress(t, data))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if r, err := c.SnapshotReader(context.Background(), "b16ddcf5c697540f", 1000); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if buf, err := ioutil.ReadAll(lz4.NewReader(r)); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := r.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if got, want := buf, data; !bytes.Equal(got, want) {
|
||||
t.Fatalf("data=%q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ErrNoPath", func(t *testing.T) {
|
||||
if _, err := file.NewReplicaClient("").WriteSnapshot(context.Background(), "b16ddcf5c697540f", 0, nil); err == nil || err.Error() != `cannot determine snapshot path: file replica path required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
t.Run("ErrNoGeneration", func(t *testing.T) {
|
||||
if _, err := file.NewReplicaClient(t.TempDir()).WriteSnapshot(context.Background(), "", 0, nil); err == nil || err.Error() != `cannot determine snapshot path: generation required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplicaClient_SnapshotReader(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(dir, "generations", "5efbd8d042012dca", "snapshots"), 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := ioutil.WriteFile(filepath.Join(dir, "generations", "5efbd8d042012dca", "snapshots", "0000000a.snapshot.lz4"), compress(t, []byte("foo")), 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c := file.NewReplicaClient(dir)
|
||||
r, err := c.SnapshotReader(context.Background(), "5efbd8d042012dca", 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
if buf, err := ioutil.ReadAll(lz4.NewReader(r)); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if got, want := buf, []byte("foo"); !bytes.Equal(got, want) {
|
||||
t.Fatalf("ReadAll=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ErrNotFound", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(dir, "generations", "5efbd8d042012dca", "snapshots"), 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c := file.NewReplicaClient(dir)
|
||||
if _, err := c.SnapshotReader(context.Background(), "5efbd8d042012dca", 1); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected not exist, got %#v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ErrNoPath", func(t *testing.T) {
|
||||
c := file.NewReplicaClient("")
|
||||
if _, err := c.SnapshotReader(context.Background(), "5efbd8d042012dca", 1); err == nil || err.Error() != `cannot determine snapshot path: file replica path required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ErrGeneration", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
c := file.NewReplicaClient(dir)
|
||||
if _, err := c.SnapshotReader(context.Background(), "", 1); err == nil || err.Error() != `cannot determine snapshot path: generation required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplicaClient_WALs(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(dir, "generations", "5efbd8d042012dca", "wal"), 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := ioutil.WriteFile(filepath.Join(dir, "generations", "5efbd8d042012dca", "wal", "00000001_00000000.wal.lz4"), nil, 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(dir, "generations", "b16ddcf5c697540f", "wal"), 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := ioutil.WriteFile(filepath.Join(dir, "generations", "b16ddcf5c697540f", "wal", "00000002_00000000.wal.lz4"), []byte("12345"), 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := ioutil.WriteFile(filepath.Join(dir, "generations", "b16ddcf5c697540f", "wal", "00000002_00000005.wal.lz4"), []byte("67"), 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := ioutil.WriteFile(filepath.Join(dir, "generations", "b16ddcf5c697540f", "wal", "00000003_00000000.wal.lz4"), []byte("xyz"), 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c := file.NewReplicaClient(dir)
|
||||
itr, err := c.WALSegments(context.Background(), "b16ddcf5c697540f")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer itr.Close()
|
||||
|
||||
// Read all WAL segment files into a slice so they can be sorted.
|
||||
a, err := litestream.SliceWALSegmentIterator(itr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if got, want := len(a), 3; got != want {
|
||||
t.Fatalf("len=%v, want %v", got, want)
|
||||
}
|
||||
sort.Sort(litestream.WALSegmentInfoSlice(a))
|
||||
|
||||
// Verify first WAL segment metadata.
|
||||
if got, want := a[0].Generation, "b16ddcf5c697540f"; got != want {
|
||||
t.Fatalf("Generation=%v, want %v", got, want)
|
||||
} else if got, want := a[0].Index, 2; got != want {
|
||||
t.Fatalf("Index=%v, want %v", got, want)
|
||||
} else if got, want := a[0].Offset, int64(0); got != want {
|
||||
t.Fatalf("Offset=%v, want %v", got, want)
|
||||
} else if got, want := a[0].Size, int64(5); got != want {
|
||||
t.Fatalf("Size=%v, want %v", got, want)
|
||||
} else if a[0].CreatedAt.IsZero() {
|
||||
t.Fatalf("expected CreatedAt")
|
||||
}
|
||||
|
||||
// Verify first WAL segment metadata.
|
||||
if got, want := a[1].Generation, "b16ddcf5c697540f"; got != want {
|
||||
t.Fatalf("Generation=%v, want %v", got, want)
|
||||
} else if got, want := a[1].Index, 2; got != want {
|
||||
t.Fatalf("Index=%v, want %v", got, want)
|
||||
} else if got, want := a[1].Offset, int64(5); got != want {
|
||||
t.Fatalf("Offset=%v, want %v", got, want)
|
||||
} else if got, want := a[1].Size, int64(2); got != want {
|
||||
t.Fatalf("Size=%v, want %v", got, want)
|
||||
} else if a[1].CreatedAt.IsZero() {
|
||||
t.Fatalf("expected CreatedAt")
|
||||
}
|
||||
|
||||
// Verify third WAL segment metadata.
|
||||
if got, want := a[2].Generation, "b16ddcf5c697540f"; got != want {
|
||||
t.Fatalf("Generation=%v, want %v", got, want)
|
||||
} else if got, want := a[2].Index, 3; got != want {
|
||||
t.Fatalf("Index=%v, want %v", got, want)
|
||||
} else if got, want := a[2].Offset, int64(0); got != want {
|
||||
t.Fatalf("Offset=%v, want %v", got, want)
|
||||
} else if got, want := a[2].Size, int64(3); got != want {
|
||||
t.Fatalf("Size=%v, want %v", got, want)
|
||||
} else if a[1].CreatedAt.IsZero() {
|
||||
t.Fatalf("expected CreatedAt")
|
||||
}
|
||||
|
||||
// Ensure close is clean.
|
||||
if err := itr.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NoGenerationDir", func(t *testing.T) {
|
||||
c := file.NewReplicaClient(t.TempDir())
|
||||
itr, err := c.WALSegments(context.Background(), "5efbd8d042012dca")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer itr.Close()
|
||||
|
||||
if itr.Next() {
|
||||
t.Fatal("expected no wal files")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NoWALs", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(dir, "generations", "5efbd8d042012dca", "wals"), 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c := file.NewReplicaClient(dir)
|
||||
itr, err := c.WALSegments(context.Background(), "5efbd8d042012dca")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer itr.Close()
|
||||
|
||||
if itr.Next() {
|
||||
t.Fatal("expected no wal files")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ErrNoPath", func(t *testing.T) {
|
||||
if _, err := file.NewReplicaClient("").WALSegments(context.Background(), "b16ddcf5c697540f"); err == nil || err.Error() != `cannot determine wal directory: file replica path required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
t.Run("ErrNoGeneration", func(t *testing.T) {
|
||||
if _, err := file.NewReplicaClient(t.TempDir()).WALSegments(context.Background(), ""); err == nil || err.Error() != `cannot determine wal directory: generation required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplicaClient_WriteWALSegment(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
data := []byte("foobar")
|
||||
|
||||
c := file.NewReplicaClient(t.TempDir())
|
||||
if _, err := c.WriteWALSegment(context.Background(), litestream.Pos{Generation: "b16ddcf5c697540f", Index: 1000, Offset: 2000}, bytes.NewReader(compress(t, data))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if r, err := c.WALSegmentReader(context.Background(), litestream.Pos{Generation: "b16ddcf5c697540f", Index: 1000, Offset: 2000}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if buf, err := ioutil.ReadAll(lz4.NewReader(r)); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := r.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if got, want := buf, data; !bytes.Equal(got, want) {
|
||||
t.Fatalf("data=%q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ErrNoPath", func(t *testing.T) {
|
||||
if _, err := file.NewReplicaClient("").WriteWALSegment(context.Background(), litestream.Pos{Generation: "b16ddcf5c697540f", Index: 0, Offset: 0}, nil); err == nil || err.Error() != `cannot determine wal segment path: file replica path required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
t.Run("ErrNoGeneration", func(t *testing.T) {
|
||||
if _, err := file.NewReplicaClient(t.TempDir()).WriteWALSegment(context.Background(), litestream.Pos{Generation: "", Index: 0, Offset: 0}, nil); err == nil || err.Error() != `cannot determine wal segment path: generation required` {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplicaClient_WALReader(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(dir, "generations", "5efbd8d042012dca", "wal"), 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := ioutil.WriteFile(filepath.Join(dir, "generations", "5efbd8d042012dca", "wal", "0000000a_00000005.wal.lz4"), compress(t, []byte("foobar")), 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c := file.NewReplicaClient(dir)
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
r, err := c.WALSegmentReader(context.Background(), litestream.Pos{Generation: "5efbd8d042012dca", Index: 10, Offset: 5})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
if buf, err := ioutil.ReadAll(lz4.NewReader(r)); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if got, want := buf, []byte("foobar"); !bytes.Equal(got, want) {
|
||||
t.Fatalf("ReadAll=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ErrNotFound", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(dir, "generations", "5efbd8d042012dca", "wal"), 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c := file.NewReplicaClient(dir)
|
||||
if _, err := c.WALSegmentReader(context.Background(), litestream.Pos{Generation: "5efbd8d042012dca", Index: 1, Offset: 0}); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected not exist, got %#v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
func TestReplica_Sync(t *testing.T) {
|
||||
// Ensure replica can successfully sync after DB has sync'd.
|
||||
t.Run("InitialSync", func(t *testing.T) {
|
||||
db, sqldb := MustOpenDBs(t)
|
||||
defer MustCloseDBs(t, db, sqldb)
|
||||
|
||||
r := litestream.NewReplica(db, "", file.NewReplicaClient(t.TempDir()))
|
||||
r.MonitorEnabled = false
|
||||
db.Replicas = []*litestream.Replica{r}
|
||||
|
||||
// Sync database & then sync replica.
|
||||
if err := db.Sync(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := r.Sync(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Ensure posistions match.
|
||||
if want, err := db.Pos(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if got, err := r.Pos(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if got != want {
|
||||
t.Fatalf("Pos()=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
// Ensure replica can successfully sync multiple times.
|
||||
t.Run("MultiSync", func(t *testing.T) {
|
||||
db, sqldb := MustOpenDBs(t)
|
||||
defer MustCloseDBs(t, db, sqldb)
|
||||
|
||||
r := litestream.NewReplica(db, "", file.NewReplicaClient(t.TempDir()))
|
||||
r.MonitorEnabled = false
|
||||
db.Replicas = []*litestream.Replica{r}
|
||||
|
||||
if _, err := sqldb.Exec(`CREATE TABLE foo (bar TEXT);`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Write to the database multiple times and sync after each write.
|
||||
for i, n := 0, db.MinCheckpointPageN*2; i < n; i++ {
|
||||
if _, err := sqldb.Exec(`INSERT INTO foo (bar) VALUES ('baz')`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Sync periodically.
|
||||
if i%100 == 0 || i == n-1 {
|
||||
if err := db.Sync(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if err := r.Sync(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure posistions match.
|
||||
pos, err := db.Pos()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if got, want := pos.Index, 2; got != want {
|
||||
t.Fatalf("Index=%v, want %v", got, want)
|
||||
}
|
||||
|
||||
if want, err := r.Pos(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if got := pos; got != want {
|
||||
t.Fatalf("Pos()=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
// Ensure replica returns an error if there is no generation available from the DB.
|
||||
t.Run("ErrNoGeneration", func(t *testing.T) {
|
||||
db, sqldb := MustOpenDBs(t)
|
||||
defer MustCloseDBs(t, db, sqldb)
|
||||
|
||||
r := litestream.NewReplica(db, "", file.NewReplicaClient(t.TempDir()))
|
||||
r.MonitorEnabled = false
|
||||
db.Replicas = []*litestream.Replica{r}
|
||||
|
||||
if err := r.Sync(context.Background()); err == nil || err.Error() != `no generation, waiting for data` {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
// compress compresses b using LZ4.
|
||||
func compress(tb testing.TB, b []byte) []byte {
|
||||
tb.Helper()
|
||||
|
||||
var buf bytes.Buffer
|
||||
zw := lz4.NewWriter(&buf)
|
||||
if _, err := zw.Write(b); err != nil {
|
||||
tb.Fatal(err)
|
||||
} else if err := zw.Close(); err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
Reference in New Issue
Block a user