Refactor replica system
This commit is contained in:
@@ -30,8 +30,8 @@ func (c *GenerationsCommand) Run(ctx context.Context, args []string) (err error)
|
||||
}
|
||||
|
||||
var db *litestream.DB
|
||||
var r litestream.Replica
|
||||
updatedAt := time.Now()
|
||||
var r *litestream.Replica
|
||||
dbUpdatedAt := time.Now()
|
||||
if isURL(fs.Arg(0)) {
|
||||
if *configPath != "" {
|
||||
return fmt.Errorf("cannot specify a replica URL and the -config flag")
|
||||
@@ -67,14 +67,14 @@ func (c *GenerationsCommand) Run(ctx context.Context, args []string) (err error)
|
||||
}
|
||||
|
||||
// Determine last time database or WAL was updated.
|
||||
if updatedAt, err = db.UpdatedAt(); err != nil {
|
||||
if dbUpdatedAt, err = db.UpdatedAt(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var replicas []litestream.Replica
|
||||
var replicas []*litestream.Replica
|
||||
if r != nil {
|
||||
replicas = []litestream.Replica{r}
|
||||
replicas = []*litestream.Replica{r}
|
||||
} else {
|
||||
replicas = db.Replicas
|
||||
}
|
||||
@@ -85,7 +85,7 @@ func (c *GenerationsCommand) Run(ctx context.Context, args []string) (err error)
|
||||
|
||||
fmt.Fprintln(w, "name\tgeneration\tlag\tstart\tend")
|
||||
for _, r := range replicas {
|
||||
generations, err := r.Generations(ctx)
|
||||
generations, err := r.Client.Generations(ctx)
|
||||
if err != nil {
|
||||
log.Printf("%s: cannot list generations: %s", r.Name(), err)
|
||||
continue
|
||||
@@ -93,18 +93,18 @@ func (c *GenerationsCommand) Run(ctx context.Context, args []string) (err error)
|
||||
|
||||
// Iterate over each generation for the replica.
|
||||
for _, generation := range generations {
|
||||
stats, err := r.GenerationStats(ctx, generation)
|
||||
createdAt, updatedAt, err := r.GenerationTimeBounds(ctx, generation)
|
||||
if err != nil {
|
||||
log.Printf("%s: cannot find generation stats: %s", r.Name(), err)
|
||||
log.Printf("%s: cannot determine generation time bounds: %s", r.Name(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
|
||||
r.Name(),
|
||||
generation,
|
||||
truncateDuration(updatedAt.Sub(stats.UpdatedAt)).String(),
|
||||
stats.CreatedAt.Format(time.RFC3339),
|
||||
stats.UpdatedAt.Format(time.RFC3339),
|
||||
truncateDuration(dbUpdatedAt.Sub(updatedAt)).String(),
|
||||
createdAt.Format(time.RFC3339),
|
||||
updatedAt.Format(time.RFC3339),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/benbjohnson/litestream"
|
||||
"github.com/benbjohnson/litestream/file"
|
||||
"github.com/benbjohnson/litestream/s3"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"gopkg.in/yaml.v2"
|
||||
@@ -272,15 +273,15 @@ func NewDBFromConfig(dbc *DBConfig) (*litestream.DB, error) {
|
||||
|
||||
// ReplicaConfig represents the configuration for a single replica in a database.
|
||||
type ReplicaConfig struct {
|
||||
Type string `yaml:"type"` // "file", "s3"
|
||||
Name string `yaml:"name"` // name of replica, optional.
|
||||
Path string `yaml:"path"`
|
||||
URL string `yaml:"url"`
|
||||
Retention time.Duration `yaml:"retention"`
|
||||
RetentionCheckInterval time.Duration `yaml:"retention-check-interval"`
|
||||
SyncInterval time.Duration `yaml:"sync-interval"` // s3 only
|
||||
SnapshotInterval time.Duration `yaml:"snapshot-interval"`
|
||||
ValidationInterval time.Duration `yaml:"validation-interval"`
|
||||
Type string `yaml:"type"` // "file", "s3"
|
||||
Name string `yaml:"name"` // name of replica, optional.
|
||||
Path string `yaml:"path"`
|
||||
URL string `yaml:"url"`
|
||||
Retention *time.Duration `yaml:"retention"`
|
||||
RetentionCheckInterval *time.Duration `yaml:"retention-check-interval"`
|
||||
SyncInterval *time.Duration `yaml:"sync-interval"`
|
||||
SnapshotInterval *time.Duration `yaml:"snapshot-interval"`
|
||||
ValidationInterval *time.Duration `yaml:"validation-interval"`
|
||||
|
||||
// S3 settings
|
||||
AccessKeyID string `yaml:"access-key-id"`
|
||||
@@ -293,24 +294,51 @@ type ReplicaConfig struct {
|
||||
}
|
||||
|
||||
// NewReplicaFromConfig instantiates a replica for a DB based on a config.
|
||||
func NewReplicaFromConfig(c *ReplicaConfig, db *litestream.DB) (litestream.Replica, error) {
|
||||
func NewReplicaFromConfig(c *ReplicaConfig, db *litestream.DB) (_ *litestream.Replica, err error) {
|
||||
// Ensure user did not specify URL in path.
|
||||
if isURL(c.Path) {
|
||||
return nil, fmt.Errorf("replica path cannot be a url, please use the 'url' field instead: %s", c.Path)
|
||||
}
|
||||
|
||||
// Build replica.
|
||||
r := litestream.NewReplica(db, c.Name)
|
||||
if v := c.Retention; v != nil {
|
||||
r.Retention = *v
|
||||
}
|
||||
if v := c.RetentionCheckInterval; v != nil {
|
||||
r.RetentionCheckInterval = *v
|
||||
}
|
||||
if v := c.SyncInterval; v != nil {
|
||||
r.SyncInterval = *v
|
||||
} else if c.ReplicaType() == "s3" {
|
||||
r.SyncInterval = 10 * time.Second // default s3 to 10s for configs
|
||||
}
|
||||
if v := c.SnapshotInterval; v != nil {
|
||||
r.SnapshotInterval = *v
|
||||
}
|
||||
if v := c.ValidationInterval; v != nil {
|
||||
r.ValidationInterval = *v
|
||||
}
|
||||
|
||||
// Build and set client on replica.
|
||||
switch c.ReplicaType() {
|
||||
case "file":
|
||||
return newFileReplicaFromConfig(c, db)
|
||||
if r.Client, err = newFileReplicaClientFromConfig(c, r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "s3":
|
||||
return newS3ReplicaFromConfig(c, db)
|
||||
if r.Client, err = newS3ReplicaClientFromConfig(c, r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown replica type in config: %q", c.Type)
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// newFileReplicaFromConfig returns a new instance of FileReplica build from config.
|
||||
func newFileReplicaFromConfig(c *ReplicaConfig, db *litestream.DB) (_ *litestream.FileReplica, err error) {
|
||||
// newFileReplicaClientFromConfig returns a new instance of file.ReplicaClient built from config.
|
||||
func newFileReplicaClientFromConfig(c *ReplicaConfig, r *litestream.Replica) (_ *file.ReplicaClient, err error) {
|
||||
// Ensure URL & path are not both specified.
|
||||
if c.URL != "" && c.Path != "" {
|
||||
return nil, fmt.Errorf("cannot specify url & path for file replica")
|
||||
@@ -335,24 +363,13 @@ func newFileReplicaFromConfig(c *ReplicaConfig, db *litestream.DB) (_ *litestrea
|
||||
}
|
||||
|
||||
// Instantiate replica and apply time fields, if set.
|
||||
r := litestream.NewFileReplica(db, c.Name, path)
|
||||
if v := c.Retention; v > 0 {
|
||||
r.Retention = v
|
||||
}
|
||||
if v := c.RetentionCheckInterval; v > 0 {
|
||||
r.RetentionCheckInterval = v
|
||||
}
|
||||
if v := c.SnapshotInterval; v > 0 {
|
||||
r.SnapshotInterval = v
|
||||
}
|
||||
if v := c.ValidationInterval; v > 0 {
|
||||
r.ValidationInterval = v
|
||||
}
|
||||
return r, nil
|
||||
client := file.NewReplicaClient(path)
|
||||
client.Replica = r
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// newS3ReplicaFromConfig returns a new instance of S3Replica build from config.
|
||||
func newS3ReplicaFromConfig(c *ReplicaConfig, db *litestream.DB) (_ *s3.Replica, err error) {
|
||||
// newS3ReplicaClientFromConfig returns a new instance of s3.ReplicaClient built from config.
|
||||
func newS3ReplicaClientFromConfig(c *ReplicaConfig, r *litestream.Replica) (_ *s3.ReplicaClient, err error) {
|
||||
// Ensure URL & constituent parts are not both specified.
|
||||
if c.URL != "" && c.Path != "" {
|
||||
return nil, fmt.Errorf("cannot specify url & path for s3 replica")
|
||||
@@ -402,32 +419,16 @@ func newS3ReplicaFromConfig(c *ReplicaConfig, db *litestream.DB) (_ *s3.Replica,
|
||||
}
|
||||
|
||||
// Build replica.
|
||||
r := s3.NewReplica(db, c.Name)
|
||||
r.AccessKeyID = c.AccessKeyID
|
||||
r.SecretAccessKey = c.SecretAccessKey
|
||||
r.Bucket = bucket
|
||||
r.Path = path
|
||||
r.Region = region
|
||||
r.Endpoint = endpoint
|
||||
r.ForcePathStyle = forcePathStyle
|
||||
r.SkipVerify = skipVerify
|
||||
|
||||
if v := c.Retention; v > 0 {
|
||||
r.Retention = v
|
||||
}
|
||||
if v := c.RetentionCheckInterval; v > 0 {
|
||||
r.RetentionCheckInterval = v
|
||||
}
|
||||
if v := c.SyncInterval; v > 0 {
|
||||
r.SyncInterval = v
|
||||
}
|
||||
if v := c.SnapshotInterval; v > 0 {
|
||||
r.SnapshotInterval = v
|
||||
}
|
||||
if v := c.ValidationInterval; v > 0 {
|
||||
r.ValidationInterval = v
|
||||
}
|
||||
return r, nil
|
||||
client := s3.NewReplicaClient()
|
||||
client.AccessKeyID = c.AccessKeyID
|
||||
client.SecretAccessKey = c.SecretAccessKey
|
||||
client.Bucket = bucket
|
||||
client.Path = path
|
||||
client.Region = region
|
||||
client.Endpoint = endpoint
|
||||
client.ForcePathStyle = forcePathStyle
|
||||
client.SkipVerify = skipVerify
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// applyLitestreamEnv copies "LITESTREAM" prefixed environment variables to
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/benbjohnson/litestream"
|
||||
main "github.com/benbjohnson/litestream/cmd/litestream"
|
||||
"github.com/benbjohnson/litestream/file"
|
||||
"github.com/benbjohnson/litestream/s3"
|
||||
)
|
||||
|
||||
@@ -96,9 +96,9 @@ func TestNewFileReplicaFromConfig(t *testing.T) {
|
||||
r, err := main.NewReplicaFromConfig(&main.ReplicaConfig{Path: "/foo"}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if r, ok := r.(*litestream.FileReplica); !ok {
|
||||
} else if client, ok := r.Client.(*file.ReplicaClient); !ok {
|
||||
t.Fatal("unexpected replica type")
|
||||
} else if got, want := r.Path(), "/foo"; got != want {
|
||||
} else if got, want := client.Path(), "/foo"; got != want {
|
||||
t.Fatalf("Path=%s, want %s", got, want)
|
||||
}
|
||||
}
|
||||
@@ -108,17 +108,17 @@ func TestNewS3ReplicaFromConfig(t *testing.T) {
|
||||
r, err := main.NewReplicaFromConfig(&main.ReplicaConfig{URL: "s3://foo/bar"}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if r, ok := r.(*s3.Replica); !ok {
|
||||
} else if client, ok := r.Client.(*s3.ReplicaClient); !ok {
|
||||
t.Fatal("unexpected replica type")
|
||||
} else if got, want := r.Bucket, "foo"; got != want {
|
||||
} else if got, want := client.Bucket, "foo"; got != want {
|
||||
t.Fatalf("Bucket=%s, want %s", got, want)
|
||||
} else if got, want := r.Path, "bar"; got != want {
|
||||
} else if got, want := client.Path, "bar"; got != want {
|
||||
t.Fatalf("Path=%s, want %s", got, want)
|
||||
} else if got, want := r.Region, ""; got != want {
|
||||
} else if got, want := client.Region, ""; got != want {
|
||||
t.Fatalf("Region=%s, want %s", got, want)
|
||||
} else if got, want := r.Endpoint, ""; got != want {
|
||||
} else if got, want := client.Endpoint, ""; got != want {
|
||||
t.Fatalf("Endpoint=%s, want %s", got, want)
|
||||
} else if got, want := r.ForcePathStyle, false; got != want {
|
||||
} else if got, want := client.ForcePathStyle, false; got != want {
|
||||
t.Fatalf("ForcePathStyle=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
@@ -127,17 +127,17 @@ func TestNewS3ReplicaFromConfig(t *testing.T) {
|
||||
r, err := main.NewReplicaFromConfig(&main.ReplicaConfig{URL: "s3://foo.localhost:9000/bar"}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if r, ok := r.(*s3.Replica); !ok {
|
||||
} else if client, ok := r.Client.(*s3.ReplicaClient); !ok {
|
||||
t.Fatal("unexpected replica type")
|
||||
} else if got, want := r.Bucket, "foo"; got != want {
|
||||
} else if got, want := client.Bucket, "foo"; got != want {
|
||||
t.Fatalf("Bucket=%s, want %s", got, want)
|
||||
} else if got, want := r.Path, "bar"; got != want {
|
||||
} else if got, want := client.Path, "bar"; got != want {
|
||||
t.Fatalf("Path=%s, want %s", got, want)
|
||||
} else if got, want := r.Region, "us-east-1"; got != want {
|
||||
} else if got, want := client.Region, "us-east-1"; got != want {
|
||||
t.Fatalf("Region=%s, want %s", got, want)
|
||||
} else if got, want := r.Endpoint, "http://localhost:9000"; got != want {
|
||||
} else if got, want := client.Endpoint, "http://localhost:9000"; got != want {
|
||||
t.Fatalf("Endpoint=%s, want %s", got, want)
|
||||
} else if got, want := r.ForcePathStyle, true; got != want {
|
||||
} else if got, want := client.ForcePathStyle, true; got != want {
|
||||
t.Fatalf("ForcePathStyle=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
@@ -146,17 +146,17 @@ func TestNewS3ReplicaFromConfig(t *testing.T) {
|
||||
r, err := main.NewReplicaFromConfig(&main.ReplicaConfig{URL: "s3://foo.s3.us-west-000.backblazeb2.com/bar"}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if r, ok := r.(*s3.Replica); !ok {
|
||||
} else if client, ok := r.Client.(*s3.ReplicaClient); !ok {
|
||||
t.Fatal("unexpected replica type")
|
||||
} else if got, want := r.Bucket, "foo"; got != want {
|
||||
} else if got, want := client.Bucket, "foo"; got != want {
|
||||
t.Fatalf("Bucket=%s, want %s", got, want)
|
||||
} else if got, want := r.Path, "bar"; got != want {
|
||||
} else if got, want := client.Path, "bar"; got != want {
|
||||
t.Fatalf("Path=%s, want %s", got, want)
|
||||
} else if got, want := r.Region, "us-west-000"; got != want {
|
||||
} else if got, want := client.Region, "us-west-000"; got != want {
|
||||
t.Fatalf("Region=%s, want %s", got, want)
|
||||
} else if got, want := r.Endpoint, "https://s3.us-west-000.backblazeb2.com"; got != want {
|
||||
} else if got, want := client.Endpoint, "https://s3.us-west-000.backblazeb2.com"; got != want {
|
||||
t.Fatalf("Endpoint=%s, want %s", got, want)
|
||||
} else if got, want := r.ForcePathStyle, true; got != want {
|
||||
} else if got, want := client.ForcePathStyle, true; got != want {
|
||||
t.Fatalf("ForcePathStyle=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
@@ -165,17 +165,17 @@ func TestNewS3ReplicaFromConfig(t *testing.T) {
|
||||
r, err := main.NewReplicaFromConfig(&main.ReplicaConfig{URL: "s3://foo.storage.googleapis.com/bar"}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if r, ok := r.(*s3.Replica); !ok {
|
||||
} else if client, ok := r.Client.(*s3.ReplicaClient); !ok {
|
||||
t.Fatal("unexpected replica type")
|
||||
} else if got, want := r.Bucket, "foo"; got != want {
|
||||
} else if got, want := client.Bucket, "foo"; got != want {
|
||||
t.Fatalf("Bucket=%s, want %s", got, want)
|
||||
} else if got, want := r.Path, "bar"; got != want {
|
||||
} else if got, want := client.Path, "bar"; got != want {
|
||||
t.Fatalf("Path=%s, want %s", got, want)
|
||||
} else if got, want := r.Region, "us-east-1"; got != want {
|
||||
} else if got, want := client.Region, "us-east-1"; got != want {
|
||||
t.Fatalf("Region=%s, want %s", got, want)
|
||||
} else if got, want := r.Endpoint, "https://storage.googleapis.com"; got != want {
|
||||
} else if got, want := client.Endpoint, "https://storage.googleapis.com"; got != want {
|
||||
t.Fatalf("Endpoint=%s, want %s", got, want)
|
||||
} else if got, want := r.ForcePathStyle, true; got != want {
|
||||
} else if got, want := client.ForcePathStyle, true; got != want {
|
||||
t.Fatalf("ForcePathStyle=%v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/benbjohnson/litestream"
|
||||
"github.com/benbjohnson/litestream/file"
|
||||
"github.com/benbjohnson/litestream/s3"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
@@ -48,9 +48,10 @@ func (c *ReplicateCommand) ParseFlags(ctx context.Context, args []string) (err e
|
||||
|
||||
dbConfig := &DBConfig{Path: fs.Arg(0)}
|
||||
for _, u := range fs.Args()[1:] {
|
||||
syncInterval := litestream.DefaultSyncInterval
|
||||
dbConfig.Replicas = append(dbConfig.Replicas, &ReplicaConfig{
|
||||
URL: u,
|
||||
SyncInterval: 1 * time.Second,
|
||||
SyncInterval: &syncInterval,
|
||||
})
|
||||
}
|
||||
c.Config.DBs = []*DBConfig{dbConfig}
|
||||
@@ -102,13 +103,13 @@ func (c *ReplicateCommand) Run(ctx context.Context) (err error) {
|
||||
for _, db := range c.DBs {
|
||||
log.Printf("initialized db: %s", db.Path())
|
||||
for _, r := range db.Replicas {
|
||||
switch r := r.(type) {
|
||||
case *litestream.FileReplica:
|
||||
log.Printf("replicating to: name=%q type=%q path=%q", r.Name(), r.Type(), r.Path())
|
||||
case *s3.Replica:
|
||||
log.Printf("replicating to: name=%q type=%q bucket=%q path=%q region=%q endpoint=%q sync-interval=%s", r.Name(), r.Type(), r.Bucket, r.Path, r.Region, r.Endpoint, r.SyncInterval)
|
||||
switch client := r.Client.(type) {
|
||||
case *file.ReplicaClient:
|
||||
log.Printf("replicating to: name=%q type=%q path=%q", r.Name(), client.Type(), client.Path())
|
||||
case *s3.ReplicaClient:
|
||||
log.Printf("replicating to: name=%q type=%q bucket=%q path=%q region=%q endpoint=%q sync-interval=%s", r.Name(), client.Type(), client.Bucket, client.Path, client.Region, client.Endpoint, r.SyncInterval)
|
||||
default:
|
||||
log.Printf("replicating to: name=%q type=%q", r.Name(), r.Type())
|
||||
log.Printf("replicating to: name=%q type=%q", r.Name(), client.Type())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ func (c *RestoreCommand) Run(ctx context.Context, args []string) (err error) {
|
||||
}
|
||||
|
||||
// Determine replica & generation to restore from.
|
||||
var r litestream.Replica
|
||||
var r *litestream.Replica
|
||||
if isURL(fs.Arg(0)) {
|
||||
if *configPath != "" {
|
||||
return fmt.Errorf("cannot specify a replica URL and the -config flag")
|
||||
@@ -80,21 +80,25 @@ func (c *RestoreCommand) Run(ctx context.Context, args []string) (err error) {
|
||||
return fmt.Errorf("no matching backups found")
|
||||
}
|
||||
|
||||
return litestream.RestoreReplica(ctx, r, opt)
|
||||
return r.Restore(ctx, opt)
|
||||
}
|
||||
|
||||
// loadFromURL creates a replica & updates the restore options from a replica URL.
|
||||
func (c *RestoreCommand) loadFromURL(ctx context.Context, replicaURL string, opt *litestream.RestoreOptions) (litestream.Replica, error) {
|
||||
r, err := NewReplicaFromConfig(&ReplicaConfig{URL: replicaURL}, nil)
|
||||
func (c *RestoreCommand) loadFromURL(ctx context.Context, replicaURL string, opt *litestream.RestoreOptions) (*litestream.Replica, error) {
|
||||
syncInterval := litestream.DefaultSyncInterval
|
||||
r, err := NewReplicaFromConfig(&ReplicaConfig{
|
||||
URL: replicaURL,
|
||||
SyncInterval: &syncInterval,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opt.Generation, _, err = litestream.CalcReplicaRestoreTarget(ctx, r, *opt)
|
||||
opt.Generation, _, err = r.CalcRestoreTarget(ctx, *opt)
|
||||
return r, err
|
||||
}
|
||||
|
||||
// loadFromConfig returns a replica & updates the restore options from a DB reference.
|
||||
func (c *RestoreCommand) loadFromConfig(ctx context.Context, dbPath, configPath string, expandEnv bool, opt *litestream.RestoreOptions) (litestream.Replica, error) {
|
||||
func (c *RestoreCommand) loadFromConfig(ctx context.Context, dbPath, configPath string, expandEnv bool, opt *litestream.RestoreOptions) (*litestream.Replica, error) {
|
||||
// Load configuration.
|
||||
config, err := ReadConfigFile(configPath, expandEnv)
|
||||
if err != nil {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
@@ -29,7 +30,7 @@ func (c *SnapshotsCommand) Run(ctx context.Context, args []string) (err error) {
|
||||
}
|
||||
|
||||
var db *litestream.DB
|
||||
var r litestream.Replica
|
||||
var r *litestream.Replica
|
||||
if isURL(fs.Arg(0)) {
|
||||
if *configPath != "" {
|
||||
return fmt.Errorf("cannot specify a replica URL and the -config flag")
|
||||
@@ -66,15 +67,11 @@ func (c *SnapshotsCommand) Run(ctx context.Context, args []string) (err error) {
|
||||
}
|
||||
|
||||
// Find snapshots by db or replica.
|
||||
var infos []*litestream.SnapshotInfo
|
||||
var replicas []*litestream.Replica
|
||||
if r != nil {
|
||||
if infos, err = r.Snapshots(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
replicas = []*litestream.Replica{r}
|
||||
} else {
|
||||
if infos, err = db.Snapshots(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
replicas = db.Replicas
|
||||
}
|
||||
|
||||
// List all snapshots.
|
||||
@@ -82,14 +79,21 @@ func (c *SnapshotsCommand) Run(ctx context.Context, args []string) (err error) {
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintln(w, "replica\tgeneration\tindex\tsize\tcreated")
|
||||
for _, info := range infos {
|
||||
fmt.Fprintf(w, "%s\t%s\t%d\t%d\t%s\n",
|
||||
info.Replica,
|
||||
info.Generation,
|
||||
info.Index,
|
||||
info.Size,
|
||||
info.CreatedAt.Format(time.RFC3339),
|
||||
)
|
||||
for _, r := range replicas {
|
||||
infos, err := r.Snapshots(ctx)
|
||||
if err != nil {
|
||||
log.Printf("cannot determine snapshots: %s", err)
|
||||
continue
|
||||
}
|
||||
for _, info := range infos {
|
||||
fmt.Fprintf(w, "%s\t%s\t%d\t%d\t%s\n",
|
||||
r.Name(),
|
||||
info.Generation,
|
||||
info.Index,
|
||||
info.Size,
|
||||
info.CreatedAt.Format(time.RFC3339),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
@@ -30,7 +31,7 @@ func (c *WALCommand) Run(ctx context.Context, args []string) (err error) {
|
||||
}
|
||||
|
||||
var db *litestream.DB
|
||||
var r litestream.Replica
|
||||
var r *litestream.Replica
|
||||
if isURL(fs.Arg(0)) {
|
||||
if *configPath != "" {
|
||||
return fmt.Errorf("cannot specify a replica URL and the -config flag")
|
||||
@@ -67,15 +68,11 @@ func (c *WALCommand) Run(ctx context.Context, args []string) (err error) {
|
||||
}
|
||||
|
||||
// Find WAL files by db or replica.
|
||||
var infos []*litestream.WALInfo
|
||||
var replicas []*litestream.Replica
|
||||
if r != nil {
|
||||
if infos, err = r.WALs(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
replicas = []*litestream.Replica{r}
|
||||
} else {
|
||||
if infos, err = db.WALs(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
replicas = db.Replicas
|
||||
}
|
||||
|
||||
// List all WAL files.
|
||||
@@ -83,19 +80,43 @@ func (c *WALCommand) Run(ctx context.Context, args []string) (err error) {
|
||||
defer w.Flush()
|
||||
|
||||
fmt.Fprintln(w, "replica\tgeneration\tindex\toffset\tsize\tcreated")
|
||||
for _, info := range infos {
|
||||
if *generation != "" && info.Generation != *generation {
|
||||
continue
|
||||
for _, r := range replicas {
|
||||
var generations []string
|
||||
if *generation != "" {
|
||||
generations = []string{*generation}
|
||||
} else {
|
||||
if generations, err = r.Client.Generations(ctx); err != nil {
|
||||
log.Printf("%s: cannot determine generations: %s", r.Name(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%s\t%s\t%d\t%d\t%d\t%s\n",
|
||||
info.Replica,
|
||||
info.Generation,
|
||||
info.Index,
|
||||
info.Offset,
|
||||
info.Size,
|
||||
info.CreatedAt.Format(time.RFC3339),
|
||||
)
|
||||
for _, generation := range generations {
|
||||
if err := func() error {
|
||||
itr, err := r.Client.WALSegments(ctx, generation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer itr.Close()
|
||||
|
||||
for itr.Next() {
|
||||
info := itr.WALSegment()
|
||||
|
||||
fmt.Fprintf(w, "%s\t%s\t%d\t%d\t%d\t%s\n",
|
||||
r.Name(),
|
||||
info.Generation,
|
||||
info.Index,
|
||||
info.Offset,
|
||||
info.Size,
|
||||
info.CreatedAt.Format(time.RFC3339),
|
||||
)
|
||||
}
|
||||
return itr.Close()
|
||||
}(); err != nil {
|
||||
log.Printf("%s: cannot fetch wal segments: %s", r.Name(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -104,7 +125,7 @@ func (c *WALCommand) Run(ctx context.Context, args []string) (err error) {
|
||||
// Usage prints the help screen to STDOUT.
|
||||
func (c *WALCommand) Usage() {
|
||||
fmt.Printf(`
|
||||
The wal command lists all wal files available for a database.
|
||||
The wal command lists all wal segments available for a database.
|
||||
|
||||
Usage:
|
||||
|
||||
@@ -129,13 +150,13 @@ Arguments:
|
||||
|
||||
Examples:
|
||||
|
||||
# List all WAL files for a database.
|
||||
# List all WAL segments for a database.
|
||||
$ litestream wal /path/to/db
|
||||
|
||||
# List all WAL files on S3 for a specific generation.
|
||||
# List all WAL segments on S3 for a specific generation.
|
||||
$ litestream wal -replica s3 -generation xxxxxxxx /path/to/db
|
||||
|
||||
# List all WAL files for replica URL.
|
||||
# List all WAL segments for replica URL.
|
||||
$ litestream wal s3://mybkt/db
|
||||
|
||||
`[1:],
|
||||
|
||||
Reference in New Issue
Block a user