Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2658695a35 | |||
| 1ac1635b3d | |||
| 4a2806f6d1 | |||
| 0a264b9b41 | |||
| a3379d6785 | |||
| 6260b75031 | |||
| 3ffb69bfb0 | |||
| 1da8f848f2 | |||
| 6ed4626df9 | |||
| c07f9ec62e | |||
| fd5b0e6831 | |||
| a4cea1b4f3 | |||
| c0b52b92d5 | |||
| 12018b3685 | |||
| a91fb873e4 | |||
| a6aa610165 | |||
| 6c87bdb9fb | |||
| b9250a41a2 | |||
| a967986a18 | |||
| c743c7b03f | |||
| a9c61b4b0b | |||
| 206d359dcd | |||
| 2d8c3692ec | |||
| 7769263c2e | |||
| 89f7c76c4e | |||
| c7771f98f5 | |||
| 299f1c906e | |||
| d3f68ae4e8 | |||
| 193abbe6b1 | |||
| 02b215128e |
+15
-13
@@ -24,7 +24,12 @@ commands:
|
||||
|
||||
apt-update-and-install-common-deps:
|
||||
steps:
|
||||
- run: sudo apt update && sudo apt install gawk make
|
||||
- run: sudo apt-get update
|
||||
- run: sudo apt-get install -y gawk make
|
||||
# CircleCI doesn't update its cimg/go images.
|
||||
# So, need to update manually to get up-to-date trust chains.
|
||||
# The need for this was required for cimg/go:1.12, but let's future proof this here and now.
|
||||
- run: sudo apt-get install -y git ca-certificates
|
||||
|
||||
restore-cache-gomod:
|
||||
steps:
|
||||
@@ -150,7 +155,7 @@ parameters:
|
||||
|
||||
release_docker_baseimage_tag:
|
||||
type: string
|
||||
default: "1.17"
|
||||
default: "1.19"
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
@@ -160,19 +165,19 @@ workflows:
|
||||
jobs:
|
||||
- quickcheck-docs
|
||||
- quickcheck-go: &quickcheck-go-smoketest
|
||||
name: quickcheck-go-amd64-linux-1.17
|
||||
goversion: &latest-go-release "1.17"
|
||||
name: quickcheck-go-amd64-linux-1.19
|
||||
goversion: &latest-go-release "1.19"
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
- test-go-on-latest-go-release:
|
||||
goversion: *latest-go-release
|
||||
- quickcheck-go:
|
||||
requires:
|
||||
- quickcheck-go-amd64-linux-1.17 #quickcheck-go-smoketest.name
|
||||
- quickcheck-go-amd64-linux-1.19 #quickcheck-go-smoketest.name
|
||||
matrix: &quickcheck-go-matrix
|
||||
alias: quickcheck-go-matrix
|
||||
parameters:
|
||||
goversion: [*latest-go-release, "1.12"]
|
||||
goversion: [*latest-go-release, "1.18"]
|
||||
goos: ["linux", "freebsd"]
|
||||
goarch: ["amd64", "arm64"]
|
||||
exclude:
|
||||
@@ -180,10 +185,6 @@ workflows:
|
||||
- goversion: *latest-go-release
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
# not supported by Go 1.12
|
||||
- goversion: "1.12"
|
||||
goos: freebsd
|
||||
goarch: arm64
|
||||
|
||||
release:
|
||||
when: << pipeline.parameters.do_release >>
|
||||
@@ -249,7 +250,7 @@ jobs:
|
||||
goarch:
|
||||
type: string
|
||||
docker:
|
||||
- image: circleci/golang:<<parameters.goversion>>
|
||||
- image: cimg/go:<<parameters.goversion>>
|
||||
environment:
|
||||
GOOS: <<parameters.goos>>
|
||||
GOARCH: <<parameters.goarch>>
|
||||
@@ -257,12 +258,13 @@ jobs:
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- install-godep
|
||||
|
||||
- restore-cache-gomod
|
||||
- run: go mod download
|
||||
- run: cd build && go mod download
|
||||
- save-cache-gomod
|
||||
|
||||
- install-godep
|
||||
- run: make formatcheck
|
||||
- run: make generate-platform-test-list
|
||||
- run: make zrepl-bin test-platform-bin
|
||||
@@ -286,7 +288,7 @@ jobs:
|
||||
goversion:
|
||||
type: string
|
||||
docker:
|
||||
- image: circleci/golang:<<parameters.goversion>>
|
||||
- image: cimg/go:<<parameters.goversion>>
|
||||
steps:
|
||||
- checkout
|
||||
- restore-cache-gomod
|
||||
|
||||
@@ -7,4 +7,10 @@ issues:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- errcheck
|
||||
# Disable staticcheck 'Empty body in an if or else branch' as it's useful
|
||||
# to put a comment into an empty else-clause that explains why whatever
|
||||
# is done in the if-caluse is not necessary if the condition is false.
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: "SA9003:"
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ GO_BUILDFLAGS := $(GO_MOD_READONLY) $(GO_EXTRA_BUILDFLAGS)
|
||||
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
|
||||
GOLANGCI_LINT := golangci-lint
|
||||
GOCOVMERGE := gocovmerge
|
||||
RELEASE_DOCKER_BASEIMAGE_TAG ?= 1.17
|
||||
RELEASE_DOCKER_BASEIMAGE_TAG ?= 1.19
|
||||
RELEASE_DOCKER_BASEIMAGE ?= golang:$(RELEASE_DOCKER_BASEIMAGE_TAG)
|
||||
|
||||
ifneq ($(GOARM),)
|
||||
@@ -55,7 +55,7 @@ release: clean
|
||||
$(MAKE) wrapup-and-checksum
|
||||
$(MAKE) check-git-clean
|
||||
ifeq (SIGN, 1)
|
||||
$(make) sign
|
||||
$(MAKE) sign
|
||||
endif
|
||||
@echo "ZREPL RELEASE ARTIFACTS AVAILABLE IN artifacts/release"
|
||||
|
||||
@@ -134,7 +134,13 @@ endif
|
||||
|
||||
deb-docker:
|
||||
docker build -t zrepl_debian_pkg --pull -f packaging/deb/Dockerfile .
|
||||
# Use a small open file limit to make fakeroot work. If we don't
|
||||
# specify it, docker daemon will use its file limit. I don't know
|
||||
# what changed (Docker, its systemd service, its Go version). But I
|
||||
# observed fakeroot iterating close(i) up to i > 1000000, which costs
|
||||
# a good amount of CPU time and makes the build slow.
|
||||
docker run --rm -i -v $(CURDIR):/build/src -u $$(id -u):$$(id -g) \
|
||||
--ulimit nofile=1024:1024 \
|
||||
zrepl_debian_pkg \
|
||||
make deb GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
|
||||
|
||||
@@ -333,12 +339,12 @@ $(ARTIFACTDIR)/go_env.txt:
|
||||
|
||||
docs: $(ARTIFACTDIR)/docs
|
||||
# https://www.sphinx-doc.org/en/master/man/sphinx-build.html
|
||||
make -C docs \
|
||||
$(MAKE) -C docs \
|
||||
html \
|
||||
BUILDDIR=../artifacts/docs \
|
||||
SPHINXOPTS="-W --keep-going -n"
|
||||
|
||||
docs-clean:
|
||||
make -C docs \
|
||||
$(MAKE) -C docs \
|
||||
clean \
|
||||
BUILDDIR=../artifacts/docs
|
||||
|
||||
+3
-6
@@ -4,13 +4,10 @@ go 1.12
|
||||
|
||||
require (
|
||||
github.com/alvaroloes/enumer v1.1.1
|
||||
github.com/golangci/golangci-lint v1.35.2
|
||||
github.com/golangci/misspell v0.3.4 // indirect
|
||||
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 // indirect
|
||||
github.com/spf13/afero v1.2.2 // indirect
|
||||
github.com/golangci/golangci-lint v1.50.1
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad
|
||||
golang.org/x/tools v0.0.0-20210105210202-9ed45478a130
|
||||
golang.org/x/tools v0.2.0
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 // indirect
|
||||
google.golang.org/protobuf v1.25.0
|
||||
google.golang.org/protobuf v1.28.0
|
||||
)
|
||||
|
||||
+1128
File diff suppressed because it is too large
Load Diff
@@ -129,7 +129,7 @@ func interactive(c Client, flag statusFlags) error {
|
||||
FSFilter: func(_ string) bool { return true },
|
||||
DetailViewWidth: 100,
|
||||
DetailViewWrap: false,
|
||||
ShortKeybindingOverview: "[::b]Q[::-] quit [::b]<TAB>[::-] switch panes [::b]Shift+M[::-] toggle navbar [::b]Shift+S[::-] signal job [::b]</>[::-] filter filesystems",
|
||||
ShortKeybindingOverview: "[::b]Q[::-] quit [::b]<TAB>[::-] switch panes [::b]W[::-] wrap lines [::b]Shift+M[::-] toggle navbar [::b]Shift+S[::-] signal job [::b]</>[::-] filter filesystems",
|
||||
}
|
||||
paramsMtx := &sync.Mutex{}
|
||||
var redraw func()
|
||||
|
||||
@@ -273,9 +273,9 @@ func printFilesystemStatus(t *stringbuilder.B, rep *report.FilesystemReport, max
|
||||
attribs = append(attribs, "resumed")
|
||||
}
|
||||
|
||||
attribs = append(attribs, fmt.Sprintf("encrypted=%s", nextStep.Info.Encrypted))
|
||||
|
||||
next += fmt.Sprintf(" (%s)", strings.Join(attribs, ", "))
|
||||
if len(attribs) > 0 {
|
||||
next += fmt.Sprintf(" (%s)", strings.Join(attribs, ", "))
|
||||
}
|
||||
} else {
|
||||
next = "" // individual FSes may still be in planning state
|
||||
}
|
||||
@@ -547,10 +547,20 @@ func renderPrunerReport(t *stringbuilder.B, r *pruner.Report, fsfilter FilterFun
|
||||
|
||||
func renderSnapperReport(t *stringbuilder.B, r *snapper.Report, fsfilter FilterFunc) {
|
||||
if r == nil {
|
||||
t.Printf("<snapshot type does not have a report>\n")
|
||||
t.Printf("<no snapshotting report available>\n")
|
||||
return
|
||||
}
|
||||
t.Printf("Type: %s\n", r.Type)
|
||||
if r.Periodic != nil {
|
||||
renderSnapperReportPeriodic(t, r.Periodic, fsfilter)
|
||||
} else if r.Cron != nil {
|
||||
renderSnapperReportCron(t, r.Cron, fsfilter)
|
||||
} else {
|
||||
t.Printf("<no details available>")
|
||||
}
|
||||
}
|
||||
|
||||
func renderSnapperReportPeriodic(t *stringbuilder.B, r *snapper.PeriodicReport, fsfilter FilterFunc) {
|
||||
t.Printf("Status: %s", r.State)
|
||||
t.Newline()
|
||||
|
||||
@@ -561,8 +571,25 @@ func renderSnapperReport(t *stringbuilder.B, r *snapper.Report, fsfilter FilterF
|
||||
t.Printf("Sleep until: %s\n", r.SleepUntil)
|
||||
}
|
||||
|
||||
sort.Slice(r.Progress, func(i, j int) bool {
|
||||
return strings.Compare(r.Progress[i].Path, r.Progress[j].Path) == -1
|
||||
renderSnapperPlanReportFilesystem(t, r.Progress, fsfilter)
|
||||
}
|
||||
|
||||
func renderSnapperReportCron(t *stringbuilder.B, r *snapper.CronReport, fsfilter FilterFunc) {
|
||||
t.Printf("State: %s\n", r.State)
|
||||
|
||||
now := time.Now()
|
||||
if r.WakeupTime.After(now) {
|
||||
t.Printf("Sleep until: %s (%s remaining)\n", r.WakeupTime, r.WakeupTime.Sub(now).Round(time.Second))
|
||||
} else {
|
||||
t.Printf("Started: %s (lasting %s)\n", r.WakeupTime, now.Sub(r.WakeupTime).Round(time.Second))
|
||||
}
|
||||
|
||||
renderSnapperPlanReportFilesystem(t, r.Progress, fsfilter)
|
||||
}
|
||||
|
||||
func renderSnapperPlanReportFilesystem(t *stringbuilder.B, fss []*snapper.ReportFilesystem, fsfilter FilterFunc) {
|
||||
sort.Slice(fss, func(i, j int) bool {
|
||||
return strings.Compare(fss[i].Path, fss[j].Path) == -1
|
||||
})
|
||||
|
||||
dur := func(d time.Duration) string {
|
||||
@@ -575,8 +602,8 @@ func renderSnapperReport(t *stringbuilder.B, r *snapper.Report, fsfilter FilterF
|
||||
var widths struct {
|
||||
path, state, duration int
|
||||
}
|
||||
rows := make([]*row, 0, len(r.Progress))
|
||||
for _, fs := range r.Progress {
|
||||
rows := make([]*row, 0, len(fss))
|
||||
for _, fs := range fss {
|
||||
if !fsfilter(fs.Path) {
|
||||
continue
|
||||
}
|
||||
@@ -619,9 +646,11 @@ func renderSnapperReport(t *stringbuilder.B, r *snapper.Report, fsfilter FilterF
|
||||
t.Printf("%s %s %s", path, state, duration)
|
||||
t.PrintfDrawIndentedAndWrappedIfMultiline(" %s", r.remainder)
|
||||
if r.hookReport != "" {
|
||||
t.PrintfDrawIndentedAndWrappedIfMultiline("%s", r.hookReport)
|
||||
t.AddIndent(1)
|
||||
t.Newline()
|
||||
t.Printf("%s", r.hookReport)
|
||||
t.AddIndent(-1)
|
||||
}
|
||||
t.Newline()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ func (f *zabsFilterFlags) registerZabsFilterFlags(s *pflag.FlagSet, verb string)
|
||||
for v := range endpoint.AbstractionTypesAll {
|
||||
variants = append(variants, string(v))
|
||||
}
|
||||
variants = sort.StringSlice(variants)
|
||||
sort.Strings(variants)
|
||||
variantsJoined := strings.Join(variants, "|")
|
||||
s.Var(&f.Types, "type", fmt.Sprintf("only %s holds of the specified type [default: all] [comma-separated list of %s]", verb, variantsJoined))
|
||||
|
||||
|
||||
@@ -44,10 +44,11 @@ func doZabsList(ctx context.Context, sc *cli.Subcommand, args []string) error {
|
||||
return errors.Wrap(err, "invalid filter specification on command line")
|
||||
}
|
||||
|
||||
abstractions, errors, err := endpoint.ListAbstractionsStreamed(ctx, q)
|
||||
abstractions, errors, drainDone, err := endpoint.ListAbstractionsStreamed(ctx, q)
|
||||
if err != nil {
|
||||
return err // context clear by invocation of command
|
||||
}
|
||||
defer drainDone()
|
||||
|
||||
var line chainlock.L
|
||||
var wg sync.WaitGroup
|
||||
|
||||
+41
-48
@@ -6,11 +6,10 @@ import (
|
||||
"log/syslog"
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/zrepl/yaml-config"
|
||||
|
||||
"github.com/zrepl/zrepl/util/datasizeunit"
|
||||
@@ -189,13 +188,10 @@ func (i *PositiveDurationOrManual) UnmarshalYAML(u func(interface{}, bool) error
|
||||
return fmt.Errorf("value must not be empty")
|
||||
default:
|
||||
i.Manual = false
|
||||
i.Interval, err = time.ParseDuration(s)
|
||||
i.Interval, err = parsePositiveDuration(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if i.Interval <= 0 {
|
||||
return fmt.Errorf("value must be a positive duration, got %q", s)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -227,10 +223,44 @@ type SnapshottingEnum struct {
|
||||
}
|
||||
|
||||
type SnapshottingPeriodic struct {
|
||||
Type string `yaml:"type"`
|
||||
Prefix string `yaml:"prefix"`
|
||||
Interval time.Duration `yaml:"interval,positive"`
|
||||
Hooks HookList `yaml:"hooks,optional"`
|
||||
Type string `yaml:"type"`
|
||||
Prefix string `yaml:"prefix"`
|
||||
Interval *PositiveDuration `yaml:"interval"`
|
||||
Hooks HookList `yaml:"hooks,optional"`
|
||||
TimestampFormat string `yaml:"timestamp_format,optional,default=dense"`
|
||||
}
|
||||
|
||||
type CronSpec struct {
|
||||
Schedule cron.Schedule
|
||||
}
|
||||
|
||||
var _ yaml.Unmarshaler = &CronSpec{}
|
||||
|
||||
func (s *CronSpec) UnmarshalYAML(unmarshal func(v interface{}, not_strict bool) error) error {
|
||||
var specString string
|
||||
if err := unmarshal(&specString, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Use standard cron format.
|
||||
// Disable the various "descriptors" (@daily, etc)
|
||||
// They are just aliases to "top of hour", "midnight", etc.
|
||||
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.SecondOptional)
|
||||
|
||||
sched, err := parser.Parse(specString)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cron syntax invalid")
|
||||
}
|
||||
s.Schedule = sched
|
||||
return nil
|
||||
}
|
||||
|
||||
type SnapshottingCron struct {
|
||||
Type string `yaml:"type"`
|
||||
Prefix string `yaml:"prefix"`
|
||||
Cron CronSpec `yaml:"cron"`
|
||||
Hooks HookList `yaml:"hooks,optional"`
|
||||
TimestampFormat string `yaml:"timestamp_format,optional,default=dense"`
|
||||
}
|
||||
|
||||
type SnapshottingManual struct {
|
||||
@@ -556,6 +586,7 @@ func (t *SnapshottingEnum) UnmarshalYAML(u func(interface{}, bool) error) (err e
|
||||
t.Ret, err = enumUnmarshal(u, map[string]interface{}{
|
||||
"periodic": &SnapshottingPeriodic{},
|
||||
"manual": &SnapshottingManual{},
|
||||
"cron": &SnapshottingCron{},
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -681,41 +712,3 @@ func ParseConfigBytes(bytes []byte) (*Config, error) {
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`)
|
||||
|
||||
func parsePositiveDuration(e string) (d time.Duration, err error) {
|
||||
comps := durationStringRegex.FindStringSubmatch(e)
|
||||
if len(comps) != 3 {
|
||||
err = fmt.Errorf("does not match regex: %s %#v", e, comps)
|
||||
return
|
||||
}
|
||||
|
||||
durationFactor, err := strconv.ParseInt(comps[1], 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if durationFactor <= 0 {
|
||||
return 0, errors.New("duration must be positive integer")
|
||||
}
|
||||
|
||||
var durationUnit time.Duration
|
||||
switch comps[2] {
|
||||
case "s":
|
||||
durationUnit = time.Second
|
||||
case "m":
|
||||
durationUnit = time.Minute
|
||||
case "h":
|
||||
durationUnit = time.Hour
|
||||
case "d":
|
||||
durationUnit = 24 * time.Hour
|
||||
case "w":
|
||||
durationUnit = 24 * 7 * time.Hour
|
||||
default:
|
||||
err = fmt.Errorf("contains unknown time unit '%s'", comps[2])
|
||||
return
|
||||
}
|
||||
|
||||
d = time.Duration(durationFactor) * durationUnit
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/kr/pretty"
|
||||
"github.com/zrepl/yaml-config"
|
||||
)
|
||||
|
||||
type Duration struct{ d time.Duration }
|
||||
|
||||
func (d Duration) Duration() time.Duration { return d.d }
|
||||
|
||||
var _ yaml.Unmarshaler = &Duration{}
|
||||
|
||||
func (d *Duration) UnmarshalYAML(unmarshal func(v interface{}, not_strict bool) error) error {
|
||||
var s string
|
||||
err := unmarshal(&s, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.d, err = parseDuration(s)
|
||||
if err != nil {
|
||||
d.d = 0
|
||||
return &yaml.TypeError{Errors: []string{fmt.Sprintf("cannot parse value %q: %s", s, err)}}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type PositiveDuration struct{ d Duration }
|
||||
|
||||
var _ yaml.Unmarshaler = &PositiveDuration{}
|
||||
|
||||
func (d PositiveDuration) Duration() time.Duration { return d.d.Duration() }
|
||||
|
||||
func (d *PositiveDuration) UnmarshalYAML(unmarshal func(v interface{}, not_strict bool) error) error {
|
||||
err := d.d.UnmarshalYAML(unmarshal)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.d.Duration() <= 0 {
|
||||
return fmt.Errorf("duration must be positive, got %s", d.d.Duration())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parsePositiveDuration(e string) (time.Duration, error) {
|
||||
d, err := parseDuration(e)
|
||||
if err != nil {
|
||||
return d, err
|
||||
}
|
||||
if d <= 0 {
|
||||
return 0, errors.New("duration must be positive integer")
|
||||
}
|
||||
return d, err
|
||||
}
|
||||
|
||||
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*([\+-]?\d+)\s*(|s|m|h|d|w)\s*$`)
|
||||
|
||||
func parseDuration(e string) (d time.Duration, err error) {
|
||||
comps := durationStringRegex.FindStringSubmatch(e)
|
||||
if comps == nil {
|
||||
err = fmt.Errorf("must match %s", durationStringRegex)
|
||||
return
|
||||
}
|
||||
if len(comps) != 3 {
|
||||
panic(pretty.Sprint(comps))
|
||||
}
|
||||
|
||||
durationFactor, err := strconv.ParseInt(comps[1], 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var durationUnit time.Duration
|
||||
switch comps[2] {
|
||||
case "":
|
||||
if durationFactor != 0 {
|
||||
err = fmt.Errorf("missing time unit")
|
||||
return
|
||||
} else {
|
||||
// It's the case where user specified '0'.
|
||||
// We want to allow this, just like time.ParseDuration.
|
||||
}
|
||||
case "s":
|
||||
durationUnit = time.Second
|
||||
case "m":
|
||||
durationUnit = time.Minute
|
||||
case "h":
|
||||
durationUnit = time.Hour
|
||||
case "d":
|
||||
durationUnit = 24 * time.Hour
|
||||
case "w":
|
||||
durationUnit = 24 * 7 * time.Hour
|
||||
default:
|
||||
err = fmt.Errorf("contains unknown time unit '%s'", comps[2])
|
||||
return
|
||||
}
|
||||
|
||||
d = time.Duration(durationFactor) * durationUnit
|
||||
return
|
||||
}
|
||||
@@ -35,8 +35,23 @@ jobs:
|
||||
snapshotting:
|
||||
type: periodic
|
||||
prefix: zrepl_
|
||||
timestamp_format: dense
|
||||
interval: 10m
|
||||
`
|
||||
cron := `
|
||||
snapshotting:
|
||||
type: cron
|
||||
prefix: zrepl_
|
||||
timestamp_format: human
|
||||
cron: "10 * * * *"
|
||||
`
|
||||
|
||||
periodicDaily := `
|
||||
snapshotting:
|
||||
type: periodic
|
||||
prefix: zrepl_
|
||||
interval: 1d
|
||||
`
|
||||
|
||||
hooks := `
|
||||
snapshotting:
|
||||
@@ -74,10 +89,27 @@ jobs:
|
||||
c = testValidConfig(t, fillSnapshotting(periodic))
|
||||
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic)
|
||||
assert.Equal(t, "periodic", snp.Type)
|
||||
assert.Equal(t, 10*time.Minute, snp.Interval)
|
||||
assert.Equal(t, 10*time.Minute, snp.Interval.Duration())
|
||||
assert.Equal(t, "zrepl_", snp.Prefix)
|
||||
})
|
||||
|
||||
t.Run("periodicDaily", func(t *testing.T) {
|
||||
c = testValidConfig(t, fillSnapshotting(periodicDaily))
|
||||
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic)
|
||||
assert.Equal(t, "periodic", snp.Type)
|
||||
assert.Equal(t, 24*time.Hour, snp.Interval.Duration())
|
||||
assert.Equal(t, "zrepl_", snp.Prefix)
|
||||
assert.Equal(t, "dense", snp.TimestampFormat)
|
||||
})
|
||||
|
||||
t.Run("cron", func(t *testing.T) {
|
||||
c = testValidConfig(t, fillSnapshotting(cron))
|
||||
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingCron)
|
||||
assert.Equal(t, "cron", snp.Type)
|
||||
assert.Equal(t, "zrepl_", snp.Prefix)
|
||||
assert.Equal(t, "human", snp.TimestampFormat)
|
||||
})
|
||||
|
||||
t.Run("hooks", func(t *testing.T) {
|
||||
c = testValidConfig(t, fillSnapshotting(hooks))
|
||||
hs := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic).Hooks
|
||||
@@ -88,3 +120,57 @@ jobs:
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestSnapshottingTimestampDefaults(t *testing.T) {
|
||||
tmpl := `
|
||||
jobs:
|
||||
- name: foo
|
||||
type: push
|
||||
connect:
|
||||
type: local
|
||||
listener_name: foo
|
||||
client_identity: bar
|
||||
filesystems: {"<": true}
|
||||
%s
|
||||
pruning:
|
||||
keep_sender:
|
||||
- type: last_n
|
||||
count: 10
|
||||
keep_receiver:
|
||||
- type: last_n
|
||||
count: 10
|
||||
`
|
||||
|
||||
periodic := `
|
||||
snapshotting:
|
||||
type: periodic
|
||||
prefix: zrepl_
|
||||
interval: 10m
|
||||
`
|
||||
cron := `
|
||||
snapshotting:
|
||||
type: cron
|
||||
prefix: zrepl_
|
||||
cron: "10 * * * *"
|
||||
`
|
||||
|
||||
fillSnapshotting := func(s string) string { return fmt.Sprintf(tmpl, s) }
|
||||
var c *Config
|
||||
|
||||
t.Run("periodic", func(t *testing.T) {
|
||||
c = testValidConfig(t, fillSnapshotting(periodic))
|
||||
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingPeriodic)
|
||||
assert.Equal(t, "periodic", snp.Type)
|
||||
assert.Equal(t, 10*time.Minute, snp.Interval.Duration())
|
||||
assert.Equal(t, "zrepl_", snp.Prefix)
|
||||
assert.Equal(t, "dense", snp.TimestampFormat) // default was set correctly
|
||||
})
|
||||
|
||||
t.Run("cron", func(t *testing.T) {
|
||||
c = testValidConfig(t, fillSnapshotting(cron))
|
||||
snp := c.Jobs[0].Ret.(*PushJob).Snapshotting.Ret.(*SnapshottingCron)
|
||||
assert.Equal(t, "cron", snp.Type)
|
||||
assert.Equal(t, "zrepl_", snp.Prefix)
|
||||
assert.Equal(t, "dense", snp.TimestampFormat) // default was set correctly
|
||||
})
|
||||
}
|
||||
|
||||
+53
-1
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/kr/pretty"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zrepl/yaml-config"
|
||||
)
|
||||
|
||||
func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
|
||||
@@ -43,7 +44,8 @@ func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
// template must be a template/text template with a single '{{ . }}' as placeholder for val
|
||||
//nolint[:deadcode,unused]
|
||||
//
|
||||
//nolint:deadcode,unused
|
||||
func testValidConfigTemplate(t *testing.T, tmpl string, val string) *Config {
|
||||
tmp, err := template.New("master").Parse(tmpl)
|
||||
if err != nil {
|
||||
@@ -86,3 +88,53 @@ func TestTrimSpaceEachLineAndPad(t *testing.T) {
|
||||
`
|
||||
assert.Equal(t, " \n foo\n bar baz\n \n", trimSpaceEachLineAndPad(foo, " "))
|
||||
}
|
||||
|
||||
func TestCronSpec(t *testing.T) {
|
||||
|
||||
expectAccept := []string{
|
||||
`"* * * * *"`,
|
||||
`"0-10 * * * *"`,
|
||||
`"* 0-5,8,12 * * *"`,
|
||||
}
|
||||
|
||||
expectFail := []string{
|
||||
`* * * *`,
|
||||
``,
|
||||
`23`,
|
||||
`"@reboot"`,
|
||||
`"@every 1h30m"`,
|
||||
`"@daily"`,
|
||||
`* * * * * *`,
|
||||
}
|
||||
|
||||
for _, input := range expectAccept {
|
||||
t.Run(input, func(t *testing.T) {
|
||||
s := fmt.Sprintf("spec: %s\n", input)
|
||||
var v struct {
|
||||
Spec CronSpec
|
||||
}
|
||||
v.Spec.Schedule = nil
|
||||
t.Logf("input:\n%s", s)
|
||||
err := yaml.UnmarshalStrict([]byte(s), &v)
|
||||
t.Logf("error: %T %s", err, err)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, v.Spec.Schedule)
|
||||
})
|
||||
}
|
||||
|
||||
for _, input := range expectFail {
|
||||
t.Run(input, func(t *testing.T) {
|
||||
s := fmt.Sprintf("spec: %s\n", input)
|
||||
var v struct {
|
||||
Spec CronSpec
|
||||
}
|
||||
v.Spec.Schedule = nil
|
||||
t.Logf("input: %q", s)
|
||||
err := yaml.UnmarshalStrict([]byte(s), &v)
|
||||
t.Logf("error: %T %s", err, err)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, v.Spec.Schedule)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
jobs:
|
||||
- name: snapjob
|
||||
type: snap
|
||||
filesystems: {
|
||||
"tank<": true,
|
||||
}
|
||||
snapshotting:
|
||||
type: cron
|
||||
prefix: zrepl_snapjob_
|
||||
cron: "*/5 * * * *"
|
||||
pruning:
|
||||
keep:
|
||||
- type: last_n
|
||||
count: 60
|
||||
@@ -5,7 +5,7 @@
|
||||
//
|
||||
// This package also provides all supported hook type implementations and abstractions around them.
|
||||
//
|
||||
// Use For Other Kinds Of ExpectStepReports
|
||||
// # Use For Other Kinds Of ExpectStepReports
|
||||
//
|
||||
// This package REQUIRES REFACTORING before it can be used for other activities than snapshots, e.g. pre- and post-replication:
|
||||
//
|
||||
@@ -15,7 +15,7 @@
|
||||
// The hook implementations should move out of this package.
|
||||
// However, there is a lot of tight coupling which to untangle isn't worth it ATM.
|
||||
//
|
||||
// How This Package Is Used By Package Snapper
|
||||
// # How This Package Is Used By Package Snapper
|
||||
//
|
||||
// Deserialize a config.List using ListFromConfig().
|
||||
// Then it MUST filter the list to only contain hooks for a particular filesystem using
|
||||
@@ -30,5 +30,4 @@
|
||||
// Command hooks make it available in the environment variable ZREPL_DRYRUN.
|
||||
//
|
||||
// Plan.Report() can be called while Plan.Run() is executing to give an overview of plan execution progress (future use in "zrepl status").
|
||||
//
|
||||
package hooks
|
||||
|
||||
@@ -93,7 +93,14 @@ func (r *CommandHookReport) String() string {
|
||||
cmdLine.WriteString(fmt.Sprintf("%s'%s'", sep, a))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("command hook invocation: \"%s\"", cmdLine.String()) // no %q to make copy-pastable
|
||||
var msg string
|
||||
if r.Err == nil {
|
||||
msg = "command hook"
|
||||
} else {
|
||||
msg = fmt.Sprintf("command hook failed with %q", r.Err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s: \"%s\"", msg, cmdLine.String()) // no %q to make copy-pastable
|
||||
}
|
||||
func (r *CommandHookReport) Error() string {
|
||||
if r.Err == nil {
|
||||
|
||||
@@ -17,19 +17,22 @@ import (
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
// Hook to implement the following recommmendation from MySQL docs
|
||||
// https://dev.mysql.com/doc/mysql-backup-excerpt/5.7/en/backup-methods.html
|
||||
//
|
||||
// Making Backups Using a File System Snapshot:
|
||||
// Making Backups Using a File System Snapshot:
|
||||
//
|
||||
// If you are using a Veritas file system, you can make a backup like this:
|
||||
// If you are using a Veritas file system, you can make a backup like this:
|
||||
//
|
||||
// From a client program, execute FLUSH TABLES WITH READ LOCK.
|
||||
// From another shell, execute mount vxfs snapshot.
|
||||
// From the first client, execute UNLOCK TABLES.
|
||||
// Copy files from the snapshot.
|
||||
// Unmount the snapshot.
|
||||
// From a client program, execute FLUSH TABLES WITH READ LOCK.
|
||||
// From another shell, execute mount vxfs snapshot.
|
||||
// From the first client, execute UNLOCK TABLES.
|
||||
// Copy files from the snapshot.
|
||||
// Unmount the snapshot.
|
||||
//
|
||||
// Similar snapshot capabilities may be available in other file systems, such as LVM or ZFS.
|
||||
// Similar snapshot capabilities may be available in other file systems, such as LVM or ZFS.
|
||||
//
|
||||
|
||||
type MySQLLockTables struct {
|
||||
errIsFatal bool
|
||||
connector sqldriver.Connector
|
||||
|
||||
@@ -161,7 +161,7 @@ jobs:
|
||||
ExpectedEdge: hooks.Pre,
|
||||
ExpectStatus: hooks.StepErr,
|
||||
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||
ErrorTest: regexpTest("^command hook failed.*exit status 1$"),
|
||||
},
|
||||
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||
expectStep{
|
||||
@@ -185,7 +185,7 @@ jobs:
|
||||
ExpectedEdge: hooks.Pre,
|
||||
ExpectStatus: hooks.StepErr,
|
||||
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||
ErrorTest: regexpTest("^command hook failed.*exit status 1$"),
|
||||
},
|
||||
expectStep{
|
||||
ExpectedEdge: hooks.Pre,
|
||||
@@ -234,7 +234,7 @@ jobs:
|
||||
ExpectedEdge: hooks.Post,
|
||||
ExpectStatus: hooks.StepErr,
|
||||
OutputTest: containsTest(fmt.Sprintf("TEST ERROR post_testing %s@%s", testFSName, testSnapshotName)),
|
||||
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||
ErrorTest: regexpTest("^command hook failed.*exit status 1$"),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -267,7 +267,7 @@ jobs:
|
||||
ExpectedEdge: hooks.Pre,
|
||||
ExpectStatus: hooks.StepErr,
|
||||
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||
ErrorTest: regexpTest("^command hook failed.*exit status 1$"),
|
||||
},
|
||||
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||
expectStep{
|
||||
@@ -295,7 +295,7 @@ jobs:
|
||||
ExpectedEdge: hooks.Pre,
|
||||
ExpectStatus: hooks.StepErr,
|
||||
OutputTest: containsTest(fmt.Sprintf("TEST ERROR pre_testing %s@%s", testFSName, testSnapshotName)),
|
||||
ErrorTest: regexpTest("^command hook invocation.*exit status 1$"),
|
||||
ErrorTest: regexpTest("^command hook failed.*exit status 1$"),
|
||||
},
|
||||
expectStep{ExpectedEdge: hooks.Callback, ExpectStatus: hooks.StepOk},
|
||||
expectStep{
|
||||
|
||||
@@ -101,7 +101,7 @@ type modePush struct {
|
||||
receiver *rpc.Client
|
||||
senderConfig *endpoint.SenderConfig
|
||||
plannerPolicy *logic.PlannerPolicy
|
||||
snapper *snapper.PeriodicOrManual
|
||||
snapper snapper.Snapper
|
||||
}
|
||||
|
||||
func (m *modePush) ConnectEndpoints(ctx context.Context, connecter transport.Connecter) {
|
||||
@@ -137,7 +137,8 @@ func (m *modePush) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}
|
||||
}
|
||||
|
||||
func (m *modePush) SnapperReport() *snapper.Report {
|
||||
return m.snapper.Report()
|
||||
r := m.snapper.Report()
|
||||
return &r
|
||||
}
|
||||
|
||||
func (m *modePush) ResetConnectBackoff() {
|
||||
@@ -168,7 +169,6 @@ func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.Job
|
||||
}
|
||||
|
||||
m.plannerPolicy = &logic.PlannerPolicy{
|
||||
EncryptedSend: logic.TriFromBool(in.Send.Encrypted),
|
||||
ConflictResolution: conflictResolution,
|
||||
ReplicationConfig: replicationConfig,
|
||||
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
|
||||
@@ -273,7 +273,6 @@ func modePullFromConfig(g *config.Global, in *config.PullJob, jobID endpoint.Job
|
||||
}
|
||||
|
||||
m.plannerPolicy = &logic.PlannerPolicy{
|
||||
EncryptedSend: logic.DontCare,
|
||||
ConflictResolution: conflictResolution,
|
||||
ReplicationConfig: replicationConfig,
|
||||
SizeEstimationConcurrency: in.Replication.Concurrency.SizeEstimates,
|
||||
|
||||
@@ -58,7 +58,7 @@ func modeSinkFromConfig(g *config.Global, in *config.SinkJob, jobID endpoint.Job
|
||||
|
||||
type modeSource struct {
|
||||
senderConfig *endpoint.SenderConfig
|
||||
snapper *snapper.PeriodicOrManual
|
||||
snapper snapper.Snapper
|
||||
}
|
||||
|
||||
func modeSourceFromConfig(g *config.Global, in *config.SourceJob, jobID endpoint.JobID) (m *modeSource, err error) {
|
||||
@@ -88,7 +88,8 @@ func (m *modeSource) RunPeriodic(ctx context.Context) {
|
||||
}
|
||||
|
||||
func (m *modeSource) SnapperReport() *snapper.Report {
|
||||
return m.snapper.Report()
|
||||
r := m.snapper.Report()
|
||||
return &r
|
||||
}
|
||||
|
||||
func passiveSideFromConfig(g *config.Global, in *config.PassiveJob, configJob interface{}, parseFlags config.ParseFlags) (s *PassiveSide, err error) {
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
type SnapJob struct {
|
||||
name endpoint.JobID
|
||||
fsfilter zfs.DatasetFilter
|
||||
snapper *snapper.PeriodicOrManual
|
||||
snapper snapper.Snapper
|
||||
|
||||
prunerFactory *pruner.LocalPrunerFactory
|
||||
|
||||
@@ -86,7 +86,8 @@ func (j *SnapJob) Status() *Status {
|
||||
s.Pruning = j.pruner.Report()
|
||||
}
|
||||
j.prunerMtx.Unlock()
|
||||
s.Snapshotting = j.snapper.Report()
|
||||
r := j.snapper.Report()
|
||||
s.Snapshotting = &r
|
||||
return &Status{Type: t, JobSpecific: s}
|
||||
}
|
||||
|
||||
@@ -137,7 +138,7 @@ outer:
|
||||
// TODO:
|
||||
// This is a work-around for the current package daemon/pruner
|
||||
// and package pruning.Snapshot limitation: they require the
|
||||
// `Replicated` getter method be present, but obviously,
|
||||
// `Replicated` getter method be present, but obviously,
|
||||
// a local job like SnapJob can't deliver on that.
|
||||
// But the pruner.Pruner gives up on an FS if no replication
|
||||
// cursor is present, which is why this pruner returns the
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// package trace provides activity tracing via ctx through Tasks and Spans
|
||||
//
|
||||
// Basic Concepts
|
||||
// # Basic Concepts
|
||||
//
|
||||
// Tracing can be used to identify where a piece of code spends its time.
|
||||
//
|
||||
@@ -10,51 +10,50 @@
|
||||
// to tech-savvy users (albeit not developers).
|
||||
//
|
||||
// This package provides the concept of Tasks and Spans to express what activity is happening within an application:
|
||||
//
|
||||
// - Neither task nor span is really tangible but instead contained within the context.Context tree
|
||||
// - Tasks represent concurrent activity (i.e. goroutines).
|
||||
// - Spans represent a semantic stack trace within a task.
|
||||
//
|
||||
// - Neither task nor span is really tangible but instead contained within the context.Context tree
|
||||
// - Tasks represent concurrent activity (i.e. goroutines).
|
||||
// - Spans represent a semantic stack trace within a task.
|
||||
// As a consequence, whenever a context is propagated across goroutine boundary, you need to create a child task:
|
||||
//
|
||||
// go func(ctx context.Context) {
|
||||
// ctx, endTask = WithTask(ctx, "what-happens-inside-the-child-task")
|
||||
// defer endTask()
|
||||
// // ...
|
||||
// }(ctx)
|
||||
// go func(ctx context.Context) {
|
||||
// ctx, endTask = WithTask(ctx, "what-happens-inside-the-child-task")
|
||||
// defer endTask()
|
||||
// // ...
|
||||
// }(ctx)
|
||||
//
|
||||
// Within the task, you can open up a hierarchy of spans.
|
||||
// In contrast to tasks, which have can multiple concurrently running child tasks,
|
||||
// spans must nest and not cross the goroutine boundary.
|
||||
//
|
||||
// ctx, endSpan = WithSpan(ctx, "copy-dir")
|
||||
// defer endSpan()
|
||||
// for _, f := range dir.Files() {
|
||||
// func() {
|
||||
// ctx, endSpan := WithSpan(ctx, fmt.Sprintf("copy-file %q", f))
|
||||
// defer endspan()
|
||||
// b, _ := ioutil.ReadFile(f)
|
||||
// _ = ioutil.WriteFile(f + ".copy", b, 0600)
|
||||
// }()
|
||||
// }
|
||||
// ctx, endSpan = WithSpan(ctx, "copy-dir")
|
||||
// defer endSpan()
|
||||
// for _, f := range dir.Files() {
|
||||
// func() {
|
||||
// ctx, endSpan := WithSpan(ctx, fmt.Sprintf("copy-file %q", f))
|
||||
// defer endspan()
|
||||
// b, _ := ioutil.ReadFile(f)
|
||||
// _ = ioutil.WriteFile(f + ".copy", b, 0600)
|
||||
// }()
|
||||
// }
|
||||
//
|
||||
// In combination:
|
||||
// ctx, endTask = WithTask(ctx, "copy-dirs")
|
||||
// defer endTask()
|
||||
// for i := range dirs {
|
||||
// go func(dir string) {
|
||||
// ctx, endTask := WithTask(ctx, "copy-dir")
|
||||
// defer endTask()
|
||||
// for _, f := range filesIn(dir) {
|
||||
// func() {
|
||||
// ctx, endSpan := WithSpan(ctx, fmt.Sprintf("copy-file %q", f))
|
||||
// defer endspan()
|
||||
// b, _ := ioutil.ReadFile(f)
|
||||
// _ = ioutil.WriteFile(f + ".copy", b, 0600)
|
||||
// }()
|
||||
// }
|
||||
// }()
|
||||
// }
|
||||
//
|
||||
// ctx, endTask = WithTask(ctx, "copy-dirs")
|
||||
// defer endTask()
|
||||
// for i := range dirs {
|
||||
// go func(dir string) {
|
||||
// ctx, endTask := WithTask(ctx, "copy-dir")
|
||||
// defer endTask()
|
||||
// for _, f := range filesIn(dir) {
|
||||
// func() {
|
||||
// ctx, endSpan := WithSpan(ctx, fmt.Sprintf("copy-file %q", f))
|
||||
// defer endspan()
|
||||
// b, _ := ioutil.ReadFile(f)
|
||||
// _ = ioutil.WriteFile(f + ".copy", b, 0600)
|
||||
// }()
|
||||
// }
|
||||
// }()
|
||||
// }
|
||||
//
|
||||
// Note that a span ends at the time you call endSpan - not before and not after that.
|
||||
// If you violate the stack-like nesting of spans by forgetting an endSpan() invocation,
|
||||
@@ -65,8 +64,7 @@
|
||||
//
|
||||
// Recovering from endSpan() or endTask() panics will corrupt the trace stack and lead to corrupt tracefile output.
|
||||
//
|
||||
//
|
||||
// Best Practices For Naming Tasks And Spans
|
||||
// # Best Practices For Naming Tasks And Spans
|
||||
//
|
||||
// Tasks should always have string constants as names, and must not contain the `#` character. WHy?
|
||||
// First, the visualization by chrome://tracing draws a horizontal bar for each task in the trace.
|
||||
@@ -74,8 +72,7 @@
|
||||
// Note that the `#NUM` suffix will be reused if a task has ended, in order to avoid an
|
||||
// infinite number of horizontal bars in the visualization.
|
||||
//
|
||||
//
|
||||
// Chrome-compatible Tracefile Support
|
||||
// # Chrome-compatible Tracefile Support
|
||||
//
|
||||
// The activity trace generated by usage of WithTask and WithSpan can be rendered to a JSON output file
|
||||
// that can be loaded into chrome://tracing .
|
||||
|
||||
@@ -11,8 +11,6 @@ import (
|
||||
// use like this:
|
||||
//
|
||||
// defer WithSpanFromStackUpdateCtx(&existingCtx)()
|
||||
//
|
||||
//
|
||||
func WithSpanFromStackUpdateCtx(ctx *context.Context) DoneFunc {
|
||||
childSpanCtx, end := WithSpan(*ctx, getMyCallerOrPanic())
|
||||
*ctx = childSpanCtx
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
package snapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/hooks"
|
||||
"github.com/zrepl/zrepl/util/suspendresumesafetimer"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func cronFromConfig(fsf zfs.DatasetFilter, in config.SnapshottingCron) (*Cron, error) {
|
||||
|
||||
hooksList, err := hooks.ListFromConfig(&in.Hooks)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "hook config error")
|
||||
}
|
||||
planArgs := planArgs{
|
||||
prefix: in.Prefix,
|
||||
timestampFormat: in.TimestampFormat,
|
||||
hooks: hooksList,
|
||||
}
|
||||
return &Cron{config: in, fsf: fsf, planArgs: planArgs}, nil
|
||||
}
|
||||
|
||||
type Cron struct {
|
||||
config config.SnapshottingCron
|
||||
fsf zfs.DatasetFilter
|
||||
planArgs planArgs
|
||||
|
||||
mtx sync.RWMutex
|
||||
|
||||
running bool
|
||||
wakeupTime time.Time // zero value means uninit
|
||||
lastError error
|
||||
lastPlan *plan
|
||||
wakeupWhileRunningCount int
|
||||
}
|
||||
|
||||
func (s *Cron) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
||||
|
||||
for {
|
||||
now := time.Now()
|
||||
s.mtx.Lock()
|
||||
s.wakeupTime = s.config.Cron.Schedule.Next(now)
|
||||
s.mtx.Unlock()
|
||||
|
||||
ctxDone := suspendresumesafetimer.SleepUntil(ctx, s.wakeupTime)
|
||||
if ctxDone != nil {
|
||||
return
|
||||
}
|
||||
|
||||
getLogger(ctx).Debug("cron timer fired")
|
||||
s.mtx.Lock()
|
||||
if s.running {
|
||||
getLogger(ctx).Warn("snapshotting triggered according to cron rules but previous snapshotting is not done; not taking a snapshot this time")
|
||||
s.wakeupWhileRunningCount++
|
||||
s.mtx.Unlock()
|
||||
continue
|
||||
}
|
||||
s.lastError = nil
|
||||
s.lastPlan = nil
|
||||
s.wakeupWhileRunningCount = 0
|
||||
s.running = true
|
||||
s.mtx.Unlock()
|
||||
go func() {
|
||||
err := s.do(ctx)
|
||||
s.mtx.Lock()
|
||||
s.lastError = err
|
||||
s.running = false
|
||||
s.mtx.Unlock()
|
||||
|
||||
select {
|
||||
case snapshotsTaken <- struct{}{}:
|
||||
default:
|
||||
if snapshotsTaken != nil {
|
||||
getLogger(ctx).Warn("callback channel is full, discarding snapshot update event")
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *Cron) do(ctx context.Context) error {
|
||||
fss, err := zfs.ZFSListMapping(ctx, s.fsf)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot list filesystems")
|
||||
}
|
||||
p := makePlan(s.planArgs, fss)
|
||||
|
||||
s.mtx.Lock()
|
||||
s.lastPlan = p
|
||||
s.lastError = nil
|
||||
s.mtx.Unlock()
|
||||
|
||||
ok := p.execute(ctx, false)
|
||||
if !ok {
|
||||
return errors.New("one or more snapshots could not be created, check logs for details")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type CronState string
|
||||
|
||||
const (
|
||||
CronStateRunning CronState = "running"
|
||||
CronStateWaiting CronState = "waiting"
|
||||
)
|
||||
|
||||
type CronReport struct {
|
||||
State CronState
|
||||
WakeupTime time.Time
|
||||
Errors []string
|
||||
Progress []*ReportFilesystem
|
||||
}
|
||||
|
||||
func (s *Cron) Report() Report {
|
||||
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
|
||||
r := CronReport{}
|
||||
|
||||
r.WakeupTime = s.wakeupTime
|
||||
|
||||
if s.running {
|
||||
r.State = CronStateRunning
|
||||
} else {
|
||||
r.State = CronStateWaiting
|
||||
}
|
||||
|
||||
if s.lastError != nil {
|
||||
r.Errors = append(r.Errors, s.lastError.Error())
|
||||
}
|
||||
if s.wakeupWhileRunningCount > 0 {
|
||||
r.Errors = append(r.Errors, fmt.Sprintf("cron frequency is too high; snapshots were not taken %d times", s.wakeupWhileRunningCount))
|
||||
}
|
||||
|
||||
r.Progress = nil
|
||||
if s.lastPlan != nil {
|
||||
r.Progress = s.lastPlan.report()
|
||||
}
|
||||
|
||||
return Report{Type: TypeCron, Cron: &r}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package snapper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zrepl/yaml-config"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
)
|
||||
|
||||
func TestCronLibraryWorks(t *testing.T) {
|
||||
|
||||
type testCase struct {
|
||||
spec string
|
||||
in time.Time
|
||||
expect time.Time
|
||||
}
|
||||
dhm := func(day, hour, minutes int) time.Time {
|
||||
return time.Date(2022, 7, day, hour, minutes, 0, 0, time.UTC)
|
||||
}
|
||||
hm := func(hour, minutes int) time.Time {
|
||||
return dhm(23, hour, minutes)
|
||||
}
|
||||
|
||||
tcs := []testCase{
|
||||
{"0-10 * * * *", dhm(17, 1, 10), dhm(17, 2, 0)},
|
||||
{"0-10 * * * *", dhm(17, 23, 10), dhm(18, 0, 0)},
|
||||
{"0-10 * * * *", hm(1, 9), hm(1, 10)},
|
||||
{"0-10 * * * *", hm(1, 9), hm(1, 10)},
|
||||
|
||||
{"1,3,5 * * * *", hm(1, 1), hm(1, 3)},
|
||||
{"1,3,5 * * * *", hm(1, 2), hm(1, 3)},
|
||||
{"1,3,5 * * * *", hm(1, 3), hm(1, 5)},
|
||||
{"1,3,5 * * * *", hm(1, 5), hm(2, 1)},
|
||||
|
||||
{"* 0-5,8,12 * * *", hm(0, 0), hm(0, 1)},
|
||||
{"* 0-5,8,12 * * *", hm(4, 59), hm(5, 0)},
|
||||
{"* 0-5,8,12 * * *", hm(5, 0), hm(5, 1)},
|
||||
{"* 0-5,8,12 * * *", hm(5, 59), hm(8, 0)},
|
||||
{"* 0-5,8,12 * * *", hm(8, 59), hm(12, 0)},
|
||||
|
||||
// https://github.com/zrepl/zrepl/pull/614#issuecomment-1188358989
|
||||
{"53 17,18,19 * * *", dhm(23, 17, 52), dhm(23, 17, 53)},
|
||||
{"53 17,18,19 * * *", dhm(23, 17, 53), dhm(23, 18, 53)},
|
||||
{"53 17,18,19 * * *", dhm(23, 18, 53), dhm(23, 19, 53)},
|
||||
{"53 17,18,19 * * *", dhm(23, 19, 53), dhm(24 /* ! */, 17, 53)},
|
||||
}
|
||||
|
||||
for i, tc := range tcs {
|
||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||
var s struct {
|
||||
Cron config.CronSpec `yaml:"cron"`
|
||||
}
|
||||
inp := fmt.Sprintf("cron: %q", tc.spec)
|
||||
fmt.Println("spec is ", inp)
|
||||
err := yaml.UnmarshalStrict([]byte(inp), &s)
|
||||
require.NoError(t, err)
|
||||
|
||||
actual := s.Cron.Schedule.Next(tc.in)
|
||||
assert.Equal(t, tc.expect, actual)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package snapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/hooks"
|
||||
"github.com/zrepl/zrepl/daemon/logging"
|
||||
"github.com/zrepl/zrepl/util/chainlock"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
type planArgs struct {
|
||||
prefix string
|
||||
timestampFormat string
|
||||
hooks *hooks.List
|
||||
}
|
||||
|
||||
type plan struct {
|
||||
mtx chainlock.L
|
||||
args planArgs
|
||||
snaps map[*zfs.DatasetPath]*snapProgress
|
||||
}
|
||||
|
||||
func makePlan(args planArgs, fss []*zfs.DatasetPath) *plan {
|
||||
snaps := make(map[*zfs.DatasetPath]*snapProgress, len(fss))
|
||||
for _, fs := range fss {
|
||||
snaps[fs] = &snapProgress{state: SnapPending}
|
||||
}
|
||||
return &plan{snaps: snaps, args: args}
|
||||
}
|
||||
|
||||
//go:generate stringer -type=SnapState
|
||||
type SnapState uint
|
||||
|
||||
const (
|
||||
SnapPending SnapState = 1 << iota
|
||||
SnapStarted
|
||||
SnapDone
|
||||
SnapError
|
||||
)
|
||||
|
||||
// All fields protected by Snapper.mtx
|
||||
type snapProgress struct {
|
||||
state SnapState
|
||||
|
||||
// SnapStarted, SnapDone, SnapError
|
||||
name string
|
||||
startAt time.Time
|
||||
hookPlan *hooks.Plan
|
||||
|
||||
// SnapDone
|
||||
doneAt time.Time
|
||||
|
||||
// SnapErr TODO disambiguate state
|
||||
runResults hooks.PlanReport
|
||||
}
|
||||
|
||||
func (plan *plan) formatNow(format string) string {
|
||||
now := time.Now().UTC()
|
||||
switch strings.ToLower(format) {
|
||||
case "dense":
|
||||
format = "20060102_150405_000"
|
||||
case "human":
|
||||
format = "2006-01-02_15:04:05"
|
||||
case "iso-8601":
|
||||
format = "2006-01-02T15:04:05.000Z"
|
||||
case "unix-seconds":
|
||||
return strconv.FormatInt(now.Unix(), 10)
|
||||
}
|
||||
return now.Format(format)
|
||||
}
|
||||
|
||||
func (plan *plan) execute(ctx context.Context, dryRun bool) (ok bool) {
|
||||
|
||||
hookMatchCount := make(map[hooks.Hook]int, len(*plan.args.hooks))
|
||||
for _, h := range *plan.args.hooks {
|
||||
hookMatchCount[h] = 0
|
||||
}
|
||||
|
||||
anyFsHadErr := false
|
||||
// TODO channel programs -> allow a little jitter?
|
||||
for fs, progress := range plan.snaps {
|
||||
suffix := plan.formatNow(plan.args.timestampFormat)
|
||||
snapname := fmt.Sprintf("%s%s", plan.args.prefix, suffix)
|
||||
|
||||
ctx := logging.WithInjectedField(ctx, "fs", fs.ToString())
|
||||
ctx = logging.WithInjectedField(ctx, "snap", snapname)
|
||||
|
||||
hookEnvExtra := hooks.Env{
|
||||
hooks.EnvFS: fs.ToString(),
|
||||
hooks.EnvSnapshot: snapname,
|
||||
}
|
||||
|
||||
jobCallback := hooks.NewCallbackHookForFilesystem("snapshot", fs, func(ctx context.Context) (err error) {
|
||||
l := getLogger(ctx)
|
||||
l.Debug("create snapshot")
|
||||
err = zfs.ZFSSnapshot(ctx, fs, snapname, false) // TODO propagate context to ZFSSnapshot
|
||||
if err != nil {
|
||||
l.WithError(err).Error("cannot create snapshot")
|
||||
}
|
||||
return
|
||||
})
|
||||
|
||||
fsHadErr := false
|
||||
var hookPlanReport hooks.PlanReport
|
||||
var hookPlan *hooks.Plan
|
||||
{
|
||||
filteredHooks, err := plan.args.hooks.CopyFilteredForFilesystem(fs)
|
||||
if err != nil {
|
||||
getLogger(ctx).WithError(err).Error("unexpected filter error")
|
||||
fsHadErr = true
|
||||
goto updateFSState
|
||||
}
|
||||
// account for running hooks
|
||||
for _, h := range filteredHooks {
|
||||
hookMatchCount[h] = hookMatchCount[h] + 1
|
||||
}
|
||||
|
||||
var planErr error
|
||||
hookPlan, planErr = hooks.NewPlan(&filteredHooks, hooks.PhaseSnapshot, jobCallback, hookEnvExtra)
|
||||
if planErr != nil {
|
||||
fsHadErr = true
|
||||
getLogger(ctx).WithError(planErr).Error("cannot create job hook plan")
|
||||
goto updateFSState
|
||||
}
|
||||
}
|
||||
|
||||
plan.mtx.HoldWhile(func() {
|
||||
progress.name = snapname
|
||||
progress.startAt = time.Now()
|
||||
progress.hookPlan = hookPlan
|
||||
progress.state = SnapStarted
|
||||
})
|
||||
|
||||
{
|
||||
getLogger(ctx).WithField("report", hookPlan.Report().String()).Debug("begin run job plan")
|
||||
hookPlan.Run(ctx, dryRun)
|
||||
hookPlanReport = hookPlan.Report()
|
||||
fsHadErr = hookPlanReport.HadError() // not just fatal errors
|
||||
if fsHadErr {
|
||||
getLogger(ctx).WithField("report", hookPlanReport.String()).Error("end run job plan with error")
|
||||
} else {
|
||||
getLogger(ctx).WithField("report", hookPlanReport.String()).Info("end run job plan successful")
|
||||
}
|
||||
}
|
||||
|
||||
updateFSState:
|
||||
anyFsHadErr = anyFsHadErr || fsHadErr
|
||||
plan.mtx.HoldWhile(func() {
|
||||
progress.doneAt = time.Now()
|
||||
progress.state = SnapDone
|
||||
if fsHadErr {
|
||||
progress.state = SnapError
|
||||
}
|
||||
progress.runResults = hookPlanReport
|
||||
})
|
||||
}
|
||||
|
||||
for h, mc := range hookMatchCount {
|
||||
if mc == 0 {
|
||||
hookIdx := -1
|
||||
for idx, ah := range *plan.args.hooks {
|
||||
if ah == h {
|
||||
hookIdx = idx
|
||||
break
|
||||
}
|
||||
}
|
||||
getLogger(ctx).WithField("hook", h.String()).WithField("hook_number", hookIdx+1).Warn("hook did not match any snapshotted filesystems")
|
||||
}
|
||||
}
|
||||
|
||||
return !anyFsHadErr
|
||||
}
|
||||
|
||||
type ReportFilesystem struct {
|
||||
Path string
|
||||
State SnapState
|
||||
|
||||
// Valid in SnapStarted and later
|
||||
SnapName string
|
||||
StartAt time.Time
|
||||
Hooks string
|
||||
HooksHadError bool
|
||||
|
||||
// Valid in SnapDone | SnapError
|
||||
DoneAt time.Time
|
||||
}
|
||||
|
||||
func (plan *plan) report() []*ReportFilesystem {
|
||||
plan.mtx.Lock()
|
||||
defer plan.mtx.Unlock()
|
||||
|
||||
pReps := make([]*ReportFilesystem, 0, len(plan.snaps))
|
||||
for fs, p := range plan.snaps {
|
||||
var hooksStr string
|
||||
var hooksHadError bool
|
||||
if p.hookPlan != nil {
|
||||
hooksStr, hooksHadError = p.report()
|
||||
}
|
||||
pReps = append(pReps, &ReportFilesystem{
|
||||
Path: fs.ToString(),
|
||||
State: p.state,
|
||||
SnapName: p.name,
|
||||
StartAt: p.startAt,
|
||||
DoneAt: p.doneAt,
|
||||
Hooks: hooksStr,
|
||||
HooksHadError: hooksHadError,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(pReps, func(i, j int) bool {
|
||||
return strings.Compare(pReps[i].Path, pReps[j].Path) == -1
|
||||
})
|
||||
|
||||
return pReps
|
||||
}
|
||||
|
||||
func (p *snapProgress) report() (hooksStr string, hooksHadError bool) {
|
||||
hr := p.hookPlan.Report()
|
||||
// FIXME: technically this belongs into client
|
||||
// but we can't serialize hooks.Step ATM
|
||||
rightPad := func(str string, length int, pad string) string {
|
||||
if len(str) > length {
|
||||
return str[:length]
|
||||
}
|
||||
return str + strings.Repeat(pad, length-len(str))
|
||||
}
|
||||
hooksHadError = hr.HadError()
|
||||
rows := make([][]string, len(hr))
|
||||
const numCols = 4
|
||||
lens := make([]int, numCols)
|
||||
for i, e := range hr {
|
||||
rows[i] = make([]string, numCols)
|
||||
rows[i][0] = fmt.Sprintf("%d", i+1)
|
||||
rows[i][1] = e.Status.String()
|
||||
runTime := "..."
|
||||
if e.Status != hooks.StepPending {
|
||||
runTime = e.End.Sub(e.Begin).Round(time.Millisecond).String()
|
||||
}
|
||||
rows[i][2] = runTime
|
||||
rows[i][3] = ""
|
||||
if e.Report != nil {
|
||||
rows[i][3] = e.Report.String()
|
||||
}
|
||||
for j, col := range lens {
|
||||
if len(rows[i][j]) > col {
|
||||
lens[j] = len(rows[i][j])
|
||||
}
|
||||
}
|
||||
}
|
||||
rowsFlat := make([]string, len(hr))
|
||||
for i, r := range rows {
|
||||
colsPadded := make([]string, len(r))
|
||||
for j, c := range r[:len(r)-1] {
|
||||
colsPadded[j] = rightPad(c, lens[j], " ")
|
||||
}
|
||||
colsPadded[len(r)-1] = r[len(r)-1]
|
||||
rowsFlat[i] = strings.Join(colsPadded, " ")
|
||||
}
|
||||
hooksStr = strings.Join(rowsFlat, "\n")
|
||||
|
||||
return hooksStr, hooksHadError
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package snapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type manual struct{}
|
||||
|
||||
func (s *manual) Run(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
func (s *manual) Report() Report {
|
||||
return Report{Type: TypeManual, Manual: &struct{}{}}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
package snapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/hooks"
|
||||
"github.com/zrepl/zrepl/daemon/logging"
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
"github.com/zrepl/zrepl/util/suspendresumesafetimer"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
func periodicFromConfig(g *config.Global, fsf zfs.DatasetFilter, in *config.SnapshottingPeriodic) (*Periodic, error) {
|
||||
if in.Prefix == "" {
|
||||
return nil, errors.New("prefix must not be empty")
|
||||
}
|
||||
if in.Interval.Duration() <= 0 {
|
||||
return nil, errors.New("interval must be positive")
|
||||
}
|
||||
|
||||
hookList, err := hooks.ListFromConfig(&in.Hooks)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "hook config error")
|
||||
}
|
||||
|
||||
args := periodicArgs{
|
||||
interval: in.Interval.Duration(),
|
||||
fsf: fsf,
|
||||
planArgs: planArgs{
|
||||
prefix: in.Prefix,
|
||||
timestampFormat: in.TimestampFormat,
|
||||
hooks: hookList,
|
||||
},
|
||||
// ctx and log is set in Run()
|
||||
}
|
||||
|
||||
return &Periodic{state: SyncUp, args: args}, nil
|
||||
}
|
||||
|
||||
type periodicArgs struct {
|
||||
ctx context.Context
|
||||
interval time.Duration
|
||||
fsf zfs.DatasetFilter
|
||||
planArgs planArgs
|
||||
snapshotsTaken chan<- struct{}
|
||||
dryRun bool
|
||||
}
|
||||
|
||||
type Periodic struct {
|
||||
args periodicArgs
|
||||
|
||||
mtx sync.Mutex
|
||||
state State
|
||||
|
||||
// set in state Plan, used in Waiting
|
||||
lastInvocation time.Time
|
||||
|
||||
// valid for state Snapshotting
|
||||
plan *plan
|
||||
|
||||
// valid for state SyncUp and Waiting
|
||||
sleepUntil time.Time
|
||||
|
||||
// valid for state Err
|
||||
err error
|
||||
}
|
||||
|
||||
//go:generate stringer -type=State
|
||||
type State uint
|
||||
|
||||
const (
|
||||
SyncUp State = 1 << iota
|
||||
SyncUpErrWait
|
||||
Planning
|
||||
Snapshotting
|
||||
Waiting
|
||||
ErrorWait
|
||||
Stopped
|
||||
)
|
||||
|
||||
func (s State) sf() state {
|
||||
m := map[State]state{
|
||||
SyncUp: periodicStateSyncUp,
|
||||
SyncUpErrWait: periodicStateWait,
|
||||
Planning: periodicStatePlan,
|
||||
Snapshotting: periodicStateSnapshot,
|
||||
Waiting: periodicStateWait,
|
||||
ErrorWait: periodicStateWait,
|
||||
Stopped: nil,
|
||||
}
|
||||
return m[s]
|
||||
}
|
||||
|
||||
type updater func(u func(*Periodic)) State
|
||||
type state func(a periodicArgs, u updater) state
|
||||
|
||||
func (s *Periodic) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||
getLogger(ctx).Debug("start")
|
||||
defer getLogger(ctx).Debug("stop")
|
||||
|
||||
s.args.snapshotsTaken = snapshotsTaken
|
||||
s.args.ctx = ctx
|
||||
s.args.dryRun = false // for future expansion
|
||||
|
||||
u := func(u func(*Periodic)) State {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
if u != nil {
|
||||
u(s)
|
||||
}
|
||||
return s.state
|
||||
}
|
||||
|
||||
var st state = periodicStateSyncUp
|
||||
|
||||
for st != nil {
|
||||
pre := u(nil)
|
||||
st = st(s.args, u)
|
||||
post := u(nil)
|
||||
getLogger(ctx).
|
||||
WithField("transition", fmt.Sprintf("%s=>%s", pre, post)).
|
||||
Debug("state transition")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func onErr(err error, u updater) state {
|
||||
return u(func(s *Periodic) {
|
||||
s.err = err
|
||||
preState := s.state
|
||||
switch s.state {
|
||||
case SyncUp:
|
||||
s.state = SyncUpErrWait
|
||||
case Planning:
|
||||
fallthrough
|
||||
case Snapshotting:
|
||||
s.state = ErrorWait
|
||||
}
|
||||
getLogger(s.args.ctx).WithError(err).WithField("pre_state", preState).WithField("post_state", s.state).Error("snapshotting error")
|
||||
}).sf()
|
||||
}
|
||||
|
||||
func onMainCtxDone(ctx context.Context, u updater) state {
|
||||
return u(func(s *Periodic) {
|
||||
s.err = ctx.Err()
|
||||
s.state = Stopped
|
||||
}).sf()
|
||||
}
|
||||
|
||||
func periodicStateSyncUp(a periodicArgs, u updater) state {
|
||||
u(func(snapper *Periodic) {
|
||||
snapper.lastInvocation = time.Now()
|
||||
})
|
||||
fss, err := listFSes(a.ctx, a.fsf)
|
||||
if err != nil {
|
||||
return onErr(err, u)
|
||||
}
|
||||
syncPoint, err := findSyncPoint(a.ctx, fss, a.planArgs.prefix, a.interval)
|
||||
if err != nil {
|
||||
return onErr(err, u)
|
||||
}
|
||||
u(func(s *Periodic) {
|
||||
s.sleepUntil = syncPoint
|
||||
})
|
||||
ctxDone := suspendresumesafetimer.SleepUntil(a.ctx, syncPoint)
|
||||
if ctxDone != nil {
|
||||
return onMainCtxDone(a.ctx, u)
|
||||
}
|
||||
return u(func(s *Periodic) {
|
||||
s.state = Planning
|
||||
}).sf()
|
||||
}
|
||||
|
||||
func periodicStatePlan(a periodicArgs, u updater) state {
|
||||
u(func(snapper *Periodic) {
|
||||
snapper.lastInvocation = time.Now()
|
||||
})
|
||||
fss, err := listFSes(a.ctx, a.fsf)
|
||||
if err != nil {
|
||||
return onErr(err, u)
|
||||
}
|
||||
p := makePlan(a.planArgs, fss)
|
||||
return u(func(s *Periodic) {
|
||||
s.state = Snapshotting
|
||||
s.plan = p
|
||||
s.err = nil
|
||||
}).sf()
|
||||
}
|
||||
|
||||
func periodicStateSnapshot(a periodicArgs, u updater) state {
|
||||
|
||||
var plan *plan
|
||||
u(func(snapper *Periodic) {
|
||||
plan = snapper.plan
|
||||
})
|
||||
|
||||
ok := plan.execute(a.ctx, false)
|
||||
|
||||
select {
|
||||
case a.snapshotsTaken <- struct{}{}:
|
||||
default:
|
||||
if a.snapshotsTaken != nil {
|
||||
getLogger(a.ctx).Warn("callback channel is full, discarding snapshot update event")
|
||||
}
|
||||
}
|
||||
|
||||
return u(func(snapper *Periodic) {
|
||||
if !ok {
|
||||
snapper.state = ErrorWait
|
||||
snapper.err = errors.New("one or more snapshots could not be created, check logs for details")
|
||||
} else {
|
||||
snapper.state = Waiting
|
||||
snapper.err = nil
|
||||
}
|
||||
}).sf()
|
||||
}
|
||||
|
||||
func periodicStateWait(a periodicArgs, u updater) state {
|
||||
var sleepUntil time.Time
|
||||
u(func(snapper *Periodic) {
|
||||
lastTick := snapper.lastInvocation
|
||||
snapper.sleepUntil = lastTick.Add(a.interval)
|
||||
sleepUntil = snapper.sleepUntil
|
||||
log := getLogger(a.ctx).WithField("sleep_until", sleepUntil).WithField("duration", a.interval)
|
||||
logFunc := log.Debug
|
||||
if snapper.state == ErrorWait || snapper.state == SyncUpErrWait {
|
||||
logFunc = log.Error
|
||||
}
|
||||
logFunc("enter wait-state after error")
|
||||
})
|
||||
|
||||
ctxDone := suspendresumesafetimer.SleepUntil(a.ctx, sleepUntil)
|
||||
if ctxDone != nil {
|
||||
return onMainCtxDone(a.ctx, u)
|
||||
}
|
||||
return u(func(snapper *Periodic) {
|
||||
snapper.state = Planning
|
||||
}).sf()
|
||||
}
|
||||
|
||||
func listFSes(ctx context.Context, mf zfs.DatasetFilter) (fss []*zfs.DatasetPath, err error) {
|
||||
return zfs.ZFSListMapping(ctx, mf)
|
||||
}
|
||||
|
||||
var syncUpWarnNoSnapshotUntilSyncupMinDuration = envconst.Duration("ZREPL_SNAPPER_SYNCUP_WARN_MIN_DURATION", 1*time.Second)
|
||||
|
||||
// see docs/snapshotting.rst
|
||||
func findSyncPoint(ctx context.Context, fss []*zfs.DatasetPath, prefix string, interval time.Duration) (syncPoint time.Time, err error) {
|
||||
|
||||
const (
|
||||
prioHasVersions int = iota
|
||||
prioNoVersions
|
||||
)
|
||||
|
||||
type snapTime struct {
|
||||
ds *zfs.DatasetPath
|
||||
prio int // lower is higher
|
||||
time time.Time
|
||||
}
|
||||
|
||||
if len(fss) == 0 {
|
||||
return time.Now(), nil
|
||||
}
|
||||
|
||||
snaptimes := make([]snapTime, 0, len(fss))
|
||||
hardErrs := 0
|
||||
|
||||
now := time.Now()
|
||||
|
||||
getLogger(ctx).Debug("examine filesystem state to find sync point")
|
||||
for _, d := range fss {
|
||||
ctx := logging.WithInjectedField(ctx, "fs", d.ToString())
|
||||
syncPoint, err := findSyncPointFSNextOptimalSnapshotTime(ctx, now, interval, prefix, d)
|
||||
if err == findSyncPointFSNoFilesystemVersionsErr {
|
||||
snaptimes = append(snaptimes, snapTime{
|
||||
ds: d,
|
||||
prio: prioNoVersions,
|
||||
time: now,
|
||||
})
|
||||
} else if err != nil {
|
||||
hardErrs++
|
||||
getLogger(ctx).WithError(err).Error("cannot determine optimal sync point for this filesystem")
|
||||
} else {
|
||||
getLogger(ctx).WithField("syncPoint", syncPoint).Debug("found optimal sync point for this filesystem")
|
||||
snaptimes = append(snaptimes, snapTime{
|
||||
ds: d,
|
||||
prio: prioHasVersions,
|
||||
time: syncPoint,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if hardErrs == len(fss) {
|
||||
return time.Time{}, fmt.Errorf("hard errors in determining sync point for every matching filesystem")
|
||||
}
|
||||
|
||||
if len(snaptimes) == 0 {
|
||||
panic("implementation error: loop must either inc hardErrs or add result to snaptimes")
|
||||
}
|
||||
|
||||
// sort ascending by (prio,time)
|
||||
// => those filesystems with versions win over those without any
|
||||
sort.Slice(snaptimes, func(i, j int) bool {
|
||||
if snaptimes[i].prio == snaptimes[j].prio {
|
||||
return snaptimes[i].time.Before(snaptimes[j].time)
|
||||
}
|
||||
return snaptimes[i].prio < snaptimes[j].prio
|
||||
})
|
||||
|
||||
winnerSyncPoint := snaptimes[0].time
|
||||
l := getLogger(ctx).WithField("syncPoint", winnerSyncPoint.String())
|
||||
l.Info("determined sync point")
|
||||
if winnerSyncPoint.Sub(now) > syncUpWarnNoSnapshotUntilSyncupMinDuration {
|
||||
for _, st := range snaptimes {
|
||||
if st.prio == prioNoVersions {
|
||||
l.WithField("fs", st.ds.ToString()).Warn("filesystem will not be snapshotted until sync point")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return snaptimes[0].time, nil
|
||||
|
||||
}
|
||||
|
||||
var findSyncPointFSNoFilesystemVersionsErr = fmt.Errorf("no filesystem versions")
|
||||
|
||||
func findSyncPointFSNextOptimalSnapshotTime(ctx context.Context, now time.Time, interval time.Duration, prefix string, d *zfs.DatasetPath) (time.Time, error) {
|
||||
|
||||
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, d, zfs.ListFilesystemVersionsOptions{
|
||||
Types: zfs.Snapshots,
|
||||
ShortnamePrefix: prefix,
|
||||
})
|
||||
if err != nil {
|
||||
return time.Time{}, errors.Wrap(err, "list filesystem versions")
|
||||
}
|
||||
if len(fsvs) <= 0 {
|
||||
return time.Time{}, findSyncPointFSNoFilesystemVersionsErr
|
||||
}
|
||||
|
||||
// Sort versions by creation
|
||||
sort.SliceStable(fsvs, func(i, j int) bool {
|
||||
return fsvs[i].CreateTXG < fsvs[j].CreateTXG
|
||||
})
|
||||
|
||||
latest := fsvs[len(fsvs)-1]
|
||||
getLogger(ctx).WithField("creation", latest.Creation).Debug("found latest snapshot")
|
||||
|
||||
since := now.Sub(latest.Creation)
|
||||
if since < 0 {
|
||||
return time.Time{}, fmt.Errorf("snapshot %q is from the future: creation=%q now=%q", latest.ToAbsPath(d), latest.Creation, now)
|
||||
}
|
||||
|
||||
return latest.Creation.Add(interval), nil
|
||||
}
|
||||
|
||||
type PeriodicReport struct {
|
||||
State State
|
||||
// valid in state SyncUp and Waiting
|
||||
SleepUntil time.Time
|
||||
// valid in state Err
|
||||
Error string
|
||||
// valid in state Snapshotting
|
||||
Progress []*ReportFilesystem
|
||||
}
|
||||
|
||||
func (s *Periodic) Report() Report {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
|
||||
var progress []*ReportFilesystem = nil
|
||||
if s.plan != nil {
|
||||
progress = s.plan.report()
|
||||
}
|
||||
|
||||
r := &PeriodicReport{
|
||||
State: s.state,
|
||||
SleepUntil: s.sleepUntil,
|
||||
Error: errOrEmptyString(s.err),
|
||||
Progress: progress,
|
||||
}
|
||||
|
||||
return Report{Type: TypePeriodic, Periodic: r}
|
||||
}
|
||||
+21
-486
@@ -3,505 +3,40 @@ package snapper
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/daemon/hooks"
|
||||
"github.com/zrepl/zrepl/daemon/logging"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
//go:generate stringer -type=SnapState
|
||||
type SnapState uint
|
||||
type Type string
|
||||
|
||||
const (
|
||||
SnapPending SnapState = 1 << iota
|
||||
SnapStarted
|
||||
SnapDone
|
||||
SnapError
|
||||
TypePeriodic Type = "periodic"
|
||||
TypeCron Type = "cron"
|
||||
TypeManual Type = "manual"
|
||||
)
|
||||
|
||||
// All fields protected by Snapper.mtx
|
||||
type snapProgress struct {
|
||||
state SnapState
|
||||
|
||||
// SnapStarted, SnapDone, SnapError
|
||||
name string
|
||||
startAt time.Time
|
||||
hookPlan *hooks.Plan
|
||||
|
||||
// SnapDone
|
||||
doneAt time.Time
|
||||
|
||||
// SnapErr TODO disambiguate state
|
||||
runResults hooks.PlanReport
|
||||
type Snapper interface {
|
||||
Run(ctx context.Context, snapshotsTaken chan<- struct{})
|
||||
Report() Report
|
||||
}
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
prefix string
|
||||
interval time.Duration
|
||||
fsf zfs.DatasetFilter
|
||||
snapshotsTaken chan<- struct{}
|
||||
hooks *hooks.List
|
||||
dryRun bool
|
||||
type Report struct {
|
||||
Type Type
|
||||
Periodic *PeriodicReport
|
||||
Cron *CronReport
|
||||
Manual *struct{}
|
||||
}
|
||||
|
||||
type Snapper struct {
|
||||
args args
|
||||
|
||||
mtx sync.Mutex
|
||||
state State
|
||||
|
||||
// set in state Plan, used in Waiting
|
||||
lastInvocation time.Time
|
||||
|
||||
// valid for state Snapshotting
|
||||
plan map[*zfs.DatasetPath]*snapProgress
|
||||
|
||||
// valid for state SyncUp and Waiting
|
||||
sleepUntil time.Time
|
||||
|
||||
// valid for state Err
|
||||
err error
|
||||
}
|
||||
|
||||
//go:generate stringer -type=State
|
||||
type State uint
|
||||
|
||||
const (
|
||||
SyncUp State = 1 << iota
|
||||
SyncUpErrWait
|
||||
Planning
|
||||
Snapshotting
|
||||
Waiting
|
||||
ErrorWait
|
||||
Stopped
|
||||
)
|
||||
|
||||
func (s State) sf() state {
|
||||
m := map[State]state{
|
||||
SyncUp: syncUp,
|
||||
SyncUpErrWait: wait,
|
||||
Planning: plan,
|
||||
Snapshotting: snapshot,
|
||||
Waiting: wait,
|
||||
ErrorWait: wait,
|
||||
Stopped: nil,
|
||||
}
|
||||
return m[s]
|
||||
}
|
||||
|
||||
type updater func(u func(*Snapper)) State
|
||||
type state func(a args, u updater) state
|
||||
|
||||
type Logger = logger.Logger
|
||||
|
||||
func getLogger(ctx context.Context) Logger {
|
||||
return logging.GetLogger(ctx, logging.SubsysSnapshot)
|
||||
}
|
||||
|
||||
func PeriodicFromConfig(g *config.Global, fsf zfs.DatasetFilter, in *config.SnapshottingPeriodic) (*Snapper, error) {
|
||||
if in.Prefix == "" {
|
||||
return nil, errors.New("prefix must not be empty")
|
||||
}
|
||||
if in.Interval <= 0 {
|
||||
return nil, errors.New("interval must be positive")
|
||||
}
|
||||
|
||||
hookList, err := hooks.ListFromConfig(&in.Hooks)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "hook config error")
|
||||
}
|
||||
|
||||
args := args{
|
||||
prefix: in.Prefix,
|
||||
interval: in.Interval,
|
||||
fsf: fsf,
|
||||
hooks: hookList,
|
||||
// ctx and log is set in Run()
|
||||
}
|
||||
|
||||
return &Snapper{state: SyncUp, args: args}, nil
|
||||
}
|
||||
|
||||
func (s *Snapper) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
||||
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
|
||||
getLogger(ctx).Debug("start")
|
||||
defer getLogger(ctx).Debug("stop")
|
||||
|
||||
s.args.snapshotsTaken = snapshotsTaken
|
||||
s.args.ctx = ctx
|
||||
s.args.dryRun = false // for future expansion
|
||||
|
||||
u := func(u func(*Snapper)) State {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
if u != nil {
|
||||
u(s)
|
||||
}
|
||||
return s.state
|
||||
}
|
||||
|
||||
var st state = syncUp
|
||||
|
||||
for st != nil {
|
||||
pre := u(nil)
|
||||
st = st(s.args, u)
|
||||
post := u(nil)
|
||||
getLogger(ctx).
|
||||
WithField("transition", fmt.Sprintf("%s=>%s", pre, post)).
|
||||
Debug("state transition")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func onErr(err error, u updater) state {
|
||||
return u(func(s *Snapper) {
|
||||
s.err = err
|
||||
preState := s.state
|
||||
switch s.state {
|
||||
case SyncUp:
|
||||
s.state = SyncUpErrWait
|
||||
case Planning:
|
||||
fallthrough
|
||||
case Snapshotting:
|
||||
s.state = ErrorWait
|
||||
}
|
||||
getLogger(s.args.ctx).WithError(err).WithField("pre_state", preState).WithField("post_state", s.state).Error("snapshotting error")
|
||||
}).sf()
|
||||
}
|
||||
|
||||
func onMainCtxDone(ctx context.Context, u updater) state {
|
||||
return u(func(s *Snapper) {
|
||||
s.err = ctx.Err()
|
||||
s.state = Stopped
|
||||
}).sf()
|
||||
}
|
||||
|
||||
func syncUp(a args, u updater) state {
|
||||
u(func(snapper *Snapper) {
|
||||
snapper.lastInvocation = time.Now()
|
||||
})
|
||||
fss, err := listFSes(a.ctx, a.fsf)
|
||||
if err != nil {
|
||||
return onErr(err, u)
|
||||
}
|
||||
syncPoint, err := findSyncPoint(a.ctx, fss, a.prefix, a.interval)
|
||||
if err != nil {
|
||||
return onErr(err, u)
|
||||
}
|
||||
u(func(s *Snapper) {
|
||||
s.sleepUntil = syncPoint
|
||||
})
|
||||
|
||||
return waitUntilThenTransitionToPlanning(a, u, syncPoint)
|
||||
}
|
||||
|
||||
func plan(a args, u updater) state {
|
||||
u(func(snapper *Snapper) {
|
||||
snapper.lastInvocation = time.Now()
|
||||
})
|
||||
fss, err := listFSes(a.ctx, a.fsf)
|
||||
if err != nil {
|
||||
return onErr(err, u)
|
||||
}
|
||||
|
||||
plan := make(map[*zfs.DatasetPath]*snapProgress, len(fss))
|
||||
for _, fs := range fss {
|
||||
plan[fs] = &snapProgress{state: SnapPending}
|
||||
}
|
||||
return u(func(s *Snapper) {
|
||||
s.state = Snapshotting
|
||||
s.plan = plan
|
||||
s.err = nil
|
||||
}).sf()
|
||||
}
|
||||
|
||||
func snapshot(a args, u updater) state {
|
||||
|
||||
var plan map[*zfs.DatasetPath]*snapProgress
|
||||
u(func(snapper *Snapper) {
|
||||
plan = snapper.plan
|
||||
})
|
||||
|
||||
hookMatchCount := make(map[hooks.Hook]int, len(*a.hooks))
|
||||
for _, h := range *a.hooks {
|
||||
hookMatchCount[h] = 0
|
||||
}
|
||||
|
||||
anyFsHadErr := false
|
||||
// TODO channel programs -> allow a little jitter?
|
||||
for fs, progress := range plan {
|
||||
suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
|
||||
snapname := fmt.Sprintf("%s%s", a.prefix, suffix)
|
||||
|
||||
ctx := logging.WithInjectedField(a.ctx, "fs", fs.ToString())
|
||||
ctx = logging.WithInjectedField(ctx, "snap", snapname)
|
||||
|
||||
hookEnvExtra := hooks.Env{
|
||||
hooks.EnvFS: fs.ToString(),
|
||||
hooks.EnvSnapshot: snapname,
|
||||
}
|
||||
|
||||
jobCallback := hooks.NewCallbackHookForFilesystem("snapshot", fs, func(ctx context.Context) (err error) {
|
||||
l := getLogger(ctx)
|
||||
l.Debug("create snapshot")
|
||||
err = zfs.ZFSSnapshot(ctx, fs, snapname, false) // TODO propagate context to ZFSSnapshot
|
||||
if err != nil {
|
||||
l.WithError(err).Error("cannot create snapshot")
|
||||
}
|
||||
return
|
||||
})
|
||||
|
||||
fsHadErr := false
|
||||
var planReport hooks.PlanReport
|
||||
var plan *hooks.Plan
|
||||
{
|
||||
filteredHooks, err := a.hooks.CopyFilteredForFilesystem(fs)
|
||||
if err != nil {
|
||||
getLogger(ctx).WithError(err).Error("unexpected filter error")
|
||||
fsHadErr = true
|
||||
goto updateFSState
|
||||
}
|
||||
// account for running hooks
|
||||
for _, h := range filteredHooks {
|
||||
hookMatchCount[h] = hookMatchCount[h] + 1
|
||||
}
|
||||
|
||||
var planErr error
|
||||
plan, planErr = hooks.NewPlan(&filteredHooks, hooks.PhaseSnapshot, jobCallback, hookEnvExtra)
|
||||
if planErr != nil {
|
||||
fsHadErr = true
|
||||
getLogger(ctx).WithError(planErr).Error("cannot create job hook plan")
|
||||
goto updateFSState
|
||||
}
|
||||
}
|
||||
u(func(snapper *Snapper) {
|
||||
progress.name = snapname
|
||||
progress.startAt = time.Now()
|
||||
progress.hookPlan = plan
|
||||
progress.state = SnapStarted
|
||||
})
|
||||
{
|
||||
getLogger(ctx).WithField("report", plan.Report().String()).Debug("begin run job plan")
|
||||
plan.Run(ctx, a.dryRun)
|
||||
planReport = plan.Report()
|
||||
fsHadErr = planReport.HadError() // not just fatal errors
|
||||
if fsHadErr {
|
||||
getLogger(ctx).WithField("report", planReport.String()).Error("end run job plan with error")
|
||||
} else {
|
||||
getLogger(ctx).WithField("report", planReport.String()).Info("end run job plan successful")
|
||||
}
|
||||
}
|
||||
|
||||
updateFSState:
|
||||
anyFsHadErr = anyFsHadErr || fsHadErr
|
||||
u(func(snapper *Snapper) {
|
||||
progress.doneAt = time.Now()
|
||||
progress.state = SnapDone
|
||||
if fsHadErr {
|
||||
progress.state = SnapError
|
||||
}
|
||||
progress.runResults = planReport
|
||||
})
|
||||
}
|
||||
|
||||
select {
|
||||
case a.snapshotsTaken <- struct{}{}:
|
||||
func FromConfig(g *config.Global, fsf zfs.DatasetFilter, in config.SnapshottingEnum) (Snapper, error) {
|
||||
switch v := in.Ret.(type) {
|
||||
case *config.SnapshottingPeriodic:
|
||||
return periodicFromConfig(g, fsf, v)
|
||||
case *config.SnapshottingCron:
|
||||
return cronFromConfig(fsf, *v)
|
||||
case *config.SnapshottingManual:
|
||||
return &manual{}, nil
|
||||
default:
|
||||
if a.snapshotsTaken != nil {
|
||||
getLogger(a.ctx).Warn("callback channel is full, discarding snapshot update event")
|
||||
}
|
||||
}
|
||||
|
||||
for h, mc := range hookMatchCount {
|
||||
if mc == 0 {
|
||||
hookIdx := -1
|
||||
for idx, ah := range *a.hooks {
|
||||
if ah == h {
|
||||
hookIdx = idx
|
||||
break
|
||||
}
|
||||
}
|
||||
getLogger(a.ctx).WithField("hook", h.String()).WithField("hook_number", hookIdx+1).Warn("hook did not match any snapshotted filesystems")
|
||||
}
|
||||
}
|
||||
|
||||
return u(func(snapper *Snapper) {
|
||||
if anyFsHadErr {
|
||||
snapper.state = ErrorWait
|
||||
snapper.err = errors.New("one or more snapshots could not be created, check logs for details")
|
||||
} else {
|
||||
snapper.state = Waiting
|
||||
snapper.err = nil
|
||||
}
|
||||
}).sf()
|
||||
}
|
||||
|
||||
func wait(a args, u updater) state {
|
||||
var sleepUntil time.Time
|
||||
u(func(snapper *Snapper) {
|
||||
lastTick := snapper.lastInvocation
|
||||
snapper.sleepUntil = lastTick.Add(a.interval)
|
||||
sleepUntil = snapper.sleepUntil
|
||||
log := getLogger(a.ctx).WithField("sleep_until", sleepUntil).WithField("duration", a.interval)
|
||||
logFunc := log.Debug
|
||||
if snapper.state == ErrorWait || snapper.state == SyncUpErrWait {
|
||||
logFunc = log.Error
|
||||
}
|
||||
logFunc("enter wait-state after error")
|
||||
})
|
||||
|
||||
return waitUntilThenTransitionToPlanning(a, u, sleepUntil)
|
||||
}
|
||||
|
||||
func waitUntilThenTransitionToPlanning(a args, u updater, sleepUntil time.Time) state {
|
||||
|
||||
ticker := time.NewTicker(333 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
lastLog := time.Now()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
now := time.Now()
|
||||
if now.Before(sleepUntil) {
|
||||
// rate limit loggin to once per minute
|
||||
if (now.Sub(lastLog)) > envconst.Duration("ZREPL_SNAPPER_DEBUG_LOG_ISSUE_611", 1*time.Minute) {
|
||||
getLogger(a.ctx).WithField("sleep_until", sleepUntil).WithField("remaining", sleepUntil.Sub(now)).Error("DEBUG-LOG-ZREPL-ISSUE-611")
|
||||
lastLog = now
|
||||
}
|
||||
continue // continue with ticker
|
||||
}
|
||||
return u(func(snapper *Snapper) {
|
||||
snapper.state = Planning
|
||||
}).sf()
|
||||
case <-a.ctx.Done():
|
||||
return onMainCtxDone(a.ctx, u)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown snapshotting type %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
func listFSes(ctx context.Context, mf zfs.DatasetFilter) (fss []*zfs.DatasetPath, err error) {
|
||||
return zfs.ZFSListMapping(ctx, mf)
|
||||
}
|
||||
|
||||
var syncUpWarnNoSnapshotUntilSyncupMinDuration = envconst.Duration("ZREPL_SNAPPER_SYNCUP_WARN_MIN_DURATION", 1*time.Second)
|
||||
|
||||
// see docs/snapshotting.rst
|
||||
func findSyncPoint(ctx context.Context, fss []*zfs.DatasetPath, prefix string, interval time.Duration) (syncPoint time.Time, err error) {
|
||||
|
||||
const (
|
||||
prioHasVersions int = iota
|
||||
prioNoVersions
|
||||
)
|
||||
|
||||
type snapTime struct {
|
||||
ds *zfs.DatasetPath
|
||||
prio int // lower is higher
|
||||
time time.Time
|
||||
}
|
||||
|
||||
if len(fss) == 0 {
|
||||
return time.Now(), nil
|
||||
}
|
||||
|
||||
snaptimes := make([]snapTime, 0, len(fss))
|
||||
hardErrs := 0
|
||||
|
||||
now := time.Now()
|
||||
|
||||
getLogger(ctx).Debug("examine filesystem state to find sync point")
|
||||
for _, d := range fss {
|
||||
ctx := logging.WithInjectedField(ctx, "fs", d.ToString())
|
||||
syncPoint, err := findSyncPointFSNextOptimalSnapshotTime(ctx, now, interval, prefix, d)
|
||||
if err == findSyncPointFSNoFilesystemVersionsErr {
|
||||
snaptimes = append(snaptimes, snapTime{
|
||||
ds: d,
|
||||
prio: prioNoVersions,
|
||||
time: now,
|
||||
})
|
||||
} else if err != nil {
|
||||
hardErrs++
|
||||
getLogger(ctx).WithError(err).Error("cannot determine optimal sync point for this filesystem")
|
||||
} else {
|
||||
getLogger(ctx).WithField("syncPoint", syncPoint).Debug("found optimal sync point for this filesystem")
|
||||
snaptimes = append(snaptimes, snapTime{
|
||||
ds: d,
|
||||
prio: prioHasVersions,
|
||||
time: syncPoint,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if hardErrs == len(fss) {
|
||||
return time.Time{}, fmt.Errorf("hard errors in determining sync point for every matching filesystem")
|
||||
}
|
||||
|
||||
if len(snaptimes) == 0 {
|
||||
panic("implementation error: loop must either inc hardErrs or add result to snaptimes")
|
||||
}
|
||||
|
||||
// sort ascending by (prio,time)
|
||||
// => those filesystems with versions win over those without any
|
||||
sort.Slice(snaptimes, func(i, j int) bool {
|
||||
if snaptimes[i].prio == snaptimes[j].prio {
|
||||
return snaptimes[i].time.Before(snaptimes[j].time)
|
||||
}
|
||||
return snaptimes[i].prio < snaptimes[j].prio
|
||||
})
|
||||
|
||||
winnerSyncPoint := snaptimes[0].time
|
||||
l := getLogger(ctx).WithField("syncPoint", winnerSyncPoint.String())
|
||||
l.Info("determined sync point")
|
||||
if winnerSyncPoint.Sub(now) > syncUpWarnNoSnapshotUntilSyncupMinDuration {
|
||||
for _, st := range snaptimes {
|
||||
if st.prio == prioNoVersions {
|
||||
l.WithField("fs", st.ds.ToString()).Warn("filesystem will not be snapshotted until sync point")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return snaptimes[0].time, nil
|
||||
|
||||
}
|
||||
|
||||
var findSyncPointFSNoFilesystemVersionsErr = fmt.Errorf("no filesystem versions")
|
||||
|
||||
func findSyncPointFSNextOptimalSnapshotTime(ctx context.Context, now time.Time, interval time.Duration, prefix string, d *zfs.DatasetPath) (time.Time, error) {
|
||||
|
||||
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, d, zfs.ListFilesystemVersionsOptions{
|
||||
Types: zfs.Snapshots,
|
||||
ShortnamePrefix: prefix,
|
||||
})
|
||||
if err != nil {
|
||||
return time.Time{}, errors.Wrap(err, "list filesystem versions")
|
||||
}
|
||||
if len(fsvs) <= 0 {
|
||||
return time.Time{}, findSyncPointFSNoFilesystemVersionsErr
|
||||
}
|
||||
|
||||
// Sort versions by creation
|
||||
sort.SliceStable(fsvs, func(i, j int) bool {
|
||||
return fsvs[i].CreateTXG < fsvs[j].CreateTXG
|
||||
})
|
||||
|
||||
latest := fsvs[len(fsvs)-1]
|
||||
getLogger(ctx).WithField("creation", latest.Creation).Debug("found latest snapshot")
|
||||
|
||||
since := now.Sub(latest.Creation)
|
||||
if since < 0 {
|
||||
return time.Time{}, fmt.Errorf("snapshot %q is from the future: creation=%q now=%q", latest.ToAbsPath(d), latest.Creation, now)
|
||||
}
|
||||
|
||||
return latest.Creation.Add(interval), nil
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package snapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/zrepl/zrepl/config"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
|
||||
// FIXME: properly abstract snapshotting:
|
||||
// - split up things that trigger snapshotting from the mechanism
|
||||
// - timer-based trigger (periodic)
|
||||
// - call from control socket (manual)
|
||||
// - mixed modes?
|
||||
// - support a `zrepl snapshot JOBNAME` subcommand for config.SnapshottingManual
|
||||
type PeriodicOrManual struct {
|
||||
s *Snapper
|
||||
}
|
||||
|
||||
func (s *PeriodicOrManual) Run(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
||||
if s.s != nil {
|
||||
s.s.Run(ctx, wakeUpCommon)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns nil if manual
|
||||
func (s *PeriodicOrManual) Report() *Report {
|
||||
if s.s != nil {
|
||||
return s.s.Report()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func FromConfig(g *config.Global, fsf zfs.DatasetFilter, in config.SnapshottingEnum) (*PeriodicOrManual, error) {
|
||||
switch v := in.Ret.(type) {
|
||||
case *config.SnapshottingPeriodic:
|
||||
snapper, err := PeriodicFromConfig(g, fsf, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PeriodicOrManual{snapper}, nil
|
||||
case *config.SnapshottingManual:
|
||||
return &PeriodicOrManual{}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown snapshotting type %T", v)
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
package snapper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/hooks"
|
||||
)
|
||||
|
||||
type Report struct {
|
||||
State State
|
||||
// valid in state SyncUp and Waiting
|
||||
SleepUntil time.Time
|
||||
// valid in state Err
|
||||
Error string
|
||||
// valid in state Snapshotting
|
||||
Progress []*ReportFilesystem
|
||||
}
|
||||
|
||||
type ReportFilesystem struct {
|
||||
Path string
|
||||
State SnapState
|
||||
|
||||
// Valid in SnapStarted and later
|
||||
SnapName string
|
||||
StartAt time.Time
|
||||
Hooks string
|
||||
HooksHadError bool
|
||||
|
||||
// Valid in SnapDone | SnapError
|
||||
DoneAt time.Time
|
||||
}
|
||||
|
||||
func errOrEmptyString(e error) string {
|
||||
if e != nil {
|
||||
return e.Error()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *Snapper) Report() *Report {
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
|
||||
pReps := make([]*ReportFilesystem, 0, len(s.plan))
|
||||
for fs, p := range s.plan {
|
||||
var hooksStr string
|
||||
var hooksHadError bool
|
||||
if p.hookPlan != nil {
|
||||
hr := p.hookPlan.Report()
|
||||
// FIXME: technically this belongs into client
|
||||
// but we can't serialize hooks.Step ATM
|
||||
rightPad := func(str string, length int, pad string) string {
|
||||
if len(str) > length {
|
||||
return str[:length]
|
||||
}
|
||||
return str + strings.Repeat(pad, length-len(str))
|
||||
}
|
||||
hooksHadError = hr.HadError()
|
||||
rows := make([][]string, len(hr))
|
||||
const numCols = 4
|
||||
lens := make([]int, numCols)
|
||||
for i, e := range hr {
|
||||
rows[i] = make([]string, numCols)
|
||||
rows[i][0] = fmt.Sprintf("%d", i+1)
|
||||
rows[i][1] = e.Status.String()
|
||||
runTime := "..."
|
||||
if e.Status != hooks.StepPending {
|
||||
runTime = e.End.Sub(e.Begin).Round(time.Millisecond).String()
|
||||
}
|
||||
rows[i][2] = runTime
|
||||
rows[i][3] = ""
|
||||
if e.Report != nil {
|
||||
rows[i][3] = e.Report.String()
|
||||
}
|
||||
for j, col := range lens {
|
||||
if len(rows[i][j]) > col {
|
||||
lens[j] = len(rows[i][j])
|
||||
}
|
||||
}
|
||||
}
|
||||
rowsFlat := make([]string, len(hr))
|
||||
for i, r := range rows {
|
||||
colsPadded := make([]string, len(r))
|
||||
for j, c := range r[:len(r)-1] {
|
||||
colsPadded[j] = rightPad(c, lens[j], " ")
|
||||
}
|
||||
colsPadded[len(r)-1] = r[len(r)-1]
|
||||
rowsFlat[i] = strings.Join(colsPadded, " ")
|
||||
}
|
||||
hooksStr = strings.Join(rowsFlat, "\n")
|
||||
}
|
||||
pReps = append(pReps, &ReportFilesystem{
|
||||
Path: fs.ToString(),
|
||||
State: p.state,
|
||||
SnapName: p.name,
|
||||
StartAt: p.startAt,
|
||||
DoneAt: p.doneAt,
|
||||
Hooks: hooksStr,
|
||||
HooksHadError: hooksHadError,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(pReps, func(i, j int) bool {
|
||||
return strings.Compare(pReps[i].Path, pReps[j].Path) == -1
|
||||
})
|
||||
|
||||
r := &Report{
|
||||
State: s.state,
|
||||
SleepUntil: s.sleepUntil,
|
||||
Error: errOrEmptyString(s.err),
|
||||
Progress: pReps,
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package snapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zrepl/zrepl/daemon/logging"
|
||||
"github.com/zrepl/zrepl/logger"
|
||||
)
|
||||
|
||||
type Logger = logger.Logger
|
||||
|
||||
func getLogger(ctx context.Context) Logger {
|
||||
return logging.GetLogger(ctx, logging.SubsysSnapshot)
|
||||
}
|
||||
|
||||
func errOrEmptyString(e error) string {
|
||||
if e != nil {
|
||||
return e.Error()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
+2
-2
@@ -1235,7 +1235,7 @@
|
||||
]
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "zrepl 0.3",
|
||||
"title": "zrepl",
|
||||
"uid": "etQuvBnGz",
|
||||
"version": 7
|
||||
"version": 8
|
||||
}
|
||||
|
||||
Vendored
+5
-1
@@ -9,6 +9,9 @@ ExecStart=/usr/local/bin/zrepl --config /etc/zrepl/zrepl.yml daemon
|
||||
RuntimeDirectory=zrepl zrepl/stdinserver
|
||||
RuntimeDirectoryMode=0700
|
||||
|
||||
# Make Go produce coredumps
|
||||
Environment=GOTRACEBACK='crash'
|
||||
|
||||
ProtectSystem=strict
|
||||
#PrivateDevices=yes # TODO ZFS needs access to /dev/zfs, could we limit this?
|
||||
ProtectKernelTunables=yes
|
||||
@@ -27,7 +30,8 @@ ProtectHome=read-only
|
||||
# SystemCallFilter
|
||||
# ~@privileged doesn't work with Ubuntu 18.04 ssh
|
||||
SystemCallFilter=~ @mount @cpu-emulation @keyring @module @obsolete @raw-io @debug @clock @resources
|
||||
|
||||
# Go1.19 added automatic RLIMIT_NOFILE changes, so, we need to allow that
|
||||
SystemCallFilter= setrlimit
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
+55
-5
@@ -16,19 +16,69 @@ Changelog
|
||||
The changelog summarizes bugfixes that are deemed relevant for users and package maintainers.
|
||||
Developers should consult the git commit log or GitHub issue tracker.
|
||||
|
||||
0.6 (Unreleased)
|
||||
----------------
|
||||
Next Release
|
||||
------------
|
||||
|
||||
* `Feature Wishlist on GitHub <https://github.com/zrepl/zrepl/discussions/547>`_
|
||||
The plan for the next release is to revisit how zrepl does snapshot management.
|
||||
High-level goals:
|
||||
|
||||
* |feature| Add ``ZREPL_DESTROY_MAX_BATCH_SIZE`` env var (default 0=unlimited).
|
||||
* |break| |feature| convert Prometheus metric ``zrepl_version_daemon`` to ``zrepl_start_time`` metric
|
||||
- Make it easy to decouple snapshot management (snapshotting, pruning) from replication.
|
||||
- Ability to include/exclude snapshots from replication.
|
||||
This is useful for aforementioned decoupling, e.g., separate snapshot prefixes for local & remote replication.
|
||||
Also, it makes explicit that by default, zrepl replicates all snapshots, and that
|
||||
replication has no concept of "zrepl-created snapshots", which is a common misconception.
|
||||
- Use of ``zfs snapshot`` comma syntax or channel programs to take snapshots of multiple
|
||||
datasets atomically.
|
||||
- Provide an alternative to the ``grid`` pruning policy.
|
||||
Most likely something based on hourly/daily/weekly/monthly "trains" plus a count.
|
||||
- Ability to prune at the granularity of the **group** of snapshots created at a given
|
||||
time, as opposed to the individual snapshots within a dataset.
|
||||
Maybe this will be addressed by the alternative to the ``grid`` pruning policy,
|
||||
as it will likely be more predictable.
|
||||
|
||||
Those changes will likely come with some breakage in the config.
|
||||
However, I want to avoid breaking **use cases** that are satisfied by the current design.
|
||||
There will be beta/RC releases to give users a chance to evaluate.
|
||||
|
||||
0.6
|
||||
---
|
||||
|
||||
* |feature| :ref:`Schedule-based snapshotting<job-snapshotting--cron>` using ``cron`` syntax instead of an interval.
|
||||
* |feature| Configurable initial replication policy.
|
||||
When a filesystem is first replicated to a receiver, this control whether just the newest
|
||||
snapshot will be replicated vs. all existing snapshots. Learn more :ref:`in the docs <conflict_resolution-initial_replication>`.
|
||||
* |feature| Configurable timestamp format for snapshot names via :ref:`timestamp_format<job-snapshotting-timestamp_format>`
|
||||
(Thanks, `@ydylla <https://github.com/ydylla>`_).
|
||||
* |feature| Add ``ZREPL_DESTROY_MAX_BATCH_SIZE`` env var (default 0=unlimited)
|
||||
(Thanks, `@3nprob <https://github.com/3nprob>`_).
|
||||
* |feature| Add ``zrepl configcheck --skip-cert-check`` flag (Thanks, `@cole-h <https://github.com/cole-h>`_).
|
||||
* |bugfix| Fix resuming from interrupted replications that use ``send.raw`` on unencrypted datasets.
|
||||
|
||||
* The send options introduced in zrepl 0.4 allow users to specify additional zfs send flags for zrepl to use.
|
||||
Before this fix, when setting ``send.raw=true`` on a job that replicates unencrypted datasets,
|
||||
zrepl would not allow an interrupted replication to resume.
|
||||
The reason were overly cautious checks to support the ``send.encrypted`` option.
|
||||
* This bugfix removes these checks from the replication planner.
|
||||
This makes ``send.encrypted`` a sender-side-only concern, much like all other ``send.*`` flags.
|
||||
* However, this means that the ``zrepl status`` UI no longer indicates whether a replication step uses encrypted sends or not.
|
||||
The setting is still effective though.
|
||||
|
||||
* |break| convert Prometheus metric ``zrepl_version_daemon`` to ``zrepl_start_time`` metric
|
||||
|
||||
* The metric still reports the zrepl version in a label.
|
||||
But the metric *value* is now the Unix timestamp at the time the daemon was started.
|
||||
The Grafana dashboard in :repomasterlink:`dist/grafana` has been updated.
|
||||
|
||||
* |bugfix| transient zrepl status error: ``Post "http://unix/status": EOF``
|
||||
* |bugfix| don't treat receive-side bookmarks as a replication conflict.
|
||||
This facilitates chaining of replication jobs. See :issue:`490`.
|
||||
* |bugfix| workaround for Go/gRPC problem on Illumos where zrepl would
|
||||
crash when using the ``local`` transport type (:issue:`598`).
|
||||
* |bugfix| fix active child tasks panic that cold occur during replication plannig (:issue:`193abbe`)
|
||||
* |bugfix| ``zrepl status`` off-by-one error in display of completed step count (:commit:`ce6701f`)
|
||||
* |bugfix| Allow using day & week units for ``snapshotting.interval`` (:commit:`ffb1d89`)
|
||||
* |docs| ``docs/overview`` improvements (Thanks, `@jtagcat <https://github.com/jtagcat>`_).
|
||||
* |maint| Update to Go 1.19.
|
||||
|
||||
0.5
|
||||
---
|
||||
|
||||
@@ -15,7 +15,7 @@ Conflict Resolution Options
|
||||
|
||||
...
|
||||
|
||||
.. _conflict_resolution-initial_replication-option-send_all_snapshots:
|
||||
.. _conflict_resolution-initial_replication:
|
||||
|
||||
|
||||
``initial_replication`` option
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
.. _miscellaneous:
|
||||
|
||||
Miscellaneous
|
||||
=============
|
||||
|
||||
|
||||
@@ -3,16 +3,17 @@ Overview & Terminology
|
||||
======================
|
||||
|
||||
All work zrepl does is performed by the zrepl daemon which is configured in a single YAML configuration file loaded on startup.
|
||||
The following paths are considered:
|
||||
The following paths are searched, in this order:
|
||||
|
||||
* If set, the location specified via the global ``--config`` flag
|
||||
* ``/etc/zrepl/zrepl.yml``
|
||||
* ``/usr/local/etc/zrepl/zrepl.yml``
|
||||
1. The path specified via the global ``--config`` flag
|
||||
2. ``/etc/zrepl/zrepl.yml``
|
||||
3. ``/usr/local/etc/zrepl/zrepl.yml``
|
||||
|
||||
The ``zrepl configcheck`` subcommand can be used to validate the configuration.
|
||||
The command will output nothing and exit with zero status code if the configuration is valid.
|
||||
``zrepl configcheck`` can be used to validate the configuration.
|
||||
If the configuration is valid, it will output nothing and exit with code ``0``.
|
||||
The error messages vary in quality and usefulness: please report confusing config errors to the tracking :issue:`155`.
|
||||
Full example configs such as in the :ref:`quick-start guides <quickstart-toc>` or the :sampleconf:`/` directory might also be helpful.
|
||||
|
||||
Full example configs are available at :ref:`quick-start guides <quickstart-toc>` and :sampleconf:`/`.
|
||||
However, copy-pasting examples is no substitute for reading documentation!
|
||||
|
||||
Config File Structure
|
||||
@@ -26,9 +27,8 @@ Config File Structure
|
||||
type: push
|
||||
- ...
|
||||
|
||||
zrepl is configured using a single YAML configuration file with two main sections: ``global`` and ``jobs``.
|
||||
The ``global`` section is filled with sensible defaults and is covered later in this chapter.
|
||||
The ``jobs`` section is a list of jobs which we are going to explain now.
|
||||
A zrepl configuration file is divided in to two main sections: ``global`` and ``jobs``.
|
||||
``global`` has sensible defaults. It is covered in :ref:`logging <logging>`, :ref:`monitoring <monitoring>` \& :ref:`miscellaneous <miscellaneous>`.
|
||||
|
||||
.. _job-overview:
|
||||
|
||||
@@ -42,8 +42,7 @@ Jobs are identified by their ``name``, both in log files and the ``zrepl status`
|
||||
.. NOTE::
|
||||
The job name is persisted in several places on disk and thus :issue:`cannot be changed easily<327>`.
|
||||
|
||||
|
||||
Replication always happens between a pair of jobs: one is the **active side**, and one the **passive side**.
|
||||
Replication always happens between a pair of jobs: one **active side** and one **passive side**.
|
||||
The active side connects to the passive side using a :ref:`transport <transport>` and starts executing the replication logic.
|
||||
The passive side responds to requests from the active side after checking its permissions.
|
||||
|
||||
@@ -72,30 +71,29 @@ How the Active Side Works
|
||||
|
||||
The active side (:ref:`push <job-push>` and :ref:`pull <job-pull>` job) executes the replication and pruning logic:
|
||||
|
||||
* Wakeup because of finished snapshotting (``push`` job) or pull interval ticker (``pull`` job).
|
||||
* Connect to the corresponding passive side using a :ref:`transport <transport>` and instantiate an RPC client.
|
||||
* Replicate data from the sending to the receiving side (see below).
|
||||
* Prune on sender & receiver.
|
||||
1. Wakeup after snapshotting (``push`` job) or pull interval ticker (``pull`` job).
|
||||
2. Connect to the passive side and instantiate an RPC client.
|
||||
3. Replicate data from the sender to the receiver.
|
||||
4. Prune on sender & receiver.
|
||||
|
||||
.. TIP::
|
||||
The progress of the active side can be watched live using the ``zrepl status`` subcommand.
|
||||
The progress of the active side can be watched live using ``zrepl status``.
|
||||
|
||||
.. _overview-passive-side--client-identity:
|
||||
|
||||
How the Passive Side Works
|
||||
--------------------------
|
||||
|
||||
The passive side (:ref:`sink <job-sink>` and :ref:`source <job-source>`) waits for connections from the corresponding active side,
|
||||
using the transport listener type specified in the ``serve`` field of the job configuration.
|
||||
When a client connects, the transport listener performS listener-specific access control (cert validation, IP ACLs, etc)
|
||||
and determines the *client identity*.
|
||||
The passive side job then uses this client identity as follows:
|
||||
The passive side (:ref:`sink <job-sink>` and :ref:`source <job-source>`) waits for connections from the active side,
|
||||
on the :ref:`transport <transport>` specified with ``serve`` in the job configuration.
|
||||
The respective transport then perfoms authentication & authorization, resulting in a stable *client identity*.
|
||||
The passive side job uses this *client identity* as follows:
|
||||
|
||||
* The ``sink`` job maps requests from different client identities to their respective sub-filesystem tree ``root_fs/${client_identity}``.
|
||||
* The ``source`` might, in the future, embed the client identity in :ref:`zrepl's ZFS abstraction names <zrepl-zfs-abstractions>` in order to support multi-host replication.
|
||||
* In ``sink`` jobs, to map requests from different *client identities* to their respective sub-filesystem tree ``root_fs/${client_identity}``.
|
||||
* *In the future, ``source`` might embed the client identity in :ref:`zrepl's ZFS abstraction names <zrepl-zfs-abstractions>`, to support multi-host replication.*
|
||||
|
||||
.. TIP::
|
||||
The implementation of the ``sink`` job requires that the connecting client identities be a valid ZFS filesystem name components.
|
||||
The use of the client identity in the ``sink`` job implies that it must be usable as a ZFS ZFS filesystem name component.
|
||||
|
||||
.. _overview-how-replication-works:
|
||||
|
||||
@@ -106,7 +104,7 @@ One of the major design goals of the replication module is to avoid any duplicat
|
||||
As such, the code works on abstract senders and receiver **endpoints**, where typically one will be implemented by a local program object and the other is an RPC client instance.
|
||||
Regardless of push- or pull-style setup, the logic executes on the active side, i.e. in the ``push`` or ``pull`` job.
|
||||
|
||||
The following high-level steps take place during replication and can be monitored using the ``zrepl status`` subcommand:
|
||||
The following high-level steps take place during replication and can be monitored using ``zrepl status``:
|
||||
|
||||
* Plan the replication:
|
||||
|
||||
|
||||
@@ -227,7 +227,7 @@ and property replication is enabled, the receiver must :ref:`inherit the followi
|
||||
.. _job-recv-options--placeholder:
|
||||
|
||||
Placeholders
|
||||
------------
|
||||
~~~~~~~~~~~~
|
||||
|
||||
::
|
||||
|
||||
|
||||
@@ -5,53 +5,138 @@
|
||||
Taking Snaphots
|
||||
===============
|
||||
|
||||
The ``push``, ``source`` and ``snap`` jobs can automatically take periodic snapshots of the filesystems matched by the ``filesystems`` filter field.
|
||||
The snapshot names are composed of a user-defined prefix followed by a UTC date formatted like ``20060102_150405_000``.
|
||||
We use UTC because it will avoid name conflicts when switching time zones or between summer and winter time.
|
||||
You can configure zrepl to take snapshots of the filesystems in the ``filesystems`` field specified in ``push``, ``source`` and ``snap`` jobs.
|
||||
|
||||
When a job is started, the snapshotter attempts to get the snapshotting rhythms of the matched ``filesystems`` in sync because snapshotting all filesystems at the same time results in a more consistent backup.
|
||||
To find that sync point, the most recent snapshot, made by the snapshotter, in any of the matched ``filesystems`` is used.
|
||||
A filesystem that does not have snapshots by the snapshotter has lower priority than filesystem that do, and thus might not be snapshotted (and replicated) until it is snapshotted at the next sync point.
|
||||
The following snapshotting types are supported:
|
||||
|
||||
For ``push`` jobs, replication is automatically triggered after all filesystems have been snapshotted.
|
||||
|
||||
Note that the ``zrepl signal wakeup JOB`` subcommand does not trigger snapshotting.
|
||||
.. list-table::
|
||||
:widths: 20 70
|
||||
:header-rows: 1
|
||||
|
||||
* - ``snapshotting.type``
|
||||
- Comment
|
||||
* - ``periodic``
|
||||
- Ensure that snapshots are taken at a particular interval.
|
||||
* - ``cron``
|
||||
- Use cron spec to take snapshots at particular points in time.
|
||||
* - ``manual``
|
||||
- zrepl does not take any snapshots by itself.
|
||||
|
||||
The ``periodic`` and ``cron`` snapshotting types share some common options and behavior:
|
||||
|
||||
* **Naming:** The snapshot names are composed of a user-defined ``prefix`` followed by a UTC date formatted like ``20060102_150405_000``.
|
||||
We use UTC because it will avoid name conflicts when switching time zones or between summer and winter time.
|
||||
* **Hooks:** You can configure hooks to run before or after zrepl takes the snapshots. See :ref:`below <job-snapshotting-hooks>` for details.
|
||||
* **Push replication:** After creating all snapshots, the snapshotter will wake up the replication part of the job, if it's a ``push`` job.
|
||||
Note that snapshotting is decoupled from replication, i.e., if it is down or takes too long, snapshots will still be taken.
|
||||
Note further that other jobs are not woken up by snapshotting.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
There is **no concept of ownership** of the snapshots that are created by ``periodic`` or ``cron``.
|
||||
Thus, there is no distinction between zrepl-created snapshots and user-created snapshots during replication or pruning.
|
||||
|
||||
In particular, pruning will take all snapshots into consideration by default.
|
||||
To constrain pruning to just zrepl-created snapshots:
|
||||
|
||||
1. Assign a unique `prefix` to the snapshotter and
|
||||
2. Use the ``regex`` functionality of the various pruning ``keep`` rules to just consider snapshots with that prefix.
|
||||
|
||||
There is currently no way to constrain replication to just zrepl-created snapshots.
|
||||
Follow and comment at :issue:`403` if you need this functionality.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
The ``zrepl signal wakeup JOB`` subcommand does not trigger snapshotting.
|
||||
|
||||
``periodic`` Snapshotting
|
||||
-------------------------
|
||||
|
||||
::
|
||||
|
||||
jobs:
|
||||
- type: push
|
||||
filesystems: {
|
||||
"<": true,
|
||||
"tmp": false
|
||||
}
|
||||
snapshotting:
|
||||
type: periodic
|
||||
prefix: zrepl_
|
||||
interval: 10m
|
||||
hooks: ...
|
||||
...
|
||||
jobs:
|
||||
- ...
|
||||
filesystems: { ... }
|
||||
snapshotting:
|
||||
type: periodic
|
||||
prefix: zrepl_
|
||||
interval: 10m
|
||||
# Timestamp format that is used as snapshot suffix.
|
||||
# Can be any of "dense" (default), "human", "iso-8601", "unix-seconds" or a custom Go time format (see https://go.dev/src/time/format.go)
|
||||
timestamp_format: dense
|
||||
hooks: ...
|
||||
pruning: ...
|
||||
|
||||
There is also a ``manual`` snapshotting type, which covers the following use cases:
|
||||
The ``periodic`` snapshotter ensures that snapshots are taken in the specified ``interval``.
|
||||
If you use zrepl for backup, this translates into your recovery point objective (RPO).
|
||||
To meet your RPO, you still need to monitor that replication, which happens asynchronously to snapshotting, actually works.
|
||||
|
||||
* Existing infrastructure for automatic snapshots: you only want to use this zrepl job for replication.
|
||||
* Handling snapshotting through a separate ``snap`` job.
|
||||
It is desirable to get all ``filesystems`` snapshotted simultaneously because it results in a more consistent backup.
|
||||
To accomplish this while still maintaining the ``interval``, the ``periodic`` snapshotter attempts to get the snapshotting rhythms in sync.
|
||||
To find that sync point, the most recent snapshot, created by the snapshotter, in any of the matched ``filesystems`` is used.
|
||||
A filesystem that does not have snapshots by the snapshotter has lower priority than filesystem that do, and thus might not be snapshotted (and replicated) until it is snapshotted at the next sync point.
|
||||
The snapshotter uses the ``prefix`` to identify which snapshots it created.
|
||||
|
||||
Note that you will have to trigger replication manually using the ``zrepl signal wakeup JOB`` subcommand in that case.
|
||||
.. _job-snapshotting--cron:
|
||||
|
||||
``cron`` Snapshotting
|
||||
---------------------
|
||||
|
||||
::
|
||||
|
||||
jobs:
|
||||
- type: snap
|
||||
filesystems: { ... }
|
||||
snapshotting:
|
||||
type: cron
|
||||
prefix: zrepl_
|
||||
# (second, optional) minute hour day-of-month month day-of-week
|
||||
# This example takes snapshots daily at 3:00.
|
||||
cron: "0 3 * * *"
|
||||
# Timestamp format that is used as snapshot suffix.
|
||||
# Can be any of "dense" (default), "human", "iso-8601", "unix-seconds" or a custom Go time format (see https://go.dev/src/time/format.go)
|
||||
timestamp_format: dense
|
||||
pruning: ...
|
||||
|
||||
In ``cron`` mode, the snapshotter takes snaphots at fixed points in time.
|
||||
See https://en.wikipedia.org/wiki/Cron for details on the syntax.
|
||||
zrepl uses the ``the github.com/robfig/cron/v3`` Go package for parsing.
|
||||
An optional field for "seconds" is supported to take snapshots at sub-minute frequencies.
|
||||
|
||||
.. _job-snapshotting-timestamp_format:
|
||||
|
||||
Timestamp Format
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The ``cron`` and ``periodic`` snapshotter support configuring a custom timestamp format that is used as suffix for the snapshot name.
|
||||
It can be used by setting ``timestamp_format`` to any of the following values:
|
||||
|
||||
* ``dense`` (default) looks like ``20060102_150405_000``
|
||||
* ``human`` looks like ``2006-01-02_15:04:05``
|
||||
* ``iso-8601`` looks like ``2006-01-02T15:04:05.000Z``
|
||||
* ``unix-seconds`` looks like ``1136214245``
|
||||
* Any custom Go time format accepted by `time.Time#Format <https://go.dev/src/time/format.go>`_.
|
||||
|
||||
|
||||
``manual`` Snapshotting
|
||||
-----------------------
|
||||
|
||||
::
|
||||
|
||||
jobs:
|
||||
- type: push
|
||||
filesystems: {
|
||||
"<": true,
|
||||
"tmp": false
|
||||
}
|
||||
snapshotting:
|
||||
type: manual
|
||||
...
|
||||
|
||||
In ``manual`` mode, zrepl does not take snapshots by itself.
|
||||
Manual snapshotting is most useful if you have existing infrastructure for snapshot management.
|
||||
Or, if you want to decouple snapshot management from replication using a zrepl ``snap`` job.
|
||||
See :ref:`this quickstart guide <quickstart-backup-to-external-disk>` for an example.
|
||||
|
||||
To trigger replication after taking snapshots, use the ``zrepl signal wakeup JOB`` command.
|
||||
|
||||
.. _job-snapshotting-hooks:
|
||||
|
||||
Pre- and Post-Snapshot Hooks
|
||||
|
||||
+1
-29
@@ -105,15 +105,10 @@ func (s *Sender) ListFilesystems(ctx context.Context, r *pdu.ListFilesystemReq)
|
||||
}
|
||||
rfss := make([]*pdu.Filesystem, len(fss))
|
||||
for i := range fss {
|
||||
encEnabled, err := zfs.ZFSGetEncryptionEnabled(ctx, fss[i].ToString())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot get filesystem encryption status")
|
||||
}
|
||||
rfss[i] = &pdu.Filesystem{
|
||||
Path: fss[i].ToString(),
|
||||
// ResumeToken does not make sense from Sender
|
||||
IsPlaceholder: false, // sender FSs are never placeholders
|
||||
IsEncrypted: encEnabled,
|
||||
}
|
||||
}
|
||||
res := &pdu.ListFilesystemRes{Filesystems: rfss}
|
||||
@@ -165,23 +160,6 @@ func (s *Sender) sendMakeArgs(ctx context.Context, r *pdu.SendReq) (sendArgs zfs
|
||||
if err != nil {
|
||||
return sendArgs, err
|
||||
}
|
||||
switch r.Encrypted {
|
||||
case pdu.Tri_DontCare:
|
||||
// use s.encrypt setting
|
||||
// ok, fallthrough outer
|
||||
case pdu.Tri_False:
|
||||
if s.config.Encrypt.B {
|
||||
return sendArgs, errors.New("only encrypted sends allowed (send -w + encryption!= off), but unencrypted send requested")
|
||||
}
|
||||
// fallthrough outer
|
||||
case pdu.Tri_True:
|
||||
if !s.config.Encrypt.B {
|
||||
return sendArgs, errors.New("only unencrypted sends allowed, but encrypted send requested")
|
||||
}
|
||||
// fallthrough outer
|
||||
default:
|
||||
return sendArgs, fmt.Errorf("unknown pdu.Tri variant %q", r.Encrypted)
|
||||
}
|
||||
|
||||
sendArgsUnvalidated := zfs.ZFSSendArgsUnvalidated{
|
||||
FS: r.Filesystem,
|
||||
@@ -461,7 +439,7 @@ type FSMap interface { // FIXME unused
|
||||
}
|
||||
|
||||
// NOTE: when adding members to this struct, remember
|
||||
// to add them to `ReceiverConfig.copyIn()`
|
||||
// to add them to `ReceiverConfig.copyIn()`
|
||||
type ReceiverConfig struct {
|
||||
JobID JobID
|
||||
|
||||
@@ -658,11 +636,6 @@ func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemR
|
||||
l.WithError(err).Error("cannot get receive resume token")
|
||||
return nil, err
|
||||
}
|
||||
encEnabled, err := zfs.ZFSGetEncryptionEnabled(ctx, a.ToString())
|
||||
if err != nil {
|
||||
l.WithError(err).Error("cannot get encryption enabled status")
|
||||
return nil, err
|
||||
}
|
||||
l.WithField("receive_resume_token", token).Debug("receive resume token")
|
||||
|
||||
a.TrimPrefix(root)
|
||||
@@ -671,7 +644,6 @@ func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemR
|
||||
Path: a.ToString(),
|
||||
IsPlaceholder: ph.IsPlaceholder,
|
||||
ResumeToken: token,
|
||||
IsEncrypted: encEnabled,
|
||||
}
|
||||
fss = append(fss, fs)
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ func (s AbstractionTypeSet) String() string {
|
||||
for i := range s {
|
||||
sts = append(sts, string(i))
|
||||
}
|
||||
sts = sort.StringSlice(sts)
|
||||
sort.Strings(sts)
|
||||
return strings.Join(sts, ",")
|
||||
}
|
||||
|
||||
@@ -516,10 +516,12 @@ func (e ListAbstractionsErrors) Error() string {
|
||||
}
|
||||
|
||||
func ListAbstractions(ctx context.Context, query ListZFSHoldsAndBookmarksQuery) (out []Abstraction, outErrs []ListAbstractionsError, err error) {
|
||||
outChan, outErrsChan, err := ListAbstractionsStreamed(ctx, query)
|
||||
outChan, outErrsChan, drainDone, err := ListAbstractionsStreamed(ctx, query)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer drainDone()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
@@ -540,19 +542,20 @@ func ListAbstractions(ctx context.Context, query ListZFSHoldsAndBookmarksQuery)
|
||||
}
|
||||
|
||||
// if err != nil, the returned channels are both nil
|
||||
// if err == nil, both channels must be fully drained by the caller to avoid leaking goroutines
|
||||
func ListAbstractionsStreamed(ctx context.Context, query ListZFSHoldsAndBookmarksQuery) (<-chan Abstraction, <-chan ListAbstractionsError, error) {
|
||||
// if err == nil, both channels must be fully drained by the caller to avoid leaking goroutines.
|
||||
// After draining is done, the caller must call the returned drainDone func.
|
||||
func ListAbstractionsStreamed(ctx context.Context, query ListZFSHoldsAndBookmarksQuery) (_ <-chan Abstraction, _ <-chan ListAbstractionsError, drainDone func(), _ error) {
|
||||
|
||||
// impl note: structure the query processing in such a way that
|
||||
// a minimum amount of zfs shell-outs needs to be done
|
||||
|
||||
if err := query.Validate(); err != nil {
|
||||
return nil, nil, errors.Wrap(err, "validate query")
|
||||
return nil, nil, nil, errors.Wrap(err, "validate query")
|
||||
}
|
||||
|
||||
fss, err := query.FS.Filesystems(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "list filesystems")
|
||||
return nil, nil, nil, errors.Wrap(err, "list filesystems")
|
||||
}
|
||||
|
||||
outErrs := make(chan ListAbstractionsError)
|
||||
@@ -574,7 +577,6 @@ func ListAbstractionsStreamed(ctx context.Context, query ListZFSHoldsAndBookmark
|
||||
sem := semaphore.New(int64(query.Concurrency))
|
||||
ctx, endTask := trace.WithTask(ctx, "list-abstractions-streamed-producer")
|
||||
go func() {
|
||||
defer endTask()
|
||||
defer close(out)
|
||||
defer close(outErrs)
|
||||
|
||||
@@ -596,7 +598,11 @@ func ListAbstractionsStreamed(ctx context.Context, query ListZFSHoldsAndBookmark
|
||||
}
|
||||
}()
|
||||
|
||||
return out, outErrs, nil
|
||||
drainDone = func() {
|
||||
endTask()
|
||||
}
|
||||
|
||||
return out, outErrs, drainDone, nil
|
||||
}
|
||||
|
||||
func listAbstractionsImplFS(ctx context.Context, fs string, query *ListZFSHoldsAndBookmarksQuery, emitCandidate putListAbstraction, errCb putListAbstractionErr) {
|
||||
|
||||
@@ -23,7 +23,7 @@ func replicationCursorBookmarkNameImpl(fs string, guid uint64, jobid string) (st
|
||||
|
||||
var ErrV1ReplicationCursor = fmt.Errorf("bookmark name is a v1-replication cursor")
|
||||
|
||||
//err != nil always means that the bookmark is not a valid replication bookmark
|
||||
// err != nil always means that the bookmark is not a valid replication bookmark
|
||||
//
|
||||
// Returns ErrV1ReplicationCursor as error if the bookmark is a v1 replication cursor
|
||||
func ParseReplicationCursorBookmarkName(fullname string) (uint64, JobID, error) {
|
||||
|
||||
@@ -29,6 +29,7 @@ require (
|
||||
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5
|
||||
github.com/prometheus/client_golang v1.2.1
|
||||
github.com/prometheus/common v0.7.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 // indirect; go1.12 thinks it needs this
|
||||
github.com/spf13/cobra v0.0.2
|
||||
github.com/spf13/pflag v1.0.5
|
||||
|
||||
@@ -161,6 +161,8 @@ github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 h1:tB9NOR21++IjLyVx3/PCPhWMwqGNCMQEH96A6dMZ/gc=
|
||||
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
@@ -53,6 +54,40 @@ type HarnessArgs struct {
|
||||
Run string
|
||||
}
|
||||
|
||||
type invocation struct {
|
||||
runFunc tests.Case
|
||||
idstring string
|
||||
result *testCaseResult
|
||||
children map[string]*invocation
|
||||
}
|
||||
|
||||
func newInvocation(runFunc tests.Case, id string) *invocation {
|
||||
return &invocation{
|
||||
runFunc: runFunc,
|
||||
idstring: id,
|
||||
children: make(map[string]*invocation),
|
||||
}
|
||||
}
|
||||
|
||||
func (i *invocation) String() string {
|
||||
idsuffix := ""
|
||||
if i.idstring != "" {
|
||||
idsuffix = fmt.Sprintf(": %s", i.idstring)
|
||||
}
|
||||
return fmt.Sprintf("%s%s", i.runFunc.String(), idsuffix)
|
||||
}
|
||||
|
||||
func (i *invocation) RegisterChild(c *invocation) error {
|
||||
if c.idstring == "" {
|
||||
return fmt.Errorf("child must have id string")
|
||||
}
|
||||
if oc := i.children[c.idstring]; oc != nil {
|
||||
return fmt.Errorf("idstring %q is already taken by %s", c.idstring, oc)
|
||||
}
|
||||
i.children[c.idstring] = c
|
||||
return nil
|
||||
}
|
||||
|
||||
func HarnessRun(args HarnessArgs) error {
|
||||
|
||||
runRE := regexp.MustCompile(args.Run)
|
||||
@@ -79,21 +114,19 @@ func HarnessRun(args HarnessArgs) error {
|
||||
ctx = logging.WithLoggers(ctx, logging.SubsystemLoggersWithUniversalLogger(logger))
|
||||
ex := platformtest.NewEx(logger)
|
||||
|
||||
type invocation struct {
|
||||
runFunc tests.Case
|
||||
result *testCaseResult
|
||||
}
|
||||
|
||||
invocations := make([]*invocation, 0, len(tests.Cases))
|
||||
testQueue := list.New()
|
||||
for _, c := range tests.Cases {
|
||||
if runRE.MatchString(c.String()) {
|
||||
invocations = append(invocations, &invocation{runFunc: c})
|
||||
testQueue.PushBack(newInvocation(c, ""))
|
||||
}
|
||||
}
|
||||
|
||||
for _, inv := range invocations {
|
||||
completedTests := list.New()
|
||||
|
||||
bold.Printf("BEGIN TEST CASE %s\n", inv.runFunc.String())
|
||||
for testQueue.Len() > 0 {
|
||||
inv := testQueue.Remove(testQueue.Front()).(*invocation)
|
||||
|
||||
bold.Printf("BEGIN TEST CASE %s\n", inv)
|
||||
|
||||
pool, err := platformtest.CreateOrReplaceZpool(ctx, ex, args.CreateArgs)
|
||||
if err != nil {
|
||||
@@ -103,6 +136,15 @@ func HarnessRun(args HarnessArgs) error {
|
||||
ctx := &platformtest.Context{
|
||||
Context: ctx,
|
||||
RootDataset: filepath.Join(pool.Name(), "rootds"),
|
||||
QueueSubtest: func(id string, stf func(*platformtest.Context)) {
|
||||
stinv := newInvocation(stf, id)
|
||||
err := inv.RegisterChild(stinv)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
bold.Printf(" QUEUING SUBTEST %q\n", id)
|
||||
testQueue.PushFront(stinv)
|
||||
},
|
||||
}
|
||||
|
||||
res := runTestCase(ctx, ex, inv.runFunc)
|
||||
@@ -120,6 +162,8 @@ func HarnessRun(args HarnessArgs) error {
|
||||
panic(fmt.Sprintf("error destroying test pool: %s", err))
|
||||
}
|
||||
|
||||
completedTests.PushBack(inv)
|
||||
|
||||
if res.failed {
|
||||
boldRed.Printf("TEST FAILED\n")
|
||||
} else if res.skipped {
|
||||
@@ -136,7 +180,8 @@ func HarnessRun(args HarnessArgs) error {
|
||||
var summary struct {
|
||||
succ, fail, skip []*invocation
|
||||
}
|
||||
for _, inv := range invocations {
|
||||
for completedTests.Len() > 0 {
|
||||
inv := completedTests.Remove(completedTests.Front()).(*invocation)
|
||||
var bucket *[]*invocation
|
||||
if inv.result.failed {
|
||||
bucket = &summary.fail
|
||||
@@ -157,7 +202,7 @@ func HarnessRun(args HarnessArgs) error {
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
for _, inv := range bucket {
|
||||
fmt.Printf(" %s\n", inv.runFunc.String())
|
||||
fmt.Printf(" %s\n", inv)
|
||||
}
|
||||
}
|
||||
printBucket("PASSING TESTS", boldGreen, summary.succ)
|
||||
|
||||
@@ -11,6 +11,13 @@ import (
|
||||
type Context struct {
|
||||
context.Context
|
||||
RootDataset string
|
||||
// Use this callback from a top-level test case to queue the
|
||||
// execution of sub-tests after this test case is complete.
|
||||
//
|
||||
// Note that the testing harness executes the subtest
|
||||
// _after_ the current top-level test. Hence, the subtest
|
||||
// cannot use any ZFS state of the top-level test.
|
||||
QueueSubtest func(id string, stf func(*Context))
|
||||
}
|
||||
|
||||
var FailNowSentinel = fmt.Errorf("platformtest: FailNow called on context")
|
||||
|
||||
@@ -36,6 +36,7 @@ var Cases = []Case{BatchDestroy,
|
||||
ReplicationStepCompletedLostBehavior__GuaranteeResumability,
|
||||
ResumableRecvAndTokenHandling,
|
||||
ResumeTokenParsing,
|
||||
SendArgsValidationEE_EncryptionAndRaw,
|
||||
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_false,
|
||||
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden__EncryptionSupported_true,
|
||||
SendArgsValidationResumeTokenDifferentFilesystemForbidden,
|
||||
|
||||
@@ -35,16 +35,17 @@ import (
|
||||
// of a new sender and receiver instance and one blocking invocation
|
||||
// of the replication engine without encryption
|
||||
type replicationInvocation struct {
|
||||
sjid, rjid endpoint.JobID
|
||||
sfs string
|
||||
sfilter *filters.DatasetMapFilter
|
||||
rfsRoot string
|
||||
interceptSender func(e *endpoint.Sender) logic.Sender
|
||||
interceptReceiver func(e *endpoint.Receiver) logic.Receiver
|
||||
guarantee *pdu.ReplicationConfigProtection
|
||||
senderConfigHook func(*endpoint.SenderConfig)
|
||||
receiverConfigHook func(*endpoint.ReceiverConfig)
|
||||
plannerPolicyHook func(*logic.PlannerPolicy)
|
||||
sjid, rjid endpoint.JobID
|
||||
sfs string
|
||||
sfilter *filters.DatasetMapFilter
|
||||
rfsRoot string
|
||||
interceptSender func(e *endpoint.Sender) logic.Sender
|
||||
interceptReceiver func(e *endpoint.Receiver) logic.Receiver
|
||||
guarantee *pdu.ReplicationConfigProtection
|
||||
senderConfigHook func(*endpoint.SenderConfig)
|
||||
receiverConfigHook func(*endpoint.ReceiverConfig)
|
||||
plannerPolicyHook func(*logic.PlannerPolicy)
|
||||
skipSendArgsValidation bool
|
||||
}
|
||||
|
||||
func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
|
||||
@@ -92,7 +93,6 @@ func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
|
||||
sender := i.interceptSender(endpoint.NewSender(senderConfig))
|
||||
receiver := i.interceptReceiver(endpoint.NewReceiver(receiverConfig))
|
||||
plannerPolicy := logic.PlannerPolicy{
|
||||
EncryptedSend: logic.TriFromBool(false),
|
||||
ReplicationConfig: &pdu.ReplicationConfig{
|
||||
Protection: i.guarantee,
|
||||
},
|
||||
@@ -105,8 +105,13 @@ func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
|
||||
i.plannerPolicyHook(&plannerPolicy)
|
||||
}
|
||||
|
||||
var doCtx context.Context = ctx
|
||||
if i.skipSendArgsValidation {
|
||||
doCtx = zfs.ZFSSendArgsSkipValidation(ctx)
|
||||
}
|
||||
|
||||
report, wait := replication.Do(
|
||||
ctx,
|
||||
doCtx,
|
||||
driver.Config{
|
||||
MaxAttempts: 1,
|
||||
StepQueueConcurrency: 1,
|
||||
|
||||
@@ -2,10 +2,16 @@ package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/kr/pretty"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zrepl/zrepl/endpoint"
|
||||
"github.com/zrepl/zrepl/platformtest"
|
||||
"github.com/zrepl/zrepl/replication/logic"
|
||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||
"github.com/zrepl/zrepl/replication/report"
|
||||
"github.com/zrepl/zrepl/util/nodefault"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
)
|
||||
@@ -208,3 +214,225 @@ func SendArgsValidationResumeTokenDifferentFilesystemForbidden(ctx *platformtest
|
||||
require.True(ctx, ok)
|
||||
require.Equal(ctx, mismatchError.What, zfs.ZFSSendArgsResumeTokenMismatchFilesystem)
|
||||
}
|
||||
|
||||
type sendArgsValidationEndToEndTestOutcome string
|
||||
|
||||
const (
|
||||
ValidationAccepts sendArgsValidationEndToEndTestOutcome = "accept"
|
||||
ValidationRejects sendArgsValidationEndToEndTestOutcome = "rejects"
|
||||
)
|
||||
|
||||
type sendArgsValidationEndToEndTest struct {
|
||||
encryptedSenderFilesystem bool
|
||||
senderConfigHook func(config *endpoint.SenderConfig)
|
||||
expectedOutcome sendArgsValidationEndToEndTestOutcome
|
||||
outcomeRejectsInspectError func(require.TestingT, *report.FilesystemReport, bool)
|
||||
inspectReceiverFSAfterSuccessfulCycle func(rfs string)
|
||||
}
|
||||
|
||||
func implSendArgsValidationEndToEndTest(ctx *platformtest.Context, setup sendArgsValidationEndToEndTest) {
|
||||
|
||||
senderEncrypted := ""
|
||||
if setup.encryptedSenderFilesystem {
|
||||
senderEncrypted = "encrypted"
|
||||
}
|
||||
|
||||
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, fmt.Sprintf(`
|
||||
CREATEROOT
|
||||
+ "sender" %s
|
||||
+ "receiver"
|
||||
R zfs create -p "${ROOTDS}/receiver/${ROOTDS}"
|
||||
`, senderEncrypted))
|
||||
|
||||
sjid := endpoint.MustMakeJobID("sender-job")
|
||||
rjid := endpoint.MustMakeJobID("receiver-job")
|
||||
|
||||
sfs := ctx.RootDataset + "/sender"
|
||||
rfsRoot := ctx.RootDataset + "/receiver"
|
||||
|
||||
sfsmp, err := zfs.ZFSGetMountpoint(ctx, sfs)
|
||||
require.NoError(ctx, err)
|
||||
require.True(ctx, sfsmp.Mounted)
|
||||
|
||||
// Two cycles. one initial replication, one incremental replication.
|
||||
// Within each cycle: interrupt replication at least once.
|
||||
// This exercises both the no-resume-token-present and the resume-token-present validation code paths.
|
||||
initial_then_incremental:
|
||||
for i := 0; i < 2; i++ {
|
||||
writeDummyData(path.Join(sfsmp.Mountpoint, "dummy.data"), 2*(1<<20))
|
||||
mustSnapshot(ctx, fmt.Sprintf("%s@%d", sfs, i))
|
||||
|
||||
rep := replicationInvocation{
|
||||
sjid: sjid,
|
||||
rjid: rjid,
|
||||
sfs: sfs,
|
||||
rfsRoot: rfsRoot,
|
||||
senderConfigHook: setup.senderConfigHook,
|
||||
interceptSender: func(e *endpoint.Sender) logic.Sender {
|
||||
return &PartialSender{Sender: e, failAfterByteCount: 1 << 20}
|
||||
},
|
||||
guarantee: pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability),
|
||||
skipSendArgsValidation: false,
|
||||
}
|
||||
|
||||
rfs := rep.ReceiveSideFilesystem()
|
||||
|
||||
// PartialSender interrupts after 1MiB, and we wrote 2 MiB of data
|
||||
// => Give it 3 attempts to replicate. after that, we should have a stable outcome
|
||||
var lastReport *report.Report
|
||||
lastResumeToken := ""
|
||||
interrupt_current_step:
|
||||
for j := 0; j < 3; j++ {
|
||||
|
||||
lastReport = rep.Do(ctx)
|
||||
ctx.Logf("\nreport=%s", pretty.Sprint(lastReport))
|
||||
require.Len(ctx, lastReport.Attempts, 1)
|
||||
require.Len(ctx, lastReport.Attempts[0].Filesystems, 1)
|
||||
lastReportFS := lastReport.Attempts[0].Filesystems[0]
|
||||
|
||||
var rfsExists bool
|
||||
rfsResumeToken, err := zfs.ZFSGetReceiveResumeTokenOrEmptyStringIfNotSupported(ctx, mustDatasetPath(rfs))
|
||||
if err != nil {
|
||||
_, ok := err.(*zfs.DatasetDoesNotExist) // no shadow
|
||||
require.True(ctx, ok, "no other errors expected")
|
||||
rfsExists = false
|
||||
rfsResumeToken = ""
|
||||
} else {
|
||||
rfsExists = true
|
||||
}
|
||||
|
||||
if setup.expectedOutcome == ValidationRejects {
|
||||
|
||||
// When expecting rejection, it should manifest immediately, before sending anything.
|
||||
// This is tested in the j=0 iteration (for both initial and incremental repl (i=0, i=1)).
|
||||
// But we also want to assert correct behavior in case zrepl observes resume tokens.
|
||||
// Specifically, cases where the send parameters encoded in the token conflict with the
|
||||
// configured encryption policy.
|
||||
// Hence, for scenarios that are expected to reject, after we validated that they reject
|
||||
// for the non-resuming case (j==0), fabricate a resuming scenario by temporarily disabling
|
||||
// send args validation. After fabricating the scenario, proceed into j==1 to exercise
|
||||
// the resume token validation.
|
||||
if j == 0 {
|
||||
if i == 0 {
|
||||
require.False(ctx, rfsExists, "the sender should not have sent anything")
|
||||
} else {
|
||||
// we fabricate a scenario where rfsExists below, hence can't assert non-existence anymore
|
||||
}
|
||||
|
||||
ctx.Logf("skipping send args validation to test resuming case")
|
||||
|
||||
rep.skipSendArgsValidation = true
|
||||
setupResumeReport := rep.Do(ctx)
|
||||
ctx.Logf("setupResumeReport=%s", pretty.Sprint(setupResumeReport))
|
||||
rep.skipSendArgsValidation = false
|
||||
rt, err := zfs.ZFSGetReceiveResumeTokenOrEmptyStringIfNotSupported(ctx, mustDatasetPath(rfs))
|
||||
require.NoError(ctx, err)
|
||||
require.NotEmpty(ctx, rt, "we disabled send args validation, so the .Do above should have resulted in a resume token on rfs")
|
||||
lastResumeToken = rt
|
||||
continue interrupt_current_step // next iteration will test resume case with send args validation enabled
|
||||
} else { // j > 0
|
||||
require.Equal(ctx, lastResumeToken, rfsResumeToken, "we expect policy to refuse replication, no progress must happen")
|
||||
_, err := zfs.ZFSGetFilesystemVersion(ctx, fmt.Sprintf("%s@%d", rfs, i))
|
||||
_, ok := err.(*zfs.DatasetDoesNotExist)
|
||||
require.True(ctx, ok, "another check that no progress is happening")
|
||||
}
|
||||
|
||||
setup.outcomeRejectsInspectError(ctx, lastReportFS, j == 0)
|
||||
|
||||
// XXX: check rejection cases for incremental replication as well
|
||||
break initial_then_incremental
|
||||
|
||||
} else {
|
||||
require.Equal(ctx, ValidationAccepts, setup.expectedOutcome)
|
||||
_, err := zfs.ZFSGetFilesystemVersion(ctx, fmt.Sprintf("%s@%d", rfs, i))
|
||||
_, notExist := err.(*zfs.DatasetDoesNotExist)
|
||||
if notExist {
|
||||
require.NotEmpty(ctx, rfsResumeToken)
|
||||
continue interrupt_current_step // next iteration will resume
|
||||
} else {
|
||||
require.NoError(ctx, err)
|
||||
// version exists
|
||||
|
||||
// make sure all the filesystem versions we created so far were replicated by the replication loop
|
||||
for j := 0; j <= i; j++ {
|
||||
_ = fsversion(ctx, rfs, fmt.Sprintf("@%d", j))
|
||||
}
|
||||
|
||||
setup.inspectReceiverFSAfterSuccessfulCycle(rfs)
|
||||
continue initial_then_incremental
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func SendArgsValidationEE_EncryptionAndRaw(ctx *platformtest.Context) {
|
||||
type TC struct {
|
||||
// create sender filesystem with encryption enabled yes/no
|
||||
SFSEnc bool
|
||||
SndEnc bool // send flag
|
||||
SndRaw bool // send flag
|
||||
RFSEnc bool
|
||||
Outcome sendArgsValidationEndToEndTestOutcome
|
||||
RejectErrorNoResume string
|
||||
RejectErrorResume string
|
||||
}
|
||||
tcs := []TC{
|
||||
// Sender FS is unencrypted
|
||||
{SFSEnc: false, SndEnc: false, SndRaw: false, RFSEnc: false, Outcome: ValidationAccepts},
|
||||
{SFSEnc: false, SndEnc: false, SndRaw: true, RFSEnc: false, Outcome: ValidationAccepts}, // allow unencrypted raw sends (#503)
|
||||
{SFSEnc: false, SndEnc: true, SndRaw: false, RFSEnc: false, Outcome: ValidationRejects,
|
||||
RejectErrorNoResume: `encrypted send mandated by policy, but filesystem .* is not encrypted`,
|
||||
RejectErrorResume: `encrypted send mandated by policy, but filesystem .* is not encrypted`,
|
||||
},
|
||||
{SFSEnc: false, SndEnc: true, SndRaw: true, RFSEnc: false, Outcome: ValidationRejects,
|
||||
RejectErrorNoResume: `encrypted send mandated by policy, but filesystem .* is not encrypted`,
|
||||
RejectErrorResume: `encrypted send mandated by policy, but filesystem .* is not encrypted`,
|
||||
},
|
||||
// Sender FS is encrypted
|
||||
{SFSEnc: true, SndEnc: false, SndRaw: false, RFSEnc: false, Outcome: ValidationAccepts}, // passes because keys are loaded, thus can send plain.
|
||||
{SFSEnc: true, SndEnc: false, SndRaw: true, RFSEnc: false, Outcome: ValidationRejects,
|
||||
RejectErrorNoResume: `policy mandates raw\+unencrypted sends, but filesystem .* is encrypted`,
|
||||
RejectErrorResume: `resume token has rawok=true which would result in encrypted send, but policy mandates unencrypted sends only`,
|
||||
},
|
||||
{SFSEnc: true, SndEnc: true, SndRaw: false, RFSEnc: true, Outcome: ValidationAccepts},
|
||||
{SFSEnc: true, SndEnc: true, SndRaw: true, RFSEnc: true, Outcome: ValidationAccepts},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
tc := tc // closure would copy by ref otherwise
|
||||
ctx.QueueSubtest(fmt.Sprintf("%#v", tc), func(ctx *platformtest.Context) {
|
||||
implSendArgsValidationEndToEndTest(ctx, sendArgsValidationEndToEndTest{
|
||||
encryptedSenderFilesystem: tc.SFSEnc,
|
||||
senderConfigHook: func(c *endpoint.SenderConfig) {
|
||||
c.Encrypt = &nodefault.Bool{B: tc.SndEnc}
|
||||
c.SendRaw = tc.SndRaw
|
||||
},
|
||||
expectedOutcome: tc.Outcome,
|
||||
outcomeRejectsInspectError: func(ctx require.TestingT, fr *report.FilesystemReport, isResume bool) {
|
||||
// this callback is only called for ValidationRejects
|
||||
|
||||
// validation should be failing during dry send => planning stage
|
||||
// XXX mock out ZFS to ensure we never call a zfs send that would send data
|
||||
// if we're expecting validation to fail
|
||||
require.Equal(ctx, report.FilesystemPlanningErrored, fr.State)
|
||||
|
||||
if isResume {
|
||||
require.NotEmpty(ctx, tc.RejectErrorResume)
|
||||
require.Regexp(ctx, tc.RejectErrorResume, fr.PlanError)
|
||||
} else {
|
||||
require.NotEmpty(ctx, tc.RejectErrorNoResume)
|
||||
require.Regexp(ctx, tc.RejectErrorNoResume, fr.PlanError)
|
||||
}
|
||||
},
|
||||
inspectReceiverFSAfterSuccessfulCycle: func(rfs string) {
|
||||
enabled, err := zfs.ZFSGetEncryptionEnabled(ctx, rfs)
|
||||
require.NoError(ctx, err)
|
||||
require.Equal(ctx, tc.RFSEnc, enabled, "receiver filesystem encryption settings unexpected")
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -797,7 +797,7 @@ func (a *attempt) errorReport() *errorReport {
|
||||
r.byClass[class] = errs
|
||||
}
|
||||
for _, err := range r.flattened {
|
||||
if neterr, ok := err.Err.(net.Error); ok && neterr.Temporary() {
|
||||
if neterr, ok := err.Err.(net.Error); ok && neterr.Timeout() {
|
||||
putClass(err, errorClassTemporaryConnectivityRelated)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
//nolint[:deadcode,unused]
|
||||
//nolint:deadcode,unused
|
||||
func debug(format string, args ...interface{}) {
|
||||
if debugEnabled {
|
||||
fmt.Fprintf(os.Stderr, "repl: driver: %s\n", fmt.Sprintf(format, args...))
|
||||
@@ -22,7 +22,7 @@ func debug(format string, args ...interface{}) {
|
||||
|
||||
type debugFunc func(format string, args ...interface{})
|
||||
|
||||
//nolint[:deadcode,unused]
|
||||
//nolint:deadcode,unused
|
||||
func debugPrefix(prefixFormat string, prefixFormatArgs ...interface{}) debugFunc {
|
||||
prefix := fmt.Sprintf(prefixFormat, prefixFormatArgs...)
|
||||
return func(format string, args ...interface{}) {
|
||||
|
||||
@@ -88,8 +88,27 @@ func SortVersionListByCreateTXGThenBookmarkLTSnapshot(fsvslice []*FilesystemVers
|
||||
return sorted
|
||||
}
|
||||
|
||||
func StripBookmarksFromVersionList(fsvslice []*FilesystemVersion) []*FilesystemVersion {
|
||||
fslice := make([]*FilesystemVersion, 0, len(fsvslice))
|
||||
for _, fv := range fsvslice {
|
||||
if fv.Type != FilesystemVersion_Bookmark {
|
||||
fslice = append(fslice, fv)
|
||||
}
|
||||
}
|
||||
return fslice
|
||||
}
|
||||
|
||||
func IncrementalPath(receiver, sender []*FilesystemVersion) (incPath []*FilesystemVersion, conflict error) {
|
||||
|
||||
// Receive-side bookmarks can't be used as incremental-from,
|
||||
// and don't cause recv to fail if there is a newer bookmark than incremetal-form on the receiver.
|
||||
// So, simply mask them out.
|
||||
// This will also hide them in the report, but it keeps the code in this function simple,
|
||||
// and a user who complains about them missing in a conflict message will likely require
|
||||
// more education about bookmarks than a slightly more accurate error message. They'll get
|
||||
// that when they open an issue.
|
||||
receiver = StripBookmarksFromVersionList(receiver)
|
||||
|
||||
receiver = SortVersionListByCreateTXGThenBookmarkLTSnapshot(receiver)
|
||||
sender = SortVersionListByCreateTXGThenBookmarkLTSnapshot(sender)
|
||||
|
||||
|
||||
@@ -152,4 +152,18 @@ func TestIncrementalPath_BookmarkSupport(t *testing.T) {
|
||||
assert.Equal(t, l("@a,1", "@b,2"), path)
|
||||
})
|
||||
|
||||
// test that receive-side bookmarks younger than the most recent common ancestor do not cause a conflict
|
||||
doTest(l("@a,1", "#b,2"), l("@a,1", "@c,3"), func(path []*FilesystemVersion, conflict error) {
|
||||
assert.NoError(t, conflict)
|
||||
require.Len(t, path, 2)
|
||||
assert.Equal(t, l("@a,1")[0], path[0])
|
||||
assert.Equal(t, l("@c,3")[0], path[1])
|
||||
})
|
||||
doTest(l("#a,1"), l("@a,1", "@b,2"), func(path []*FilesystemVersion, conflict error) {
|
||||
assert.Nil(t, path)
|
||||
ca, ok := conflict.(*ConflictNoCommonAncestor)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, l(), ca.SortedReceiverVersions, "See comment in IncrementalPath() on why we don't include the boomkmark here")
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
+209
-283
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.25.0
|
||||
// protoc v3.12.4
|
||||
// protoc v3.14.0
|
||||
// source: pdu.proto
|
||||
|
||||
package pdu
|
||||
@@ -25,55 +25,6 @@ const (
|
||||
// of the legacy proto package is being used.
|
||||
const _ = proto.ProtoPackageIsVersion4
|
||||
|
||||
type Tri int32
|
||||
|
||||
const (
|
||||
Tri_DontCare Tri = 0
|
||||
Tri_False Tri = 1
|
||||
Tri_True Tri = 2
|
||||
)
|
||||
|
||||
// Enum value maps for Tri.
|
||||
var (
|
||||
Tri_name = map[int32]string{
|
||||
0: "DontCare",
|
||||
1: "False",
|
||||
2: "True",
|
||||
}
|
||||
Tri_value = map[string]int32{
|
||||
"DontCare": 0,
|
||||
"False": 1,
|
||||
"True": 2,
|
||||
}
|
||||
)
|
||||
|
||||
func (x Tri) Enum() *Tri {
|
||||
p := new(Tri)
|
||||
*p = x
|
||||
return p
|
||||
}
|
||||
|
||||
func (x Tri) String() string {
|
||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||
}
|
||||
|
||||
func (Tri) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_pdu_proto_enumTypes[0].Descriptor()
|
||||
}
|
||||
|
||||
func (Tri) Type() protoreflect.EnumType {
|
||||
return &file_pdu_proto_enumTypes[0]
|
||||
}
|
||||
|
||||
func (x Tri) Number() protoreflect.EnumNumber {
|
||||
return protoreflect.EnumNumber(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Tri.Descriptor instead.
|
||||
func (Tri) EnumDescriptor() ([]byte, []int) {
|
||||
return file_pdu_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
type ReplicationGuaranteeKind int32
|
||||
|
||||
const (
|
||||
@@ -110,11 +61,11 @@ func (x ReplicationGuaranteeKind) String() string {
|
||||
}
|
||||
|
||||
func (ReplicationGuaranteeKind) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_pdu_proto_enumTypes[1].Descriptor()
|
||||
return file_pdu_proto_enumTypes[0].Descriptor()
|
||||
}
|
||||
|
||||
func (ReplicationGuaranteeKind) Type() protoreflect.EnumType {
|
||||
return &file_pdu_proto_enumTypes[1]
|
||||
return &file_pdu_proto_enumTypes[0]
|
||||
}
|
||||
|
||||
func (x ReplicationGuaranteeKind) Number() protoreflect.EnumNumber {
|
||||
@@ -123,7 +74,7 @@ func (x ReplicationGuaranteeKind) Number() protoreflect.EnumNumber {
|
||||
|
||||
// Deprecated: Use ReplicationGuaranteeKind.Descriptor instead.
|
||||
func (ReplicationGuaranteeKind) EnumDescriptor() ([]byte, []int) {
|
||||
return file_pdu_proto_rawDescGZIP(), []int{1}
|
||||
return file_pdu_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
type FilesystemVersion_VersionType int32
|
||||
@@ -156,11 +107,11 @@ func (x FilesystemVersion_VersionType) String() string {
|
||||
}
|
||||
|
||||
func (FilesystemVersion_VersionType) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_pdu_proto_enumTypes[2].Descriptor()
|
||||
return file_pdu_proto_enumTypes[1].Descriptor()
|
||||
}
|
||||
|
||||
func (FilesystemVersion_VersionType) Type() protoreflect.EnumType {
|
||||
return &file_pdu_proto_enumTypes[2]
|
||||
return &file_pdu_proto_enumTypes[1]
|
||||
}
|
||||
|
||||
func (x FilesystemVersion_VersionType) Number() protoreflect.EnumNumber {
|
||||
@@ -265,7 +216,6 @@ type Filesystem struct {
|
||||
Path string `protobuf:"bytes,1,opt,name=Path,proto3" json:"Path,omitempty"`
|
||||
ResumeToken string `protobuf:"bytes,2,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"`
|
||||
IsPlaceholder bool `protobuf:"varint,3,opt,name=IsPlaceholder,proto3" json:"IsPlaceholder,omitempty"`
|
||||
IsEncrypted bool `protobuf:"varint,4,opt,name=IsEncrypted,proto3" json:"IsEncrypted,omitempty"`
|
||||
}
|
||||
|
||||
func (x *Filesystem) Reset() {
|
||||
@@ -321,13 +271,6 @@ func (x *Filesystem) GetIsPlaceholder() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Filesystem) GetIsEncrypted() bool {
|
||||
if x != nil {
|
||||
return x.IsEncrypted
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ListFilesystemVersionsReq struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
@@ -517,7 +460,6 @@ type SendReq struct {
|
||||
// ResumeToken is not empty, the GUIDs of From and To MUST correspond to those
|
||||
// encoded in the ResumeToken. Otherwise, the Sender MUST return an error.
|
||||
ResumeToken string `protobuf:"bytes,4,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"`
|
||||
Encrypted Tri `protobuf:"varint,5,opt,name=Encrypted,proto3,enum=Tri" json:"Encrypted,omitempty"`
|
||||
ReplicationConfig *ReplicationConfig `protobuf:"bytes,6,opt,name=ReplicationConfig,proto3" json:"ReplicationConfig,omitempty"`
|
||||
}
|
||||
|
||||
@@ -581,13 +523,6 @@ func (x *SendReq) GetResumeToken() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SendReq) GetEncrypted() Tri {
|
||||
if x != nil {
|
||||
return x.Encrypted
|
||||
}
|
||||
return Tri_DontCare
|
||||
}
|
||||
|
||||
func (x *SendReq) GetReplicationConfig() *ReplicationConfig {
|
||||
if x != nil {
|
||||
return x.ReplicationConfig
|
||||
@@ -1396,162 +1331,155 @@ var file_pdu_proto_rawDesc = []byte{
|
||||
0x65, 0x6d, 0x52, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x0b, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
|
||||
0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x46, 0x69, 0x6c,
|
||||
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x0b, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
|
||||
0x74, 0x65, 0x6d, 0x73, 0x22, 0x8a, 0x01, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
|
||||
0x74, 0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x04, 0x50, 0x61, 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x52, 0x65, 0x73, 0x75, 0x6d,
|
||||
0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x52, 0x65,
|
||||
0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x49, 0x73, 0x50,
|
||||
0x6c, 0x61, 0x63, 0x65, 0x68, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
|
||||
0x52, 0x0d, 0x49, 0x73, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x68, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12,
|
||||
0x20, 0x0a, 0x0b, 0x49, 0x73, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x18, 0x04,
|
||||
0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x49, 0x73, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65,
|
||||
0x64, 0x22, 0x3b, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
|
||||
0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x12, 0x1e,
|
||||
0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, 0x4b,
|
||||
0x74, 0x65, 0x6d, 0x73, 0x22, 0x68, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74,
|
||||
0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x04, 0x50, 0x61, 0x74, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65,
|
||||
0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x52, 0x65, 0x73,
|
||||
0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x49, 0x73, 0x50, 0x6c,
|
||||
0x61, 0x63, 0x65, 0x68, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52,
|
||||
0x0d, 0x49, 0x73, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x68, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x22, 0x3b,
|
||||
0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
|
||||
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x08, 0x56,
|
||||
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e,
|
||||
0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
|
||||
0x6e, 0x52, 0x08, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xd4, 0x01, 0x0a, 0x11,
|
||||
0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
|
||||
0x6e, 0x12, 0x32, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32,
|
||||
0x1e, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73,
|
||||
0x69, 0x6f, 0x6e, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52,
|
||||
0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x47, 0x75, 0x69,
|
||||
0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x47, 0x75, 0x69, 0x64, 0x12, 0x1c, 0x0a,
|
||||
0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x58, 0x47, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04,
|
||||
0x52, 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x58, 0x47, 0x12, 0x1a, 0x0a, 0x08, 0x43,
|
||||
0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43,
|
||||
0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x29, 0x0a, 0x0b, 0x56, 0x65, 0x72, 0x73, 0x69,
|
||||
0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68,
|
||||
0x6f, 0x74, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x42, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, 0x72, 0x6b,
|
||||
0x10, 0x01, 0x22, 0xfd, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x12, 0x1e,
|
||||
0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x26,
|
||||
0x0a, 0x04, 0x46, 0x72, 0x6f, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46,
|
||||
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46,
|
||||
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, 0x4b, 0x0a, 0x19, 0x4c,
|
||||
0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72,
|
||||
0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x08, 0x56, 0x65, 0x72, 0x73,
|
||||
0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c,
|
||||
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x08,
|
||||
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xd4, 0x01, 0x0a, 0x11, 0x46, 0x69, 0x6c,
|
||||
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x32,
|
||||
0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x46,
|
||||
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
|
||||
0x52, 0x04, 0x46, 0x72, 0x6f, 0x6d, 0x12, 0x22, 0x0a, 0x02, 0x54, 0x6f, 0x18, 0x03, 0x20, 0x01,
|
||||
0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56,
|
||||
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x02, 0x54, 0x6f, 0x12, 0x20, 0x0a, 0x0b, 0x52, 0x65,
|
||||
0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x0b, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x22, 0x0a, 0x09,
|
||||
0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32,
|
||||
0x04, 0x2e, 0x54, 0x72, 0x69, 0x52, 0x09, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64,
|
||||
0x12, 0x40, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43,
|
||||
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x52, 0x65,
|
||||
0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52,
|
||||
0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66,
|
||||
0x69, 0x67, 0x22, 0x51, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
|
||||
0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3c, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x65,
|
||||
0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x52, 0x65,
|
||||
0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50,
|
||||
0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x65,
|
||||
0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x8f, 0x01, 0x0a, 0x1b, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63,
|
||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x65,
|
||||
0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e,
|
||||
0x64, 0x52, 0x07, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x3b, 0x0a, 0x0b, 0x49, 0x6e,
|
||||
0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32,
|
||||
0x19, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x75, 0x61,
|
||||
0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x0b, 0x49, 0x6e, 0x63, 0x72,
|
||||
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x22, 0x34, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x70, 0x65,
|
||||
0x72, 0x74, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x57, 0x0a,
|
||||
0x07, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x0f, 0x55, 0x73, 0x65, 0x64,
|
||||
0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x08, 0x52, 0x0f, 0x55, 0x73, 0x65, 0x64, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b,
|
||||
0x65, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x45, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x69,
|
||||
0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x45, 0x78, 0x70, 0x65, 0x63, 0x74,
|
||||
0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x3e, 0x0a, 0x10, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f,
|
||||
0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x12, 0x2a, 0x0a, 0x0b, 0x4f, 0x72,
|
||||
0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||
0x08, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x52, 0x0b, 0x4f, 0x72, 0x69, 0x67, 0x69,
|
||||
0x6e, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x22, 0x12, 0x0a, 0x10, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f,
|
||||
0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x22, 0xbe, 0x01, 0x0a, 0x0a, 0x52,
|
||||
0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x6c,
|
||||
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46,
|
||||
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x22, 0x0a, 0x02, 0x54, 0x6f, 0x18,
|
||||
0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74,
|
||||
0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x02, 0x54, 0x6f, 0x12, 0x2a, 0x0a,
|
||||
0x10, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65,
|
||||
0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x65,
|
||||
0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x54, 0x79,
|
||||
0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x47, 0x75, 0x69, 0x64, 0x18, 0x03,
|
||||
0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x47, 0x75, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x43, 0x72,
|
||||
0x65, 0x61, 0x74, 0x65, 0x54, 0x58, 0x47, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x43,
|
||||
0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x58, 0x47, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x72, 0x65, 0x61,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x72, 0x65, 0x61,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x22, 0x29, 0x0a, 0x0b, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x54,
|
||||
0x79, 0x70, 0x65, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x10,
|
||||
0x00, 0x12, 0x0c, 0x0a, 0x08, 0x42, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, 0x72, 0x6b, 0x10, 0x01, 0x22,
|
||||
0xd9, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46,
|
||||
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x26, 0x0a, 0x04, 0x46,
|
||||
0x72, 0x6f, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65,
|
||||
0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x04, 0x46,
|
||||
0x72, 0x6f, 0x6d, 0x12, 0x22, 0x0a, 0x02, 0x54, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||
0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73,
|
||||
0x69, 0x6f, 0x6e, 0x52, 0x02, 0x54, 0x6f, 0x12, 0x20, 0x0a, 0x0b, 0x52, 0x65, 0x73, 0x75, 0x6d,
|
||||
0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x52, 0x65,
|
||||
0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x40, 0x0a, 0x11, 0x52, 0x65, 0x70,
|
||||
0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x04,
|
||||
0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06,
|
||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63,
|
||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x0c, 0x0a, 0x0a, 0x52,
|
||||
0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x52, 0x65, 0x73, 0x22, 0x67, 0x0a, 0x13, 0x44, 0x65, 0x73,
|
||||
0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71,
|
||||
0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
|
||||
0x12, 0x30, 0x0a, 0x09, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x18, 0x02, 0x20,
|
||||
0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
|
||||
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f,
|
||||
0x74, 0x73, 0x22, 0x5a, 0x0a, 0x12, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61,
|
||||
0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x08, 0x53, 0x6e, 0x61, 0x70,
|
||||
0x73, 0x68, 0x6f, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c,
|
||||
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x08,
|
||||
0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x45, 0x72, 0x72, 0x6f,
|
||||
0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x44,
|
||||
0x0a, 0x13, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f,
|
||||
0x74, 0x73, 0x52, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73,
|
||||
0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79,
|
||||
0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65, 0x73, 0x52, 0x07, 0x52, 0x65, 0x73,
|
||||
0x75, 0x6c, 0x74, 0x73, 0x22, 0x36, 0x0a, 0x14, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74,
|
||||
0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a,
|
||||
0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, 0x54, 0x0a, 0x14,
|
||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x51, 0x0a, 0x11, 0x52,
|
||||
0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
||||
0x12, 0x3c, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x8f,
|
||||
0x01, 0x0a, 0x1b, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f,
|
||||
0x6e, 0x66, 0x69, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x33,
|
||||
0x0a, 0x07, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32,
|
||||
0x19, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x75, 0x61,
|
||||
0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x07, 0x49, 0x6e, 0x69, 0x74,
|
||||
0x69, 0x61, 0x6c, 0x12, 0x3b, 0x0a, 0x0b, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74,
|
||||
0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69,
|
||||
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b,
|
||||
0x69, 0x6e, 0x64, 0x52, 0x0b, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c,
|
||||
0x22, 0x34, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x12, 0x12, 0x0a, 0x04,
|
||||
0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65,
|
||||
0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x57, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65,
|
||||
0x73, 0x12, 0x28, 0x0a, 0x0f, 0x55, 0x73, 0x65, 0x64, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54,
|
||||
0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x55, 0x73, 0x65, 0x64,
|
||||
0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x45,
|
||||
0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
|
||||
0x04, 0x52, 0x0c, 0x45, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x22,
|
||||
0x3e, 0x0a, 0x10, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64,
|
||||
0x52, 0x65, 0x71, 0x12, 0x2a, 0x0a, 0x0b, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52,
|
||||
0x65, 0x71, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52,
|
||||
0x65, 0x71, 0x52, 0x0b, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x22,
|
||||
0x12, 0x0a, 0x10, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64,
|
||||
0x52, 0x65, 0x73, 0x22, 0xbe, 0x01, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x52,
|
||||
0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74,
|
||||
0x65, 0x6d, 0x12, 0x22, 0x0a, 0x02, 0x54, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12,
|
||||
0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69,
|
||||
0x6f, 0x6e, 0x52, 0x02, 0x54, 0x6f, 0x12, 0x2a, 0x0a, 0x10, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52,
|
||||
0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
|
||||
0x52, 0x10, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6d, 0x65, 0x54, 0x6f, 0x6b,
|
||||
0x65, 0x6e, 0x12, 0x40, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
|
||||
0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e,
|
||||
0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69,
|
||||
0x67, 0x52, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f,
|
||||
0x6e, 0x66, 0x69, 0x67, 0x22, 0x0c, 0x0a, 0x0a, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x52,
|
||||
0x65, 0x73, 0x22, 0x67, 0x0a, 0x13, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61,
|
||||
0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x6c,
|
||||
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46,
|
||||
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x30, 0x0a, 0x09, 0x53, 0x6e, 0x61,
|
||||
0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46,
|
||||
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
|
||||
0x52, 0x09, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x22, 0x5a, 0x0a, 0x12, 0x44,
|
||||
0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x52, 0x65,
|
||||
0x73, 0x12, 0x2e, 0x0a, 0x08, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d,
|
||||
0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f,
|
||||
0x74, 0x12, 0x14, 0x0a, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x44, 0x0a, 0x13, 0x44, 0x65, 0x73, 0x74, 0x72,
|
||||
0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, 0x12, 0x2d,
|
||||
0x0a, 0x07, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
|
||||
0x13, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f,
|
||||
0x74, 0x52, 0x65, 0x73, 0x52, 0x07, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x22, 0x36, 0x0a,
|
||||
0x14, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73,
|
||||
0x6f, 0x72, 0x52, 0x65, 0x71, 0x12, 0x1e, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73,
|
||||
0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x73,
|
||||
0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, 0x54, 0x0a, 0x14, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x12, 0x14, 0x0a,
|
||||
0x04, 0x47, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x48, 0x00, 0x52, 0x04, 0x47,
|
||||
0x75, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x08, 0x4e, 0x6f, 0x74, 0x65, 0x78, 0x69, 0x73, 0x74, 0x18,
|
||||
0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x08, 0x4e, 0x6f, 0x74, 0x65, 0x78, 0x69, 0x73,
|
||||
0x74, 0x42, 0x08, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x23, 0x0a, 0x07, 0x50,
|
||||
0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x12, 0x18, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67,
|
||||
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
|
||||
0x22, 0x1d, 0x0a, 0x07, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x45,
|
||||
0x63, 0x68, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x2a,
|
||||
0x86, 0x01, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x47,
|
||||
0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x14, 0x0a, 0x10,
|
||||
0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64,
|
||||
0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x52,
|
||||
0x65, 0x73, 0x75, 0x6d, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x10, 0x01, 0x12, 0x23, 0x0a,
|
||||
0x1f, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d,
|
||||
0x65, 0x6e, 0x74, 0x61, 0x6c, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4e,
|
||||
0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x10, 0x03, 0x32, 0x8f, 0x03, 0x0a, 0x0b, 0x52, 0x65, 0x70,
|
||||
0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67,
|
||||
0x12, 0x08, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x1a, 0x08, 0x2e, 0x50, 0x69, 0x6e,
|
||||
0x67, 0x52, 0x65, 0x73, 0x12, 0x39, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65,
|
||||
0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x12, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69,
|
||||
0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, 0x4c, 0x69,
|
||||
0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x73, 0x12,
|
||||
0x50, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65,
|
||||
0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1a, 0x2e, 0x4c, 0x69, 0x73, 0x74,
|
||||
0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
|
||||
0x6e, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1a, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65,
|
||||
0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65,
|
||||
0x73, 0x12, 0x3e, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70,
|
||||
0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x14, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53,
|
||||
0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x14, 0x2e, 0x44, 0x65,
|
||||
0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65,
|
||||
0x73, 0x12, 0x41, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x12, 0x15, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x15, 0x2e,
|
||||
0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f,
|
||||
0x72, 0x52, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x04, 0x47, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x04, 0x48, 0x00, 0x52, 0x04, 0x47, 0x75, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x08, 0x4e, 0x6f,
|
||||
0x74, 0x65, 0x78, 0x69, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x08,
|
||||
0x4e, 0x6f, 0x74, 0x65, 0x78, 0x69, 0x73, 0x74, 0x42, 0x08, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75,
|
||||
0x6c, 0x74, 0x22, 0x23, 0x0a, 0x07, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x12, 0x18, 0x0a,
|
||||
0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
|
||||
0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x1d, 0x0a, 0x07, 0x50, 0x69, 0x6e, 0x67, 0x52,
|
||||
0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x2a, 0x28, 0x0a, 0x03, 0x54, 0x72, 0x69, 0x12, 0x0c, 0x0a,
|
||||
0x08, 0x44, 0x6f, 0x6e, 0x74, 0x43, 0x61, 0x72, 0x65, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x46,
|
||||
0x61, 0x6c, 0x73, 0x65, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x54, 0x72, 0x75, 0x65, 0x10, 0x02,
|
||||
0x2a, 0x86, 0x01, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x14, 0x0a,
|
||||
0x10, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69,
|
||||
0x64, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65,
|
||||
0x52, 0x65, 0x73, 0x75, 0x6d, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x10, 0x01, 0x12, 0x23,
|
||||
0x0a, 0x1f, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x49, 0x6e, 0x63, 0x72, 0x65,
|
||||
0x6d, 0x65, 0x6e, 0x74, 0x61, 0x6c, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
|
||||
0x6e, 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x47, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65,
|
||||
0x4e, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x10, 0x03, 0x32, 0x8f, 0x03, 0x0a, 0x0b, 0x52, 0x65,
|
||||
0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x04, 0x50, 0x69, 0x6e,
|
||||
0x67, 0x12, 0x08, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x1a, 0x08, 0x2e, 0x50, 0x69,
|
||||
0x6e, 0x67, 0x52, 0x65, 0x73, 0x12, 0x39, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c,
|
||||
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x12, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46,
|
||||
0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x71, 0x1a, 0x12, 0x2e, 0x4c,
|
||||
0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x73,
|
||||
0x12, 0x50, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74,
|
||||
0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1a, 0x2e, 0x4c, 0x69, 0x73,
|
||||
0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69,
|
||||
0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x1a, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x46, 0x69, 0x6c,
|
||||
0x65, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x52,
|
||||
0x65, 0x73, 0x12, 0x3e, 0x0a, 0x10, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61,
|
||||
0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x14, 0x2e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79,
|
||||
0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x1a, 0x14, 0x2e, 0x44,
|
||||
0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52,
|
||||
0x65, 0x73, 0x12, 0x41, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
|
||||
0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x12, 0x15, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63,
|
||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x52, 0x65, 0x71, 0x1a, 0x15,
|
||||
0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x75, 0x72, 0x73,
|
||||
0x6f, 0x72, 0x52, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x44, 0x72, 0x79,
|
||||
0x12, 0x08, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x08, 0x2e, 0x53, 0x65, 0x6e,
|
||||
0x64, 0x52, 0x65, 0x73, 0x12, 0x35, 0x0a, 0x0d, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70,
|
||||
0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x11, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70,
|
||||
0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x11, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43,
|
||||
0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x42, 0x07, 0x5a, 0x05, 0x2e,
|
||||
0x3b, 0x70, 0x64, 0x75, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x72, 0x52, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x07, 0x53, 0x65, 0x6e, 0x64, 0x44, 0x72, 0x79, 0x12,
|
||||
0x08, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x08, 0x2e, 0x53, 0x65, 0x6e, 0x64,
|
||||
0x52, 0x65, 0x73, 0x12, 0x35, 0x0a, 0x0d, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c,
|
||||
0x65, 0x74, 0x65, 0x64, 0x12, 0x11, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6c,
|
||||
0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x71, 0x1a, 0x11, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x43, 0x6f,
|
||||
0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x42, 0x07, 0x5a, 0x05, 0x2e, 0x3b,
|
||||
0x70, 0x64, 0x75, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -1566,71 +1494,69 @@ func file_pdu_proto_rawDescGZIP() []byte {
|
||||
return file_pdu_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_pdu_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
|
||||
var file_pdu_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
|
||||
var file_pdu_proto_msgTypes = make([]protoimpl.MessageInfo, 22)
|
||||
var file_pdu_proto_goTypes = []interface{}{
|
||||
(Tri)(0), // 0: Tri
|
||||
(ReplicationGuaranteeKind)(0), // 1: ReplicationGuaranteeKind
|
||||
(FilesystemVersion_VersionType)(0), // 2: FilesystemVersion.VersionType
|
||||
(*ListFilesystemReq)(nil), // 3: ListFilesystemReq
|
||||
(*ListFilesystemRes)(nil), // 4: ListFilesystemRes
|
||||
(*Filesystem)(nil), // 5: Filesystem
|
||||
(*ListFilesystemVersionsReq)(nil), // 6: ListFilesystemVersionsReq
|
||||
(*ListFilesystemVersionsRes)(nil), // 7: ListFilesystemVersionsRes
|
||||
(*FilesystemVersion)(nil), // 8: FilesystemVersion
|
||||
(*SendReq)(nil), // 9: SendReq
|
||||
(*ReplicationConfig)(nil), // 10: ReplicationConfig
|
||||
(*ReplicationConfigProtection)(nil), // 11: ReplicationConfigProtection
|
||||
(*Property)(nil), // 12: Property
|
||||
(*SendRes)(nil), // 13: SendRes
|
||||
(*SendCompletedReq)(nil), // 14: SendCompletedReq
|
||||
(*SendCompletedRes)(nil), // 15: SendCompletedRes
|
||||
(*ReceiveReq)(nil), // 16: ReceiveReq
|
||||
(*ReceiveRes)(nil), // 17: ReceiveRes
|
||||
(*DestroySnapshotsReq)(nil), // 18: DestroySnapshotsReq
|
||||
(*DestroySnapshotRes)(nil), // 19: DestroySnapshotRes
|
||||
(*DestroySnapshotsRes)(nil), // 20: DestroySnapshotsRes
|
||||
(*ReplicationCursorReq)(nil), // 21: ReplicationCursorReq
|
||||
(*ReplicationCursorRes)(nil), // 22: ReplicationCursorRes
|
||||
(*PingReq)(nil), // 23: PingReq
|
||||
(*PingRes)(nil), // 24: PingRes
|
||||
(ReplicationGuaranteeKind)(0), // 0: ReplicationGuaranteeKind
|
||||
(FilesystemVersion_VersionType)(0), // 1: FilesystemVersion.VersionType
|
||||
(*ListFilesystemReq)(nil), // 2: ListFilesystemReq
|
||||
(*ListFilesystemRes)(nil), // 3: ListFilesystemRes
|
||||
(*Filesystem)(nil), // 4: Filesystem
|
||||
(*ListFilesystemVersionsReq)(nil), // 5: ListFilesystemVersionsReq
|
||||
(*ListFilesystemVersionsRes)(nil), // 6: ListFilesystemVersionsRes
|
||||
(*FilesystemVersion)(nil), // 7: FilesystemVersion
|
||||
(*SendReq)(nil), // 8: SendReq
|
||||
(*ReplicationConfig)(nil), // 9: ReplicationConfig
|
||||
(*ReplicationConfigProtection)(nil), // 10: ReplicationConfigProtection
|
||||
(*Property)(nil), // 11: Property
|
||||
(*SendRes)(nil), // 12: SendRes
|
||||
(*SendCompletedReq)(nil), // 13: SendCompletedReq
|
||||
(*SendCompletedRes)(nil), // 14: SendCompletedRes
|
||||
(*ReceiveReq)(nil), // 15: ReceiveReq
|
||||
(*ReceiveRes)(nil), // 16: ReceiveRes
|
||||
(*DestroySnapshotsReq)(nil), // 17: DestroySnapshotsReq
|
||||
(*DestroySnapshotRes)(nil), // 18: DestroySnapshotRes
|
||||
(*DestroySnapshotsRes)(nil), // 19: DestroySnapshotsRes
|
||||
(*ReplicationCursorReq)(nil), // 20: ReplicationCursorReq
|
||||
(*ReplicationCursorRes)(nil), // 21: ReplicationCursorRes
|
||||
(*PingReq)(nil), // 22: PingReq
|
||||
(*PingRes)(nil), // 23: PingRes
|
||||
}
|
||||
var file_pdu_proto_depIdxs = []int32{
|
||||
5, // 0: ListFilesystemRes.Filesystems:type_name -> Filesystem
|
||||
8, // 1: ListFilesystemVersionsRes.Versions:type_name -> FilesystemVersion
|
||||
2, // 2: FilesystemVersion.Type:type_name -> FilesystemVersion.VersionType
|
||||
8, // 3: SendReq.From:type_name -> FilesystemVersion
|
||||
8, // 4: SendReq.To:type_name -> FilesystemVersion
|
||||
0, // 5: SendReq.Encrypted:type_name -> Tri
|
||||
10, // 6: SendReq.ReplicationConfig:type_name -> ReplicationConfig
|
||||
11, // 7: ReplicationConfig.protection:type_name -> ReplicationConfigProtection
|
||||
1, // 8: ReplicationConfigProtection.Initial:type_name -> ReplicationGuaranteeKind
|
||||
1, // 9: ReplicationConfigProtection.Incremental:type_name -> ReplicationGuaranteeKind
|
||||
9, // 10: SendCompletedReq.OriginalReq:type_name -> SendReq
|
||||
8, // 11: ReceiveReq.To:type_name -> FilesystemVersion
|
||||
10, // 12: ReceiveReq.ReplicationConfig:type_name -> ReplicationConfig
|
||||
8, // 13: DestroySnapshotsReq.Snapshots:type_name -> FilesystemVersion
|
||||
8, // 14: DestroySnapshotRes.Snapshot:type_name -> FilesystemVersion
|
||||
19, // 15: DestroySnapshotsRes.Results:type_name -> DestroySnapshotRes
|
||||
23, // 16: Replication.Ping:input_type -> PingReq
|
||||
3, // 17: Replication.ListFilesystems:input_type -> ListFilesystemReq
|
||||
6, // 18: Replication.ListFilesystemVersions:input_type -> ListFilesystemVersionsReq
|
||||
18, // 19: Replication.DestroySnapshots:input_type -> DestroySnapshotsReq
|
||||
21, // 20: Replication.ReplicationCursor:input_type -> ReplicationCursorReq
|
||||
9, // 21: Replication.SendDry:input_type -> SendReq
|
||||
14, // 22: Replication.SendCompleted:input_type -> SendCompletedReq
|
||||
24, // 23: Replication.Ping:output_type -> PingRes
|
||||
4, // 24: Replication.ListFilesystems:output_type -> ListFilesystemRes
|
||||
7, // 25: Replication.ListFilesystemVersions:output_type -> ListFilesystemVersionsRes
|
||||
20, // 26: Replication.DestroySnapshots:output_type -> DestroySnapshotsRes
|
||||
22, // 27: Replication.ReplicationCursor:output_type -> ReplicationCursorRes
|
||||
13, // 28: Replication.SendDry:output_type -> SendRes
|
||||
15, // 29: Replication.SendCompleted:output_type -> SendCompletedRes
|
||||
23, // [23:30] is the sub-list for method output_type
|
||||
16, // [16:23] is the sub-list for method input_type
|
||||
16, // [16:16] is the sub-list for extension type_name
|
||||
16, // [16:16] is the sub-list for extension extendee
|
||||
0, // [0:16] is the sub-list for field type_name
|
||||
4, // 0: ListFilesystemRes.Filesystems:type_name -> Filesystem
|
||||
7, // 1: ListFilesystemVersionsRes.Versions:type_name -> FilesystemVersion
|
||||
1, // 2: FilesystemVersion.Type:type_name -> FilesystemVersion.VersionType
|
||||
7, // 3: SendReq.From:type_name -> FilesystemVersion
|
||||
7, // 4: SendReq.To:type_name -> FilesystemVersion
|
||||
9, // 5: SendReq.ReplicationConfig:type_name -> ReplicationConfig
|
||||
10, // 6: ReplicationConfig.protection:type_name -> ReplicationConfigProtection
|
||||
0, // 7: ReplicationConfigProtection.Initial:type_name -> ReplicationGuaranteeKind
|
||||
0, // 8: ReplicationConfigProtection.Incremental:type_name -> ReplicationGuaranteeKind
|
||||
8, // 9: SendCompletedReq.OriginalReq:type_name -> SendReq
|
||||
7, // 10: ReceiveReq.To:type_name -> FilesystemVersion
|
||||
9, // 11: ReceiveReq.ReplicationConfig:type_name -> ReplicationConfig
|
||||
7, // 12: DestroySnapshotsReq.Snapshots:type_name -> FilesystemVersion
|
||||
7, // 13: DestroySnapshotRes.Snapshot:type_name -> FilesystemVersion
|
||||
18, // 14: DestroySnapshotsRes.Results:type_name -> DestroySnapshotRes
|
||||
22, // 15: Replication.Ping:input_type -> PingReq
|
||||
2, // 16: Replication.ListFilesystems:input_type -> ListFilesystemReq
|
||||
5, // 17: Replication.ListFilesystemVersions:input_type -> ListFilesystemVersionsReq
|
||||
17, // 18: Replication.DestroySnapshots:input_type -> DestroySnapshotsReq
|
||||
20, // 19: Replication.ReplicationCursor:input_type -> ReplicationCursorReq
|
||||
8, // 20: Replication.SendDry:input_type -> SendReq
|
||||
13, // 21: Replication.SendCompleted:input_type -> SendCompletedReq
|
||||
23, // 22: Replication.Ping:output_type -> PingRes
|
||||
3, // 23: Replication.ListFilesystems:output_type -> ListFilesystemRes
|
||||
6, // 24: Replication.ListFilesystemVersions:output_type -> ListFilesystemVersionsRes
|
||||
19, // 25: Replication.DestroySnapshots:output_type -> DestroySnapshotsRes
|
||||
21, // 26: Replication.ReplicationCursor:output_type -> ReplicationCursorRes
|
||||
12, // 27: Replication.SendDry:output_type -> SendRes
|
||||
14, // 28: Replication.SendCompleted:output_type -> SendCompletedRes
|
||||
22, // [22:29] is the sub-list for method output_type
|
||||
15, // [15:22] is the sub-list for method input_type
|
||||
15, // [15:15] is the sub-list for extension type_name
|
||||
15, // [15:15] is the sub-list for extension extendee
|
||||
0, // [0:15] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_pdu_proto_init() }
|
||||
@@ -1913,7 +1839,7 @@ func file_pdu_proto_init() {
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_pdu_proto_rawDesc,
|
||||
NumEnums: 3,
|
||||
NumEnums: 2,
|
||||
NumMessages: 22,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
|
||||
@@ -21,7 +21,6 @@ message Filesystem {
|
||||
string Path = 1;
|
||||
string ResumeToken = 2;
|
||||
bool IsPlaceholder = 3;
|
||||
bool IsEncrypted = 4;
|
||||
}
|
||||
|
||||
message ListFilesystemVersionsReq { string Filesystem = 1; }
|
||||
@@ -40,12 +39,6 @@ message FilesystemVersion {
|
||||
string Creation = 5; // RFC 3339
|
||||
}
|
||||
|
||||
enum Tri {
|
||||
DontCare = 0;
|
||||
False = 1;
|
||||
True = 2;
|
||||
}
|
||||
|
||||
message SendReq {
|
||||
string Filesystem = 1;
|
||||
// May be empty / null to request a full transfer of To
|
||||
@@ -59,7 +52,6 @@ message SendReq {
|
||||
// ResumeToken is not empty, the GUIDs of From and To MUST correspond to those
|
||||
// encoded in the ResumeToken. Otherwise, the Sender MUST return an error.
|
||||
string ResumeToken = 4;
|
||||
Tri Encrypted = 5;
|
||||
|
||||
ReplicationConfig ReplicationConfig = 6;
|
||||
}
|
||||
|
||||
@@ -156,8 +156,7 @@ type Step struct {
|
||||
|
||||
parent *Filesystem
|
||||
from, to *pdu.FilesystemVersion // from may be nil, indicating full send
|
||||
encrypt tri
|
||||
resumeToken string // empty means no resume token shall be used
|
||||
resumeToken string // empty means no resume token shall be used
|
||||
|
||||
expectedSize uint64 // 0 means no size estimate present / possible
|
||||
|
||||
@@ -201,22 +200,10 @@ func (s *Step) ReportInfo() *report.StepInfo {
|
||||
if s.from != nil {
|
||||
from = s.from.RelName()
|
||||
}
|
||||
var encrypted report.EncryptedEnum
|
||||
switch s.encrypt {
|
||||
case DontCare:
|
||||
encrypted = report.EncryptedSenderDependent
|
||||
case True:
|
||||
encrypted = report.EncryptedTrue
|
||||
case False:
|
||||
encrypted = report.EncryptedFalse
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown variant %s", s.encrypt))
|
||||
}
|
||||
return &report.StepInfo{
|
||||
From: from,
|
||||
To: s.to.RelName(),
|
||||
Resumed: s.resumeToken != "",
|
||||
Encrypted: encrypted,
|
||||
BytesExpected: s.expectedSize,
|
||||
BytesReplicated: byteCounter,
|
||||
}
|
||||
@@ -346,10 +333,6 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
|
||||
|
||||
log(ctx).Debug("assessing filesystem")
|
||||
|
||||
if fs.policy.EncryptedSend == True && !fs.senderFS.GetIsEncrypted() {
|
||||
return nil, fmt.Errorf("sender filesystem is not encrypted but policy mandates encrypted send")
|
||||
}
|
||||
|
||||
sfsvsres, err := fs.sender.ListFilesystemVersions(ctx, &pdu.ListFilesystemVersionsReq{Filesystem: fs.Path})
|
||||
if err != nil {
|
||||
log(ctx).WithError(err).Error("cannot get remote filesystem versions")
|
||||
@@ -423,24 +406,7 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
|
||||
}
|
||||
}
|
||||
|
||||
encryptionMatches := false
|
||||
switch fs.policy.EncryptedSend {
|
||||
case True:
|
||||
encryptionMatches = resumeToken.RawOK && resumeToken.CompressOK
|
||||
case False:
|
||||
encryptionMatches = !resumeToken.RawOK && !resumeToken.CompressOK
|
||||
case DontCare:
|
||||
encryptionMatches = true
|
||||
}
|
||||
|
||||
log(ctx).WithField("fromVersion", fromVersion).
|
||||
WithField("toVersion", toVersion).
|
||||
WithField("encryptionMatches", encryptionMatches).
|
||||
Debug("result of resume-token-matching to sender's versions")
|
||||
|
||||
if !encryptionMatches {
|
||||
return nil, fmt.Errorf("resume token `rawok`=%v and `compressok`=%v are incompatible with encryption policy=%v", resumeToken.RawOK, resumeToken.CompressOK, fs.policy.EncryptedSend)
|
||||
} else if toVersion == nil {
|
||||
if toVersion == nil {
|
||||
return nil, fmt.Errorf("resume token `toguid` = %v not found on sender (`toname` = %q)", resumeToken.ToGUID, resumeToken.ToName)
|
||||
} else if fromVersion == toVersion {
|
||||
return nil, fmt.Errorf("resume token `fromguid` and `toguid` match same version on sener")
|
||||
@@ -452,9 +418,8 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
|
||||
sender: fs.sender,
|
||||
receiver: fs.receiver,
|
||||
|
||||
from: fromVersion,
|
||||
to: toVersion,
|
||||
encrypt: fs.policy.EncryptedSend,
|
||||
from: fromVersion,
|
||||
to: toVersion,
|
||||
|
||||
resumeToken: resumeTokenRaw,
|
||||
}
|
||||
@@ -480,7 +445,6 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
|
||||
receiver: fs.receiver,
|
||||
from: remainingSFSVs[i],
|
||||
to: remainingSFSVs[i+1],
|
||||
encrypt: fs.policy.EncryptedSend,
|
||||
})
|
||||
}
|
||||
} else { // resumeToken == nil
|
||||
@@ -510,9 +474,8 @@ func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
|
||||
sender: fs.sender,
|
||||
receiver: fs.receiver,
|
||||
|
||||
from: path[i], // nil in case of initial repl
|
||||
to: path[i+1],
|
||||
encrypt: fs.policy.EncryptedSend,
|
||||
from: path[i], // nil in case of initial repl
|
||||
to: path[i+1],
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -592,7 +555,6 @@ func (s *Step) buildSendRequest() (sr *pdu.SendReq) {
|
||||
Filesystem: fs,
|
||||
From: s.from, // may be nil
|
||||
To: s.to,
|
||||
Encrypted: s.encrypt.ToPDU(),
|
||||
ResumeToken: s.resumeToken,
|
||||
ReplicationConfig: s.parent.policy.ReplicationConfig,
|
||||
}
|
||||
|
||||
@@ -63,7 +63,6 @@ func ConflictResolutionFromConfig(in *config.ConflictResolution) (*ConflictResol
|
||||
}
|
||||
|
||||
type PlannerPolicy struct {
|
||||
EncryptedSend tri // all sends must be encrypted (send -w, and encryption!=off)
|
||||
ConflictResolution *ConflictResolution `validate:"ne=nil"`
|
||||
ReplicationConfig *pdu.ReplicationConfig `validate:"ne=nil"`
|
||||
SizeEstimationConcurrency int `validate:"gte=1"`
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||
)
|
||||
|
||||
type tri int
|
||||
|
||||
const (
|
||||
DontCare = 0x0
|
||||
False = 0x1
|
||||
True = 0x2
|
||||
)
|
||||
|
||||
func (t tri) String() string {
|
||||
switch t {
|
||||
case DontCare:
|
||||
return "dontcare"
|
||||
case False:
|
||||
return "false"
|
||||
case True:
|
||||
return "true"
|
||||
}
|
||||
panic(fmt.Sprintf("unknown variant %v", int(t)))
|
||||
}
|
||||
|
||||
func (t tri) ToPDU() pdu.Tri {
|
||||
switch t {
|
||||
case DontCare:
|
||||
return pdu.Tri_DontCare
|
||||
case False:
|
||||
return pdu.Tri_False
|
||||
case True:
|
||||
return pdu.Tri_True
|
||||
}
|
||||
panic(fmt.Sprintf("unknown variant %v", int(t)))
|
||||
}
|
||||
|
||||
func TriFromBool(b bool) tri {
|
||||
if b {
|
||||
return True
|
||||
}
|
||||
return False
|
||||
}
|
||||
@@ -97,18 +97,9 @@ type StepReport struct {
|
||||
Info *StepInfo
|
||||
}
|
||||
|
||||
type EncryptedEnum string
|
||||
|
||||
const (
|
||||
EncryptedTrue EncryptedEnum = "yes"
|
||||
EncryptedFalse EncryptedEnum = "no"
|
||||
EncryptedSenderDependent EncryptedEnum = "sender-dependent"
|
||||
)
|
||||
|
||||
type StepInfo struct {
|
||||
From, To string
|
||||
Resumed bool
|
||||
Encrypted EncryptedEnum
|
||||
BytesExpected uint64
|
||||
BytesReplicated uint64
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
//nolint[:deadcode,unused]
|
||||
//nolint:deadcode,unused
|
||||
func debug(format string, args ...interface{}) {
|
||||
if debugEnabled {
|
||||
fmt.Fprintf(os.Stderr, "rpc/dataconn: %s\n", fmt.Sprintf(format, args...))
|
||||
|
||||
@@ -24,6 +24,9 @@ func (e HeartbeatTimeout) Error() string {
|
||||
return "heartbeat timeout"
|
||||
}
|
||||
|
||||
// This function is deprecated in net.Error and since this
|
||||
// function is not involved in .Accept() code path, nothing
|
||||
// really needs this method to be here.
|
||||
func (e HeartbeatTimeout) Temporary() bool { return true }
|
||||
|
||||
func (e HeartbeatTimeout) Timeout() bool { return true }
|
||||
|
||||
@@ -13,7 +13,7 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
//nolint[:deadcode,unused]
|
||||
//nolint:deadcode,unused
|
||||
func debug(format string, args ...interface{}) {
|
||||
if debugEnabled {
|
||||
fmt.Fprintf(os.Stderr, "rpc/dataconn/heartbeatconn: %s\n", fmt.Sprintf(format, args...))
|
||||
|
||||
+20
-24
@@ -6,12 +6,12 @@
|
||||
// In commit 082335df5d85e1b0b9faa35ff182c71886142d3e and earlier, heartbeatconn would fail
|
||||
// this benchmark with a writev I/O timeout (here the ss(8) output at the time of failure)
|
||||
//
|
||||
// ESTAB 33369 0 127.0.0.1:12345 127.0.0.1:57282 users:(("heartbeatconn_i",pid=25953,fd=5))
|
||||
// cubic wscale:7,7 rto:203 rtt:2.992/5.849 ato:162 mss:32768 pmtu:65535 rcvmss:32741 advmss:65483 cwnd:10 bytes_sent:48 bytes_acked:48 bytes_received:195401 segs_out:44 segs_in:57 data_segs_out:6 data_segs_in:34 send 876.1Mbps lastsnd:125 lastrcv:9390 lastack:125 pacing_rate 1752.0Mbps delivery_rate 6393.8Mbps delivered:7 app_limited busy:42ms rcv_rtt:1 rcv_space:65483 rcv_ssthresh:65483 minrtt:0.029
|
||||
// --
|
||||
// ESTAB 0 3956805 127.0.0.1:57282 127.0.0.1:12345 users:(("heartbeatconn_i",pid=26100,fd=3))
|
||||
// cubic wscale:7,7 rto:211 backoff:5 rtt:10.38/16.937 ato:40 mss:32768 pmtu:65535 rcvmss:536 advmss:65483 cwnd:10 bytes_sent:195401 bytes_acked:195402 bytes_received:48 segs_out:57 segs_in:45 data_segs_out:34 data_segs_in:6 send 252.5Mbps lastsnd:9390 lastrcv:125 lastack:125 pacing_rate 505.1Mbps delivery_rate 1971.0Mbps delivered:35 busy:30127ms rwnd_limited:30086ms(99.9%) rcv_space:65495 rcv_ssthresh:65495 notsent:3956805 minrtt:0.007
|
||||
// panic: writev tcp 127.0.0.1:57282->127.0.0.1:12345: i/o timeout
|
||||
// ESTAB 33369 0 127.0.0.1:12345 127.0.0.1:57282 users:(("heartbeatconn_i",pid=25953,fd=5))
|
||||
// cubic wscale:7,7 rto:203 rtt:2.992/5.849 ato:162 mss:32768 pmtu:65535 rcvmss:32741 advmss:65483 cwnd:10 bytes_sent:48 bytes_acked:48 bytes_received:195401 segs_out:44 segs_in:57 data_segs_out:6 data_segs_in:34 send 876.1Mbps lastsnd:125 lastrcv:9390 lastack:125 pacing_rate 1752.0Mbps delivery_rate 6393.8Mbps delivered:7 app_limited busy:42ms rcv_rtt:1 rcv_space:65483 rcv_ssthresh:65483 minrtt:0.029
|
||||
// --
|
||||
// ESTAB 0 3956805 127.0.0.1:57282 127.0.0.1:12345 users:(("heartbeatconn_i",pid=26100,fd=3))
|
||||
// cubic wscale:7,7 rto:211 backoff:5 rtt:10.38/16.937 ato:40 mss:32768 pmtu:65535 rcvmss:536 advmss:65483 cwnd:10 bytes_sent:195401 bytes_acked:195402 bytes_received:48 segs_out:57 segs_in:45 data_segs_out:34 data_segs_in:6 send 252.5Mbps lastsnd:9390 lastrcv:125 lastack:125 pacing_rate 505.1Mbps delivery_rate 1971.0Mbps delivered:35 busy:30127ms rwnd_limited:30086ms(99.9%) rcv_space:65495 rcv_ssthresh:65495 notsent:3956805 minrtt:0.007
|
||||
// panic: writev tcp 127.0.0.1:57282->127.0.0.1:12345: i/o timeout
|
||||
//
|
||||
// The assumed reason for those writev timeouts is the following:
|
||||
// - Sporadic server stalls (sever data handling, usually I/O) cause TCP exponential backoff on the client for client->server
|
||||
@@ -22,27 +22,23 @@
|
||||
// The fix contained in the commit this message was committed with resets the deadline whenever
|
||||
// a heartbeat is received from the server.
|
||||
//
|
||||
//
|
||||
// How to run this integration test:
|
||||
//
|
||||
// Terminal 1:
|
||||
// $ ZREPL_RPC_DATACONN_HEARTBEATCONN_DEBUG=1 go run heartbeatconn_integration_variablereceiverate.go -mode server -addr 127.0.0.1:12345
|
||||
// rpc/dataconn/heartbeatconn: send heartbeat
|
||||
// rpc/dataconn/heartbeatconn: send heartbeat
|
||||
// ...
|
||||
//
|
||||
// Terminal 1:
|
||||
// $ ZREPL_RPC_DATACONN_HEARTBEATCONN_DEBUG=1 go run heartbeatconn_integration_variablereceiverate.go -mode server -addr 127.0.0.1:12345
|
||||
// rpc/dataconn/heartbeatconn: send heartbeat
|
||||
// rpc/dataconn/heartbeatconn: send heartbeat
|
||||
// ...
|
||||
//
|
||||
// Terminal 2:
|
||||
// $ ZREPL_RPC_DATACONN_HEARTBEATCONN_DEBUG=1 go run heartbeatconn_integration_variablereceiverate.go -mode client -addr 127.0.0.1:12345
|
||||
// rpc/dataconn/heartbeatconn: received heartbeat, resetting write timeout
|
||||
// rpc/dataconn/heartbeatconn: renew frameconn write timeout returned errT=<nil> err=%!s(<nil>)
|
||||
// rpc/dataconn/heartbeatconn: send heartbeat
|
||||
// rpc/dataconn/heartbeatconn: received heartbeat, resetting write timeout
|
||||
// rpc/dataconn/heartbeatconn: renew frameconn write timeout returned errT=<nil> err=%!s(<nil>)
|
||||
// rpc/dataconn/heartbeatconn: received heartbeat, resetting write timeout
|
||||
// ...
|
||||
//
|
||||
// You should observe
|
||||
// Terminal 2:
|
||||
// $ ZREPL_RPC_DATACONN_HEARTBEATCONN_DEBUG=1 go run heartbeatconn_integration_variablereceiverate.go -mode client -addr 127.0.0.1:12345
|
||||
// rpc/dataconn/heartbeatconn: received heartbeat, resetting write timeout
|
||||
// rpc/dataconn/heartbeatconn: renew frameconn write timeout returned errT=<nil> err=%!s(<nil>)
|
||||
// rpc/dataconn/heartbeatconn: send heartbeat
|
||||
// rpc/dataconn/heartbeatconn: received heartbeat, resetting write timeout
|
||||
// rpc/dataconn/heartbeatconn: renew frameconn write timeout returned errT=<nil> err=%!s(<nil>)
|
||||
// rpc/dataconn/heartbeatconn: received heartbeat, resetting write timeout
|
||||
// ...
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
//
|
||||
// With stdin / stdout on client and server, simulating zfs send|recv piping
|
||||
//
|
||||
// ./microbenchmark -appmode server | pv -r > /dev/null
|
||||
// ./microbenchmark -appmode client -direction recv < /dev/zero
|
||||
//
|
||||
// ./microbenchmark -appmode server | pv -r > /dev/null
|
||||
// ./microbenchmark -appmode client -direction recv < /dev/zero
|
||||
//
|
||||
// Without the overhead of pipes (just protocol performance, mostly useful with perf bc no bw measurement)
|
||||
//
|
||||
// ./microbenchmark -appmode client -direction recv -devnoopWriter -devnoopReader
|
||||
// ./microbenchmark -appmode server -devnoopReader -devnoopWriter
|
||||
//
|
||||
// ./microbenchmark -appmode client -direction recv -devnoopWriter -devnoopReader
|
||||
// ./microbenchmark -appmode server -devnoopReader -devnoopWriter
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -29,7 +29,7 @@ func WithLogger(ctx context.Context, log Logger) context.Context {
|
||||
return context.WithValue(ctx, contextKeyLogger, log)
|
||||
}
|
||||
|
||||
//nolint[:deadcode,unused]
|
||||
//nolint:deadcode,unused
|
||||
func getLog(ctx context.Context) Logger {
|
||||
log, ok := ctx.Value(contextKeyLogger).(Logger)
|
||||
if !ok {
|
||||
@@ -181,23 +181,19 @@ func (e *ReadStreamError) Error() string {
|
||||
|
||||
var _ net.Error = &ReadStreamError{}
|
||||
|
||||
func (e ReadStreamError) netErr() net.Error {
|
||||
if netErr, ok := e.Err.(net.Error); ok {
|
||||
return netErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e ReadStreamError) Timeout() bool {
|
||||
if netErr := e.netErr(); netErr != nil {
|
||||
if netErr, ok := e.Err.(net.Error); ok {
|
||||
return netErr.Timeout()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// This function is deprecated in net.Error and since this
|
||||
// function is not involved in .Accept() code path, nothing
|
||||
// really needs this method to be here.
|
||||
func (e ReadStreamError) Temporary() bool {
|
||||
if netErr := e.netErr(); netErr != nil {
|
||||
return netErr.Temporary()
|
||||
if te, ok := e.Err.(interface{ Temporary() bool }); ok {
|
||||
return te.Temporary()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -232,7 +232,12 @@ var _ net.Error = (*closeStateErrConnectionClosed)(nil)
|
||||
func (e *closeStateErrConnectionClosed) Error() string {
|
||||
return "connection closed"
|
||||
}
|
||||
func (e *closeStateErrConnectionClosed) Timeout() bool { return false }
|
||||
|
||||
func (e *closeStateErrConnectionClosed) Timeout() bool { return false }
|
||||
|
||||
// This function is deprecated in net.Error and since this
|
||||
// function is not involved in .Accept() code path, nothing
|
||||
// really needs this method to be here.
|
||||
func (e *closeStateErrConnectionClosed) Temporary() bool { return false }
|
||||
|
||||
func (s *closeState) CloseEntry() error {
|
||||
|
||||
@@ -13,7 +13,7 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
//nolint[:deadcode,unused]
|
||||
//nolint:deadcode,unused
|
||||
func debug(format string, args ...interface{}) {
|
||||
if debugEnabled {
|
||||
fmt.Fprintf(os.Stderr, "rpc/dataconn/stream: %s\n", fmt.Sprintf(format, args...))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.25.0
|
||||
// protoc v3.12.4
|
||||
// protoc v3.14.0
|
||||
// source: grpcauth.proto
|
||||
|
||||
package pdu
|
||||
|
||||
@@ -1,25 +1,5 @@
|
||||
// Package netadaptor implements an adaptor from
|
||||
// transport.AuthenticatedListener to net.Listener.
|
||||
//
|
||||
// In contrast to transport.AuthenticatedListener,
|
||||
// net.Listener is commonly expected (e.g. by net/http.Server.Serve),
|
||||
// to return errors that fulfill the Temporary interface:
|
||||
// interface Temporary { Temporary() bool }
|
||||
// Common behavior of packages consuming net.Listener is to return
|
||||
// from the serve-loop if an error returned by Accept is not Temporary,
|
||||
// i.e., does not implement the interface or is !net.Error.Temporary().
|
||||
//
|
||||
// The zrepl transport infrastructure was written with the
|
||||
// idea that Accept() may return any kind of error, and the consumer
|
||||
// would just log the error and continue calling Accept().
|
||||
// We have to adapt these listeners' behavior to the expectations
|
||||
// of net/http.Server.
|
||||
//
|
||||
// Hence, Listener does not return an error at all but blocks the
|
||||
// caller of Accept() until we get a (successfully authenticated)
|
||||
// connection without errors from the transport.
|
||||
// Accept errors returned from the transport are logged as errors
|
||||
// to the logger passed on initialization.
|
||||
package netadaptor
|
||||
|
||||
import (
|
||||
|
||||
+2
-2
@@ -188,8 +188,8 @@ func (c *Client) WaitForConnectivity(ctx context.Context) error {
|
||||
data, dataErr := c.dataClient.ReqPing(ctx, &req)
|
||||
// dataClient uses transport.Connecter, which doesn't expose WaitForReady(true)
|
||||
// => we need to mask dial timeouts
|
||||
if err, ok := dataErr.(interface{ Temporary() bool }); ok && err.Temporary() {
|
||||
// Rate-limit pings here in case Temporary() is a mis-classification
|
||||
if err, ok := dataErr.(interface{ Timeout() bool }); ok && err.Timeout() {
|
||||
// Rate-limit pings here in case Timeout() == true is a mis-classification
|
||||
// or returns immediately (this is a tight loop in that case)
|
||||
// TODO keep this in lockstep with controlClient
|
||||
// => don't use FailFast for control, but check that both control and data worked
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
//nolint[:deadcode,unused]
|
||||
//nolint:deadcode,unused
|
||||
func debug(format string, args ...interface{}) {
|
||||
if debugEnabled {
|
||||
fmt.Fprintf(os.Stderr, "rpc: %s\n", fmt.Sprintf(format, args...))
|
||||
|
||||
+62
-64
@@ -3,7 +3,7 @@
|
||||
// The zrepl documentation refers to the client as the
|
||||
// `active side` and to the server as the `passive side`.
|
||||
//
|
||||
// Design Considerations
|
||||
// # Design Considerations
|
||||
//
|
||||
// zrepl has non-standard requirements to remote procedure calls (RPC):
|
||||
// whereas the coordination of replication (the planning phase) mostly
|
||||
@@ -35,7 +35,7 @@
|
||||
//
|
||||
// Hence, this package attempts to combine the best of both worlds:
|
||||
//
|
||||
// GRPC for Coordination and Dataconn for Bulk Data Transfer
|
||||
// # GRPC for Coordination and Dataconn for Bulk Data Transfer
|
||||
//
|
||||
// This package's Client uses its transport.Connecter to maintain
|
||||
// separate control and data connections to the Server.
|
||||
@@ -47,68 +47,66 @@
|
||||
// The following ASCII diagram gives an overview of how the individual
|
||||
// building blocks are glued together:
|
||||
//
|
||||
// +------------+
|
||||
// | rpc.Client |
|
||||
// +------------+
|
||||
// | |
|
||||
// +--------+ +------------+
|
||||
// | |
|
||||
// +---------v-----------+ +--------v------+
|
||||
// |pdu.ReplicationClient| |dataconn.Client|
|
||||
// +---------------------+ +--------v------+
|
||||
// | label: label: |
|
||||
// | zrepl_control zrepl_data |
|
||||
// +--------+ +------------+
|
||||
// | |
|
||||
// +--v---------v---+
|
||||
// | transportmux |
|
||||
// +-------+--------+
|
||||
// | uses
|
||||
// +-------v--------+
|
||||
// |versionhandshake|
|
||||
// +-------+--------+
|
||||
// | uses
|
||||
// +------v------+
|
||||
// | transport |
|
||||
// +------+------+
|
||||
// |
|
||||
// NETWORK
|
||||
// |
|
||||
// +------+------+
|
||||
// | transport |
|
||||
// +------^------+
|
||||
// | uses
|
||||
// +-------+--------+
|
||||
// |versionhandshake|
|
||||
// +-------^--------+
|
||||
// | uses
|
||||
// +-------+--------+
|
||||
// | transportmux |
|
||||
// +--^--------^----+
|
||||
// | |
|
||||
// +--------+ --------------+ ---
|
||||
// | | |
|
||||
// | label: label: | |
|
||||
// | zrepl_control zrepl_data | |
|
||||
// +-----+----+ +-----------+---+ |
|
||||
// |netadaptor| |dataconn.Server| | rpc.Server
|
||||
// | + | +------+--------+ |
|
||||
// |grpcclient| | |
|
||||
// |identity | | |
|
||||
// +-----+----+ | |
|
||||
// | | |
|
||||
// +---------v-----------+ | |
|
||||
// |pdu.ReplicationServer| | |
|
||||
// +---------+-----------+ | |
|
||||
// | | ---
|
||||
// +----------+ +------------+
|
||||
// | |
|
||||
// +---v--v-----+
|
||||
// | Handler |
|
||||
// +------------+
|
||||
// (usually endpoint.{Sender,Receiver})
|
||||
//
|
||||
//
|
||||
// +------------+
|
||||
// | rpc.Client |
|
||||
// +------------+
|
||||
// | |
|
||||
// +--------+ +------------+
|
||||
// | |
|
||||
// +---------v-----------+ +--------v------+
|
||||
// |pdu.ReplicationClient| |dataconn.Client|
|
||||
// +---------------------+ +--------v------+
|
||||
// | label: label: |
|
||||
// | zrepl_control zrepl_data |
|
||||
// +--------+ +------------+
|
||||
// | |
|
||||
// +--v---------v---+
|
||||
// | transportmux |
|
||||
// +-------+--------+
|
||||
// | uses
|
||||
// +-------v--------+
|
||||
// |versionhandshake|
|
||||
// +-------+--------+
|
||||
// | uses
|
||||
// +------v------+
|
||||
// | transport |
|
||||
// +------+------+
|
||||
// |
|
||||
// NETWORK
|
||||
// |
|
||||
// +------+------+
|
||||
// | transport |
|
||||
// +------^------+
|
||||
// | uses
|
||||
// +-------+--------+
|
||||
// |versionhandshake|
|
||||
// +-------^--------+
|
||||
// | uses
|
||||
// +-------+--------+
|
||||
// | transportmux |
|
||||
// +--^--------^----+
|
||||
// | |
|
||||
// +--------+ --------------+ ---
|
||||
// | | |
|
||||
// | label: label: | |
|
||||
// | zrepl_control zrepl_data | |
|
||||
// +-----+----+ +-----------+---+ |
|
||||
// |netadaptor| |dataconn.Server| | rpc.Server
|
||||
// | + | +------+--------+ |
|
||||
// |grpcclient| | |
|
||||
// |identity | | |
|
||||
// +-----+----+ | |
|
||||
// | | |
|
||||
// +---------v-----------+ | |
|
||||
// |pdu.ReplicationServer| | |
|
||||
// +---------+-----------+ | |
|
||||
// | | ---
|
||||
// +----------+ +------------+
|
||||
// | |
|
||||
// +---v--v-----+
|
||||
// | Handler |
|
||||
// +------------+
|
||||
// (usually endpoint.{Sender,Receiver})
|
||||
package rpc
|
||||
|
||||
// edit trick for the ASCII art above:
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
type Logger = logger.Logger
|
||||
|
||||
/// All fields must be non-nil
|
||||
// All fields must be non-nil
|
||||
type Loggers struct {
|
||||
General Logger
|
||||
Control Logger
|
||||
|
||||
@@ -33,8 +33,47 @@ var _ net.Error = &HandshakeError{}
|
||||
|
||||
func (e HandshakeError) Error() string { return e.msg }
|
||||
|
||||
// Like with net.OpErr (Go issue 6163), a client failing to handshake
|
||||
// should be a temporary Accept error toward the Listener .
|
||||
// When a net.Listener.Accept() returns an error, the server must
|
||||
// decide whether to retry calling Accept() or not.
|
||||
// On some platforms (e.g., Linux), Accept() can return errors
|
||||
// related to the specific protocol connection that was supposed
|
||||
// to be returned as asocket FD. Obviously, we want to ignore,
|
||||
// maybe log, those errors and retry Accept() immediately to
|
||||
// serve other connections.
|
||||
// But there are also conditions where we get Accept() errors because
|
||||
// the process has run out of file descriptors. In that case, retrying
|
||||
// won't help. We need to close some file descriptor to make progress.
|
||||
// Note that there could be lots of open file descriptors because we
|
||||
// have accepted, and not yet closed, lots of connections in the past.
|
||||
// And then, of course there can be errors where we just want
|
||||
// to return, e.g., if there's a programming error and we're getting
|
||||
// an EBADFD or whatever.
|
||||
//
|
||||
// So, the serve loops in net/http.Server.Serve() or gRPC's server.Serve()
|
||||
// must inspect the error and decide what to do.
|
||||
// The vehicle for this is the
|
||||
//
|
||||
// interface { Temporary() bool }
|
||||
//
|
||||
// Behavior in both of the aforementioned Serve() loops:
|
||||
//
|
||||
// - if the error doesn't implement the interface, stop serving and return
|
||||
// - `Temporary() == true`: retry with back-off
|
||||
// - `Temporary() == false`: stop serving and return
|
||||
//
|
||||
// So, to make this package's HandshakeListener work with these
|
||||
// Serve() loops, we return Temporary() == true if the handshake fails.
|
||||
// In the aforementioned categories, that's the case of a per-connection
|
||||
// protocol error.
|
||||
//
|
||||
// Note: the net.Error interface has deprecated the Temporary() method
|
||||
// in go.dev/issue/45729, but there is no replacement for users of .Accept().
|
||||
// Existing users of .Accept() continue to check for the interface.
|
||||
// So, we need to continue supporting Temporary() until there's a different
|
||||
// mechanism for serve loops to decide whether to retry or not.
|
||||
// The following mailing list post proposes to eliminate the retries
|
||||
// completely, but it seems like the effort has stalled.
|
||||
// https://groups.google.com/g/golang-nuts/c/-JcZzOkyqYI/m/xwaZzjCgAwAJ
|
||||
func (e HandshakeError) Temporary() bool {
|
||||
if e.isAcceptError {
|
||||
return true
|
||||
@@ -152,7 +191,7 @@ func (m *HandshakeMessage) DecodeReader(r io.Reader, maxLen int) error {
|
||||
|
||||
func DoHandshakeCurrentVersion(conn net.Conn, deadline time.Time) *HandshakeError {
|
||||
// current protocol version is hardcoded here
|
||||
return DoHandshakeVersion(conn, deadline, 6)
|
||||
return DoHandshakeVersion(conn, deadline, 7)
|
||||
}
|
||||
|
||||
const HandshakeMessageMaxLen = 16 * 4096
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
//
|
||||
// Intended Usage
|
||||
//
|
||||
// defer s.lock().unlock()
|
||||
// // drop lock while waiting for wait group
|
||||
// func() {
|
||||
// defer a.l.Unlock().Lock()
|
||||
// fssesDone.Wait()
|
||||
// }()
|
||||
//
|
||||
// defer s.lock().unlock()
|
||||
// // drop lock while waiting for wait group
|
||||
// func() {
|
||||
// defer a.l.Unlock().Lock()
|
||||
// fssesDone.Wait()
|
||||
// }()
|
||||
package chainlock
|
||||
|
||||
import "sync"
|
||||
|
||||
+11
-12
@@ -7,26 +7,25 @@
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// // This field must be set to a non-nil value,
|
||||
// // forcing the caller to make their mind up
|
||||
// // about this field.
|
||||
// CriticalSetting *nodefault.Bool
|
||||
// }
|
||||
// type Config struct {
|
||||
// // This field must be set to a non-nil value,
|
||||
// // forcing the caller to make their mind up
|
||||
// // about this field.
|
||||
// CriticalSetting *nodefault.Bool
|
||||
// }
|
||||
//
|
||||
// An function that takes such a Config should _not_ check for nil-ness:
|
||||
// and instead unconditionally dereference:
|
||||
//
|
||||
// func f(c Config) {
|
||||
// if (c.CriticalSetting) { }
|
||||
// }
|
||||
// func f(c Config) {
|
||||
// if (c.CriticalSetting) { }
|
||||
// }
|
||||
//
|
||||
// If the caller of f forgot to specify the .CriticalSetting
|
||||
// field, the Go runtime will issue a nil-pointer deref panic
|
||||
// and it'll be clear that the caller did not read the docs of Config.
|
||||
//
|
||||
// f(Config{}) // crashes
|
||||
//
|
||||
// f Config{ CriticalSetting: &nodefault.Bool{B: false}} // doesn't crash
|
||||
// f(Config{}) // crashes
|
||||
//
|
||||
// f Config{ CriticalSetting: &nodefault.Bool{B: false}} // doesn't crash
|
||||
package nodefault
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package suspendresumesafetimer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zrepl/zrepl/util/envconst"
|
||||
)
|
||||
|
||||
// The returned error is guaranteed to be the ctx.Err()
|
||||
func SleepUntil(ctx context.Context, sleepUntil time.Time) error {
|
||||
|
||||
// We use .Round(0) to strip the monotonic clock reading from the time.Time
|
||||
// returned by time.Now(). That will make the before/after check in the ticker
|
||||
// for-loop compare wall-clock times instead of monotonic time.
|
||||
// Comparing wall clock time is necessary because monotonic time does not progress
|
||||
// while the system is suspended.
|
||||
//
|
||||
// Background
|
||||
//
|
||||
// A time.Time carries a wallclock timestamp and optionally a monotonic clock timestamp.
|
||||
// time.Now() returns a time.Time that carries both.
|
||||
// time.Time.Add() applies the same delta to both timestamps in the time.Time.
|
||||
// x.Sub(y) will return the *monotonic* delta if both x and y carry a monotonic timestamp.
|
||||
// time.Until(x) == x.Sub(now) where `now` will have a monotonic timestamp.
|
||||
// So, time.Until(x) with an `x` that has monotonic timestamp will return monotonic delta.
|
||||
//
|
||||
// Why Do We Care?
|
||||
//
|
||||
// On systems that suspend/resume, wall clock time progresses during suspend but
|
||||
// monotonic time does not.
|
||||
//
|
||||
// So, suppose the following sequence of events:
|
||||
// x <== time.Now()
|
||||
// System suspends for 1 hour
|
||||
// delta <== time.Now().Sub(x)
|
||||
// `delta` will be near 0 because time.Until() subtracts the monotonic
|
||||
// timestamps, and monotonic time didn't progress during suspend.
|
||||
//
|
||||
// Now strip the timestamp using .Round(0)
|
||||
// x <== time.Now().Round(0)
|
||||
// System suspends for 1 hour
|
||||
// delta <== time.Now().Sub(x)
|
||||
// `delta` will be 1 hour because time.Sub() subtracted wallclock timestamps
|
||||
// because x didn't have a monotonic timestamp because we stripped it using .Round(0).
|
||||
//
|
||||
//
|
||||
sleepUntil = sleepUntil.Round(0)
|
||||
|
||||
// Set up a timer so that, if the system doesn't suspend/resume,
|
||||
// we get a precise wake-up time from the native Go timer.
|
||||
monotonicClockTimer := time.NewTimer(time.Until(sleepUntil))
|
||||
defer func() {
|
||||
if !monotonicClockTimer.Stop() {
|
||||
// non-blocking read since we can come here when
|
||||
// we've already drained the channel through
|
||||
// case <-monotonicClockTimer.C
|
||||
// in the `for` loop below.
|
||||
select {
|
||||
case <-monotonicClockTimer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Set up a ticker so that we're guaranteed to wake up periodically.
|
||||
// We'll then get the current wall-clock time and check ourselves
|
||||
// whether we're past the requested expiration time.
|
||||
// Pick a 10 second check interval by default since it's rare that
|
||||
// suspend/resume is done more frequently.
|
||||
ticker := time.NewTicker(envconst.Duration("ZREPL_WALLCLOCKTIMER_MAX_DELAY", 10*time.Second))
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-monotonicClockTimer.C:
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
now := time.Now()
|
||||
if now.Before(sleepUntil) {
|
||||
// Continue waiting.
|
||||
|
||||
// Reset the monotonic timer to reset drift.
|
||||
if !monotonicClockTimer.Stop() {
|
||||
<-monotonicClockTimer.C
|
||||
}
|
||||
monotonicClockTimer.Reset(time.Until(sleepUntil))
|
||||
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -53,7 +53,6 @@ var componentValidChar = regexp.MustCompile(`^[0-9a-zA-Z-_\.: ]+$`)
|
||||
// characters:
|
||||
//
|
||||
// [-_.: ]
|
||||
//
|
||||
func ComponentNamecheck(datasetPathComponent string) error {
|
||||
if len(datasetPathComponent) == 0 {
|
||||
return fmt.Errorf("path component must not be empty")
|
||||
@@ -91,8 +90,8 @@ func (e *PathValidationError) Error() string {
|
||||
|
||||
// combines
|
||||
//
|
||||
// lib/libzfs/libzfs_dataset.c: zfs_validate_name
|
||||
// module/zcommon/zfs_namecheck.c: entity_namecheck
|
||||
// lib/libzfs/libzfs_dataset.c: zfs_validate_name
|
||||
// module/zcommon/zfs_namecheck.c: entity_namecheck
|
||||
//
|
||||
// The '%' character is not allowed because it's reserved for zfs-internal use
|
||||
func EntityNamecheck(path string, t EntityType) (err *PathValidationError) {
|
||||
|
||||
+79
-26
@@ -343,7 +343,6 @@ const (
|
||||
|
||||
type SendStream struct {
|
||||
cmd *zfscmd.Cmd
|
||||
kill context.CancelFunc
|
||||
stdoutReader io.ReadCloser // not *os.File for mocking during platformtest
|
||||
stderrBuf *circlog.CircularLog
|
||||
|
||||
@@ -416,8 +415,29 @@ func (s *SendStream) killAndWait() error {
|
||||
debug("sendStream: killAndWait enter")
|
||||
defer debug("sendStream: killAndWait leave")
|
||||
|
||||
// ensure this function is only called once
|
||||
if s.state != sendStreamOpen {
|
||||
panic(s.state)
|
||||
}
|
||||
|
||||
// send SIGKILL
|
||||
s.kill()
|
||||
// In an earlier version, we used the starting context.Context's cancel function
|
||||
// for this, but in Go > 1.19, doing so will cause .Wait() to return the
|
||||
// context cancel error instead of the *exec.ExitError.
|
||||
err := s.cmd.Process().Kill()
|
||||
if err != nil {
|
||||
if err == os.ErrProcessDone {
|
||||
// This can happen if
|
||||
// (1) the process has already been .Wait()ed, or
|
||||
// (2) some other goroutine cancels the context, likely further up
|
||||
// the context tree.
|
||||
// Case (1) can't happen to us because we only call
|
||||
// this function in sendStreamOpen state.
|
||||
// In Case (2), it's still our job to .Wait(), so, fallthrough.
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Close our read-end of the pipe.
|
||||
//
|
||||
@@ -632,6 +652,14 @@ func (e ZFSSendArgsValidationError) Error() string {
|
||||
return e.Msg.Error()
|
||||
}
|
||||
|
||||
type zfsSendArgsSkipValidationKeyType struct{}
|
||||
|
||||
var zfsSendArgsSkipValidationKey = zfsSendArgsSkipValidationKeyType{}
|
||||
|
||||
func ZFSSendArgsSkipValidation(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, zfsSendArgsSkipValidationKey, true)
|
||||
}
|
||||
|
||||
// - Recursively call Validate on each field.
|
||||
// - Make sure that if ResumeToken != "", it reflects the same operation as the other parameters would.
|
||||
//
|
||||
@@ -659,6 +687,16 @@ func (a ZFSSendArgsUnvalidated) Validate(ctx context.Context) (v ZFSSendArgsVali
|
||||
// fallthrough
|
||||
}
|
||||
|
||||
validated := ZFSSendArgsValidated{
|
||||
ZFSSendArgsUnvalidated: a,
|
||||
FromVersion: fromVersion,
|
||||
ToVersion: toVersion,
|
||||
}
|
||||
|
||||
if ctx.Value(zfsSendArgsSkipValidationKey) != nil {
|
||||
return validated, nil
|
||||
}
|
||||
|
||||
if err := a.ZFSSendFlags.Validate(); err != nil {
|
||||
return v, newGenericValidationError(a, errors.Wrap(err, "send flags invalid"))
|
||||
}
|
||||
@@ -673,18 +711,19 @@ func (a ZFSSendArgsUnvalidated) Validate(ctx context.Context) (v ZFSSendArgsVali
|
||||
|
||||
if a.Encrypted.B && !fsEncrypted {
|
||||
return v, newValidationError(a, ZFSSendArgsEncryptedSendRequestedButFSUnencrypted,
|
||||
errors.Errorf("encrypted send requested, but filesystem %q is not encrypted", a.FS))
|
||||
errors.Errorf("encrypted send mandated by policy, but filesystem %q is not encrypted", a.FS))
|
||||
}
|
||||
|
||||
if a.Raw && fsEncrypted && !a.Encrypted.B {
|
||||
return v, newValidationError(a, ZFSSendArgsGenericValidationError,
|
||||
errors.Errorf("policy mandates raw+unencrypted sends, but filesystem %q is encrypted", a.FS))
|
||||
}
|
||||
|
||||
if err := a.validateEncryptionFlagsCorrespondToResumeToken(ctx, valCtx); err != nil {
|
||||
return v, newValidationError(a, ZFSSendArgsResumeTokenMismatch, err)
|
||||
}
|
||||
|
||||
return ZFSSendArgsValidated{
|
||||
ZFSSendArgsUnvalidated: a,
|
||||
FromVersion: fromVersion,
|
||||
ToVersion: toVersion,
|
||||
}, nil
|
||||
return validated, nil
|
||||
}
|
||||
|
||||
func (f ZFSSendFlags) Validate() error {
|
||||
@@ -852,21 +891,34 @@ func (a ZFSSendArgsUnvalidated) validateEncryptionFlagsCorrespondToResumeToken(c
|
||||
return gen.fmt("resume token `toguid` != expected: %v != %v", t.ToGUID, a.To.GUID)
|
||||
}
|
||||
|
||||
if a.Encrypted.B {
|
||||
if !(t.RawOK && t.CompressOK) {
|
||||
return ZFSSendArgsResumeTokenMismatchEncryptionNotSet.fmt(
|
||||
"resume token must have `rawok` and `compressok` = true but got %v %v", t.RawOK, t.CompressOK)
|
||||
}
|
||||
// fallthrough
|
||||
} else {
|
||||
if t.RawOK || t.CompressOK {
|
||||
return ZFSSendArgsResumeTokenMismatchEncryptionSet.fmt(
|
||||
"resume token must not have `rawok` or `compressok` set but got %v %v", t.RawOK, t.CompressOK)
|
||||
}
|
||||
// fallthrough
|
||||
// ensure resume stream will be encrypted/unencrypted as specified in policy
|
||||
if err := valCtx.encEnabled.ValidateNoDefault(); err != nil {
|
||||
panic(valCtx)
|
||||
}
|
||||
wouldSendEncryptedIfFilesystemIsEncrypted := t.RawOK
|
||||
filesystemIsEncrypted := valCtx.encEnabled.B
|
||||
resumeWillBeEncryptedSend := filesystemIsEncrypted && wouldSendEncryptedIfFilesystemIsEncrypted
|
||||
if a.Encrypted.B {
|
||||
if resumeWillBeEncryptedSend {
|
||||
return nil // encrypted send in policy, and that's what's going to happen
|
||||
} else {
|
||||
if !filesystemIsEncrypted {
|
||||
// NB: a.Encrypted.B && !valCtx.encEnabled.B
|
||||
// is handled in the caller, because it doesn't concern the resume token (different kind of error)
|
||||
panic("caller should have already raised an error")
|
||||
}
|
||||
// XXX we have no test coverage for this case. We'd need to forge a resume token for that.
|
||||
return ZFSSendArgsResumeTokenMismatchEncryptionNotSet.fmt(
|
||||
"resume token does not have rawok=true which would result in an unencrypted send, but policy mandates encrypted sends only")
|
||||
}
|
||||
} else {
|
||||
if resumeWillBeEncryptedSend {
|
||||
return ZFSSendArgsResumeTokenMismatchEncryptionSet.fmt(
|
||||
"resume token has rawok=true which would result in encrypted send, but policy mandates unencrypted sends only")
|
||||
} else {
|
||||
return nil // unencrypted send in policy, and that's what's going to happen
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var zfsSendStderrCaptureMaxSize = envconst.Int("ZREPL_ZFS_SEND_STDERR_MAX_CAPTURE_SIZE", 1<<15)
|
||||
@@ -931,10 +983,10 @@ func ZFSSend(ctx context.Context, sendArgs ZFSSendArgsValidated) (*SendStream, e
|
||||
|
||||
stream := &SendStream{
|
||||
cmd: cmd,
|
||||
kill: cancel,
|
||||
stdoutReader: stdoutReader,
|
||||
stderrBuf: stderrBuf,
|
||||
}
|
||||
_ = cancel // the SendStream.killAndWait() will kill the process
|
||||
|
||||
return stream, nil
|
||||
}
|
||||
@@ -990,8 +1042,10 @@ func (s *DrySendInfo) unmarshalZFSOutput(output []byte) (err error) {
|
||||
}
|
||||
|
||||
// unmarshal info line, looks like this:
|
||||
// full zroot/test/a@1 5389768
|
||||
// incremental zroot/test/a@1 zroot/test/a@2 5383936
|
||||
//
|
||||
// full zroot/test/a@1 5389768
|
||||
// incremental zroot/test/a@1 zroot/test/a@2 5383936
|
||||
//
|
||||
// => see test cases
|
||||
func (s *DrySendInfo) unmarshalInfoLine(l string) (regexMatched bool, err error) {
|
||||
|
||||
@@ -1823,7 +1877,6 @@ var ErrBookmarkCloningNotSupported = fmt.Errorf("bookmark cloning feature is not
|
||||
// unless a bookmark with the name `bookmark` exists and has the same idenitty (zfs.FilesystemVersionEqualIdentity)
|
||||
//
|
||||
// v must be validated by the caller
|
||||
//
|
||||
func ZFSBookmark(ctx context.Context, fs string, v FilesystemVersion, bookmark string) (bm FilesystemVersion, err error) {
|
||||
|
||||
bm = FilesystemVersion{
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
//nolint[:deadcode,unused]
|
||||
//nolint:deadcode,unused
|
||||
func debug(format string, args ...interface{}) {
|
||||
if debugEnabled {
|
||||
fmt.Fprintf(os.Stderr, "zfs: %s\n", fmt.Sprintf(format, args...))
|
||||
|
||||
Reference in New Issue
Block a user