Compare commits

..

1 Commits

Author SHA1 Message Date
Christian Schwarz 0eb7032735 daemon/control: envconst timeout for control socket server-side timeouts
refs #262
2020-02-17 22:36:32 +01:00
197 changed files with 3257 additions and 9124 deletions
+3 -45
View File
@@ -6,7 +6,6 @@ workflows:
- build-1.11
- build-1.12
- build-1.13
- build-1.14
- build-latest
- test-build-in-docker
jobs:
@@ -59,12 +58,11 @@ jobs:
- run: ./lazy.sh devsetup
- run: make zrepl-bin
- run: make test-platform-bin
- run: make vet
- run: make test
- run: make lint
- run: make release
- run: make test-go
# cannot run test-platform because circle-ci runs in linux containers
- store_artifacts:
path: ./artifacts/release
@@ -86,7 +84,6 @@ jobs:
mc config host add --api s3v4 zrepl-minio https://minio.cschwarz.com ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}
# Upload artifacts
mkdir -p artifacts/release
echo "$CIRCLE_BUILD_URL" > ./artifacts/release/cirlceci_build_url
mc cp -r artifacts/release "zrepl-minio/zrepl-ci-artifacts/${CIRCLE_SHA1}/${CIRCLE_JOB}/"
@@ -94,45 +91,11 @@ jobs:
REPO="zrepl/zrepl"
COMMIT="${CIRCLE_SHA1}"
JOB_NAME="${CIRCLE_JOB}"
curl "https://api.github.com/repos/$REPO/statuses/$COMMIT" \
curl "https://api.github.com/repos/$REPO/statuses/$COMMIT?access_token=$GITHUB_COMMIT_STATUS_TOKEN" \
-H "Content-Type: application/json" \
-H "Authorization: token $GITHUB_COMMIT_STATUS_TOKEN" \
-X POST \
-d '{"context":"zrepl/publish-ci-artifacts", "state": "success", "description":"CI Build Artifacts for '"$JOB_NAME"'", "target_url":"https://minio.cschwarz.com/minio/zrepl-ci-artifacts/'"$COMMIT"'/"}'
- run:
shell: /bin/bash -eo pipefail
when: always
command: |
if [ -n "$CIRCLE_PR_NUMBER" ]; then # CIRCLE_PR_NUMBER is guaranteed to be only present in forked PRs (external)
echo "Forked PR detected. Sry, can't trust you with credentials to external artifact store, use CircleCI's instead."
exit 0
fi
set -u # from now on
# Trigger Platform Tests
# ZREPL_BOT_GIT_PRWN_EU_ACCESS_TOKEN from secrets
## Sync this commit to the GitLab
git remote add prwn-gitlab "https://zrepl-bot:$ZREPL_BOT_GIT_PRWN_EU_ACCESS_TOKEN@git.prwn.eu/problame/zrepl.git"
REMOTE_REF=circleci/$CIRCLE_BRANCH/$CIRCLE_BUILD_NUM
git push -f prwn-gitlab "HEAD:$REMOTE_REF"
## Trigger Tests
curl -v --request POST \
--header "PRIVATE-TOKEN: $ZREPL_BOT_GIT_PRWN_EU_ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--data '{ "ref":"'"$REMOTE_REF"'", "variables": [ {"key":"ZREPL_MAIN_REPO_COMMIT", "value": "'"$CIRCLE_SHA1"'" }, { "key":"ZREPL_CI_BUILDJOB", "value": "'"$CIRCLE_JOB"'" } ] } ' \
"https://git.prwn.eu/api/v4/projects/13/pipeline"
- run:
shell: /bin/bash -euo pipefail
command: |
# Trigger Debian Package Build
curl -v -X POST https://api.github.com/repos/zrepl/debian-binary-packaging/dispatches \
-H 'Accept: application/vnd.github.v3+json' \
-H "Authorization: token $ZREPL_DEBIAN_BINARYPACKAGIN_TRIGGER_BUILD_GITHUB_TOKEN" \
--data '{"event_type": "push", "client_payload": { "zrepl_main_repo_commit": "'"$CIRCLE_SHA1"'", "go_version": "'"${CIRCLE_JOB##build-}"'" }}'
build-1.11:
<<: *build-latest
docker:
@@ -148,11 +111,6 @@ jobs:
docker:
- image: circleci/golang:1.13
build-1.14:
<<: *build-latest
docker:
- image: circleci/golang:1.14
# this job tries to mimic the build-in-docker instructions
# given in docs/installation.rst
#
-17
View File
@@ -1,17 +0,0 @@
build-and-run-platformtests:
variables:
GIT_STRATEGY: none
script:
- mkdir artifacts
# platformtests are installed by the CI environment
- /opt/platformtest-linux-amd64
-test.coverprofile artifacts/platformtest.cover
-test.v
__DEVEL--i-heard-you-like-tests
-poolname zreplplatformtest
-imagepath /tmp/zreplplatformtest.pool.img
-mountpoint /tmp/zreplplatformtest.pool
artifacts:
paths:
- artifacts
image: "images:ubuntu/focal/cloud"
-16
View File
@@ -1,16 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")"
commit="$1"
build="$2"
src="$3"
dst="$4"
[ -n "$commit" -a -n "$build" -a -n "$src" -a -n "$dst" ] || ( echo "arguments must not be empty"; exit 1 )
aws="aws --endpoint-url https://minio.cschwarz.com --no-sign-request"
path="s3://zrepl-ci-artifacts/$commit/$build/$src"
$aws s3 cp "$path" "$dst"
+22 -63
View File
@@ -23,12 +23,9 @@ GOHOSTARCH ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOHOSTARCH"')
GO_ENV_VARS := GO111MODULE=on
GO_LDFLAGS := "-X github.com/zrepl/zrepl/version.zreplVersion=$(_ZREPL_VERSION)"
GO_MOD_READONLY := -mod=readonly
GO_EXTRA_BUILDFLAGS :=
GO_BUILDFLAGS := $(GO_MOD_READONLY) $(GO_EXTRA_BUILDFLAGS)
GO_BUILDFLAGS := $(GO_MOD_READONLY)
GO_BUILD := $(GO_ENV_VARS) $(GO) build $(GO_BUILDFLAGS) -ldflags $(GO_LDFLAGS)
GOLANGCI_LINT := golangci-lint
GOCOVMERGE := gocovmerge
ifneq ($(GOARM),)
ZREPL_TARGET_TUPLE := $(GOOS)-$(GOARCH)v$(GOARM)
else
@@ -47,7 +44,7 @@ printvars:
release: clean
# no cross-platform support for target test
$(MAKE) test-go
$(MAKE) test
$(MAKE) bins-all
$(MAKE) noarch
$(MAKE) wrapup-and-checksum
@@ -68,7 +65,6 @@ wrapup-and-checksum:
-acf $(NOARCH_TARBALL) \
$(ARTIFACTDIR)/docs/html \
$(ARTIFACTDIR)/bash_completion \
$(ARTIFACTDIR)/_zrepl.zsh_completion \
$(ARTIFACTDIR)/go_env.txt \
dist \
config/samples
@@ -101,9 +97,9 @@ clean: docs-clean
rm -rf "$(ARTIFACTDIR)"
##################### BINARIES #####################
.PHONY: bins-all lint test-go test-platform cover-merge cover-html vet zrepl-bin test-platform-bin generate-platform-test-list
.PHONY: bins-all lint test vet zrepl-bin platformtest-bin
BINS_ALL_TARGETS := zrepl-bin test-platform-bin vet lint
BINS_ALL_TARGETS := zrepl-bin platformtest-bin vet lint
GO_SUPPORTS_ILLUMOS := $(shell $(GO) version | gawk -F '.' '/^go version /{split($$0, comps, " "); split(comps[3], v, "."); if (v[1] == "go1" && v[2] >= 13) { print "illumos"; } else { print "noillumos"; }}')
bins-all:
$(MAKE) $(BINS_ALL_TARGETS) GOOS=freebsd GOARCH=amd64
@@ -124,34 +120,27 @@ endif
lint:
$(GO_ENV_VARS) $(GOLANGCI_LINT) run ./...
test:
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) ./...
vet:
$(GO_ENV_VARS) $(GO) vet $(GO_BUILDFLAGS) ./...
test-go: $(ARTIFACTDIR)
rm -f "$(ARTIFACTDIR)/gotest.cover"
ifeq ($(COVER),1)
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
-coverpkg github.com/zrepl/zrepl/... \
-covermode atomic \
-coverprofile "$(ARTIFACTDIR)/gotest.cover" \
./...
else
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
./...
endif
zrepl-bin:
$(GO_BUILD) -o "$(ARTIFACTDIR)/zrepl-$(ZREPL_TARGET_TUPLE)"
generate-platform-test-list:
$(GO_BUILD) -o $(ARTIFACTDIR)/generate-platform-test-list ./platformtest/tests/gen
platformtest-bin:
$(GO_BUILD) -o "$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" ./platformtest/harness
test-platform-bin:
$(GO_ENV_VARS) $(GO) test $(GO_BUILDFLAGS) \
-c -o "$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" \
-covermode=atomic -cover -coverpkg github.com/zrepl/zrepl/... \
./platformtest/harness
##################### DEV TARGETS #####################
# not part of the build, must do that manually
.PHONY: generate format platformtest
generate:
protoc -I=replication/logic/pdu --go_out=plugins=grpc:replication/logic/pdu replication/logic/pdu/pdu.proto
$(GO_ENV_VARS) $(GO) generate $(GO_BUILDFLAGS) -x ./...
format:
goimports -srcdir . -local 'github.com/zrepl/zrepl' -w $(shell find . -type f -name '*.go' -not -path "./vendor/*" -not -name '*.pb.go' -not -name '*_enumer.go')
ZREPL_PLATFORMTEST_POOLNAME := zreplplatformtest
ZREPL_PLATFORMTEST_IMAGEPATH := /tmp/zreplplatformtest.pool.img
@@ -159,44 +148,18 @@ ZREPL_PLATFORMTEST_MOUNTPOINT := /tmp/zreplplatformtest.pool
ZREPL_PLATFORMTEST_ZFS_LOG := /tmp/zreplplatformtest.zfs.log
# ZREPL_PLATFORMTEST_STOP_AND_KEEP := -failure.stop-and-keep-pool
ZREPL_PLATFORMTEST_ARGS :=
test-platform: $(ARTIFACTDIR) # do not track dependency on test-platform-bin to allow build of platformtest outside of test VM
platformtest: # do not track dependency on platformtest-bin to allow build of platformtest outside of test VM
rm -f "$(ZREPL_PLATFORMTEST_ZFS_LOG)"
rm -f "$(ARTIFACTDIR)/platformtest.cover"
platformtest/logmockzfs/logzfsenv "$(ZREPL_PLATFORMTEST_ZFS_LOG)" `which zfs` \
"$(ARTIFACTDIR)/platformtest-$(ZREPL_TARGET_TUPLE)" \
-test.coverprofile "$(ARTIFACTDIR)/platformtest.cover" \
-test.v \
__DEVEL--i-heard-you-like-tests \
-poolname "$(ZREPL_PLATFORMTEST_POOLNAME)" \
-imagepath "$(ZREPL_PLATFORMTEST_IMAGEPATH)" \
-mountpoint "$(ZREPL_PLATFORMTEST_MOUNTPOINT)" \
$(ZREPL_PLATFORMTEST_STOP_AND_KEEP) \
$(ZREPL_PLATFORMTEST_ARGS)
cover-merge: $(ARTIFACTDIR)
$(GOCOVMERGE) $(ARTIFACTDIR)/platformtest.cover $(ARTIFACTDIR)/gotest.cover > $(ARTIFACTDIR)/merged.cover
cover-html: cover-merge
$(GO) tool cover -html "$(ARTIFACTDIR)/merged.cover" -o "$(ARTIFACTDIR)/merged.cover.html"
test-full:
test "$$(id -u)" = "0" || echo "MUST RUN AS ROOT" 1>&2
$(MAKE) test-go COVER=1
$(MAKE) test-platform
$(MAKE) cover-html
##################### DEV TARGETS #####################
# not part of the build, must do that manually
.PHONY: generate format
generate: generate-platform-test-list
protoc -I=replication/logic/pdu --go_out=plugins=grpc:replication/logic/pdu replication/logic/pdu/pdu.proto
$(GO_ENV_VARS) $(GO) generate $(GO_BUILDFLAGS) -x ./...
format:
goimports -srcdir . -local 'github.com/zrepl/zrepl' -w $(shell find . -type f -name '*.go' -not -path "./vendor/*" -not -name '*.pb.go' -not -name '*_enumer.go')
##################### NOARCH #####################
.PHONY: noarch $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs docs-clean
.PHONY: noarch $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/go_env.txt docs docs-clean
$(ARTIFACTDIR):
@@ -204,16 +167,12 @@ $(ARTIFACTDIR):
$(ARTIFACTDIR)/docs: $(ARTIFACTDIR)
mkdir -p "$@"
noarch: $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/_zrepl.zsh_completion $(ARTIFACTDIR)/go_env.txt docs
noarch: $(ARTIFACTDIR)/bash_completion $(ARTIFACTDIR)/go_env.txt docs
# pass
$(ARTIFACTDIR)/bash_completion:
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) gencompletion bash "$@"
$(ARTIFACTDIR)/_zrepl.zsh_completion:
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) gencompletion zsh "$@"
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) bashcomp "$@"
$(ARTIFACTDIR)/go_env.txt:
$(GO_ENV_VARS) $(GO) env > $@
+3 -11
View File
@@ -39,7 +39,7 @@ Check out the *Coding Workflow* section below for details.
* Ship other material provided in `./dist`, e.g. in `/usr/share/zrepl/`.
* Use `make release ZREPL_VERSION='mydistro-1.2.3_1'`
* Your distro's name and any versioning supplemental to zrepl's (e.g. package revision) should be in this string
* Use `sudo make test-platform` **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
* Use `make platformtest` **on a test system** to validate that zrepl's abstractions on top of ZFS work with the system ZFS.
* Make sure you are informed about new zrepl versions, e.g. by subscribing to GitHub's release RSS feed.
## Developer Documentation
@@ -47,18 +47,10 @@ Check out the *Coding Workflow* section below for details.
zrepl is written in [Go](https://golang.org) and uses [Go modules](https://github.com/golang/go/wiki/Modules) to manage dependencies.
The documentation is written in [ReStructured Text](http://docutils.sourceforge.net/rst.html) using the [Sphinx](https://www.sphinx-doc.org) framework.
Install **build dependencies** using `./lazy.sh devsetup`.
To get started, run `./lazy.sh devsetup` to easily install build dependencies and read `docs/installation.rst -> Compiling from Source`.
`lazy.sh` uses `python3-pip` to fetch the build dependencies for the docs - you might want to use a [venv](https://docs.python.org/3/library/venv.html).
If you just want to install the Go dependencies, run `./lazy.sh godep`.
The **test suite** is split into pure **Go tests** (`make test-go`) and **platform tests** that interact with ZFS and thus generally **require root privileges** (`sudo make test-platform`).
Platform tests run on their own pool with the name `zreplplatformtest`, which is created using the file vdev in `/tmp`.
For a full **code coverage** profile, run `make test-go COVER=1 && sudo make test-platform && make cover-merge`.
An HTML report can be generated using `make cover-html`.
**Code generation** is triggered by `make generate`. Generated code is committed to the source tree.
### Project Structure
```
@@ -144,7 +136,7 @@ However, new contributions & patches should fix naming without further notice in
### RPC debugging
Optionally, there are various RPC-related environment variables, that if set to something != `""` will produce additional debug output on stderr:
Optionally, there are various RPC-related environment varibles, that if set to something != `""` will produce additional debug output on stderr:
https://github.com/zrepl/zrepl/blob/master/rpc/rpc_debug.go#L11
-1
View File
@@ -28,7 +28,6 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/viper v1.3.2 // indirect
github.com/stretchr/testify v1.4.0 // indirect
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad // indirect
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 // indirect
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 // indirect
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b
-2
View File
@@ -214,8 +214,6 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC
github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad h1:W0LEBv82YCGEtcmPA3uNZBI33/qF//HAAs3MawDjRa0=
github.com/wadey/gocovmerge v0.0.0-20160331181800-b5bfa59ec0ad/go.mod h1:Hy8o65+MXnS6EwGElrSRjUzQDLXreJlzYLlWiHtt8hM=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-1
View File
@@ -6,7 +6,6 @@ import (
_ "github.com/alvaroloes/enumer"
_ "github.com/golang/protobuf/protoc-gen-go"
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
_ "github.com/wadey/gocovmerge"
_ "golang.org/x/tools/cmd/goimports"
_ "golang.org/x/tools/cmd/stringer"
)
+21 -53
View File
@@ -1,13 +1,11 @@
package cli
import (
"context"
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/config"
)
@@ -21,55 +19,29 @@ var rootCmd = &cobra.Command{
Short: "One-stop ZFS replication solution",
}
func init() {
rootCmd.PersistentFlags().StringVar(&rootArgs.configPath, "config", "", "config file path")
}
var genCompletionCmd = &cobra.Command{
Use: "gencompletion",
Short: "generate shell auto-completions",
}
type completionCmdInfo struct {
genFunc func(outpath string) error
help string
}
var completionCmdMap = map[string]completionCmdInfo{
"zsh": {
rootCmd.GenZshCompletionFile,
" save to file `_zrepl` in your zsh's $fpath",
},
"bash": {
rootCmd.GenBashCompletionFile,
" save to a path and source that path in your .bashrc",
var bashcompCmd = &cobra.Command{
Use: "bashcomp path/to/out/file",
Short: "generate bash completions",
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 1 {
fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n")
err := cmd.Usage()
if err != nil {
panic(err)
}
os.Exit(1)
}
if err := rootCmd.GenBashCompletionFile(args[0]); err != nil {
fmt.Fprintf(os.Stderr, "error generating bash completion: %s", err)
os.Exit(1)
}
},
Hidden: true,
}
func init() {
for sh, info := range completionCmdMap {
sh, info := sh, info
genCompletionCmd.AddCommand(&cobra.Command{
Use: fmt.Sprintf("%s path/to/out/file", sh),
Short: fmt.Sprintf("generate %s completions", sh),
Example: info.help,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 1 {
fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n")
err := cmd.Usage()
if err != nil {
panic(err)
}
os.Exit(1)
}
if err := info.genFunc(args[0]); err != nil {
fmt.Fprintf(os.Stderr, "error generating %s completion: %s", sh, err)
os.Exit(1)
}
},
})
}
rootCmd.AddCommand(genCompletionCmd)
rootCmd.PersistentFlags().StringVar(&rootArgs.configPath, "config", "", "config file path")
rootCmd.AddCommand(bashcompCmd)
}
type Subcommand struct {
@@ -77,7 +49,7 @@ type Subcommand struct {
Short string
Example string
NoRequireConfig bool
Run func(ctx context.Context, subcommand *Subcommand, args []string) error
Run func(subcommand *Subcommand, args []string) error
SetupFlags func(f *pflag.FlagSet)
SetupSubcommands func() []*Subcommand
@@ -98,11 +70,7 @@ func (s *Subcommand) Config() *config.Config {
func (s *Subcommand) run(cmd *cobra.Command, args []string) {
s.tryParseConfig()
ctx := context.Background()
endTask := trace.WithTaskFromStackUpdateCtx(&ctx)
defer endTask()
err := s.Run(ctx, s, args)
endTask()
err := s.Run(s, args)
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1)
+1 -2
View File
@@ -1,7 +1,6 @@
package client
import (
"context"
"encoding/json"
"fmt"
"os"
@@ -30,7 +29,7 @@ var ConfigcheckCmd = &cli.Subcommand{
f.StringVar(&configcheckArgs.format, "format", "", "dump parsed config object [pretty|yaml|json]")
f.StringVar(&configcheckArgs.what, "what", "all", "what to print [all|config|jobs|logging]")
},
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
Run: func(subcommand *cli.Subcommand, args []string) error {
formatMap := map[string]func(interface{}){
"": func(i interface{}) {},
"pretty": func(i interface{}) {
+87
View File
@@ -0,0 +1,87 @@
package client
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/cli"
"github.com/zrepl/zrepl/daemon/filters"
"github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/zfs"
)
var (
HoldsCmd = &cli.Subcommand{
Use: "holds",
Short: "manage holds & step bookmarks",
SetupSubcommands: func() []*cli.Subcommand {
return holdsList
},
}
)
var holdsList = []*cli.Subcommand{
&cli.Subcommand{
Use: "list [FSFILTER]",
Run: doHoldsList,
NoRequireConfig: true,
Short: `
FSFILTER SYNTAX:
representation of a 'filesystems' filter statement on the command line
`,
},
}
func fsfilterFromCliArg(arg string) (zfs.DatasetFilter, error) {
mappings := strings.Split(arg, ",")
f := filters.NewDatasetMapFilter(len(mappings), true)
for _, m := range mappings {
thisMappingErr := fmt.Errorf("expecting comma-separated list of <dataset-pattern>:<ok|!> pairs, got %q", m)
lhsrhs := strings.SplitN(m, ":", 2)
if len(lhsrhs) != 2 {
return nil, thisMappingErr
}
err := f.Add(lhsrhs[0], lhsrhs[1])
if err != nil {
return nil, fmt.Errorf("%s: %s", thisMappingErr, err)
}
}
return f.AsFilter(), nil
}
func doHoldsList(sc *cli.Subcommand, args []string) error {
var err error
ctx := context.Background()
if len(args) > 1 {
return errors.New("this subcommand takes at most one argument")
}
var filter zfs.DatasetFilter
if len(args) == 0 {
filter = zfs.NoFilter()
} else {
filter, err = fsfilterFromCliArg(args[0])
if err != nil {
return errors.Wrap(err, "cannot parse filesystem filter args")
}
}
listing, err := endpoint.ListZFSHoldsAndBookmarks(ctx, filter)
if err != nil {
return err // context clear by invocation of command
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent(" ", " ")
if err := enc.Encode(listing); err != nil {
panic(err)
}
return nil
}
+15 -10
View File
@@ -48,13 +48,14 @@ var migratePlaceholder0_1Args struct {
dryRun bool
}
func doMigratePlaceholder0_1(ctx context.Context, sc *cli.Subcommand, args []string) error {
func doMigratePlaceholder0_1(sc *cli.Subcommand, args []string) error {
if len(args) != 0 {
return fmt.Errorf("migration does not take arguments, got %v", args)
}
cfg := sc.Config()
ctx := context.Background()
allFSS, err := zfs.ZFSListMapping(ctx, zfs.NoFilter())
if err != nil {
return errors.Wrap(err, "cannot list filesystems")
@@ -98,7 +99,7 @@ func doMigratePlaceholder0_1(ctx context.Context, sc *cli.Subcommand, args []str
}
for _, fs := range wi.fss {
fmt.Printf("\t%q ... ", fs.ToString())
r, err := zfs.ZFSMigrateHashBasedPlaceholderToCurrent(ctx, fs, migratePlaceholder0_1Args.dryRun)
r, err := zfs.ZFSMigrateHashBasedPlaceholderToCurrent(fs, migratePlaceholder0_1Args.dryRun)
if err != nil {
fmt.Printf("error: %s\n", err)
} else if !r.NeedsModification {
@@ -123,7 +124,7 @@ var fail = color.New(color.FgRed)
var migrateReplicationCursorSkipSentinel = fmt.Errorf("skipping this filesystem")
func doMigrateReplicationCursor(ctx context.Context, sc *cli.Subcommand, args []string) error {
func doMigrateReplicationCursor(sc *cli.Subcommand, args []string) error {
if len(args) != 0 {
return fmt.Errorf("migration does not take arguments, got %v", args)
}
@@ -136,6 +137,8 @@ func doMigrateReplicationCursor(ctx context.Context, sc *cli.Subcommand, args []
return fmt.Errorf("exiting migration after error")
}
ctx := context.Background()
v1cursorJobs := make([]job.Job, 0, len(cfg.Jobs))
for i, j := range cfg.Jobs {
if jobs[i].Name() != j.Name() {
@@ -162,7 +165,7 @@ func doMigrateReplicationCursor(ctx context.Context, sc *cli.Subcommand, args []
var hadError bool
for _, fs := range fss {
bold.Printf("INSPECT FILESYSTEM %q\n", fs.ToString())
bold.Printf("INSPECT FILESYTEM %q\n", fs.ToString())
err := doMigrateReplicationCursorFS(ctx, v1cursorJobs, fs)
if err == migrateReplicationCursorSkipSentinel {
@@ -208,15 +211,17 @@ func doMigrateReplicationCursorFS(ctx context.Context, v1CursorJobs []job.Job, f
}
fmt.Printf("identified owning job %q\n", owningJob.Name())
bookmarks, err := zfs.ZFSListFilesystemVersions(ctx, fs, zfs.ListFilesystemVersionsOptions{
Types: zfs.Bookmarks,
})
versions, err := zfs.ZFSListFilesystemVersions(fs, nil)
if err != nil {
return errors.Wrapf(err, "list filesystem versions of %q", fs.ToString())
}
var oldCursor *zfs.FilesystemVersion
for i, fsv := range bookmarks {
for i, fsv := range versions {
if fsv.Type != zfs.Bookmark {
continue
}
_, _, err := endpoint.ParseReplicationCursorBookmarkName(fsv.ToAbsPath(fs))
if err != endpoint.ErrV1ReplicationCursor {
continue
@@ -227,7 +232,7 @@ func doMigrateReplicationCursorFS(ctx context.Context, v1CursorJobs []job.Job, f
return errors.Wrap(err, "multiple filesystem versions identified as v1 replication cursors")
}
oldCursor = &bookmarks[i]
oldCursor = &versions[i]
}
@@ -259,7 +264,7 @@ func doMigrateReplicationCursorFS(ctx context.Context, v1CursorJobs []job.Job, f
if migrateReplicationCursorArgs.dryRun {
succ.Printf("DRY RUN\n")
} else {
if err := zfs.ZFSDestroyFilesystemVersion(ctx, fs, oldCursor); err != nil {
if err := zfs.ZFSDestroyFilesystemVersion(fs, oldCursor); err != nil {
return err
}
}
+61 -4
View File
@@ -1,10 +1,67 @@
package client
import "github.com/zrepl/zrepl/cli"
import (
"errors"
"log"
"os"
"github.com/zrepl/zrepl/cli"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon"
)
var pprofArgs struct {
daemon.PprofServerControlMsg
}
var PprofCmd = &cli.Subcommand{
Use: "pprof",
SetupSubcommands: func() []*cli.Subcommand {
return []*cli.Subcommand{PprofListenCmd, pprofActivityTraceCmd}
Use: "pprof off | [on TCP_LISTEN_ADDRESS]",
Short: "start a http server exposing go-tool-compatible profiling endpoints at TCP_LISTEN_ADDRESS",
Run: func(subcommand *cli.Subcommand, args []string) error {
if len(args) < 1 {
goto enargs
}
switch args[0] {
case "on":
pprofArgs.Run = true
if len(args) != 2 {
return errors.New("must specify TCP_LISTEN_ADDRESS as second positional argument")
}
pprofArgs.HttpListenAddress = args[1]
case "off":
if len(args) != 1 {
goto enargs
}
pprofArgs.Run = false
}
RunPProf(subcommand.Config())
return nil
enargs:
return errors.New("invalid number of positional arguments")
},
}
func RunPProf(conf *config.Config) {
log := log.New(os.Stderr, "", 0)
die := func() {
log.Printf("exiting after error")
os.Exit(1)
}
log.Printf("connecting to zrepl daemon")
httpc, err := controlHttpClient(conf.Global.Control.SockPath)
if err != nil {
log.Printf("error creating http client: %s", err)
die()
}
err = jsonRequestResponse(httpc, daemon.ControlJobEndpointPProf, pprofArgs.PprofServerControlMsg, struct{}{})
if err != nil {
log.Printf("error sending control message: %s", err)
die()
}
log.Printf("finished")
}
-44
View File
@@ -1,44 +0,0 @@
package client
import (
"context"
"io"
"log"
"os"
"golang.org/x/net/websocket"
"github.com/zrepl/zrepl/cli"
)
var pprofActivityTraceCmd = &cli.Subcommand{
Use: "activity-trace ZREPL_PPROF_HOST:ZREPL_PPROF_PORT",
Short: "attach to zrepl daemon with activated pprof listener and dump an activity-trace to stdout",
Run: runPProfActivityTrace,
}
func runPProfActivityTrace(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
log := log.New(os.Stderr, "", 0)
die := func() {
log.Printf("exiting after error")
os.Exit(1)
}
if len(args) != 1 {
log.Printf("exactly one positional argument is required")
die()
}
url := "ws://" + args[0] + "/debug/zrepl/activity-trace" // FIXME dont' repeat that
log.Printf("attaching to activity trace stream %s", url)
ws, err := websocket.Dial(url, "", url)
if err != nil {
log.Printf("error: %s", err)
die()
}
_, err = io.Copy(os.Stdout, ws)
return err
}
-68
View File
@@ -1,68 +0,0 @@
package client
import (
"context"
"errors"
"log"
"os"
"github.com/zrepl/zrepl/cli"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon"
)
var pprofListenCmd struct {
daemon.PprofServerControlMsg
}
var PprofListenCmd = &cli.Subcommand{
Use: "listen off | [on TCP_LISTEN_ADDRESS]",
Short: "start a http server exposing go-tool-compatible profiling endpoints at TCP_LISTEN_ADDRESS",
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
if len(args) < 1 {
goto enargs
}
switch args[0] {
case "on":
pprofListenCmd.Run = true
if len(args) != 2 {
return errors.New("must specify TCP_LISTEN_ADDRESS as second positional argument")
}
pprofListenCmd.HttpListenAddress = args[1]
case "off":
if len(args) != 1 {
goto enargs
}
pprofListenCmd.Run = false
}
RunPProf(subcommand.Config())
return nil
enargs:
return errors.New("invalid number of positional arguments")
},
}
func RunPProf(conf *config.Config) {
log := log.New(os.Stderr, "", 0)
die := func() {
log.Printf("exiting after error")
os.Exit(1)
}
log.Printf("connecting to zrepl daemon")
httpc, err := controlHttpClient(conf.Global.Control.SockPath)
if err != nil {
log.Printf("error creating http client: %s", err)
die()
}
err = jsonRequestResponse(httpc, daemon.ControlJobEndpointPProf, pprofListenCmd.PprofServerControlMsg, struct{}{})
if err != nil {
log.Printf("error sending control message: %s", err)
die()
}
log.Printf("finished")
}
+1 -3
View File
@@ -1,8 +1,6 @@
package client
import (
"context"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/cli"
@@ -13,7 +11,7 @@ import (
var SignalCmd = &cli.Subcommand{
Use: "signal [wakeup|reset] JOB",
Short: "wake up a job from wait state or abort its current invocation",
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
Run: func(subcommand *cli.Subcommand, args []string) error {
return runSignalCmd(subcommand.Config(), args)
},
}
+8 -9
View File
@@ -1,7 +1,6 @@
package client
import (
"context"
"fmt"
"io"
"math"
@@ -12,7 +11,7 @@ import (
"sync"
"time"
// tcell is the termbox-compatible library for abstracting away escape sequences, etc.
// tcell is the termbox-compatbile library for abstracting away escape sequences, etc.
// as of tcell#252, the number of default distributed terminals is relatively limited
// additional terminal definitions can be included via side-effect import
// See https://github.com/gdamore/tcell/blob/master/terminfo/base/base.go
@@ -82,7 +81,7 @@ type tui struct {
indent int
lock sync.Mutex //For report and error
report map[string]*job.Status
report map[string]job.Status
err error
jobFilter string
@@ -181,7 +180,7 @@ var StatusCmd = &cli.Subcommand{
Run: runStatus,
}
func runStatus(ctx context.Context, s *cli.Subcommand, args []string) error {
func runStatus(s *cli.Subcommand, args []string) error {
httpc, err := controlHttpClient(s.Config().Global.Control.SockPath)
if err != nil {
return err
@@ -220,7 +219,7 @@ func runStatus(ctx context.Context, s *cli.Subcommand, args []string) error {
defer termbox.Close()
update := func() {
var m daemon.Status
m := make(map[string]job.Status)
err2 := jsonRequestResponse(httpc, daemon.ControlJobEndpointStatus,
struct{}{},
@@ -229,7 +228,7 @@ func runStatus(ctx context.Context, s *cli.Subcommand, args []string) error {
t.lock.Lock()
t.err = err2
t.report = m.Jobs
t.report = m
t.lock.Unlock()
t.draw()
}
@@ -265,7 +264,7 @@ loop:
}
func (t *tui) getReplicationProgressHistory(jobName string) *bytesProgressHistory {
func (t *tui) getReplicationProgresHistory(jobName string) *bytesProgressHistory {
p, ok := t.replicationProgress[jobName]
if !ok {
p = &bytesProgressHistory{}
@@ -330,7 +329,7 @@ func (t *tui) draw() {
t.printf("Replication:")
t.newline()
t.addIndent(1)
t.renderReplicationReport(activeStatus.Replication, t.getReplicationProgressHistory(k))
t.renderReplicationReport(activeStatus.Replication, t.getReplicationProgresHistory(k))
t.addIndent(-1)
t.printf("Pruning Sender:")
@@ -698,7 +697,7 @@ func rightPad(str string, length int, pad string) string {
var arrowPositions = `>\|/`
// changeCount = 0 indicates stall / no progress
// changeCount = 0 indicates stall / no progresss
func (t *tui) drawBar(length int, bytes, totalBytes int64, changeCount int) {
var completedLength int
if totalBytes > 0 {
+1 -1
View File
@@ -17,7 +17,7 @@ import (
var StdinserverCmd = &cli.Subcommand{
Use: "stdinserver CLIENT_IDENTITY",
Short: "stdinserver transport mode (started from authorized_keys file as forced command)",
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
Run: func(subcommand *cli.Subcommand, args []string) error {
return runStdinserver(subcommand.Config(), args)
},
}
+7 -7
View File
@@ -39,7 +39,7 @@ var testFilter = &cli.Subcommand{
Run: runTestFilterCmd,
}
func runTestFilterCmd(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
if testFilterArgs.job == "" {
return fmt.Errorf("must specify --job flag")
@@ -75,7 +75,7 @@ func runTestFilterCmd(ctx context.Context, subcommand *cli.Subcommand, args []st
if testFilterArgs.input != "" {
fsnames = []string{testFilterArgs.input}
} else {
out, err := zfs.ZFSList(ctx, []string{"name"})
out, err := zfs.ZFSList([]string{"name"})
if err != nil {
return fmt.Errorf("could not list ZFS filesystems: %s", err)
}
@@ -136,13 +136,13 @@ var testPlaceholder = &cli.Subcommand{
Run: runTestPlaceholder,
}
func runTestPlaceholder(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
func runTestPlaceholder(subcommand *cli.Subcommand, args []string) error {
var checkDPs []*zfs.DatasetPath
// all actions first
if testPlaceholderArgs.all {
out, err := zfs.ZFSList(ctx, []string{"name"})
out, err := zfs.ZFSList([]string{"name"})
if err != nil {
return errors.Wrap(err, "could not list ZFS filesystems")
}
@@ -166,7 +166,7 @@ func runTestPlaceholder(ctx context.Context, subcommand *cli.Subcommand, args []
fmt.Printf("IS_PLACEHOLDER\tDATASET\tzrepl:placeholder\n")
for _, dp := range checkDPs {
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, dp)
ph, err := zfs.ZFSGetFilesystemPlaceholderState(dp)
if err != nil {
return errors.Wrap(err, "cannot get placeholder state")
}
@@ -195,11 +195,11 @@ var testDecodeResumeToken = &cli.Subcommand{
Run: runTestDecodeResumeTokenCmd,
}
func runTestDecodeResumeTokenCmd(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
func runTestDecodeResumeTokenCmd(subcommand *cli.Subcommand, args []string) error {
if testDecodeResumeTokenArgs.token == "" {
return fmt.Errorf("token argument must be specified")
}
token, err := zfs.ParseResumeToken(ctx, testDecodeResumeTokenArgs.token)
token, err := zfs.ParseResumeToken(context.Background(), testDecodeResumeTokenArgs.token)
if err != nil {
return err
}
+1 -2
View File
@@ -1,7 +1,6 @@
package client
import (
"context"
"fmt"
"os"
@@ -26,7 +25,7 @@ var VersionCmd = &cli.Subcommand{
SetupFlags: func(f *pflag.FlagSet) {
f.StringVar(&versionArgs.Show, "show", "", "version info to show (client|daemon)")
},
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
Run: func(subcommand *cli.Subcommand, args []string) error {
versionArgs.Config = subcommand.Config()
versionArgs.ConfigErr = subcommand.ConfigParsingError()
return runVersionCmd()
-147
View File
@@ -1,147 +0,0 @@
package client
import (
"fmt"
"sort"
"strings"
"github.com/spf13/pflag"
"github.com/zrepl/zrepl/cli"
"github.com/zrepl/zrepl/daemon/filters"
"github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/zfs"
)
var (
ZFSAbstractionsCmd = &cli.Subcommand{
Use: "zfs-abstraction",
Short: "manage abstractions that zrepl builds on top of ZFS",
SetupSubcommands: func() []*cli.Subcommand {
return []*cli.Subcommand{
zabsCmdList,
zabsCmdReleaseAll,
zabsCmdReleaseStale,
zabsCmdCreate,
}
},
}
)
// a common set of CLI flags that map to the fields of an
// endpoint.ListZFSHoldsAndBookmarksQuery
type zabsFilterFlags struct {
Filesystems FilesystemsFilterFlag
Job JobIDFlag
Types AbstractionTypesFlag
Concurrency int64
}
// produce a query from the CLI flags
func (f zabsFilterFlags) Query() (endpoint.ListZFSHoldsAndBookmarksQuery, error) {
q := endpoint.ListZFSHoldsAndBookmarksQuery{
FS: f.Filesystems.FlagValue(),
What: f.Types.FlagValue(),
JobID: f.Job.FlagValue(),
Concurrency: f.Concurrency,
}
return q, q.Validate()
}
func (f *zabsFilterFlags) registerZabsFilterFlags(s *pflag.FlagSet, verb string) {
// Note: the default value is defined in the .FlagValue methods
s.Var(&f.Filesystems, "fs", fmt.Sprintf("only %s holds on the specified filesystem [default: all filesystems] [comma-separated list of <dataset-pattern>:<ok|!> pairs]", verb))
s.Var(&f.Job, "job", fmt.Sprintf("only %s holds created by the specified job [default: any job]", verb))
variants := make([]string, 0, len(endpoint.AbstractionTypesAll))
for v := range endpoint.AbstractionTypesAll {
variants = append(variants, string(v))
}
variants = sort.StringSlice(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))
s.Int64VarP(&f.Concurrency, "concurrency", "p", 1, "number of concurrently queried filesystems")
}
type JobIDFlag struct{ J *endpoint.JobID }
func (f *JobIDFlag) Set(s string) error {
if len(s) == 0 {
*f = JobIDFlag{J: nil}
return nil
}
jobID, err := endpoint.MakeJobID(s)
if err != nil {
return err
}
*f = JobIDFlag{J: &jobID}
return nil
}
func (f JobIDFlag) Type() string { return "job-ID" }
func (f JobIDFlag) String() string { return fmt.Sprint(f.J) }
func (f JobIDFlag) FlagValue() *endpoint.JobID { return f.J }
type AbstractionTypesFlag map[endpoint.AbstractionType]bool
func (f *AbstractionTypesFlag) Set(s string) error {
ats, err := endpoint.AbstractionTypeSetFromStrings(strings.Split(s, ","))
if err != nil {
return err
}
*f = AbstractionTypesFlag(ats)
return nil
}
func (f AbstractionTypesFlag) Type() string { return "abstraction-type" }
func (f AbstractionTypesFlag) String() string {
return endpoint.AbstractionTypeSet(f).String()
}
func (f AbstractionTypesFlag) FlagValue() map[endpoint.AbstractionType]bool {
if len(f) > 0 {
return f
}
return endpoint.AbstractionTypesAll
}
type FilesystemsFilterFlag struct {
F endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter
}
func (flag *FilesystemsFilterFlag) Set(s string) error {
mappings := strings.Split(s, ",")
if len(mappings) == 1 && !strings.Contains(mappings[0], ":") {
flag.F = endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{
FS: &mappings[0],
}
return nil
}
f := filters.NewDatasetMapFilter(len(mappings), true)
for _, m := range mappings {
thisMappingErr := fmt.Errorf("expecting comma-separated list of <dataset-pattern>:<ok|!> pairs, got %q", m)
lhsrhs := strings.SplitN(m, ":", 2)
if len(lhsrhs) != 2 {
return thisMappingErr
}
err := f.Add(lhsrhs[0], lhsrhs[1])
if err != nil {
return fmt.Errorf("%s: %s", thisMappingErr, err)
}
}
flag.F = endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{
Filter: f,
}
return nil
}
func (flag FilesystemsFilterFlag) Type() string { return "filesystem filter spec" }
func (flag FilesystemsFilterFlag) String() string {
return fmt.Sprintf("%v", flag.F)
}
func (flag FilesystemsFilterFlag) FlagValue() endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter {
var z FilesystemsFilterFlag
if flag == z {
return endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{Filter: zfs.NoFilter()}
}
return flag.F
}
-14
View File
@@ -1,14 +0,0 @@
package client
import "github.com/zrepl/zrepl/cli"
var zabsCmdCreate = &cli.Subcommand{
Use: "create",
NoRequireConfig: true,
Short: `create zrepl ZFS abstractions (mostly useful for debugging & development, users should not need to use this command)`,
SetupSubcommands: func() []*cli.Subcommand {
return []*cli.Subcommand{
zabsCmdCreateStepHold,
}
},
}
@@ -1,58 +0,0 @@
package client
import (
"context"
"fmt"
"github.com/pkg/errors"
"github.com/spf13/pflag"
"github.com/zrepl/zrepl/cli"
"github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/zfs"
)
var zabsCreateStepHoldFlags struct {
target string
jobid JobIDFlag
}
var zabsCmdCreateStepHold = &cli.Subcommand{
Use: "step",
Run: doZabsCreateStep,
NoRequireConfig: true,
Short: `create a step hold or bookmark`,
SetupFlags: func(f *pflag.FlagSet) {
f.StringVarP(&zabsCreateStepHoldFlags.target, "target", "t", "", "snapshot to be held / bookmark to be held")
f.VarP(&zabsCreateStepHoldFlags.jobid, "jobid", "j", "jobid for which the hold is installed")
},
}
func doZabsCreateStep(ctx context.Context, sc *cli.Subcommand, args []string) error {
if len(args) > 0 {
return errors.New("subcommand takes no arguments")
}
f := &zabsCreateStepHoldFlags
fs, _, _, err := zfs.DecomposeVersionString(f.target)
if err != nil {
return errors.Wrapf(err, "%q invalid target", f.target)
}
if f.jobid.FlagValue() == nil {
return errors.Errorf("jobid must be set")
}
v, err := zfs.ZFSGetFilesystemVersion(ctx, f.target)
if err != nil {
return errors.Wrapf(err, "get info about target %q", f.target)
}
step, err := endpoint.HoldStep(ctx, fs, v, *f.jobid.FlagValue())
if err != nil {
return errors.Wrap(err, "create step hold")
}
fmt.Println(step.String())
return nil
}
-99
View File
@@ -1,99 +0,0 @@
package client
import (
"context"
"encoding/json"
"fmt"
"os"
"sync"
"github.com/fatih/color"
"github.com/pkg/errors"
"github.com/spf13/pflag"
"github.com/zrepl/zrepl/cli"
"github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/util/chainlock"
)
var zabsListFlags struct {
Filter zabsFilterFlags
Json bool
}
var zabsCmdList = &cli.Subcommand{
Use: "list",
Short: `list zrepl ZFS abstractions`,
Run: doZabsList,
NoRequireConfig: true,
SetupFlags: func(f *pflag.FlagSet) {
zabsListFlags.Filter.registerZabsFilterFlags(f, "list")
f.BoolVar(&zabsListFlags.Json, "json", false, "emit JSON")
},
}
func doZabsList(ctx context.Context, sc *cli.Subcommand, args []string) error {
var err error
if len(args) > 0 {
return errors.New("this subcommand takes no positional arguments")
}
q, err := zabsListFlags.Filter.Query()
if err != nil {
return errors.Wrap(err, "invalid filter specification on command line")
}
abstractions, errors, err := endpoint.ListAbstractionsStreamed(ctx, q)
if err != nil {
return err // context clear by invocation of command
}
var line chainlock.L
var wg sync.WaitGroup
defer wg.Wait()
wg.Add(1)
// print results
go func() {
defer wg.Done()
enc := json.NewEncoder(os.Stdout)
for a := range abstractions {
func() {
defer line.Lock().Unlock()
if zabsListFlags.Json {
enc.SetIndent("", " ")
if err := enc.Encode(abstractions); err != nil {
panic(err)
}
fmt.Println()
} else {
fmt.Println(a)
}
}()
}
}()
// print errors to stderr
errorColor := color.New(color.FgRed)
var errorsSlice []endpoint.ListAbstractionsError
wg.Add(1)
go func() {
defer wg.Done()
for err := range errors {
func() {
defer line.Lock().Unlock()
errorsSlice = append(errorsSlice, err)
errorColor.Fprintf(os.Stderr, "%s\n", err)
}()
}
}()
wg.Wait()
if len(errorsSlice) > 0 {
errorColor.Add(color.Bold).Fprintf(os.Stderr, "there were errors in listing the abstractions")
return fmt.Errorf("")
} else {
return nil
}
}
-143
View File
@@ -1,143 +0,0 @@
package client
import (
"context"
"encoding/json"
"fmt"
"os"
"github.com/fatih/color"
"github.com/pkg/errors"
"github.com/spf13/pflag"
"github.com/zrepl/zrepl/cli"
"github.com/zrepl/zrepl/endpoint"
)
// shared between release-all and release-step
var zabsReleaseFlags struct {
Filter zabsFilterFlags
Json bool
DryRun bool
}
func registerZabsReleaseFlags(s *pflag.FlagSet) {
zabsReleaseFlags.Filter.registerZabsFilterFlags(s, "release")
s.BoolVar(&zabsReleaseFlags.Json, "json", false, "emit json instead of pretty-printed")
s.BoolVar(&zabsReleaseFlags.DryRun, "dry-run", false, "do a dry-run")
}
var zabsCmdReleaseAll = &cli.Subcommand{
Use: "release-all",
Run: doZabsReleaseAll,
NoRequireConfig: true,
Short: `(DANGEROUS) release ALL zrepl ZFS abstractions (mostly useful for uninstalling zrepl completely or for "de-zrepl-ing" a filesystem)`,
SetupFlags: registerZabsReleaseFlags,
}
var zabsCmdReleaseStale = &cli.Subcommand{
Use: "release-stale",
Run: doZabsReleaseStale,
NoRequireConfig: true,
Short: `release stale zrepl ZFS abstractions (useful if zrepl has a bug and does not do it by itself)`,
SetupFlags: registerZabsReleaseFlags,
}
func doZabsReleaseAll(ctx context.Context, sc *cli.Subcommand, args []string) error {
var err error
if len(args) > 0 {
return errors.New("this subcommand takes no positional arguments")
}
q, err := zabsReleaseFlags.Filter.Query()
if err != nil {
return errors.Wrap(err, "invalid filter specification on command line")
}
abstractions, listErrors, err := endpoint.ListAbstractions(ctx, q)
if err != nil {
return err // context clear by invocation of command
}
if len(listErrors) > 0 {
color.New(color.FgRed).Fprintf(os.Stderr, "there were errors in listing the abstractions:\n%s\n", listErrors)
// proceed anyways with rest of abstractions
}
return doZabsRelease_Common(ctx, abstractions)
}
func doZabsReleaseStale(ctx context.Context, sc *cli.Subcommand, args []string) error {
var err error
if len(args) > 0 {
return errors.New("this subcommand takes no positional arguments")
}
q, err := zabsReleaseFlags.Filter.Query()
if err != nil {
return errors.Wrap(err, "invalid filter specification on command line")
}
stalenessInfo, err := endpoint.ListStale(ctx, q)
if err != nil {
return err // context clear by invocation of command
}
return doZabsRelease_Common(ctx, stalenessInfo.Stale)
}
func doZabsRelease_Common(ctx context.Context, destroy []endpoint.Abstraction) error {
if zabsReleaseFlags.DryRun {
if zabsReleaseFlags.Json {
m, err := json.MarshalIndent(destroy, "", " ")
if err != nil {
panic(err)
}
if _, err := os.Stdout.Write(m); err != nil {
panic(err)
}
fmt.Println()
} else {
for _, a := range destroy {
fmt.Printf("would destroy %s\n", a)
}
}
return nil
}
outcome := endpoint.BatchDestroy(ctx, destroy)
hadErr := false
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
colorErr := color.New(color.FgRed)
printfSuccess := color.New(color.FgGreen).FprintfFunc()
printfSection := color.New(color.Bold).FprintfFunc()
for res := range outcome {
hadErr = hadErr || res.DestroyErr != nil
if zabsReleaseFlags.Json {
err := enc.Encode(res)
if err != nil {
colorErr.Fprintf(os.Stderr, "cannot marshal there were errors in destroying the abstractions")
}
} else {
printfSection(os.Stdout, "destroy %s ...", res.Abstraction)
if res.DestroyErr != nil {
colorErr.Fprintf(os.Stdout, " failed:\n%s\n", res.DestroyErr)
} else {
printfSuccess(os.Stdout, " OK\n")
}
}
}
if hadErr {
colorErr.Add(color.Bold).Fprintf(os.Stderr, "there were errors in destroying the abstractions")
return fmt.Errorf("")
} else {
return nil
}
}
+2 -7
View File
@@ -76,12 +76,7 @@ type SnapJob struct {
}
type SendOptions struct {
Encrypted bool `yaml:"encrypted"`
StepHolds SendOptionsStepHolds `yaml:"step_holds,optional"`
}
type SendOptionsStepHolds struct {
DisableIncremental bool `yaml:"disable_incremental,optional"`
Encrypted bool `yaml:"encrypted"`
}
var _ yaml.Defaulter = (*SendOptions)(nil)
@@ -625,7 +620,7 @@ func ParseConfigBytes(bytes []byte) (*Config, error) {
var durationStringRegex *regexp.Regexp = regexp.MustCompile(`^\s*(\d+)\s*(s|m|h|d|w)\s*$`)
func parsePositiveDuration(e string) (d time.Duration, err error) {
func parsePostitiveDuration(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)
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
clients: {
"10.0.0.1":"foo"
}
root_fs: zroot/foo
root_fs: zoot/foo
`
_, err := ParseConfigBytes([]byte(jobdef))
require.NoError(t, err)
+1 -1
View File
@@ -42,7 +42,7 @@ func TestSampleConfigsAreParsedWithoutErrors(t *testing.T) {
}
// template must be a template/text template with a single '{{ . }}' as placeholder for val
// template must be a template/text template with a single '{{ . }}' as placehodler for val
//nolint[:deadcode,unused]
func testValidConfigTemplate(t *testing.T, tmpl string, val string) *Config {
tmp, err := template.New("master").Parse(tmpl)
+1 -1
View File
@@ -64,7 +64,7 @@ func parseRetentionGridIntervalString(e string) (intervals []RetentionInterval,
return nil, fmt.Errorf("contains factor <= 0")
}
duration, err := parsePositiveDuration(comps[2])
duration, err := parsePostitiveDuration(comps[2])
if err != nil {
return nil, err
}
+1 -5
View File
@@ -5,11 +5,7 @@ jobs:
type: tcp
listen: "0.0.0.0:8888"
clients: {
"192.168.122.123" : "mysql01",
"192.168.122.42" : "mx01",
"2001:0db8:85a3::8a2e:0370:7334": "gateway",
"10.23.42.0/24": "cluster-*",
"fde4:8dba:82e1::/64": "san-*",
"192.168.122.123" : "client1"
}
filesystems: {
"<": true,
+5 -14
View File
@@ -20,7 +20,6 @@ import (
"github.com/zrepl/zrepl/util/envconst"
"github.com/zrepl/zrepl/version"
"github.com/zrepl/zrepl/zfs"
"github.com/zrepl/zrepl/zfs/zfscmd"
)
type controlJob struct {
@@ -65,7 +64,7 @@ func (j *controlJob) RegisterMetrics(registerer prometheus.Registerer) {
Namespace: "zrepl",
Subsystem: "control",
Name: "request_finished",
Help: "time it took a request to finish",
Help: "time it took a request to finih",
Buckets: []float64{1e-6, 10e-6, 100e-6, 500e-6, 1e-3, 10e-3, 100e-3, 200e-3, 400e-3, 800e-3, 1, 10, 20},
}, []string{"endpoint"})
registerer.MustRegister(promControl.requestBegin)
@@ -118,15 +117,7 @@ func (j *controlJob) Run(ctx context.Context) {
mux.Handle(ControlJobEndpointStatus,
// don't log requests to status endpoint, too spammy
jsonResponder{log, func() (interface{}, error) {
jobs := j.jobs.status()
globalZFS := zfscmd.GetReport()
envconstReport := envconst.GetReport()
s := Status{
Jobs: jobs,
Global: GlobalStatus{
ZFSCmds: globalZFS,
Envconst: envconstReport,
}}
s := j.jobs.status()
return s, nil
}})
@@ -156,8 +147,8 @@ func (j *controlJob) Run(ctx context.Context) {
server := http.Server{
Handler: mux,
// control socket is local, 1s timeout should be more than sufficient, even on a loaded system
WriteTimeout: 1 * time.Second,
ReadTimeout: 1 * time.Second,
WriteTimeout: envconst.Duration("ZREPL_DAEMON_CONTROL_WRITE_TIMEOUT", 1*time.Second),
ReadTimeout: envconst.Duration("ZREPL_DAEMON_CONTROL_READ_TIMEOUT", 1*time.Second),
}
outer:
@@ -259,7 +250,7 @@ func (j jsonRequestResponder) ServeHTTP(w http.ResponseWriter, r *http.Request)
var buf bytes.Buffer
encodeErr := json.NewEncoder(&buf).Encode(res)
if encodeErr != nil {
j.log.WithError(producerErr).Error("control handler json marshal error")
j.log.WithError(producerErr).Error("control handler json marhsal error")
w.WriteHeader(http.StatusInternalServerError)
_, err := io.WriteString(w, encodeErr.Error())
logIoErr(err)
+15 -41
View File
@@ -12,9 +12,6 @@ import (
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/util/envconst"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/job"
@@ -23,11 +20,11 @@ import (
"github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/version"
"github.com/zrepl/zrepl/zfs/zfscmd"
)
func Run(ctx context.Context, conf *config.Config) error {
ctx, cancel := context.WithCancel(ctx)
func Run(conf *config.Config) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigChan := make(chan os.Signal, 1)
@@ -41,7 +38,6 @@ func Run(ctx context.Context, conf *config.Config) error {
if err != nil {
return errors.Wrap(err, "cannot build logging from config")
}
outlets.Add(newPrometheusLogOutlet(), logger.Debug)
confJobs, err := job.JobsFromConfig(conf)
if err != nil {
@@ -51,23 +47,14 @@ func Run(ctx context.Context, conf *config.Config) error {
log := logger.NewLogger(outlets, 1*time.Second)
log.Info(version.NewZreplVersionInformation().String())
ctx = logging.WithLoggers(ctx, logging.SubsystemLoggersWithUniversalLogger(log))
trace.RegisterCallback(trace.Callback{
OnBegin: func(ctx context.Context) { logging.GetLogger(ctx, logging.SubsysTraceData).Debug("begin span") },
OnEnd: func(ctx context.Context, spanInfo trace.SpanInfo) {
logging.
GetLogger(ctx, logging.SubsysTraceData).
WithField("duration_s", spanInfo.EndedAt().Sub(spanInfo.StartedAt()).Seconds()).
Debug("finished span " + spanInfo.TaskAndSpanStack(trace.SpanStackKindAnnotation))
},
})
for _, job := range confJobs {
if IsInternalJobName(job.Name()) {
panic(fmt.Sprintf("internal job name used for config job '%s'", job.Name())) //FIXME
}
}
ctx = job.WithLogger(ctx, log)
jobs := newJobs()
// start control socket
@@ -94,11 +81,6 @@ func Run(ctx context.Context, conf *config.Config) error {
jobs.start(ctx, job, true)
}
// register global (=non job-local) metrics
zfscmd.RegisterMetrics(prometheus.DefaultRegisterer)
trace.RegisterMetrics(prometheus.DefaultRegisterer)
endpoint.RegisterMetrics(prometheus.DefaultRegisterer)
log.Info("starting daemon")
// start regular jobs
@@ -112,8 +94,6 @@ func Run(ctx context.Context, conf *config.Config) error {
case <-ctx.Done():
log.WithError(ctx.Err()).Info("context finished")
}
log.Info("waiting for jobs to finish")
<-jobs.wait()
log.Info("daemon exiting")
return nil
}
@@ -136,25 +116,18 @@ func newJobs() *jobs {
}
}
const (
logJobField string = "job"
)
func (s *jobs) wait() <-chan struct{} {
ch := make(chan struct{})
go func() {
s.wg.Wait()
close(ch)
}()
return ch
}
type Status struct {
Jobs map[string]*job.Status
Global GlobalStatus
}
type GlobalStatus struct {
ZFSCmds *zfscmd.Report
Envconst *envconst.Report
}
func (s *jobs) status() map[string]*job.Status {
s.m.RLock()
defer s.m.RUnlock()
@@ -216,8 +189,9 @@ func (s *jobs) start(ctx context.Context, j job.Job, internal bool) {
s.m.Lock()
defer s.m.Unlock()
ctx = logging.WithInjectedField(ctx, logging.JobField, j.Name())
jobLog := job.GetLogger(ctx).
WithField(logJobField, j.Name()).
WithOutlet(newPrometheusLogOutlet(j.Name()), logger.Debug)
jobName := j.Name()
if !internal && IsInternalJobName(jobName) {
panic(fmt.Sprintf("internal job name used for non-internal job %s", jobName))
@@ -232,7 +206,7 @@ func (s *jobs) start(ctx context.Context, j job.Job, internal bool) {
j.RegisterMetrics(prometheus.DefaultRegisterer)
s.jobs[jobName] = j
ctx = zfscmd.WithJobID(ctx, j.Name())
ctx = job.WithLogger(ctx, jobLog)
ctx, wakeup := wakeup.Context(ctx)
ctx, resetFunc := reset.Context(ctx)
s.wakeups[jobName] = wakeup
@@ -241,8 +215,8 @@ func (s *jobs) start(ctx context.Context, j job.Job, internal bool) {
s.wg.Add(1)
go func() {
defer s.wg.Done()
job.GetLogger(ctx).Info("starting job")
defer job.GetLogger(ctx).Info("job exited")
jobLog.Info("starting job")
defer jobLog.Info("job exited")
j.Run(ctx)
}()
}
+1 -1
View File
@@ -161,7 +161,7 @@ func (m DatasetMapFilter) Filter(p *zfs.DatasetPath) (pass bool, err error) {
}
// Construct a new filter-only DatasetMapFilter from a mapping
// The new filter allows exactly those paths that were not forbidden by the mapping.
// The new filter allows excactly those paths that were not forbidden by the mapping.
func (m DatasetMapFilter) InvertedFilter() (inv *DatasetMapFilter, err error) {
if m.filterMode {
+41
View File
@@ -0,0 +1,41 @@
package filters
import (
"strings"
"github.com/zrepl/zrepl/zfs"
)
type AnyFSVFilter struct{}
func NewAnyFSVFilter() AnyFSVFilter {
return AnyFSVFilter{}
}
var _ zfs.FilesystemVersionFilter = AnyFSVFilter{}
func (AnyFSVFilter) Filter(t zfs.VersionType, name string) (accept bool, err error) {
return true, nil
}
type PrefixFilter struct {
prefix string
fstype zfs.VersionType
fstypeSet bool // optionals anyone?
}
var _ zfs.FilesystemVersionFilter = &PrefixFilter{}
func NewPrefixFilter(prefix string) *PrefixFilter {
return &PrefixFilter{prefix: prefix}
}
func NewTypedPrefixFilter(prefix string, versionType zfs.VersionType) *PrefixFilter {
return &PrefixFilter{prefix, versionType, true}
}
func (f *PrefixFilter) Filter(t zfs.VersionType, name string) (accept bool, err error) {
fstypeMatches := (!f.fstypeSet || t == f.fstype)
prefixMatches := strings.HasPrefix(name, f.prefix)
return fstypeMatches && prefixMatches, nil
}
+14 -2
View File
@@ -6,17 +6,29 @@ import (
"context"
"sync"
"github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/util/envconst"
)
type contextKey int
const (
contextKeyLog contextKey = 0
)
type Logger = logger.Logger
func WithLogger(ctx context.Context, log Logger) context.Context {
return context.WithValue(ctx, contextKeyLog, log)
}
func GetLogger(ctx context.Context) Logger { return getLogger(ctx) }
func getLogger(ctx context.Context) Logger {
return logging.GetLogger(ctx, logging.SubsysHooks)
if log, ok := ctx.Value(contextKeyLog).(Logger); ok {
return log
}
return logger.NewNullLogger()
}
const MAX_HOOK_LOG_SIZE_DEFAULT int = 1 << 20
+2 -6
View File
@@ -10,11 +10,9 @@ import (
"text/template"
"github.com/stretchr/testify/require"
"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/zfs"
)
@@ -71,9 +69,6 @@ func curry(f comparisonAssertionFunc, expected interface{}, right bool) (ret val
}
func TestHooks(t *testing.T) {
ctx, end := trace.WithTaskFromStack(context.Background())
defer end()
testFSName := "testpool/testdataset"
testSnapshotName := "testsnap"
@@ -423,8 +418,9 @@ jobs:
cbReached = false
ctx := context.Background()
if testing.Verbose() && !tt.SuppressOutput {
ctx = logging.WithLoggers(ctx, logging.SubsystemLoggersWithUniversalLogger(log))
ctx = hooks.WithLogger(ctx, log)
}
plan.Run(ctx, false)
report := plan.Report()
+26 -47
View File
@@ -8,13 +8,12 @@ import (
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/log"
"github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/filters"
"github.com/zrepl/zrepl/daemon/job/reset"
"github.com/zrepl/zrepl/daemon/job/wakeup"
"github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/daemon/pruner"
"github.com/zrepl/zrepl/daemon/snapper"
"github.com/zrepl/zrepl/endpoint"
@@ -80,7 +79,7 @@ func (a *ActiveSide) updateTasks(u func(*activeSideTasks)) activeSideTasks {
}
type activeMode interface {
ConnectEndpoints(ctx context.Context, connecter transport.Connecter)
ConnectEndpoints(rpcLoggers rpc.Loggers, connecter transport.Connecter)
DisconnectEndpoints()
SenderReceiver() (logic.Sender, logic.Receiver)
Type() Type
@@ -99,14 +98,14 @@ type modePush struct {
snapper *snapper.PeriodicOrManual
}
func (m *modePush) ConnectEndpoints(ctx context.Context, connecter transport.Connecter) {
func (m *modePush) ConnectEndpoints(loggers rpc.Loggers, connecter transport.Connecter) {
m.setupMtx.Lock()
defer m.setupMtx.Unlock()
if m.receiver != nil || m.sender != nil {
panic("inconsistent use of ConnectEndpoints and DisconnectEndpoints")
}
m.sender = endpoint.NewSender(*m.senderConfig)
m.receiver = rpc.NewClient(connecter, rpc.GetLoggersOrPanic(ctx))
m.receiver = rpc.NewClient(connecter, loggers)
}
func (m *modePush) DisconnectEndpoints() {
@@ -148,14 +147,13 @@ func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.Job
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
if err != nil {
return nil, errors.Wrap(err, "cannot build filesystem filter")
return nil, errors.Wrap(err, "cannnot build filesystem filter")
}
m.senderConfig = &endpoint.SenderConfig{
FSF: fsf,
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
DisableIncrementalStepHolds: in.Send.StepHolds.DisableIncremental,
JobID: jobID,
FSF: fsf,
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
JobID: jobID,
}
m.plannerPolicy = &logic.PlannerPolicy{
EncryptedSend: logic.TriFromBool(in.Send.Encrypted),
@@ -178,14 +176,14 @@ type modePull struct {
interval config.PositiveDurationOrManual
}
func (m *modePull) ConnectEndpoints(ctx context.Context, connecter transport.Connecter) {
func (m *modePull) ConnectEndpoints(loggers rpc.Loggers, connecter transport.Connecter) {
m.setupMtx.Lock()
defer m.setupMtx.Unlock()
if m.receiver != nil || m.sender != nil {
panic("inconsistent use of ConnectEndpoints and DisconnectEndpoints")
}
m.receiver = endpoint.NewReceiver(m.receiverConfig)
m.sender = rpc.NewClient(connecter, rpc.GetLoggersOrPanic(ctx))
m.sender = rpc.NewClient(connecter, loggers)
}
func (m *modePull) DisconnectEndpoints() {
@@ -377,31 +375,16 @@ func (j *ActiveSide) SenderConfig() *endpoint.SenderConfig {
return push.senderConfig
}
// The active side of a replication uses one end (sender or receiver)
// directly by method invocation, without going through a transport that
// provides a client identity.
// However, in order to avoid the need to distinguish between direct-method-invocating
// clients and RPC client, we use an invalid client identity as a sentinel value.
func FakeActiveSideDirectMethodInvocationClientIdentity(jobId endpoint.JobID) string {
return fmt.Sprintf("<local><active><job><client><identity><job=%q>", jobId.String())
}
func (j *ActiveSide) Run(ctx context.Context) {
ctx, endTask := trace.WithTaskAndSpan(ctx, "active-side-job", j.Name())
defer endTask()
ctx = context.WithValue(ctx, endpoint.ClientIdentityKey, FakeActiveSideDirectMethodInvocationClientIdentity(j.name))
log := GetLogger(ctx)
ctx = logging.WithSubsystemLoggers(ctx, log)
defer log.Info("job exiting")
periodicDone := make(chan struct{})
ctx, cancel := context.WithCancel(ctx)
defer cancel()
periodicCtx, endTask := trace.WithTask(ctx, "periodic")
defer endTask()
go j.mode.RunPeriodic(periodicCtx, periodicDone)
go j.mode.RunPeriodic(ctx, periodicDone)
invocationCount := 0
outer:
@@ -417,15 +400,17 @@ outer:
case <-periodicDone:
}
invocationCount++
invocationCtx, endSpan := trace.WithSpan(ctx, fmt.Sprintf("invocation-%d", invocationCount))
j.do(invocationCtx)
endSpan()
invLog := log.WithField("invocation", invocationCount)
j.do(WithLogger(ctx, invLog))
}
}
func (j *ActiveSide) do(ctx context.Context) {
j.mode.ConnectEndpoints(ctx, j.connecter)
log := GetLogger(ctx)
ctx = logging.WithSubsystemLoggers(ctx, log)
loggers := rpc.GetLoggersOrPanic(ctx) // filled by WithSubsystemLoggers
j.mode.ConnectEndpoints(loggers, j.connecter)
defer j.mode.DisconnectEndpoints()
// allow cancellation of an invocation (this function)
@@ -448,22 +433,20 @@ func (j *ActiveSide) do(ctx context.Context) {
return
default:
}
ctx, endSpan := trace.WithSpan(ctx, "replication")
ctx, repCancel := context.WithCancel(ctx)
var repWait driver.WaitFunc
j.updateTasks(func(tasks *activeSideTasks) {
// reset it
*tasks = activeSideTasks{}
tasks.replicationCancel = func() { repCancel(); endSpan() }
tasks.replicationCancel = repCancel
tasks.replicationReport, repWait = replication.Do(
ctx, logic.NewPlanner(j.promRepStateSecs, j.promBytesReplicated, sender, receiver, j.mode.PlannerPolicy()),
)
tasks.state = ActiveSideReplicating
})
GetLogger(ctx).Info("start replication")
log.Info("start replication")
repWait(true) // wait blocking
repCancel() // always cancel to free up context resources
endSpan()
}
{
@@ -472,18 +455,16 @@ func (j *ActiveSide) do(ctx context.Context) {
return
default:
}
ctx, endSpan := trace.WithSpan(ctx, "prune_sender")
ctx, senderCancel := context.WithCancel(ctx)
tasks := j.updateTasks(func(tasks *activeSideTasks) {
tasks.prunerSender = j.prunerFactory.BuildSenderPruner(ctx, sender, sender)
tasks.prunerSenderCancel = func() { senderCancel(); endSpan() }
tasks.prunerSenderCancel = senderCancel
tasks.state = ActiveSidePruneSender
})
GetLogger(ctx).Info("start pruning sender")
log.Info("start pruning sender")
tasks.prunerSender.Prune()
GetLogger(ctx).Info("finished pruning sender")
log.Info("finished pruning sender")
senderCancel()
endSpan()
}
{
select {
@@ -491,18 +472,16 @@ func (j *ActiveSide) do(ctx context.Context) {
return
default:
}
ctx, endSpan := trace.WithSpan(ctx, "prune_recever")
ctx, receiverCancel := context.WithCancel(ctx)
tasks := j.updateTasks(func(tasks *activeSideTasks) {
tasks.prunerReceiver = j.prunerFactory.BuildReceiverPruner(ctx, receiver, sender)
tasks.prunerReceiverCancel = func() { receiverCancel(); endSpan() }
tasks.prunerReceiverCancel = receiverCancel
tasks.state = ActiveSidePruneReceiver
})
GetLogger(ctx).Info("start pruning receiver")
log.Info("start pruning receiver")
tasks.prunerReceiver.Prune()
GetLogger(ctx).Info("finished pruning receiver")
log.Info("finished pruning receiver")
receiverCancel()
endSpan()
}
j.updateTasks(func(tasks *activeSideTasks) {
-19
View File
@@ -1,19 +0,0 @@
package job
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/transport"
)
func TestFakeActiveSideDirectMethodInvocationClientIdentityDoesNotPassValidityTest(t *testing.T) {
jobid, err := endpoint.MakeJobID("validjobname")
require.NoError(t, err)
clientIdentity := FakeActiveSideDirectMethodInvocationClientIdentity(jobid)
t.Logf("%v", clientIdentity)
err = transport.ValidateClientIdentity(clientIdentity)
assert.Error(t, err)
}
+14 -2
View File
@@ -7,7 +7,6 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/zfs"
@@ -15,8 +14,21 @@ import (
type Logger = logger.Logger
type contextKey int
const (
contextKeyLog contextKey = iota
)
func GetLogger(ctx context.Context) Logger {
return logging.GetLogger(ctx, logging.SubsysJob)
if l, ok := ctx.Value(contextKeyLog).(Logger); ok {
return l
}
return logger.NewNullLogger()
}
func WithLogger(ctx context.Context, l Logger) context.Context {
return context.WithValue(ctx, contextKeyLog, l)
}
type Job interface {
+9 -19
View File
@@ -6,7 +6,6 @@ import (
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/filters"
@@ -76,13 +75,12 @@ func modeSourceFromConfig(g *config.Global, in *config.SourceJob, jobID endpoint
m = &modeSource{}
fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems)
if err != nil {
return nil, errors.Wrap(err, "cannot build filesystem filter")
return nil, errors.Wrap(err, "cannnot build filesystem filter")
}
m.senderConfig = &endpoint.SenderConfig{
FSF: fsf,
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
DisableIncrementalStepHolds: in.Send.StepHolds.DisableIncremental,
JobID: jobID,
FSF: fsf,
Encrypt: &zfs.NilBool{B: in.Send.Encrypted},
JobID: jobID,
}
if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil {
@@ -166,14 +164,12 @@ func (j *PassiveSide) SenderConfig() *endpoint.SenderConfig {
func (*PassiveSide) RegisterMetrics(registerer prometheus.Registerer) {}
func (j *PassiveSide) Run(ctx context.Context) {
ctx, endTask := trace.WithTaskAndSpan(ctx, "passive-side-job", j.Name())
defer endTask()
log := GetLogger(ctx)
defer log.Info("job exiting")
ctx = logging.WithSubsystemLoggers(ctx, log)
{
ctx, endTask := trace.WithTask(ctx, "periodic") // shadowing
defer endTask()
ctx, cancel := context.WithCancel(ctx)
ctx, cancel := context.WithCancel(ctx) // shadowing
defer cancel()
go j.mode.RunPeriodic(ctx)
}
@@ -183,14 +179,8 @@ func (j *PassiveSide) Run(ctx context.Context) {
panic(fmt.Sprintf("implementation error: j.mode.Handler() returned nil: %#v", j))
}
ctxInterceptor := func(handlerCtx context.Context, info rpc.HandlerContextInterceptorData, handler func(ctx context.Context)) {
// the handlerCtx is clean => need to inherit logging and tracing config from job context
handlerCtx = logging.WithInherit(handlerCtx, ctx)
handlerCtx = trace.WithInherit(handlerCtx, ctx)
handlerCtx, endTask := trace.WithTaskAndSpan(handlerCtx, "handler", fmt.Sprintf("job=%q client=%q method=%q", j.Name(), info.ClientIdentity(), info.FullMethod()))
defer endTask()
handler(handlerCtx)
ctxInterceptor := func(handlerCtx context.Context) context.Context {
return logging.WithSubsystemLoggers(handlerCtx, log)
}
rpcLoggers := rpc.GetLoggersOrPanic(ctx) // WithSubsystemLoggers above
+6 -15
View File
@@ -2,16 +2,15 @@ package job
import (
"context"
"fmt"
"sort"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/filters"
"github.com/zrepl/zrepl/daemon/job/wakeup"
"github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/daemon/pruner"
"github.com/zrepl/zrepl/daemon/snapper"
"github.com/zrepl/zrepl/endpoint"
@@ -90,18 +89,15 @@ func (j *SnapJob) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) {
func (j *SnapJob) SenderConfig() *endpoint.SenderConfig { return nil }
func (j *SnapJob) Run(ctx context.Context) {
ctx, endTask := trace.WithTaskAndSpan(ctx, "snap-job", j.Name())
defer endTask()
log := GetLogger(ctx)
ctx = logging.WithSubsystemLoggers(ctx, log)
defer log.Info("job exiting")
periodicDone := make(chan struct{})
ctx, cancel := context.WithCancel(ctx)
defer cancel()
periodicCtx, endTask := trace.WithTask(ctx, "snapshotting")
defer endTask()
go j.snapper.Run(periodicCtx, periodicDone)
go j.snapper.Run(ctx, periodicDone)
invocationCount := 0
outer:
@@ -116,10 +112,8 @@ outer:
case <-periodicDone:
}
invocationCount++
invocationCtx, endSpan := trace.WithSpan(ctx, fmt.Sprintf("invocation-%d", invocationCount))
j.doPrune(invocationCtx)
endSpan()
invLog := log.WithField("invocation", invocationCount)
j.doPrune(WithLogger(ctx, invLog))
}
}
@@ -167,16 +161,13 @@ func (h alwaysUpToDateReplicationCursorHistory) ListFilesystems(ctx context.Cont
}
func (j *SnapJob) doPrune(ctx context.Context) {
ctx, endSpan := trace.WithSpan(ctx, "snap-job-do-prune")
defer endSpan()
log := GetLogger(ctx)
ctx = logging.WithSubsystemLoggers(ctx, log)
sender := endpoint.NewSender(endpoint.SenderConfig{
JobID: j.name,
FSF: j.fsfilter,
// FIXME encryption setting is irrelevant for SnapJob because the endpoint is only used as pruner.Target
Encrypt: &zfs.NilBool{B: true},
// FIXME DisableIncrementalStepHolds setting is irrelevant for SnapJob because the endpoint is only used as pruner.Target
DisableIncrementalStepHolds: false,
})
j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender})
log.Info("start pruning")
+30 -98
View File
@@ -9,10 +9,19 @@ import (
"github.com/mattn/go-isatty"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/daemon/hooks"
"github.com/zrepl/zrepl/daemon/pruner"
"github.com/zrepl/zrepl/daemon/snapper"
"github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/replication/driver"
"github.com/zrepl/zrepl/replication/logic"
"github.com/zrepl/zrepl/rpc"
"github.com/zrepl/zrepl/rpc/transportmux"
"github.com/zrepl/zrepl/tlsconf"
"github.com/zrepl/zrepl/transport"
)
func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error) {
@@ -60,10 +69,8 @@ func OutletsFromConfig(in config.LoggingOutletEnumList) (*logger.Outlets, error)
type Subsystem string
const (
SubsysMeta Subsystem = "meta"
SubsysJob Subsystem = "job"
SubsysReplication Subsystem = "repl"
SubsysEndpoint Subsystem = "endpoint"
SubsyEndpoint Subsystem = "endpoint"
SubsysPruning Subsystem = "pruning"
SubsysSnapshot Subsystem = "snapshot"
SubsysHooks Subsystem = "hook"
@@ -72,104 +79,29 @@ const (
SubsysRPC Subsystem = "rpc"
SubsysRPCControl Subsystem = "rpc.ctrl"
SubsysRPCData Subsystem = "rpc.data"
SubsysZFSCmd Subsystem = "zfs.cmd"
SubsysTraceData Subsystem = "trace.data"
SubsysPlatformtest Subsystem = "platformtest"
)
var AllSubsystems = []Subsystem{
SubsysMeta,
SubsysJob,
SubsysReplication,
SubsysEndpoint,
SubsysPruning,
SubsysSnapshot,
SubsysHooks,
SubsysTransport,
SubsysTransportMux,
SubsysRPC,
SubsysRPCControl,
SubsysRPCData,
SubsysZFSCmd,
SubsysTraceData,
SubsysPlatformtest,
func WithSubsystemLoggers(ctx context.Context, log logger.Logger) context.Context {
ctx = logic.WithLogger(ctx, log.WithField(SubsysField, SubsysReplication))
ctx = driver.WithLogger(ctx, log.WithField(SubsysField, SubsysReplication))
ctx = endpoint.WithLogger(ctx, log.WithField(SubsysField, SubsyEndpoint))
ctx = pruner.WithLogger(ctx, log.WithField(SubsysField, SubsysPruning))
ctx = snapper.WithLogger(ctx, log.WithField(SubsysField, SubsysSnapshot))
ctx = hooks.WithLogger(ctx, log.WithField(SubsysField, SubsysHooks))
ctx = transport.WithLogger(ctx, log.WithField(SubsysField, SubsysTransport))
ctx = transportmux.WithLogger(ctx, log.WithField(SubsysField, SubsysTransportMux))
ctx = rpc.WithLoggers(ctx,
rpc.Loggers{
General: log.WithField(SubsysField, SubsysRPC),
Control: log.WithField(SubsysField, SubsysRPCControl),
Data: log.WithField(SubsysField, SubsysRPCData),
},
)
return ctx
}
type injectedField struct {
field string
value interface{}
parent *injectedField
}
func WithInjectedField(ctx context.Context, field string, value interface{}) context.Context {
var parent *injectedField
parentI := ctx.Value(contextKeyInjectedField)
if parentI != nil {
parent = parentI.(*injectedField)
}
// TODO sanity-check `field` now
this := &injectedField{field, value, parent}
return context.WithValue(ctx, contextKeyInjectedField, this)
}
func iterInjectedFields(ctx context.Context, cb func(field string, value interface{})) {
injI := ctx.Value(contextKeyInjectedField)
if injI == nil {
return
}
inj := injI.(*injectedField)
for ; inj != nil; inj = inj.parent {
cb(inj.field, inj.value)
}
}
type SubsystemLoggers map[Subsystem]logger.Logger
func SubsystemLoggersWithUniversalLogger(l logger.Logger) SubsystemLoggers {
loggers := make(SubsystemLoggers)
for _, s := range AllSubsystems {
loggers[s] = l
}
return loggers
}
func WithLoggers(ctx context.Context, loggers SubsystemLoggers) context.Context {
return context.WithValue(ctx, contextKeyLoggers, loggers)
}
func GetLoggers(ctx context.Context) SubsystemLoggers {
loggers, ok := ctx.Value(contextKeyLoggers).(SubsystemLoggers)
if !ok {
return nil
}
return loggers
}
func GetLogger(ctx context.Context, subsys Subsystem) logger.Logger {
return getLoggerImpl(ctx, subsys, true)
}
func getLoggerImpl(ctx context.Context, subsys Subsystem, panicIfEnded bool) logger.Logger {
loggers, ok := ctx.Value(contextKeyLoggers).(SubsystemLoggers)
if !ok || loggers == nil {
return logger.NewNullLogger()
}
l, ok := loggers[subsys]
if !ok {
return logger.NewNullLogger()
}
l = l.WithField(SubsysField, subsys)
l = l.WithField(SpanField, trace.GetSpanStackOrDefault(ctx, *trace.StackKindId, "NOSPAN"))
fields := make(logger.Fields)
iterInjectedFields(ctx, func(field string, value interface{}) {
fields[field] = value
})
l = l.WithFields(fields)
return l
func LogSubsystem(log logger.Logger, subsys Subsystem) logger.Logger {
return log.ReplaceField(SubsysField, subsys)
}
func parseLogFormat(i interface{}) (f EntryFormatter, err error) {
-24
View File
@@ -1,24 +0,0 @@
package logging
import "context"
type contextKey int
const (
contextKeyLoggers contextKey = 1 + iota
contextKeyInjectedField
)
var contextKeys = []contextKey{
contextKeyLoggers,
contextKeyInjectedField,
}
func WithInherit(ctx, inheritFrom context.Context) context.Context {
for _, k := range contextKeys {
if v := inheritFrom.Value(k); v != nil {
ctx = context.WithValue(ctx, k, v) // no shadow
}
}
return ctx
}
+4 -5
View File
@@ -22,7 +22,6 @@ const (
const (
JobField string = "job"
SubsysField string = "subsystem"
SpanField string = "span"
)
type MetadataFlags int64
@@ -86,7 +85,7 @@ func (f *HumanFormatter) Format(e *logger.Entry) (out []byte, err error) {
fmt.Fprintf(&line, "[%s]", col.Sprint(e.Level.Short()))
}
prefixFields := []string{JobField, SubsysField, SpanField}
prefixFields := []string{JobField, SubsysField}
prefixed := make(map[string]bool, len(prefixFields)+2)
for _, field := range prefixFields {
val, ok := e.Fields[field]
@@ -175,8 +174,8 @@ func (f *LogfmtFormatter) Format(e *logger.Entry) ([]byte, error) {
}
// at least try and put job and task in front
prefixed := make(map[string]bool, 3)
prefix := []string{JobField, SubsysField, SpanField}
prefixed := make(map[string]bool, 2)
prefix := []string{JobField, SubsysField}
for _, pf := range prefix {
v, ok := e.Fields[pf]
if !ok {
@@ -212,7 +211,7 @@ func logfmtTryEncodeKeyval(enc *logfmt.Encoder, field, value interface{}) error
case logfmt.ErrUnsupportedValueType:
err := enc.EncodeKeyval(field, fmt.Sprintf("<%T>", value))
if err != nil {
return errors.Wrap(err, "cannot encode unsupported value type Go type")
return errors.Wrap(err, "cannot encode unsuuported value type Go type")
}
return nil
}
+1 -1
View File
@@ -63,7 +63,7 @@ func NewTCPOutlet(formatter EntryFormatter, network, address string, tlsConfig *
return
}
entryChan := make(chan *bytes.Buffer, 1) // allow one message in flight while previous is in io.Copy()
entryChan := make(chan *bytes.Buffer, 1) // allow one message in flight while previos is in io.Copy()
o := &TCPOutlet{
formatter: formatter,
-386
View File
@@ -1,386 +0,0 @@
// package trace provides activity tracing via ctx through Tasks and Spans
//
// Basic Concepts
//
// Tracing can be used to identify where a piece of code spends its time.
//
// The Go standard library provides package runtime/trace which is useful to identify CPU bottlenecks or
// to understand what happens inside the Go runtime.
// However, it is not ideal for application level tracing, in particular if those traces should be understandable
// 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.
//
// 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)
//
// 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)
// }()
// }
//
// 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)
// }()
// }
// }()
// }
//
// 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,
// the out-of-order endSpan() will panic.
//
// A similar rule applies to the endTask closure returned by WithTask:
// If a task has live child tasks at the time you call endTask(), the call will panic.
//
// Recovering from endSpan() or endTask() panics will corrupt the trace stack and lead to corrupt tracefile output.
//
//
// 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.
// Also, the package appends `#NUM` for each concurrently running instance of a task name.
// 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
//
// 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 .
// Apart from function GetSpanStackOrDefault, this is the main benefit of this package.
//
// First, there is a convenience environment variable 'ZREPL_ACTIVITY_TRACE' that can be set to an output path.
// From process start onward, a trace is written to that path.
//
// More consumers can attach to the activity trace through the ChrometraceClientWebsocketHandler websocket handler.
//
// If a write error is encountered with any consumer (including the env-var based one), the consumer is closed and
// will not receive further trace output.
package trace
import (
"context"
"fmt"
"strings"
"time"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/util/chainlock"
)
var metrics struct {
activeTasks prometheus.Gauge
}
var taskNamer *uniqueConcurrentTaskNamer = newUniqueTaskNamer()
func init() {
metrics.activeTasks = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "zrepl",
Subsystem: "trace",
Name: "active_tasks",
Help: "number of active (tracing-level) tasks in the daemon",
})
}
func RegisterMetrics(r prometheus.Registerer) {
r.MustRegister(metrics.activeTasks)
}
type traceNode struct {
id string
annotation string
parentTask *traceNode
mtx chainlock.L
activeChildTasks int32 // only for task nodes, insignificant for span nodes
parentSpan *traceNode
activeChildSpan *traceNode // nil if task or span doesn't have an active child span
startedAt time.Time
endedAt time.Time
}
func (s *traceNode) StartedAt() time.Time { return s.startedAt }
func (s *traceNode) EndedAt() time.Time { return s.endedAt }
// Returned from WithTask or WithSpan.
// Must be called once the task or span ends.
// See package-level docs for nesting rules.
// Wrong call order / forgetting to call it will result in panics.
type DoneFunc func()
var ErrTaskStillHasActiveChildTasks = fmt.Errorf("end task: task still has active child tasks")
// Start a new root task or create a child task of an existing task.
//
// This is required when starting a new goroutine and
// passing an existing task context to it.
//
// taskName should be a constantand must not contain '#'
//
// The implementation ensures that,
// if multiple tasks with the same name exist simultaneously,
// a unique suffix is appended to uniquely identify the task opened with this function.
func WithTask(ctx context.Context, taskName string) (context.Context, DoneFunc) {
var parentTask *traceNode
nodeI := ctx.Value(contextKeyTraceNode)
if nodeI != nil {
node := nodeI.(*traceNode)
if node.parentSpan != nil {
parentTask = node.parentTask
} else {
parentTask = node
}
}
// find the first ancestor that hasn't ended yet (nil if need be)
if parentTask != nil {
parentTask.mtx.Lock()
}
for parentTask != nil && !parentTask.endedAt.IsZero() {
thisParent := parentTask
parentTask = parentTask.parentTask
// lock parent first such that it isn't modified by other callers to thisParent
if parentTask != nil {
parentTask.mtx.Lock()
}
thisParent.mtx.Unlock()
}
// invariant: either parentTask != nil and we hold the lock on parentTask, or parentTask is nil
taskName, taskNameDone := taskNamer.UniqueConcurrentTaskName(taskName)
this := &traceNode{
id: genID(),
annotation: taskName,
parentTask: parentTask,
activeChildTasks: 0,
parentSpan: nil,
activeChildSpan: nil,
startedAt: time.Now(),
endedAt: time.Time{},
}
if parentTask != nil {
this.parentTask.activeChildTasks++
parentTask.mtx.Unlock()
}
ctx = context.WithValue(ctx, contextKeyTraceNode, this)
chrometraceBeginTask(this)
metrics.activeTasks.Inc()
endTaskFunc := func() {
// only hold locks while manipulating the tree
// (trace writer might block too long and unlike spans, tasks are updated concurrently)
alreadyEnded := func() (alreadyEnded bool) {
if this.parentTask != nil {
defer this.parentTask.mtx.Lock().Unlock()
}
defer this.mtx.Lock().Unlock()
if this.activeChildTasks != 0 {
panic(errors.Wrapf(ErrTaskStillHasActiveChildTasks, "end task: %v active child tasks", this.activeChildTasks))
}
// support idempotent task ends
if !this.endedAt.IsZero() {
return true
}
this.endedAt = time.Now()
if this.parentTask != nil {
this.parentTask.activeChildTasks--
if this.parentTask.activeChildTasks < 0 {
panic("impl error: parent task with negative activeChildTasks count")
}
}
return false
}()
if alreadyEnded {
return
}
chrometraceEndTask(this)
metrics.activeTasks.Dec()
taskNameDone()
}
return ctx, endTaskFunc
}
var ErrAlreadyActiveChildSpan = fmt.Errorf("create child span: span already has an active child span")
var ErrSpanStillHasActiveChildSpan = fmt.Errorf("end span: span still has active child spans")
// Start a new span.
// Important: ctx must have an active task (see WithTask)
func WithSpan(ctx context.Context, annotation string) (context.Context, DoneFunc) {
var parentSpan, parentTask *traceNode
nodeI := ctx.Value(contextKeyTraceNode)
if nodeI != nil {
parentSpan = nodeI.(*traceNode)
if parentSpan.parentSpan == nil {
parentTask = parentSpan
} else {
parentTask = parentSpan.parentTask
}
} else {
panic("must be called from within a task")
}
this := &traceNode{
id: genID(),
annotation: annotation,
parentTask: parentTask,
parentSpan: parentSpan,
activeChildSpan: nil,
startedAt: time.Now(),
endedAt: time.Time{},
}
parentSpan.mtx.HoldWhile(func() {
if parentSpan.activeChildSpan != nil {
panic(ErrAlreadyActiveChildSpan)
}
parentSpan.activeChildSpan = this
})
ctx = context.WithValue(ctx, contextKeyTraceNode, this)
chrometraceBeginSpan(this)
callbackEndSpan := callbackBeginSpan(ctx)
endTaskFunc := func() {
defer parentSpan.mtx.Lock().Unlock()
if parentSpan.activeChildSpan != this && this.endedAt.IsZero() {
panic("impl error: activeChildSpan should not change while != nil because there can only be one")
}
defer this.mtx.Lock().Unlock()
if this.activeChildSpan != nil {
panic(ErrSpanStillHasActiveChildSpan)
}
if !this.endedAt.IsZero() {
return // support idempotent span ends
}
parentSpan.activeChildSpan = nil
this.endedAt = time.Now()
chrometraceEndSpan(this)
callbackEndSpan(this)
}
return ctx, endTaskFunc
}
type StackKind struct {
symbolizeTask func(t *traceNode) string
symbolizeSpan func(s *traceNode) string
}
var (
StackKindId = &StackKind{
symbolizeTask: func(t *traceNode) string { return t.id },
symbolizeSpan: func(s *traceNode) string { return s.id },
}
SpanStackKindCombined = &StackKind{
symbolizeTask: func(t *traceNode) string { return fmt.Sprintf("(%s %q)", t.id, t.annotation) },
symbolizeSpan: func(s *traceNode) string { return fmt.Sprintf("(%s %q)", s.id, s.annotation) },
}
SpanStackKindAnnotation = &StackKind{
symbolizeTask: func(t *traceNode) string { return t.annotation },
symbolizeSpan: func(s *traceNode) string { return s.annotation },
}
)
func (n *traceNode) task() *traceNode {
task := n.parentTask
if n.parentSpan == nil {
task = n
}
return task
}
func (n *traceNode) TaskName() string {
task := n.task()
return task.annotation
}
func (this *traceNode) TaskAndSpanStack(kind *StackKind) (spanIdStack string) {
task := this.task()
var spansInTask []*traceNode
for s := this; s != nil; s = s.parentSpan {
spansInTask = append(spansInTask, s)
}
var tasks []*traceNode
for t := task; t != nil; t = t.parentTask {
tasks = append(tasks, t)
}
var taskIdsRev []string
for i := len(tasks) - 1; i >= 0; i-- {
taskIdsRev = append(taskIdsRev, kind.symbolizeTask(tasks[i]))
}
var spanIdsRev []string
for i := len(spansInTask) - 1; i >= 0; i-- {
spanIdsRev = append(spanIdsRev, kind.symbolizeSpan(spansInTask[i]))
}
taskStack := strings.Join(taskIdsRev, "$")
return fmt.Sprintf("%s$%s", taskStack, strings.Join(spanIdsRev, "."))
}
func GetSpanStackOrDefault(ctx context.Context, kind StackKind, def string) string {
if nI := ctx.Value(contextKeyTraceNode); nI != nil {
n := nI.(*traceNode)
return n.TaskAndSpanStack(StackKindId)
} else {
return def
}
}
-54
View File
@@ -1,54 +0,0 @@
package trace
import (
"context"
"time"
"github.com/zrepl/zrepl/util/chainlock"
)
type SpanInfo interface {
StartedAt() time.Time
EndedAt() time.Time
TaskAndSpanStack(kind *StackKind) string
}
type Callback struct {
OnBegin func(ctx context.Context)
OnEnd func(ctx context.Context, spanInfo SpanInfo)
}
var callbacks struct {
mtx chainlock.L
cs []Callback
}
func RegisterCallback(c Callback) {
callbacks.mtx.HoldWhile(func() {
callbacks.cs = append(callbacks.cs, c)
})
}
func callbackBeginSpan(ctx context.Context) func(SpanInfo) {
// capture the current state of callbacks into a local variable
// this is safe because the slice is append-only and immutable
// (it is important that a callback registered _after_ callbackBeginSpin is called does not get called on OnEnd)
var cbs []Callback
callbacks.mtx.HoldWhile(func() {
cbs = callbacks.cs
})
for _, cb := range cbs {
if cb.OnBegin != nil {
cb.OnBegin(ctx)
}
}
return func(spanInfo SpanInfo) {
for _, cb := range cbs {
if cb.OnEnd != nil {
cb.OnEnd(ctx, spanInfo)
}
}
}
}
-230
View File
@@ -1,230 +0,0 @@
package trace
// The functions in this file are concerned with the generation
// of trace files based on the information from WithTask and WithSpan.
//
// The emitted trace files are open-ended array of JSON objects
// that follow the Chrome trace file format:
// https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview
//
// The emitted JSON can be loaded into Chrome's chrome://tracing view.
//
// The trace file can be written to a file whose path is specified in an env file,
// and be written to web sockets established on ChrometraceHttpHandler
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"sync"
"sync/atomic"
"golang.org/x/net/websocket"
"github.com/zrepl/zrepl/util/envconst"
)
var chrometracePID string
func init() {
var err error
chrometracePID, err = os.Hostname()
if err != nil {
panic(err)
}
chrometracePID = fmt.Sprintf("%q", chrometracePID)
}
type chrometraceEvent struct {
Cat string `json:"cat,omitempty"`
Name string `json:"name"`
Stack []string `json:"stack,omitempty"`
Phase string `json:"ph"`
TimestampUnixMicroseconds int64 `json:"ts"`
DurationMicroseconds int64 `json:"dur,omitempty"`
Pid string `json:"pid"`
Tid string `json:"tid"`
Id string `json:"id,omitempty"`
}
func chrometraceBeginSpan(s *traceNode) {
taskName := s.TaskName()
chrometraceWrite(chrometraceEvent{
Name: s.annotation,
Phase: "B",
TimestampUnixMicroseconds: s.startedAt.UnixNano() / 1000,
Pid: chrometracePID,
Tid: taskName,
})
}
func chrometraceEndSpan(s *traceNode) {
taskName := s.TaskName()
chrometraceWrite(chrometraceEvent{
Name: s.annotation,
Phase: "E",
TimestampUnixMicroseconds: s.endedAt.UnixNano() / 1000,
Pid: chrometracePID,
Tid: taskName,
})
}
var chrometraceFlowId uint64
func chrometraceBeginTask(s *traceNode) {
chrometraceBeginSpan(s)
if s.parentTask == nil {
return
}
// beginning of a task that has a parent
// => use flow events to link parent and child
flowId := atomic.AddUint64(&chrometraceFlowId, 1)
flowIdStr := fmt.Sprintf("%x", flowId)
parentTask := s.parentTask.TaskName()
chrometraceWrite(chrometraceEvent{
Cat: "task", // seems to be necessary, otherwise the GUI shows some `indexOf` JS error
Name: "child-task",
Phase: "s",
TimestampUnixMicroseconds: s.startedAt.UnixNano() / 1000, // yes, the child's timestamp (=> from-point of the flow line is at right x-position of parent's bar)
Pid: chrometracePID,
Tid: parentTask,
Id: flowIdStr,
})
childTask := s.TaskName()
if parentTask == childTask {
panic(parentTask)
}
chrometraceWrite(chrometraceEvent{
Cat: "task", // seems to be necessary, otherwise the GUI shows some `indexOf` JS error
Name: "child-task",
Phase: "f",
TimestampUnixMicroseconds: s.startedAt.UnixNano() / 1000,
Pid: chrometracePID,
Tid: childTask,
Id: flowIdStr,
})
}
func chrometraceEndTask(s *traceNode) {
chrometraceEndSpan(s)
}
type chrometraceConsumerRegistration struct {
w io.Writer
// errored must have capacity 1, the writer thread will send to it non-blocking, then close it
errored chan error
}
var chrometraceConsumers struct {
register chan chrometraceConsumerRegistration
consumers map[chrometraceConsumerRegistration]bool
write chan []byte
}
func init() {
chrometraceConsumers.register = make(chan chrometraceConsumerRegistration)
chrometraceConsumers.consumers = make(map[chrometraceConsumerRegistration]bool)
chrometraceConsumers.write = make(chan []byte)
go func() {
kickConsumer := func(c chrometraceConsumerRegistration, err error) {
debug("chrometrace kicking consumer %#v after error %v", c, err)
select {
case c.errored <- err:
default:
}
close(c.errored)
delete(chrometraceConsumers.consumers, c)
}
for {
select {
case reg := <-chrometraceConsumers.register:
debug("registered chrometrace consumer %#v", reg)
chrometraceConsumers.consumers[reg] = true
n, err := reg.w.Write([]byte("[\n"))
if err != nil {
kickConsumer(reg, err)
} else if n != 2 {
kickConsumer(reg, fmt.Errorf("short write: %v", n))
}
// successfully registered
case buf := <-chrometraceConsumers.write:
debug("chrometrace write request: %s", string(buf))
var r bytes.Reader
for c := range chrometraceConsumers.consumers {
r.Reset(buf)
n, err := io.Copy(c.w, &r)
debug("chrometrace wrote n=%v bytes to consumer %#v", n, c)
if err != nil {
kickConsumer(c, err)
}
}
}
}
}()
}
func chrometraceWrite(i interface{}) {
var buf bytes.Buffer
err := json.NewEncoder(&buf).Encode(i)
if err != nil {
panic(err)
}
buf.WriteString(",")
chrometraceConsumers.write <- buf.Bytes()
}
func ChrometraceClientWebsocketHandler(conn *websocket.Conn) {
defer conn.Close()
var wg sync.WaitGroup
defer wg.Wait()
wg.Add(1)
go func() {
defer wg.Done()
r := bufio.NewReader(conn)
_, _, _ = r.ReadLine() // ignore errors
conn.Close()
}()
errored := make(chan error, 1)
chrometraceConsumers.register <- chrometraceConsumerRegistration{
w: conn,
errored: errored,
}
wg.Add(1)
go func() {
defer wg.Done()
<-errored
conn.Close()
}()
}
var chrometraceFileConsumerPath = envconst.String("ZREPL_ACTIVITY_TRACE", "")
func init() {
if chrometraceFileConsumerPath != "" {
var err error
f, err := os.Create(chrometraceFileConsumerPath)
if err != nil {
panic(err)
}
errored := make(chan error, 1)
chrometraceConsumers.register <- chrometraceConsumerRegistration{
w: f,
errored: errored,
}
go func() {
<-errored
f.Close()
}()
}
}
-27
View File
@@ -1,27 +0,0 @@
package trace
import "context"
type contextKey int
const (
contextKeyTraceNode contextKey = 1 + iota
)
var contextKeys = []contextKey{
contextKeyTraceNode,
}
// WithInherit inherits the task hierarchy from inheritFrom into ctx.
// The returned context is a child of ctx, but its task and span are those of inheritFrom.
//
// Note that in most use cases, callers most likely want to call WithTask since it will most likely
// be in some sort of connection handler context.
func WithInherit(ctx, inheritFrom context.Context) context.Context {
for _, k := range contextKeys {
if v := inheritFrom.Value(k); v != nil {
ctx = context.WithValue(ctx, k, v) // no shadow
}
}
return ctx
}
-74
View File
@@ -1,74 +0,0 @@
package trace
import (
"context"
"fmt"
"runtime"
"strings"
"sync"
)
// use like this:
//
// defer WithSpanFromStackUpdateCtx(&existingCtx)()
//
//
func WithSpanFromStackUpdateCtx(ctx *context.Context) DoneFunc {
childSpanCtx, end := WithSpan(*ctx, getMyCallerOrPanic())
*ctx = childSpanCtx
return end
}
// derive task name from call stack (caller's name)
func WithTaskFromStack(ctx context.Context) (context.Context, DoneFunc) {
return WithTask(ctx, getMyCallerOrPanic())
}
// derive task name from call stack (caller's name) and update *ctx
// to point to be the child task ctx
func WithTaskFromStackUpdateCtx(ctx *context.Context) DoneFunc {
child, end := WithTask(*ctx, getMyCallerOrPanic())
*ctx = child
return end
}
// create a task and a span within it in one call
func WithTaskAndSpan(ctx context.Context, task string, span string) (context.Context, DoneFunc) {
ctx, endTask := WithTask(ctx, task)
ctx, endSpan := WithSpan(ctx, fmt.Sprintf("%s %s", task, span))
return ctx, func() {
endSpan()
endTask()
}
}
// create a span during which several child tasks are spawned using the `add` function
func WithTaskGroup(ctx context.Context, taskGroup string) (_ context.Context, add func(f func(context.Context)), waitEnd DoneFunc) {
var wg sync.WaitGroup
ctx, endSpan := WithSpan(ctx, taskGroup)
add = func(f func(context.Context)) {
wg.Add(1)
defer wg.Done()
ctx, endTask := WithTask(ctx, taskGroup)
defer endTask()
f(ctx)
}
waitEnd = func() {
wg.Wait()
endSpan()
}
return ctx, add, waitEnd
}
func getMyCallerOrPanic() string {
pc, _, _, ok := runtime.Caller(2)
if !ok {
panic("cannot get caller")
}
details := runtime.FuncForPC(pc)
if ok && details != nil {
const prefix = "github.com/zrepl/zrepl"
return strings.TrimPrefix(strings.TrimPrefix(details.Name(), prefix), "/")
}
return ""
}
@@ -1,16 +0,0 @@
package trace
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetCallerOrPanic(t *testing.T) {
withStackFromCtxMock := func() string {
return getMyCallerOrPanic()
}
ret := withStackFromCtxMock()
// zrepl prefix is stripped
assert.Equal(t, "daemon/logging/trace.TestGetCallerOrPanic", ret)
}
-15
View File
@@ -1,15 +0,0 @@
package trace
import (
"fmt"
"os"
)
const debugEnabled = false
func debug(format string, args ...interface{}) {
if !debugEnabled {
return
}
fmt.Fprintf(os.Stderr, format+"\n", args...)
}
-47
View File
@@ -1,47 +0,0 @@
package trace
import (
"encoding/base64"
"math/rand"
"os"
"strings"
"time"
"github.com/zrepl/zrepl/util/envconst"
)
var genIdPRNG = rand.New(rand.NewSource(1))
func init() {
genIdPRNG.Seed(time.Now().UnixNano())
genIdPRNG.Seed(int64(os.Getpid()))
}
var genIdNumBytes = envconst.Int("ZREPL_TRACE_ID_NUM_BYTES", 3)
func init() {
if genIdNumBytes < 1 {
panic("trace node id byte length must be at least 1")
}
}
func genID() string {
var out strings.Builder
enc := base64.NewEncoder(base64.RawStdEncoding, &out)
buf := make([]byte, genIdNumBytes)
for i := 0; i < len(buf); {
n, err := genIdPRNG.Read(buf[i:])
if err != nil {
panic(err)
}
i += n
}
n, err := enc.Write(buf[:])
if err != nil || n != len(buf) {
panic(err)
}
if err := enc.Close(); err != nil {
panic(err)
}
return out.String()
}
-205
View File
@@ -1,205 +0,0 @@
package trace
import (
"context"
"fmt"
"testing"
"github.com/gitchander/permutation"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRegularSpanUsage(t *testing.T) {
root, endRoot := WithTask(context.Background(), "root")
defer endRoot()
s1, endS1 := WithSpan(root, "parent")
s2, endS2 := WithSpan(s1, "child")
_, endS3 := WithSpan(s2, "grand-child")
require.NotPanics(t, func() { endS3() })
require.NotPanics(t, func() { endS2() })
// reuse
_, endS4 := WithSpan(s1, "child-2")
require.NotPanics(t, func() { endS4() })
// close parent
require.NotPanics(t, func() { endS1() })
}
func TestMultipleActiveChildSpansNotAllowed(t *testing.T) {
root, endRoot := WithTask(context.Background(), "root")
defer endRoot()
s1, _ := WithSpan(root, "s1")
_, endS2 := WithSpan(s1, "s1-child1")
require.PanicsWithValue(t, ErrAlreadyActiveChildSpan, func() {
_, _ = WithSpan(s1, "s1-child2")
})
endS2()
require.NotPanics(t, func() {
_, _ = WithSpan(s1, "s1-child2")
})
}
func TestForkingChildSpansNotAllowed(t *testing.T) {
root, endRoot := WithTask(context.Background(), "root")
defer endRoot()
s1, _ := WithSpan(root, "s1")
sc, endSC := WithSpan(s1, "s1-child")
_, _ = WithSpan(sc, "s1-child-child")
require.PanicsWithValue(t, ErrSpanStillHasActiveChildSpan, func() {
endSC()
})
}
func TestRegularTaskUsage(t *testing.T) {
// assert concurrent activities on different tasks can end in any order
closeOrder := []int{0, 1, 2}
closeOrders := permutation.New(permutation.IntSlice(closeOrder))
for closeOrders.Next() {
t.Run(fmt.Sprintf("%v", closeOrder), func(t *testing.T) {
root, endRoot := WithTask(context.Background(), "root")
defer endRoot()
c1, endC1 := WithTask(root, "c1")
defer endC1()
c2, endC2 := WithTask(root, "c2")
defer endC2()
// begin 3 concurrent activities
_, endAR := WithSpan(root, "aR")
_, endAC1 := WithSpan(c1, "aC1")
_, endAC2 := WithSpan(c2, "aC2")
endFuncs := []DoneFunc{endAR, endAC1, endAC2}
for _, i := range closeOrder {
require.NotPanics(t, func() {
endFuncs[i]()
}, "%v", i)
}
})
}
}
func TestTaskEndWithActiveChildTaskNotAllowed(t *testing.T) {
root, _ := WithTask(context.Background(), "root")
c, endC := WithTask(root, "child")
_, _ = WithTask(c, "grand-child")
func() {
defer func() {
r := recover()
require.NotNil(t, r)
err, ok := r.(error)
require.True(t, ok)
require.Equal(t, ErrTaskStillHasActiveChildTasks, errors.Cause(err))
}()
endC()
}()
}
func TestIdempotentEndTask(t *testing.T) {
_, end := WithTask(context.Background(), "root")
end()
require.NotPanics(t, func() { end() })
}
func TestSpansPanicIfNoParentTask(t *testing.T) {
require.Panics(t, func() { WithSpan(context.Background(), "taskless-span") })
}
func TestIdempotentEndSpan(t *testing.T) {
root, _ := WithTask(context.Background(), "root")
_, end := WithSpan(root, "span")
end()
require.NotPanics(t, func() { end() })
}
func logAndGetTraceNode(t *testing.T, descr string, ctx context.Context) *traceNode {
n, ok := ctx.Value(contextKeyTraceNode).(*traceNode)
require.True(t, ok)
t.Logf("% 20s %p %#v", descr, n, n)
return n
}
func TestWhiteboxHierachy(t *testing.T) {
root, e1 := WithTask(context.Background(), "root")
rootN := logAndGetTraceNode(t, "root", root)
assert.Nil(t, rootN.parentTask)
assert.Nil(t, rootN.parentSpan)
child, e2 := WithSpan(root, "child")
childN := logAndGetTraceNode(t, "child", child)
assert.Equal(t, rootN, childN.parentTask)
assert.Equal(t, rootN, childN.parentSpan)
grandchild, e3 := WithSpan(child, "grandchild")
grandchildN := logAndGetTraceNode(t, "grandchild", grandchild)
assert.Equal(t, rootN, grandchildN.parentTask)
assert.Equal(t, childN, grandchildN.parentSpan)
gcTask, e4 := WithTask(grandchild, "grandchild-task")
gcTaskN := logAndGetTraceNode(t, "grandchild-task", gcTask)
assert.Equal(t, rootN, gcTaskN.parentTask)
assert.Nil(t, gcTaskN.parentSpan)
// it is allowed that a child task outlives the _span_ in which it was created
// (albeit not its parent task)
e3()
e2()
gcTaskSpan, e5 := WithSpan(gcTask, "granschild-task-span")
gcTaskSpanN := logAndGetTraceNode(t, "granschild-task-span", gcTaskSpan)
assert.Equal(t, gcTaskN, gcTaskSpanN.parentTask)
assert.Equal(t, gcTaskN, gcTaskSpanN.parentSpan)
e5()
e4()
e1()
}
func TestOrphanTasksBecomeNewRootWhenAllAncestorsAreDead(t *testing.T) {
parent, e1 := WithTask(context.Background(), "parent")
_ = logAndGetTraceNode(t, "parent-task", parent)
e1()
var child context.Context
var e2 DoneFunc
require.NotPanics(t, func() {
child, e2 = WithTask(parent, "child")
})
childN := logAndGetTraceNode(t, "child-task", child)
assert.Nil(t, childN.parentTask)
grandchild, _ := WithTask(child, "grandchild")
grandchildN := logAndGetTraceNode(t, "grandchild-task", grandchild)
assert.Equal(t, childN, grandchildN.parentTask)
require.Panics(t, func() { e2() }, "if the parent was alive at creation of child task, wrong termination order remains a panicable offense though")
}
func TestOrphanTaskBecomesChildToNearestLiveAncestor(t *testing.T) {
parent, e1 := WithTask(context.Background(), "parent")
parentN := logAndGetTraceNode(t, "parent-task", parent)
child, e2 := WithTask(parent, "child")
childN := logAndGetTraceNode(t, "child-task", child)
assert.Equal(t, parentN, childN.parentTask)
e2()
grandchild, e3 := WithTask(child, "grandchild")
grandchildN := logAndGetTraceNode(t, "grandchild-task", grandchild)
assert.Equal(t, parentN, grandchildN.parentTask) // child is already dead
require.NotPanics(t, func() {
e3()
e1()
})
}
@@ -1,53 +0,0 @@
package trace
import (
"fmt"
"strings"
"sync"
"github.com/willf/bitset"
)
type uniqueConcurrentTaskNamer struct {
mtx sync.Mutex
active map[string]*bitset.BitSet
}
// bitvecLengthGauge may be nil
func newUniqueTaskNamer() *uniqueConcurrentTaskNamer {
return &uniqueConcurrentTaskNamer{
active: make(map[string]*bitset.BitSet),
}
}
// appends `#%d` to `name` such that until `done` is called,
// it is guaranteed that `#%d` is not returned a second time for the same `name`
func (namer *uniqueConcurrentTaskNamer) UniqueConcurrentTaskName(name string) (uniqueName string, done func()) {
if strings.Contains(name, "#") {
panic(name)
}
namer.mtx.Lock()
act, ok := namer.active[name]
if !ok {
act = bitset.New(64) // FIXME magic const
namer.active[name] = act
}
id, ok := act.NextClear(0)
if !ok {
// if !ok, all bits are 1 and act.Len() returns the next bit
id = act.Len()
// FIXME unbounded growth without reclamation
}
act.Set(id)
namer.mtx.Unlock()
return fmt.Sprintf("%s#%d", name, id), func() {
namer.mtx.Lock()
defer namer.mtx.Unlock()
act, ok := namer.active[name]
if !ok {
panic("must be initialized upon entry")
}
act.Clear(id)
}
}
@@ -1,58 +0,0 @@
package trace
import (
"fmt"
"sync"
"sync/atomic"
"testing"
"github.com/stretchr/testify/require"
"github.com/willf/bitset"
)
func TestBitsetFeaturesForUniqueConcurrentTaskNamer(t *testing.T) {
var b bitset.BitSet
require.Equal(t, uint(0), b.Len())
require.Equal(t, uint(0), b.Count())
b.Set(0)
require.Equal(t, uint(1), b.Len())
require.Equal(t, uint(1), b.Count())
b.Set(8)
require.Equal(t, uint(9), b.Len())
require.Equal(t, uint(2), b.Count())
b.Set(1)
require.Equal(t, uint(9), b.Len())
require.Equal(t, uint(3), b.Count())
}
func TestUniqueConcurrentTaskNamer(t *testing.T) {
namer := newUniqueTaskNamer()
var wg sync.WaitGroup
const N = 8128
const Q = 23
var fails uint32
var m sync.Map
wg.Add(N)
for i := 0; i < N; i++ {
go func(i int) {
defer wg.Done()
name := fmt.Sprintf("%d", i/Q)
uniqueName, done := namer.UniqueConcurrentTaskName(name)
act, _ := m.LoadOrStore(uniqueName, i)
if act.(int) != i {
atomic.AddUint32(&fails, 1)
}
m.Delete(uniqueName)
done()
}(i)
}
wg.Wait()
require.Equal(t, uint32(0), fails)
}
+2 -4
View File
@@ -1,8 +1,6 @@
package daemon
import (
"context"
"github.com/zrepl/zrepl/cli"
"github.com/zrepl/zrepl/logger"
)
@@ -12,7 +10,7 @@ type Logger = logger.Logger
var DaemonCmd = &cli.Subcommand{
Use: "daemon",
Short: "run the zrepl daemon",
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
return Run(ctx, subcommand.Config())
Run: func(subcommand *cli.Subcommand, args []string) error {
return Run(subcommand.Config())
},
}
-6
View File
@@ -8,11 +8,7 @@ import (
"net"
"net/http/pprof"
"github.com/prometheus/client_golang/prometheus/promhttp"
"golang.org/x/net/websocket"
"github.com/zrepl/zrepl/daemon/job"
"github.com/zrepl/zrepl/daemon/logging/trace"
)
type pprofServer struct {
@@ -68,8 +64,6 @@ outer:
mux.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile))
mux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol))
mux.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace))
mux.Handle("/metrics", promhttp.Handler())
mux.Handle("/debug/zrepl/activity-trace", websocket.Handler(trace.ChrometraceClientWebsocketHandler))
go func() {
err := http.Serve(s.listener, mux)
if ctx.Err() != nil {
+4 -8
View File
@@ -10,7 +10,6 @@ import (
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/job"
"github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/rpc/dataconn/frameconn"
@@ -87,19 +86,16 @@ func (j *prometheusJob) Run(ctx context.Context) {
}
type prometheusJobOutlet struct {
jobName string
}
var _ logger.Outlet = prometheusJobOutlet{}
func newPrometheusLogOutlet() prometheusJobOutlet {
return prometheusJobOutlet{}
func newPrometheusLogOutlet(jobName string) prometheusJobOutlet {
return prometheusJobOutlet{jobName}
}
func (o prometheusJobOutlet) WriteEntry(entry logger.Entry) error {
jobFieldVal, ok := entry.Fields[logging.JobField].(string)
if !ok {
jobFieldVal = "_nojobid"
}
prom.taskLogEntries.WithLabelValues(jobFieldVal, entry.Level.String()).Inc()
prom.taskLogEntries.WithLabelValues(o.jobName, entry.Level.String()).Inc()
return nil
}
+14 -11
View File
@@ -12,20 +12,19 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/pruning"
"github.com/zrepl/zrepl/replication/logic/pdu"
"github.com/zrepl/zrepl/util/envconst"
)
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
// Try to keep it compatible with gitub.com/zrepl/zrepl/endpoint.Endpoint
type History interface {
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
}
// Try to keep it compatible with github.com/zrepl/zrepl/endpoint.Endpoint
// Try to keep it compatible with gitub.com/zrepl/zrepl/endpoint.Endpoint
type Target interface {
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
ListFilesystemVersions(ctx context.Context, req *pdu.ListFilesystemVersionsReq) (*pdu.ListFilesystemVersionsRes, error)
@@ -36,13 +35,17 @@ type Logger = logger.Logger
type contextKey int
const (
contextKeyPruneSide contextKey = 1 + iota
)
const contextKeyLogger contextKey = 0
func WithLogger(ctx context.Context, log Logger) context.Context {
return context.WithValue(ctx, contextKeyLogger, log)
}
func GetLogger(ctx context.Context) Logger {
pruneSide := ctx.Value(contextKeyPruneSide).(string)
return logging.GetLogger(ctx, logging.SubsysPruning).WithField("prune_side", pruneSide)
if l, ok := ctx.Value(contextKeyLogger).(Logger); ok {
return l
}
return logger.NewNullLogger()
}
type args struct {
@@ -135,7 +138,7 @@ func NewPrunerFactory(in config.PruningSenderReceiver, promPruneSecs *prometheus
func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, receiver History) *Pruner {
p := &Pruner{
args: args{
context.WithValue(ctx, contextKeyPruneSide, "sender"),
WithLogger(ctx, GetLogger(ctx).WithField("prune_side", "sender")),
target,
receiver,
f.senderRules,
@@ -151,7 +154,7 @@ func (f *PrunerFactory) BuildSenderPruner(ctx context.Context, target Target, re
func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target, receiver History) *Pruner {
p := &Pruner{
args: args{
context.WithValue(ctx, contextKeyPruneSide, "receiver"),
WithLogger(ctx, GetLogger(ctx).WithField("prune_side", "receiver")),
target,
receiver,
f.receiverRules,
@@ -167,7 +170,7 @@ func (f *PrunerFactory) BuildReceiverPruner(ctx context.Context, target Target,
func (f *LocalPrunerFactory) BuildLocalPruner(ctx context.Context, target Target, receiver History) *Pruner {
p := &Pruner{
args: args{
context.WithValue(ctx, contextKeyPruneSide, "local"),
ctx,
target,
receiver,
f.keepRules,
+44 -33
View File
@@ -8,12 +8,10 @@ import (
"time"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/filters"
"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"
@@ -47,6 +45,7 @@ type snapProgress struct {
type args struct {
ctx context.Context
log Logger
prefix string
interval time.Duration
fsf *filters.DatasetMapFilter
@@ -103,10 +102,23 @@ func (s State) sf() state {
type updater func(u func(*Snapper)) State
type state func(a args, u updater) state
type contextKey int
const (
contextKeyLog contextKey = 0
)
type Logger = logger.Logger
func WithLogger(ctx context.Context, log Logger) context.Context {
return context.WithValue(ctx, contextKeyLog, log)
}
func getLogger(ctx context.Context) Logger {
return logging.GetLogger(ctx, logging.SubsysSnapshot)
if log, ok := ctx.Value(contextKeyLog).(Logger); ok {
return log
}
return logger.NewNullLogger()
}
func PeriodicFromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in *config.SnapshottingPeriodic) (*Snapper, error) {
@@ -134,12 +146,13 @@ func PeriodicFromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in *con
}
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.log = getLogger(ctx)
s.args.dryRun = false // for future expansion
u := func(u func(*Snapper)) State {
@@ -177,7 +190,7 @@ func onErr(err error, u updater) state {
case Snapshotting:
s.state = ErrorWait
}
getLogger(s.args.ctx).WithError(err).WithField("pre_state", preState).WithField("post_state", s.state).Error("snapshotting error")
s.args.log.WithError(err).WithField("pre_state", preState).WithField("post_state", s.state).Error("snapshotting error")
}).sf()
}
@@ -196,7 +209,7 @@ func syncUp(a args, u updater) state {
if err != nil {
return onErr(err, u)
}
syncPoint, err := findSyncPoint(a.ctx, fss, a.prefix, a.interval)
syncPoint, err := findSyncPoint(a.log, fss, a.prefix, a.interval)
if err != nil {
return onErr(err, u)
}
@@ -253,18 +266,18 @@ func snapshot(a args, u updater) state {
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)
l := a.log.
WithField("fs", fs.ToString()).
WithField("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)
jobCallback := hooks.NewCallbackHookForFilesystem("snapshot", fs, func(_ context.Context) (err error) {
l.Debug("create snapshot")
err = zfs.ZFSSnapshot(ctx, fs, snapname, false) // TODO propagate context to ZFSSnapshot
err = zfs.ZFSSnapshot(fs, snapname, false) // TODO propagagte context to ZFSSnapshot
if err != nil {
l.WithError(err).Error("cannot create snapshot")
}
@@ -277,7 +290,7 @@ func snapshot(a args, u updater) state {
{
filteredHooks, err := a.hooks.CopyFilteredForFilesystem(fs)
if err != nil {
getLogger(ctx).WithError(err).Error("unexpected filter error")
l.WithError(err).Error("unexpected filter error")
fsHadErr = true
goto updateFSState
}
@@ -290,7 +303,7 @@ func snapshot(a args, u updater) state {
plan, planErr = hooks.NewPlan(&filteredHooks, hooks.PhaseSnapshot, jobCallback, hookEnvExtra)
if planErr != nil {
fsHadErr = true
getLogger(ctx).WithError(planErr).Error("cannot create job hook plan")
l.WithError(planErr).Error("cannot create job hook plan")
goto updateFSState
}
}
@@ -301,14 +314,15 @@ func snapshot(a args, u updater) state {
progress.state = SnapStarted
})
{
getLogger(ctx).WithField("report", plan.Report().String()).Debug("begin run job plan")
plan.Run(ctx, a.dryRun)
l := hooks.GetLogger(a.ctx).WithField("fs", fs.ToString()).WithField("snap", snapname)
l.WithField("report", plan.Report().String()).Debug("begin run job plan")
plan.Run(hooks.WithLogger(a.ctx, l), 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")
l.WithField("report", planReport.String()).Error("end run job plan with error")
} else {
getLogger(ctx).WithField("report", planReport.String()).Info("end run job plan successful")
l.WithField("report", planReport.String()).Info("end run job plan successful")
}
}
@@ -328,7 +342,7 @@ func snapshot(a args, u updater) state {
case a.snapshotsTaken <- struct{}{}:
default:
if a.snapshotsTaken != nil {
getLogger(a.ctx).Warn("callback channel is full, discarding snapshot update event")
a.log.Warn("callback channel is full, discarding snapshot update event")
}
}
@@ -341,7 +355,7 @@ func snapshot(a args, u updater) state {
break
}
}
getLogger(a.ctx).WithField("hook", h.String()).WithField("hook_number", hookIdx+1).Warn("hook did not match any snapshotted filesystems")
a.log.WithField("hook", h.String()).WithField("hook_number", hookIdx+1).Warn("hook did not match any snapshotted filesystems")
}
}
@@ -362,7 +376,7 @@ func wait(a args, u updater) state {
lastTick := snapper.lastInvocation
snapper.sleepUntil = lastTick.Add(a.interval)
sleepUntil = snapper.sleepUntil
log := getLogger(a.ctx).WithField("sleep_until", sleepUntil).WithField("duration", a.interval)
log := a.log.WithField("sleep_until", sleepUntil).WithField("duration", a.interval)
logFunc := log.Debug
if snapper.state == ErrorWait || snapper.state == SyncUpErrWait {
logFunc = log.Error
@@ -390,7 +404,7 @@ func listFSes(ctx context.Context, mf *filters.DatasetMapFilter) (fss []*zfs.Dat
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) {
func findSyncPoint(log Logger, fss []*zfs.DatasetPath, prefix string, interval time.Duration) (syncPoint time.Time, err error) {
const (
prioHasVersions int = iota
@@ -412,10 +426,10 @@ func findSyncPoint(ctx context.Context, fss []*zfs.DatasetPath, prefix string, i
now := time.Now()
getLogger(ctx).Debug("examine filesystem state to find sync point")
log.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)
l := log.WithField("fs", d.ToString())
syncPoint, err := findSyncPointFSNextOptimalSnapshotTime(l, now, interval, prefix, d)
if err == findSyncPointFSNoFilesystemVersionsErr {
snaptimes = append(snaptimes, snapTime{
ds: d,
@@ -424,9 +438,9 @@ func findSyncPoint(ctx context.Context, fss []*zfs.DatasetPath, prefix string, i
})
} else if err != nil {
hardErrs++
getLogger(ctx).WithError(err).Error("cannot determine optimal sync point for this filesystem")
l.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")
l.WithField("syncPoint", syncPoint).Debug("found optimal sync point for this filesystem")
snaptimes = append(snaptimes, snapTime{
ds: d,
prio: prioHasVersions,
@@ -453,7 +467,7 @@ func findSyncPoint(ctx context.Context, fss []*zfs.DatasetPath, prefix string, i
})
winnerSyncPoint := snaptimes[0].time
l := getLogger(ctx).WithField("syncPoint", winnerSyncPoint.String())
l := log.WithField("syncPoint", winnerSyncPoint.String())
l.Info("determined sync point")
if winnerSyncPoint.Sub(now) > syncUpWarnNoSnapshotUntilSyncupMinDuration {
for _, st := range snaptimes {
@@ -469,12 +483,9 @@ func findSyncPoint(ctx context.Context, fss []*zfs.DatasetPath, prefix string, i
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) {
func findSyncPointFSNextOptimalSnapshotTime(l Logger, 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,
})
fsvs, err := zfs.ZFSListFilesystemVersions(d, filters.NewTypedPrefixFilter(prefix, zfs.Snapshot))
if err != nil {
return time.Time{}, errors.Wrap(err, "list filesystem versions")
}
@@ -488,7 +499,7 @@ func findSyncPointFSNextOptimalSnapshotTime(ctx context.Context, now time.Time,
})
latest := fsvs[len(fsvs)-1]
getLogger(ctx).WithField("creation", latest.Creation).Debug("found latest snapshot")
l.WithField("creation", latest.Creation).Debug("found latest snapshot")
since := now.Sub(latest.Creation)
if since < 0 {
+15 -27
View File
@@ -35,23 +35,18 @@ This is a big one! Headlining features:
* **Resumable Send & Recv Support**
No knobs required, automatically used where supported.
* **Hold-Protected Send & Recv**
Automatic ZFS holds to ensure that we can always use resumable send&recv for a replication step.
* **Encrypted Send & Recv Support** for OpenZFS native encryption,
:ref:`configurable <job-send-options>` at the job level, i.e., for all filesystems a job is responsible for.
Automatic ZFS holds to ensure that we can always resume a replication step.
* **Encrypted Send & Recv Support** for OpenZFS native encryption.
:ref:`Configurable <job-send-options>` at the job level, i.e., for all filesystems a job is responsible for.
* **Receive-side hold on last received dataset**
The counterpart to the replication cursor bookmark on the send-side.
Ensures that incremental replication will always be possible between a sender and receiver.
.. TIP::
Actual changelog:
We highly recommend studying the :ref:`overview section of the configuration chapter <overview-how-replication-works>` to understand how replication works.
* |break_config| **more restrictive job names than in prior zrepl versions**
Starting with this version, job names are going to be embedded into ZFS holds and bookmark names.
Additional changelog:
* |break| |break_config| **more restrictive job names than in prior zrepl versions**
Starting with this version, job names are going to be embedded into ZFS holds and bookmark names (see :ref:`here<replication-cursor-and-last-received-hold>` and :ref:`here<step-holds-and-bookmarks>`).
Therefore you might need to adjust your job names.
**Note that jobs** :issue:`cannot be renamed easily<327>` **once you start using zrepl 0.3.**
* |break| |mig| replication cursor representation changed
* zrepl now manages the :ref:`replication cursor bookmark <replication-cursor-and-last-received-hold>` per job-filesystem tuple instead of a single replication cursor per filesystem.
@@ -61,28 +56,13 @@ Additional changelog:
* Run ``zrepl migrate replication-cursor:v1-v2`` to safely destroy old-format cursors.
The migration will ensure that only those old-format cursors are destroyed that have been superseeded by new-format cursors.
* |feature| New option ``listen_freebind`` (tcp, tls, prometheus listener)
* |feature| :issue:`<265>` transport/tcp: support for CIDR masks in client IP whitelist
* |feature| documented subcommand to generate ``bash`` and ``zsh`` completions
* |feature| :issue:`<307>` ``chrome://trace`` -compatible activity tracing of zrepl daemon activity
* |feature| logging: trace IDs for better log entry correlation with concurrent replication jobs
* |feature| experimental environment variable for parallel replication (see :issue:`<306>` )
* |bugfix| missing logger context vars in control connection handlers
* |bugfix| improved error messages on ``zfs send`` errors
* |feature| New option ``listen_freebind`` (tcp, tls, prometheus listener)
* |bugfix| |docs| snapshotting: clarify sync-up behavior and warn about filesystems
* |bugfix| transport/ssh: do not leak zombie ssh process on connection failures
that will not be snapshotted until the sync-up phase is over
* |docs| Installation: :ref:`FreeBSD jail with iocage <installation-freebsd-jail-with-iocage>`
* |docs| Document new replication features in the :ref:`config overview <overview-how-replication-works>` and :repomasterlink:`replication/design.md`.
* **[MAINTAINER NOTICE]** New platform tests in this version, please make sure you run them for your distro!
* **[MAINTAINER NOTICE]** Please add the shell completions to the zrepl packages.
.. NOTE::
| zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
| You can support maintenance and feature development through one of the following services:
| |Donate via Patreon| |Donate via Liberapay| |Donate via PayPal|
| Note that PayPal processing fees are relatively high for small donations.
| For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
0.2.1
-----
@@ -120,6 +100,14 @@ Additional changelog:
The pool name ``zreplplatformtest`` is reserved for this use case.
Only run ``make platformtest`` on test systems, e.g. a FreeBSD VM image.
.. NOTE::
| zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
| You can support maintenance and feature development through one of the following services:
| |Donate via Patreon| |Donate via Liberapay| |Donate via PayPal|
| Note that PayPal processing fees are relatively high for small donations.
| For SEPA wire transfer and **commerical support**, please `contact Christian directly <https://cschwarz.com>`_.
0.1.1
-----
+5 -5
View File
@@ -19,7 +19,7 @@ Job Type ``push``
* - ``type``
- = ``push``
* - ``name``
- unique name of the job :issue:`(must not change)<327>`
- unique name of the job
* - ``connect``
- |connect-transport|
* - ``filesystems``
@@ -47,7 +47,7 @@ Job Type ``sink``
* - ``type``
- = ``sink``
* - ``name``
- unique name of the job :issue:`(must not change)<327>`
- unique name of the job
* - ``serve``
- |serve-transport|
* - ``root_fs``
@@ -70,7 +70,7 @@ Job Type ``pull``
* - ``type``
- = ``pull``
* - ``name``
- unique name of the job :issue:`(must not change)<327>`
- unique name of the job
* - ``connect``
- |connect-transport|
* - ``root_fs``
@@ -98,7 +98,7 @@ Job Type ``source``
* - ``type``
- = ``source``
* - ``name``
- unique name of the job :issue:`(must not change)<327>`
- unique name of the job
* - ``serve``
- |serve-transport|
* - ``filesystems``
@@ -137,7 +137,7 @@ Job type that only takes snapshots and performs pruning on the local machine.
* - ``type``
- = ``snap``
* - ``name``
- unique name of the job :issue:`(must not change)<327>`
- unique name of the job
* - ``filesystems``
- |filter-spec| for filesystems to be snapshotted
* - ``snapshotting``
+1 -1
View File
@@ -202,7 +202,7 @@ The latter is particularly useful in combination with log aggregation services.
.. WARNING::
zrepl drops log messages to the TCP outlet if the underlying connection is not fast enough.
Note that TCP buffering in the kernel must first run full before messages are dropped.
Note that TCP buffering in the kernel must first run full becfore messages are dropped.
Make sure to always configure a ``stdout`` outlet as the special error outlet to be informed about problems
with the TCP outlet (see :ref:`above <logging-error-outlet>` ).
+1 -1
View File
@@ -15,7 +15,7 @@ Prometheus & Grafana
zrepl can expose `Prometheus metrics <https://prometheus.io/docs/instrumenting/exposition_formats/>`_ via HTTP.
The ``listen`` attribute is a `net.Listen <https://golang.org/pkg/net/#Listen>`_ string for tcp, e.g. ``:9091`` or ``127.0.0.1:9091``.
The ``listen_freebind`` attribute is :ref:`explained here <listen-freebind-explanation>`.
The Prometheus monitoring job appears in the ``zrepl control`` job list and may be specified **at most once**.
The Prometheues monitoring job appears in the ``zrepl control`` job list and may be specified **at most once**.
zrepl also ships with an importable `Grafana <https://grafana.com>`_ dashboard that consumes the Prometheus metrics:
see :repomasterlink:`dist/grafana`.
+27 -90
View File
@@ -26,9 +26,9 @@ Config File Structure
type: push
- ...
zrepl is configured using a single YAML configuration file with two main sections: ``global`` and ``jobs``.
zrepl is confgured 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.
The ``jobs`` section is a list of jobs which we are goind to explain now.
.. _job-overview:
@@ -39,13 +39,9 @@ A *job* is the unit of activity tracked by the zrepl daemon.
The ``type`` of a job determines its role in a replication setup and in snapshot management.
Jobs are identified by their ``name``, both in log files and the ``zrepl status`` command.
.. 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**.
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.
The passive side responds to requests from the active side after checking its persmissions.
The following table shows how different job types can be combined to achieve **both push and pull mode setups**.
Note that snapshot-creation denoted by "(snap)" is orthogonal to whether a job is active or passive.
@@ -103,7 +99,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 steps take place during replication and can be monitored using the ``zrepl status`` subcommand:
* Plan the replication:
@@ -111,9 +107,9 @@ The following high-level steps take place during replication and can be monitore
* Build the **replication plan**
* Per filesystem, compute a diff between sender and receiver snapshots
* Build a list of **replication steps**
* Build a list of replication steps
* If possible, use incremental and resumable sends
* If possible, use incremental and resumable sends (``zfs send -i``)
* Otherwise, use full send of most recent snapshot on sender
* Retry on errors that are likely temporary (i.e. network failures).
@@ -124,7 +120,7 @@ The following high-level steps take place during replication and can be monitore
* Perform replication steps in the following order:
Among all filesystems with pending replication steps, pick the filesystem whose next replication step's snapshot is the oldest.
* Create placeholder filesystems on the receiving side to mirror the dataset paths on the sender to ``root_fs/${client_identity}``.
* Acquire send-side *step-holds* on the step's `from` and `to` snapshots.
* Aquire send-side step-holds on the step's `from` and `to` snapshots.
* Perform the replication step.
* Move the **replication cursor** bookmark on the sending side (see below).
* Move the **last-received-hold** on the receiving side (see below).
@@ -132,95 +128,36 @@ The following high-level steps take place during replication and can be monitore
The idea behind the execution order of replication steps is that if the sender snapshots all filesystems simultaneously at fixed intervals, the receiver will have all filesystems snapshotted at time ``T1`` before the first snapshot at ``T2 = T1 + $interval`` is replicated.
Placeholder Filesystems
^^^^^^^^^^^^^^^^^^^^^^^
.. _replication-cursor-and-last-received-hold:
**Replication cursor** bookmark and **last-received-hold** are managed by zrepl to ensure that future replications can always be done incrementally:
the replication cursor is a send-side bookmark of the most recent successfully replicated snapshot,
and the last-received-hold is a hold of that snapshot on the receiving side.
The replication cursor has the format ``#zrepl_CUSOR_G_<GUID>_J_<JOBNAME>``.
The last-received-hold tag has the format ``#zrepl_last_received_J_<JOBNAME>``.
Encoding the job name in the names ensures that multiple sending jobs can replicate the same filesystem to different receivers without interference.
The ``zrepl holds list`` provides a listing of all bookmarks and holds managed by zrepl.
.. _replication-placeholder-property:
**Placeholder filesystems** on the receiving side are regular ZFS filesystems with the placeholder property ``zrepl:placeholder=on``.
Placeholders allow the receiving side to mirror the sender's ZFS dataset hierarchy without replicating every filesystem at every intermediary dataset path component.
Placeholders allow the receiving side to mirror the sender's ZFS dataset hierachy without replicating every filesystem at every intermediary dataset path component.
Consider the following example: ``S/H/J`` shall be replicated to ``R/sink/job/S/H/J``, but neither ``S/H`` nor ``S`` shall be replicated.
ZFS requires the existence of ``R/sink/job/S`` and ``R/sink/job/S/H`` in order to receive into ``R/sink/job/S/H/J``.
Thus, zrepl creates the parent filesystems as placeholders on the receiving side.
If at some point ``S/H`` and ``S`` shall be replicated, the receiving side invalidates the placeholder flag automatically.
The ``zrepl test placeholder`` command can be used to check whether a filesystem is a placeholder.
ZFS Background Knowledge
^^^^^^^^^^^^^^^^^^^^^^^^
This section gives some background knowledge about ZFS features that zrepl uses to guarantee that
**incremental replication is always possible and that started replication steps can always be resumed if they are interrupted.**
**ZFS Send Modes & Bookmarks**
ZFS supports full sends (``zfs send fs@to``) and incremental sends (``zfs send -i @from fs@to``).
Full sends are used to create a new filesystem on the receiver with the send-side state of ``fs@to``.
Incremental sends only transfer the delta between ``@from`` and ``@to``.
Incremental sends require that ``@from`` be present on the receiving side when receiving the incremental stream.
Incremental sends can also use a ZFS bookmark as *from* on the sending side (``zfs send -i #bm_from fs@to``), where ``#bm_from`` was created using ``zfs bookmark fs@from fs#bm_from``.
The receiving side must always have the actual snapshot ``@from``, regardless of whether the sending side uses ``@from`` or a bookmark of it.
**Resumable Send & Recv**
The ``-s`` flag for ``zfs recv`` tells zfs to save the partially received send stream in case it is interrupted.
To resume the replication, the receiving side filesystem's ``receive_resume_token`` must be passed to a new ``zfs send -t <value> | zfs recv`` command.
A full send can only be resumed if ``@to`` still exists.
An incremental send can only be resumed if ``@to`` still exists *and* either ``@from`` still exists *or* a bookmark ``#fbm`` of ``@from`` still exists.
**ZFS Holds**
ZFS holds prevent a snapshot from being deleted through ``zfs destroy``, letting the destroy fail with a ``datset is busy`` error.
Holds are created and referred to by a user-defined *tag*. They can be thought of as a named, persistent lock on the snapshot.
.. _replication-cursor-and-last-received-hold:
Guaranteeing That Incremental Sends Are Always Possible
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
**Replication cursor** bookmark and **last-received-hold** are managed by zrepl to ensure that future replications can always be done incrementally.
The replication cursor is a send-side bookmark of the most recent successfully replicated snapshot,
and the last-received-hold is a hold of that snapshot on the receiving side.
Both are moved aomically after the receiving side has confirmed that a replication step is complete.
The replication cursor has the format ``#zrepl_CUSOR_G_<GUID>_J_<JOBNAME>``.
The last-received-hold tag has the format ``zrepl_last_received_J_<JOBNAME>``.
Encoding the job name in the names ensures that multiple sending jobs can replicate the same filesystem to different receivers without interference.
The ``zrepl zfs-abstraction list`` command provides a listing of all bookmarks and holds managed by zrepl.
.. _step-holds-and-bookmarks:
Guaranteeing That Sends Are Always Resumable
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
**Step holds** are zfs holds managed by zrepl to ensure that a replication step can always be resumed if it is interrupted, e.g., due to network outage.
zrepl creates step holds before it attempts a replication step and releases them after the receiver confirms that the replication step is complete.
For an initial replication ``full @initial_snap``, zrepl puts a zfs hold on ``@initial_snap``.
For an incremental send ``@from -> @to``, zrepl puts a zfs hold on both ``@from`` and ``@to``.
Note that ``@from`` is not strictly necessary for resumability -- a bookmark on the sending side would be sufficient --, but size-estimation in currently used OpenZFS versions only works if ``@from`` is a snapshot.
The hold tag has the format ``zrepl_STEP_J_<JOBNAME>``.
A job only ever has one active send per filesystem.
Thus, there are never more than two step holds for a given pair of ``(job,filesystem)``.
**Step bookmarks** are zrepl's equivalent for holds on bookmarks (ZFS does not support putting holds on bookmarks).
They are intended for a situation where a replication step uses a bookmark ``#bm`` as incremental ``from`` that is not managed by zrepl.
To ensure resumability, zrepl copies ``#bm`` to step bookmark ``#zrepl_STEP_G_<GUID>_J_<JOBNAME>``.
If the replication is interrupted and ``#bm`` is deleted by the user, the step bookmark remains as an incremental source for the resumable send.
Note that zrepl does not yet support creating step bookmarks because the `corresponding ZFS feature for copying bookmarks <https://github.com/openzfs/zfs/pull/9571>`_ is not yet widely available .
Subscribe to zrepl :issue:`326` for details.
The ``zrepl zfs-abstraction list`` command provides a listing of all bookmarks and holds managed by zrepl.
.. NOTE::
More details can be found in the design document :repomasterlink:`replication/design.md`.
Limitations
^^^^^^^^^^^
.. ATTENTION::
Currently, zrepl does not replicate filesystem properties.
When receiving a filesystem, it is never mounted (`-u` flag) and `mountpoint=none` is set.
This is temporary and being worked on :issue:`24`.
.. NOTE::
More details can be found in the design document :repomasterlink:`replication/design.md`.
.. _jobs-multiple-jobs:
@@ -240,26 +177,26 @@ Multiple Jobs & More than 2 Machines
If you would like to see improvements to multi-job setups, please `open an issue on GitHub <https://github.com/zrepl/zrepl/issues/new>`_.
No Overlapping
^^^^^^^^^^^^^^
~~~~~~~~~~~~~~
Jobs run independently of each other.
If two jobs match the same filesystem with their ``filesystems`` filter, they will operate on that filesystem independently and potentially in parallel.
For example, if job A prunes snapshots that job B is planning to replicate, the replication will fail because B assumed the snapshot to still be present.
For example, if job A prunes snapshots that job B is planning to replicate, the replication will fail because B asssumed the snapshot to still be present.
However, the next replication attempt will re-examine the situation from scratch and should work.
N push jobs to 1 sink
^^^^^^^^^^^^^^^^^^^^^
~~~~~~~~~~~~~~~~~~~~~
The :ref:`sink job <job-sink>` namespaces by client identity.
It is thus safe to push to one sink job with different client identities.
If the push jobs have the same client identity, the filesystems matched by the push jobs must be disjoint to avoid races.
N pull jobs from 1 source
^^^^^^^^^^^^^^^^^^^^^^^^^
~~~~~~~~~~~~~~~~~~~~~~~~~
Multiple pull jobs pulling from the same source have potential for race conditions during pruning:
each pull job prunes the source side independently, causing replication-prune and prune-prune races.
There is currently no way for a pull job to filter which snapshots it should attempt to replicate.
Thus, it is not possible to just manually assert that the prune rules of all pull jobs are disjoint to avoid replication-prune and prune-prune races.
Thus, it is not possibe to just manually assert that the prune rules of all pull jobs are disjoint to avoid replication-prune and prune-prune races.
+1 -1
View File
@@ -154,7 +154,7 @@ Policy ``regex``
negate: true
regex: "^zrepl_.*"
``regex`` keeps all snapshots whose names are matched by the regular expression in ``regex``.
``regex`` keeps all snapshots whose names are matched by the regular expressionin ``regex``.
Like all other regular expression fields in prune policies, zrepl uses Go's `regexp.Regexp <https://golang.org/pkg/regexp/#Compile>`_ Perl-compatible regular expressions (`Syntax <https://golang.org/pkg/regexp/syntax>`_).
The optional `negate` boolean field inverts the semantics: Use it if you want to keep all snapshots that *do not* match the given regex.
+1 -14
View File
@@ -16,8 +16,6 @@ Send Options
filesystems: ...
send:
encrypted: true
step_holds:
disable_incremental: false
...
:ref:`Source<job-source>` and :ref:`push<job-push>` jobs have an optional ``send`` configuration section.
@@ -26,7 +24,7 @@ Send Options
---------------------
The ``encryption`` variable controls whether the matched filesystems are sent as `OpenZFS native encryption <http://open-zfs.org/wiki/ZFS-Native_Encryption>`_ raw sends.
More specifically, if ``encryption=true``, zrepl
More specificially, if ``encryption=true``, zrepl
* checks for any of the filesystems matched by ``filesystems`` whether the ZFS ``encryption`` property indicates that the filesystem is actually encrypted with ZFS native encryption and
* invokes the ``zfs send`` subcommand with the ``-w`` option (raw sends) and
@@ -36,17 +34,6 @@ Filesystems matched by ``filesystems`` that are not encrypted are not sent and w
If ``encryption=false``, zrepl expects that filesystems matching ``filesystems`` are not encrypted or have loaded encryption keys.
``step_holds.disable_incremental`` option
-----------------------------------------
The ``step_holds.disable_incremental`` variable controls whether the creation of :ref:`step holds <step-holds-and-bookmarks>` should be disabled for incremental replication.
The default value is ``false``.
Disabling step holds has the disadvantage that steps :ref:`might not be resumable <step-holds-and-bookmarks>` if interrupted.
However, the advantage and :issue:`reason for existence <288>` of this flag is that it allows the pruner to delete snapshots of interrupted replication steps
which is useful if replication happens so rarely (or fails so frequently) that the amount of disk space exclusively referenced by the step's snapshots becomes intolerable.
.. _job-recv-options:
Recv Options
+2 -9
View File
@@ -52,15 +52,8 @@ Serve
listen: ":8888"
listen_freebind: true # optional, default false
clients: {
"192.168.122.123" : "mysql01",
"192.168.122.42" : "mx01",
"2001:0db8:85a3::8a2e:0370:7334": "gateway",
# CIDR masks require a '*' in the client identity string
# that is expanded to the client's IP address
"10.23.42.0/24": "cluster-*"
"fde4:8dba:82e1::/64": "san-*"
"192.168.122.123" : "mysql01"
"192.168.122.123" : "mx01"
}
...
+129 -10
View File
@@ -1,19 +1,138 @@
.. _binary releases: https://github.com/zrepl/zrepl/releases
.. _installation_toc:
.. _installation:
************
Installation
************
============
.. TIP::
Note: check out the :ref:`tutorial` if you want a first impression of zrepl.
.. toctree::
User Privileges
---------------
installation/user-privileges
installation/packages
installation/apt-repos
installation/compile-from-source
installation/freebsd-jail-with-iocage
installation/what-next
It is possible to run zrepl as an unprivileged user in combination with
`ZFS delegation <https://www.freebsd.org/doc/handbook/zfs-zfs-allow.html>`_.
Also, there is the possibility to run it in a jail on FreeBSD by delegating a dataset to the jail.
However, until we get around documenting those setups, you will have to run zrepl as root or experiment yourself :)
Packages
--------
zrepl source releases are signed & tagged by the author in the git repository.
Your OS vendor may provide binary packages of zrepl through the package manager.
Additionally, `binary releases`_ are provided on GitHub.
The following list may be incomplete, feel free to submit a PR with an update:
.. list-table::
:header-rows: 1
* - OS / Distro
- Install Command
- Link
* - FreeBSD
- ``pkg install zrepl``
- `<https://www.freshports.org/sysutils/zrepl/>`_
* - MacOS
- ``brew install zrepl``
- Available on `homebrew <https://brew.sh>`_
* - Arch Linux
- ``yay install zrepl``
- Available on `AUR <https://aur.archlinux.org/packages/zrepl>`_
* - Fedora
- ``dnf install zrepl``
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
* - CentOS/RHEL
- ``yum install zrepl``
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
* - Debian + Ubuntu
- ``apt install zrepl``
- APT repository config :ref:`see below <installation-apt-repos>`
* - OmniOS
- ``pkg install zrepl``
- Available since `r151030 <https://pkg.omniosce.org/r151030/extra/en/search.shtml?token=zrepl&action=Search>`_
* - Void Linux
- ``xbps-install zrepl``
- Available since `a88a2a4 <https://github.com/void-linux/void-packages/commit/a88a2a4d7bf56072dadf61ab56b8424e39155890>`_
* - Others
-
- Use `binary releases`_ or build from source.
.. _installation-apt-repos:
Debian / Ubuntu APT repositories
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
We maintain APT repositories for Debian, Ubuntu and derivatives.
The fingerprint of the signing key is ``E101 418F D3D6 FBCB 9D65 A62D 7086 99FC 5F2E BF16``.
It is available at `<https://zrepl.cschwarz.com/apt/apt-key.asc>`_ .
Please open an issue `in the packaging repository <https://github.com/zrepl/debian-binary-packaging>`_ if you encounter any issues with the repository.
The following snippet configure the repository for your Debian or Ubuntu release:
::
apt update && apt install curl gnupg lsb-release; \
ARCH="$(dpkg --print-architecture)"; \
CODENAME="$(lsb_release -i -s | tr '[:upper:]' '[:lower:]') $(lsb_release -c -s | tr '[:upper:]' '[:lower:]')"; \
echo "Using Distro and Codename: $CODENAME"; \
(curl https://zrepl.cschwarz.com/apt/apt-key.asc | apt-key add -) && \
(echo "deb [arch=$ARCH] https://zrepl.cschwarz.com/apt/$CODENAME main" > /etc/apt/sources.list.d/zrepl.list) && \
apt update
.. NOTE::
Until zrepl reaches 1.0, all APT repositories will be updated to the latest zrepl release immediately.
This includes breaking changes between zrepl versions.
Use ``apt-mark hold zrepl`` to prevent upgrades of zrepl.
Compile From Source
~~~~~~~~~~~~~~~~~~~
Producing a release requires **Go 1.11** or newer and **Python 3** + **pip3** + ``docs/requirements.txt`` for the Sphinx documentation.
A tutorial to install Go is available over at `golang.org <https://golang.org/doc/install>`_.
Python and pip3 should probably be installed via your distro's package manager.
Alternatively, you can use the Docker build process:
it is used to produce the official zrepl `binary releases`_
and serves as a reference for build dependencies and procedure:
::
git clone https://github.com/zrepl/zrepl.git && \
cd zrepl && \
sudo docker build -t zrepl_build -f build.Dockerfile . && \
sudo docker run -it --rm \
-v "${PWD}:/src" \
--user "$(id -u):$(id -g)" \
zrepl_build make release
Alternatively, you can install build dependencies on your local system and then build in your ``$GOPATH``:
::
mkdir -p "${GOPATH}/src/github.com/zrepl/zrepl"
git clone https://github.com/zrepl/zrepl.git "${GOPATH}/src/github.com/zrepl/zrepl"
cd "${GOPATH}/src/github.com/zrepl/zrepl"
python3 -m venv3
source venv3/bin/activate
./lazy.sh devsetup
make release
The Python venv is used for the documentation build dependencies.
If you just want to build the zrepl binary, leave it out and use `./lazy.sh godep` instead.
Either way, all build results are located in the ``artifacts/`` directory.
.. NOTE::
It is your job to install the apropriate binary in the zrepl users's ``$PATH``, e.g. ``/usr/local/bin/zrepl``.
Otherwise, the examples in the :ref:`tutorial` may need to be adjusted.
What next?
----------
Read the :ref:`configuration chapter<configuration_toc>` and then continue with the :ref:`usage chapter<usage>`.
**Reminder**: If you want a quick introduction, please read the :ref:`tutorial`.
-29
View File
@@ -1,29 +0,0 @@
.. _installation-apt-repos:
Debian / Ubuntu APT repositories
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
We maintain APT repositories for Debian, Ubuntu and derivatives.
The fingerprint of the signing key is ``E101 418F D3D6 FBCB 9D65 A62D 7086 99FC 5F2E BF16``.
It is available at `<https://zrepl.cschwarz.com/apt/apt-key.asc>`_ .
Please open an issue `in the packaging repository <https://github.com/zrepl/debian-binary-packaging>`_ if you encounter any issues with the repository.
The following snippet configure the repository for your Debian or Ubuntu release:
::
apt update && apt install curl gnupg lsb-release; \
ARCH="$(dpkg --print-architecture)"; \
CODENAME="$(lsb_release -i -s | tr '[:upper:]' '[:lower:]') $(lsb_release -c -s | tr '[:upper:]' '[:lower:]')"; \
echo "Using Distro and Codename: $CODENAME"; \
(curl https://zrepl.cschwarz.com/apt/apt-key.asc | apt-key add -) && \
(echo "deb [arch=$ARCH] https://zrepl.cschwarz.com/apt/$CODENAME main" > /etc/apt/sources.list.d/zrepl.list) && \
apt update
.. NOTE::
Until zrepl reaches 1.0, all APT repositories will be updated to the latest zrepl release immediately.
This includes breaking changes between zrepl versions.
Use ``apt-mark hold zrepl`` to prevent upgrades of zrepl.
-45
View File
@@ -1,45 +0,0 @@
.. _binary releases: https://github.com/zrepl/zrepl/releases
.. _installation-compile-from-source:
Compile From Source
~~~~~~~~~~~~~~~~~~~
Producing a release requires **Go 1.11** or newer and **Python 3** + **pip3** + ``docs/requirements.txt`` for the Sphinx documentation.
A tutorial to install Go is available over at `golang.org <https://golang.org/doc/install>`_.
Python and pip3 should probably be installed via your distro's package manager.
Alternatively, you can use the Docker build process:
it is used to produce the official zrepl `binary releases`_
and serves as a reference for build dependencies and procedure:
::
git clone https://github.com/zrepl/zrepl.git && \
cd zrepl && \
sudo docker build -t zrepl_build -f build.Dockerfile . && \
sudo docker run -it --rm \
-v "${PWD}:/src" \
--user "$(id -u):$(id -g)" \
zrepl_build make release
Alternatively, you can install build dependencies on your local system and then build in your ``$GOPATH``:
::
mkdir -p "${GOPATH}/src/github.com/zrepl/zrepl"
git clone https://github.com/zrepl/zrepl.git "${GOPATH}/src/github.com/zrepl/zrepl"
cd "${GOPATH}/src/github.com/zrepl/zrepl"
python3 -m venv3
source venv3/bin/activate
./lazy.sh devsetup
make release
The Python venv is used for the documentation build dependencies.
If you just want to build the zrepl binary, leave it out and use `./lazy.sh godep` instead.
Either way, all build results are located in the ``artifacts/`` directory.
.. NOTE::
It is your job to install the appropriate binary in the zrepl users's ``$PATH``, e.g. ``/usr/local/bin/zrepl``.
Otherwise, the examples in the :ref:`tutorial` may need to be adjusted.
@@ -1,140 +0,0 @@
.. include:: ../global.rst.inc
.. _installation-freebsd-jail-with-iocage:
FreeBSD Jail With iocage
========================
This tutorial shows how zrepl can be installed on FreeBSD, or FreeNAS in a jail using iocage.
While this tutorial focuses on using iocage, much of the setup would be similar
using a different jail manager.
.. NOTE::
From a security perspective, just keep in mind that ``zfs send``/``recv`` was never designed with
jails in mind, an attacker could probably crash the receive-side kernel or worse induce stateful
damage to the receive-side pool if they were able to get access to the jail.
The jail doesn't provide security benefits, but only management ones.
Requirements
------------
A dataset that will be delegated to the jail needs to be created if one does not already exist.
For the tutorial ``tank/zrepl`` will be used.
.. code-block:: bash
zfs create -o mountpoint=none tank/zrepl
The only software requirements on the host system are ``iocage``, which can be installed
from ports or packages.
.. code-block:: bash
pkg install py37-iocage
.. NOTE::
By default ``iocage`` will "activate" on first use which will set up some defaults such as
which pool will be used. To activate ``iocage`` manually the ``iocage activate`` command can be used.
Jail Creation
-------------
There are two options for jail creation using FreeBSD.
1. Manually set up the jail from scratch
2. Create the jail using the ``zrepl`` plugin. On FreeNAS this is possible from the user interface using the community index.
Manual Jail
###########
Create a jail, using the same release as the host, called ``zrepl`` that will be automatically started at boot.
The jail will have ``tank/zrepl`` delegated into it.
.. code-block:: bash
iocage create --release "$(freebsd-version -k | cut -d '-' -f '1,2')" --name zrepl \
boot=on nat=1 \
jail_zfs=on \
jail_zfs_dataset=zrepl \
jail_zfs_mountpoint='none'
Enter the jail:
.. code-block:: bash
iocage console zrepl
Install ``zrepl``
.. code-block:: bash
pkg update && pkg upgrade
pkg install zrepl
Create the log file ``/var/log/zrepl.log``
.. code-block:: bash
touch /var/log/zrepl.log && service newsyslog restart
Tell syslogd to redirect facility local0 to the ``zrepl.log`` file:
.. code-block:: bash
service syslogd reload
Enable the zrepl daemon to start automatically at boot:
.. code-block:: bash
sysrc zrepl_enable="YES"
Plugin
######
When using the plugin, ``zrepl`` will be installed for you in a jail using the following ``iocage`` properties.
* ``nat=1``
* ``jail_zfs=on``
* ``jail_zfs_mountpoint=none``
Additionally the delegated dataset should be specified upon creation, and optionally start on boot can be set.
This can also be done from the FreeNAS webui.
.. code-block:: bash
fetch https://raw.githubusercontent.com/ix-plugin-hub/iocage-plugin-index/master/zrepl.json -o /tmp/zrepl.json
iocage fetch -P /tmp/zrepl.json --name zrepl jail_zfs_dataset=zrepl boot=on
Configuration
-------------
Now ``zrepl`` can be configured.
Enter the jail.
.. code-block:: bash
iocage console zrepl
Modify the ``/usr/local/etc/zrepl/zrepl.yml`` configuration file.
.. TIP::
Note: check out the :ref:`tutorial` for examples of a ``sink`` job.
Now ``zrepl`` can be started.
.. code-block:: bash
service zrepl start
Summary
-------
Congratulations, you have a working jail!
-50
View File
@@ -1,50 +0,0 @@
.. _installation-packages:
.. _binary releases: https://github.com/zrepl/zrepl/releases
Packages
--------
zrepl source releases are signed & tagged by the author in the git repository.
Your OS vendor may provide binary packages of zrepl through the package manager.
Additionally, `binary releases`_ are provided on GitHub.
The following list may be incomplete, feel free to submit a PR with an update:
.. list-table::
:header-rows: 1
* - OS / Distro
- Install Command
- Link
* - FreeBSD
- ``pkg install zrepl``
- `<https://www.freshports.org/sysutils/zrepl/>`_
:ref:`installation-freebsd-jail-with-iocage`
* - FreeNAS
-
- :ref:`installation-freebsd-jail-with-iocage`
* - MacOS
- ``brew install zrepl``
- Available on `homebrew <https://brew.sh>`_
* - Arch Linux
- ``yay install zrepl``
- Available on `AUR <https://aur.archlinux.org/packages/zrepl>`_
* - Fedora
- ``dnf install zrepl``
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
* - CentOS/RHEL
- ``yum install zrepl``
- Available on `COPR <https://copr.fedorainfracloud.org/coprs/poettlerric/zrepl/>`_
* - Debian + Ubuntu
- ``apt install zrepl``
- APT repository config :ref:`see below <installation-apt-repos>`
* - OmniOS
- ``pkg install zrepl``
- Available since `r151030 <https://pkg.omniosce.org/r151030/extra/en/search.shtml?token=zrepl&action=Search>`_
* - Void Linux
- ``xbps-install zrepl``
- Available since `a88a2a4 <https://github.com/void-linux/void-packages/commit/a88a2a4d7bf56072dadf61ab56b8424e39155890>`_
* - Others
-
- Use `binary releases`_ or build from source.
-12
View File
@@ -1,12 +0,0 @@
.. _installation-user-privileges:
User Privileges
---------------
It is possible to run zrepl as an unprivileged user in combination with
`ZFS delegation <https://www.freebsd.org/doc/handbook/zfs-zfs-allow.html>`_.
Also, there is the possibility to run it in a jail on FreeBSD by delegating a dataset to the jail.
.. TIP::
Note: check out the :ref:`installation-freebsd-jail-with-iocage` for FreeBSD jail setup instructions.
-8
View File
@@ -1,8 +0,0 @@
.. _installation-what-next:
What next?
----------
Read the :ref:`configuration chapter<configuration_toc>` and then continue with the :ref:`usage chapter<usage>`.
**Reminder**: If you want a quick introduction, please read the :ref:`tutorial`.
+1 -4
View File
@@ -6,7 +6,7 @@
zrepl is a spare-time project primarily developed by `Christian Schwarz <https://cschwarz.com>`_.
You can support maintenance and feature development through one of the services listed above.
For SEPA wire transfer and **commercial support**, please `contact Christian directly <https://cschwarz.com>`_.
For SEPA wire transfer and **commerical support**, please `contact Christian directly <https://cschwarz.com>`_.
**Thanks for your support!**
@@ -32,9 +32,6 @@ We would like to thank the following people and organizations for supporting zre
<div class="fa fa-code" style="width: 1em;"></div>
* |supporter-std| Henning Kessler
* |supporter-std| `John Ramsden <https://github.com/johnramsden>`_
* |supporter-std| `DrLuke <https://github.com/drluke>`_
* |supporter-gold| `Mateusz Kwiatkowski (runhyve.app) <https://runhyve.app>`_
* |supporter-std| `Gaelan D'costa <https://www.robot-disco.net>`_
* |supporter-std| `Tenzin Lhakhang <https://confluence.tenzin.io>`_
+1 -1
View File
@@ -47,7 +47,7 @@ We can model this situation as two jobs:
Install zrepl
-------------
Follow the :ref:`OS-specific installation instructions <installation_toc>` and come back here.
Follow the :ref:`OS-specific installation instructions <installation>` and come back here.
Generate TLS Certificates
-------------------------
+2 -4
View File
@@ -38,8 +38,6 @@ CLI Overview
* - ``zrepl migrate``
- | perform on-disk state / ZFS property migrations
| (see :ref:`changelog <changelog>` for details)
* - ``zrepl zfs-abstractions``
- list and remove zrepl's abstractions on top of ZFS, e.g. holds and step bookmarks (see :ref:`overview <replication-cursor-and-last-received-hold>` )
.. _usage-zrepl-daemon:
@@ -50,7 +48,7 @@ zrepl daemon
All actual work zrepl does is performed by a daemon process.
The daemon supports structured :ref:`logging <logging>` and provides :ref:`monitoring endpoints <monitoring>`.
When installing from a package, the package maintainer should have provided an init script / systemd.service file.
When installating from a package, the package maintainer should have provided an init script / systemd.service file.
You should thus be able to start zrepl daemon using your init system.
Alternatively, or for running zrepl in the foreground, simply execute ``zrepl daemon``.
@@ -75,6 +73,6 @@ The daemon exits as soon as all jobs have reported shut down.
Systemd Unit File
~~~~~~~~~~~~~~~~~
A systemd service definition template is available in :repomasterlink:`dist/systemd`.
A systemd service defintion template is available in :repomasterlink:`dist/systemd`.
Note that some of the options only work on recent versions of systemd.
Any help & improvements are very welcome, see :issue:`145`.
+11 -4
View File
@@ -3,18 +3,25 @@ package endpoint
import (
"context"
"github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/logger"
)
type contextKey int
const (
ClientIdentityKey contextKey = iota
contextKeyLogger contextKey = iota
ClientIdentityKey
)
type Logger = logger.Logger
func getLogger(ctx context.Context) Logger {
return logging.GetLogger(ctx, logging.SubsysEndpoint)
func WithLogger(ctx context.Context, log Logger) context.Context {
return context.WithValue(ctx, contextKeyLogger, log)
}
func getLogger(ctx context.Context) Logger {
if l, ok := ctx.Value(contextKeyLogger).(Logger); ok {
return l
}
return logger.NewNullLogger()
}
+166 -288
View File
@@ -2,18 +2,14 @@
package endpoint
import (
"bytes"
"context"
"fmt"
"io"
"path"
"sync"
"github.com/kr/pretty"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/replication/logic/pdu"
"github.com/zrepl/zrepl/util/chainedio"
"github.com/zrepl/zrepl/util/chainlock"
"github.com/zrepl/zrepl/util/envconst"
"github.com/zrepl/zrepl/util/semaphore"
@@ -21,10 +17,9 @@ import (
)
type SenderConfig struct {
FSF zfs.DatasetFilter
Encrypt *zfs.NilBool
DisableIncrementalStepHolds bool
JobID JobID
FSF zfs.DatasetFilter
Encrypt *zfs.NilBool
JobID JobID
}
func (c *SenderConfig) Validate() error {
@@ -40,10 +35,9 @@ func (c *SenderConfig) Validate() error {
// Sender implements replication.ReplicationEndpoint for a sending side
type Sender struct {
FSFilter zfs.DatasetFilter
encrypt *zfs.NilBool
disableIncrementalStepHolds bool
jobId JobID
FSFilter zfs.DatasetFilter
encrypt *zfs.NilBool
jobId JobID
}
func NewSender(conf SenderConfig) *Sender {
@@ -51,10 +45,9 @@ func NewSender(conf SenderConfig) *Sender {
panic("invalid config" + err.Error())
}
return &Sender{
FSFilter: conf.FSF,
encrypt: conf.Encrypt,
disableIncrementalStepHolds: conf.DisableIncrementalStepHolds,
jobId: conf.JobID,
FSFilter: conf.FSF,
encrypt: conf.Encrypt,
jobId: conf.JobID,
}
}
@@ -77,8 +70,6 @@ func (s *Sender) filterCheckFS(fs string) (*zfs.DatasetPath, error) {
}
func (s *Sender) ListFilesystems(ctx context.Context, r *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
fss, err := zfs.ZFSListMapping(ctx, s.FSFilter)
if err != nil {
return nil, err
@@ -101,13 +92,11 @@ func (s *Sender) ListFilesystems(ctx context.Context, r *pdu.ListFilesystemReq)
}
func (s *Sender) ListFilesystemVersions(ctx context.Context, r *pdu.ListFilesystemVersionsReq) (*pdu.ListFilesystemVersionsRes, error) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
lp, err := s.filterCheckFS(r.GetFilesystem())
if err != nil {
return nil, err
}
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, lp, zfs.ListFilesystemVersionsOptions{})
fsvs, err := zfs.ZFSListFilesystemVersions(lp, nil)
if err != nil {
return nil, err
}
@@ -120,8 +109,36 @@ func (s *Sender) ListFilesystemVersions(ctx context.Context, r *pdu.ListFilesyst
}
var maxConcurrentZFSSend = envconst.Int64("ZREPL_ENDPOINT_MAX_CONCURRENT_SEND", 10)
var maxConcurrentZFSSendSemaphore = semaphore.New(maxConcurrentZFSSend)
func (p *Sender) HintMostRecentCommonAncestor(ctx context.Context, r *pdu.HintMostRecentCommonAncestorReq) (*pdu.HintMostRecentCommonAncestorRes, error) {
var err error
fs := r.GetFilesystem()
mostRecent, err := sendArgsFromPDUAndValidateExists(ctx, fs, r.GetSenderVersion())
if err != nil {
msg := "HintMostRecentCommonAncestor rpc with nonexistent most recent version"
getLogger(ctx).WithField("fs", fs).WithField("hinted_most_recent", fmt.Sprintf("%#v", mostRecent)).
Warn(msg)
return nil, errors.Wrap(err, msg)
}
// move replication cursor to this position
_, err = MoveReplicationCursor(ctx, fs, mostRecent, p.jobId)
if err == zfs.ErrBookmarkCloningNotSupported {
getLogger(ctx).Debug("not creating replication cursor from bookmark because ZFS does not support it")
// fallthrough
} else if err != nil {
return nil, errors.Wrap(err, "cannot set replication cursor to hinted version")
}
// cleanup previous steps
if err := ReleaseStepAll(ctx, fs, mostRecent, p.jobId); err != nil {
return nil, errors.Wrap(err, "cannot cleanup prior invocation's step holds and bookmarks")
}
return &pdu.HintMostRecentCommonAncestorRes{}, nil
}
var maxConcurrentZFSSendSemaphore = semaphore.New(envconst.Int64("ZREPL_ENDPOINT_MAX_CONCURRENT_SEND", 10))
func uncheckedSendArgsFromPDU(fsv *pdu.FilesystemVersion) *zfs.ZFSSendArgVersion {
if fsv == nil {
@@ -130,20 +147,18 @@ func uncheckedSendArgsFromPDU(fsv *pdu.FilesystemVersion) *zfs.ZFSSendArgVersion
return &zfs.ZFSSendArgVersion{RelName: fsv.GetRelName(), GUID: fsv.Guid}
}
func sendArgsFromPDUAndValidateExistsAndGetVersion(ctx context.Context, fs string, fsv *pdu.FilesystemVersion) (v zfs.FilesystemVersion, err error) {
sendArgs := uncheckedSendArgsFromPDU(fsv)
if sendArgs == nil {
return v, errors.New("must not be nil")
func sendArgsFromPDUAndValidateExists(ctx context.Context, fs string, fsv *pdu.FilesystemVersion) (*zfs.ZFSSendArgVersion, error) {
v := uncheckedSendArgsFromPDU(fsv)
if v == nil {
return nil, errors.New("must not be nil")
}
version, err := sendArgs.ValidateExistsAndGetVersion(ctx, fs)
if err != nil {
return v, err
if err := v.ValidateExists(ctx, fs); err != nil {
return nil, err
}
return version, nil
return v, nil
}
func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) {
_, err := s.filterCheckFS(r.Filesystem)
if err != nil {
@@ -155,7 +170,7 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
// ok, fallthrough outer
case pdu.Tri_False:
if s.encrypt.B {
return nil, nil, errors.New("only encrypted sends allowed (send -w + encryption!= off), but unencrypted send requested")
return nil, nil, errors.New("only encrytped sends allowed (send -w + encryption!= off), but unencrytped send requested")
}
// fallthrough outer
case pdu.Tri_True:
@@ -167,7 +182,7 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
return nil, nil, fmt.Errorf("unknown pdu.Tri variant %q", r.Encrypted)
}
sendArgsUnvalidated := zfs.ZFSSendArgsUnvalidated{
sendArgs := zfs.ZFSSendArgs{
FS: r.Filesystem,
From: uncheckedSendArgsFromPDU(r.GetFrom()), // validated by zfs.ZFSSendDry / zfs.ZFSSend
To: uncheckedSendArgsFromPDU(r.GetTo()), // validated by zfs.ZFSSendDry / zfs.ZFSSend
@@ -175,11 +190,6 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
ResumeToken: r.ResumeToken, // nil or not nil, depending on decoding success
}
sendArgs, err := sendArgsUnvalidated.Validate(ctx)
if err != nil {
return nil, nil, errors.Wrap(err, "validate send arguments")
}
getLogger(ctx).Debug("acquire concurrent send semaphore")
// TODO use try-acquire and fail with resource-exhaustion rpc status
// => would require handling on the client-side
@@ -196,7 +206,7 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
}
// From now on, assume that sendArgs has been validated by ZFSSendDry
// (because validation involves shelling out, it's actually a little expensive)
// (because validation invovles shelling out, it's actually a little expensive)
var expSize int64 = 0 // protocol says 0 means no estimate
if si.SizeEstimate != -1 { // but si returns -1 for no size estimate
@@ -211,11 +221,10 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
return res, nil, nil
}
// create a replication cursor for `From` (usually an idempotent no-op because SendCompleted already created it before)
var fromReplicationCursor Abstraction
// update replication cursor
if sendArgs.From != nil {
// For all but the first replication, this should always be a no-op because SendCompleted already moved the cursor
fromReplicationCursor, err = CreateReplicationCursor(ctx, sendArgs.FS, *sendArgs.FromVersion, s.jobId) // no shadow
_, err = MoveReplicationCursor(ctx, sendArgs.FS, sendArgs.From, s.jobId)
if err == zfs.ErrBookmarkCloningNotSupported {
getLogger(ctx).Debug("not creating replication cursor from bookmark because ZFS does not support it")
// fallthrough
@@ -224,148 +233,122 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
}
}
takeStepHolds := sendArgs.FromVersion == nil || !s.disableIncrementalStepHolds
var fromHold, toHold Abstraction
// make sure `From` doesn't go away in order to make this step resumable
if sendArgs.From != nil && takeStepHolds {
fromHold, err = HoldStep(ctx, sendArgs.FS, *sendArgs.FromVersion, s.jobId) // no shadow
if sendArgs.From != nil {
err := HoldStep(ctx, sendArgs.FS, sendArgs.From, s.jobId)
if err == zfs.ErrBookmarkCloningNotSupported {
getLogger(ctx).Debug("not creating step bookmark because ZFS does not support it")
// fallthrough
} else if err != nil {
return nil, nil, errors.Wrapf(err, "cannot hold `from` version %q before starting send", *sendArgs.FromVersion)
return nil, nil, errors.Wrap(err, "cannot create step bookmark")
}
}
if takeStepHolds {
// make sure `To` doesn't go away in order to make this step resumable
toHold, err = HoldStep(ctx, sendArgs.FS, sendArgs.ToVersion, s.jobId)
if err != nil {
return nil, nil, errors.Wrapf(err, "cannot hold `to` version %q before starting send", sendArgs.ToVersion)
}
}
// cleanup the mess that _this function_ might have created in prior failed attempts:
//
// In summary, we delete every endpoint ZFS abstraction created on this filesystem for this job id,
// except for the ones we just created above.
//
// This is the most robust approach to avoid leaking (= forgetting to clean up) endpoint ZFS abstractions,
// all under the assumption that there will only ever be one send for a (jobId,fs) combination at any given time.
//
// Note that the SendCompleted rpc can't be relied upon for this purpose:
// - it might be lost due to network errors,
// - or never be sent by a potentially malicious or buggy client,
// - or never be send because the replication step failed at some point
// (potentially leaving a resumable state on the receiver, which is the case where we really do not want to blow away the step holds too soon.)
//
// Note further that a resuming send, due to the idempotent nature of func CreateReplicationCursor and HoldStep,
// will never lose its step holds because we just (idempotently re-)created them above, before attempting the cleanup.
liveAbs := []Abstraction{fromHold, toHold, fromReplicationCursor}
func() {
ctx, endSpan := trace.WithSpan(ctx, "cleanup-stale-abstractions")
defer endSpan()
keep := func(a Abstraction) (keep bool) {
keep = false
for _, k := range liveAbs {
keep = keep || AbstractionEquals(a, k)
}
return keep
}
check := func(obsoleteAbs []Abstraction) {
// last line of defense: check that we don't destroy the incremental `from` and `to`
// if we did that, we might be about to blow away the last common filesystem version between sender and receiver
mustLiveVersions := []zfs.FilesystemVersion{sendArgs.ToVersion}
if sendArgs.FromVersion != nil {
mustLiveVersions = append(mustLiveVersions, *sendArgs.FromVersion)
}
for _, staleVersion := range obsoleteAbs {
for _, mustLiveVersion := range mustLiveVersions {
if zfs.FilesystemVersionEqualIdentity(mustLiveVersion, staleVersion.GetFilesystemVersion()) {
panic(fmt.Sprintf("impl error: %q would be destroyed because it is considered stale but it is part of of sendArgs=%s", mustLiveVersion.String(), pretty.Sprint(sendArgs)))
}
}
}
}
sendAbstractionsCacheSingleton.TryBatchDestroy(ctx, s.jobId, sendArgs.FS, keep, check)
}()
// now add the newly created abstractions to the cleaned-up cache
for _, a := range liveAbs {
if a != nil {
sendAbstractionsCacheSingleton.Put(a)
}
}
sendStream, err := zfs.ZFSSend(ctx, sendArgs)
// make sure `To` doesn't go away in order to make this step resumable
err = HoldStep(ctx, sendArgs.FS, sendArgs.To, s.jobId)
if err != nil {
return nil, nil, errors.Wrapf(err, "cannot hold `to` version %q before starting send", sendArgs.To.RelName)
}
// step holds & replication cursor released / moved forward in s.SendCompleted => s.moveCursorAndReleaseSendHolds
streamCopier, err := zfs.ZFSSend(ctx, sendArgs)
if err != nil {
// it's ok to not destroy the abstractions we just created here, a new send attempt will take care of it
return nil, nil, errors.Wrap(err, "zfs send failed")
}
return res, sendStream, nil
return res, streamCopier, nil
}
func (p *Sender) SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
orig := r.GetOriginalReq() // may be nil, always use proto getters
fsp, err := p.filterCheckFS(orig.GetFilesystem())
if err != nil {
return nil, err
}
fs := fsp.ToString()
fs := orig.GetFilesystem()
var from *zfs.FilesystemVersion
var err error
var from *zfs.ZFSSendArgVersion
if orig.GetFrom() != nil {
f, err := sendArgsFromPDUAndValidateExistsAndGetVersion(ctx, fs, orig.GetFrom()) // no shadow
from, err = sendArgsFromPDUAndValidateExists(ctx, fs, orig.GetFrom()) // no shadow
if err != nil {
return nil, errors.Wrap(err, "validate `from` exists")
}
from = &f
}
to, err := sendArgsFromPDUAndValidateExistsAndGetVersion(ctx, fs, orig.GetTo())
to, err := sendArgsFromPDUAndValidateExists(ctx, fs, orig.GetTo())
if err != nil {
return nil, errors.Wrap(err, "validate `to` exists")
}
log := func(ctx context.Context) Logger {
log := getLogger(ctx).WithField("to_guid", to.Guid).
WithField("fs", fs).
WithField("to", to.RelName)
if from != nil {
log = log.WithField("from", from.RelName).WithField("from_guid", from.Guid)
}
return log
log := getLogger(ctx).WithField("to_guid", to.GUID).
WithField("fs", fs).
WithField("to", to.RelName)
if from != nil {
log = log.WithField("from", from.RelName).WithField("from_guid", from.GUID)
}
toReplicationCursor, err := CreateReplicationCursor(ctx, fs, to, p.jobId)
log.Debug("move replication cursor to most recent common version")
destroyedCursors, err := MoveReplicationCursor(ctx, fs, to, p.jobId)
if err != nil {
if err == zfs.ErrBookmarkCloningNotSupported {
log(ctx).Debug("not setting replication cursor, bookmark cloning not supported")
log.Debug("not setting replication cursor, bookmark cloning not supported")
} else {
msg := "cannot move replication cursor, keeping hold on `to` until successful"
log(ctx).WithError(err).Error(msg)
log.WithError(err).Error(msg)
err = errors.Wrap(err, msg)
// it is correct to not destroy from and to step holds if we can't move the cursor!
// it is correct to not release the hold if we can't move the cursor!
return &pdu.SendCompletedRes{}, err
}
} else {
log(ctx).WithField("to_cursor", toReplicationCursor.String()).Info("successfully created `to` replication cursor")
log.Info("successfully moved replication cursor")
}
keep := func(a Abstraction) bool {
return AbstractionEquals(a, toReplicationCursor)
}
sendAbstractionsCacheSingleton.TryBatchDestroy(ctx, p.jobId, fs, keep, nil)
// kick off releasing of step holds / bookmarks
// if we fail to release them, don't bother the caller:
// they are merely an implementation detail on the sender for better resumability
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
log.Debug("release step-hold of or step-bookmark on `to`")
err = ReleaseStep(ctx, fs, to, p.jobId)
if err != nil {
log.WithError(err).Error("cannot release step-holds on or destroy step-bookmark of `to`")
} else {
log.Info("successfully released step-holds on or destroyed step-bookmark of `to`")
}
}()
go func() {
defer wg.Done()
if from == nil {
return
}
log.Debug("release step-hold of or step-bookmark on `from`")
err := ReleaseStep(ctx, fs, from, p.jobId)
if err != nil {
if dne, ok := err.(*zfs.DatasetDoesNotExist); ok {
// If bookmark cloning is not supported, `from` might be the old replication cursor
// and thus have already been destroyed by MoveReplicationCursor above
// In that case, nonexistence of `from` is not an error, otherwise it is.
fsp, err := zfs.NewDatasetPath(fs)
if err != nil {
panic(err) // fs has been validated multiple times above
}
for _, fsv := range destroyedCursors {
if fsv.ToAbsPath(fsp) == dne.Path {
log.Info("`from` was a replication cursor and has already been destroyed")
return
}
}
// fallthrough
}
log.WithError(err).Error("cannot release step-holds on or destroy step-bookmark of `from`")
} else {
log.Info("successfully released step-holds on or destroyed step-bookmark of `from`")
}
}()
wg.Wait()
return &pdu.SendCompletedRes{}, nil
}
func (p *Sender) DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshotsReq) (*pdu.DestroySnapshotsRes, error) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
dp, err := p.filterCheckFS(req.Filesystem)
if err != nil {
return nil, err
@@ -374,8 +357,6 @@ func (p *Sender) DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshots
}
func (p *Sender) Ping(ctx context.Context, req *pdu.PingReq) (*pdu.PingRes, error) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
res := pdu.PingRes{
Echo: req.GetMessage(),
}
@@ -383,20 +364,14 @@ func (p *Sender) Ping(ctx context.Context, req *pdu.PingReq) (*pdu.PingRes, erro
}
func (p *Sender) PingDataconn(ctx context.Context, req *pdu.PingReq) (*pdu.PingRes, error) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
return p.Ping(ctx, req)
}
func (p *Sender) WaitForConnectivity(ctx context.Context) error {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
return nil
}
func (p *Sender) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
dp, err := p.filterCheckFS(req.Filesystem)
if err != nil {
return nil, err
@@ -412,7 +387,7 @@ func (p *Sender) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCurs
return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Guid{Guid: cursor.Guid}}, nil
}
func (p *Sender) Receive(ctx context.Context, r *pdu.ReceiveReq, _ io.ReadCloser) (*pdu.ReceiveRes, error) {
func (p *Sender) Receive(ctx context.Context, r *pdu.ReceiveReq, receive zfs.StreamCopier) (*pdu.ReceiveRes, error) {
return nil, fmt.Errorf("sender does not implement Receive()")
}
@@ -527,15 +502,6 @@ func (f subroot) MapToLocal(fs string) (*zfs.DatasetPath, error) {
}
func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
// first make sure that root_fs is imported
if rphs, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, s.conf.RootWithoutClientComponent); err != nil {
return nil, errors.Wrap(err, "cannot determine whether root_fs exists")
} else if !rphs.FSExists {
return nil, errors.New("root_fs does not exist")
}
root := s.clientRootFromCtx(ctx)
filtered, err := zfs.ZFSListMapping(ctx, subroot{root})
if err != nil {
@@ -545,7 +511,7 @@ func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemR
fss := make([]*pdu.Filesystem, 0, len(filtered))
for _, a := range filtered {
l := getLogger(ctx).WithField("fs", a)
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, a)
ph, err := zfs.ZFSGetFilesystemPlaceholderState(a)
if err != nil {
l.WithError(err).Error("error getting placeholder state")
return nil, errors.Wrapf(err, "cannot get placeholder state for fs %q", a)
@@ -586,16 +552,13 @@ func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemR
}
func (s *Receiver) ListFilesystemVersions(ctx context.Context, req *pdu.ListFilesystemVersionsReq) (*pdu.ListFilesystemVersionsRes, error) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
root := s.clientRootFromCtx(ctx)
lp, err := subroot{root}.MapToLocal(req.GetFilesystem())
if err != nil {
return nil, err
}
// TODO share following code with sender
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, lp, zfs.ListFilesystemVersionsOptions{})
fsvs, err := zfs.ZFSListFilesystemVersions(lp, nil)
if err != nil {
return nil, err
}
@@ -609,8 +572,6 @@ func (s *Receiver) ListFilesystemVersions(ctx context.Context, req *pdu.ListFile
}
func (s *Receiver) Ping(ctx context.Context, req *pdu.PingReq) (*pdu.PingRes, error) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
res := pdu.PingRes{
Echo: req.GetMessage(),
}
@@ -618,30 +579,24 @@ func (s *Receiver) Ping(ctx context.Context, req *pdu.PingReq) (*pdu.PingRes, er
}
func (s *Receiver) PingDataconn(ctx context.Context, req *pdu.PingReq) (*pdu.PingRes, error) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
return s.Ping(ctx, req)
}
func (s *Receiver) WaitForConnectivity(ctx context.Context) error {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
return nil
}
func (s *Receiver) ReplicationCursor(ctx context.Context, _ *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
func (s *Receiver) ReplicationCursor(context.Context, *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) {
return nil, fmt.Errorf("ReplicationCursor not implemented for Receiver")
}
func (s *Receiver) Send(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
func (s *Receiver) Send(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) {
return nil, nil, fmt.Errorf("receiver does not implement Send()")
}
var maxConcurrentZFSRecvSemaphore = semaphore.New(envconst.Int64("ZREPL_ENDPOINT_MAX_CONCURRENT_RECV", 10))
func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.ReadCloser) (*pdu.ReceiveRes, error) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs.StreamCopier) (*pdu.ReceiveRes, error) {
getLogger(ctx).Debug("incoming Receive")
defer receive.Close()
@@ -655,6 +610,9 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
if to == nil {
return nil, errors.New("`To` must not be nil")
}
if err := to.ValidateInMemory(lp.ToString()); err != nil {
return nil, errors.Wrap(err, "`To` invalid")
}
if !to.IsSnapshot() {
return nil, errors.New("`To` must be a snapshot")
}
@@ -667,9 +625,9 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
// ZFS dataset hierarchy subtrees.
var visitErr error
func() {
getLogger(ctx).Debug("begin acquire recvParentCreationMtx")
getLogger(ctx).Debug("begin aquire recvParentCreationMtx")
defer s.recvParentCreationMtx.Lock().Unlock()
getLogger(ctx).Debug("end acquire recvParentCreationMtx")
getLogger(ctx).Debug("end aquire recvParentCreationMtx")
defer getLogger(ctx).Debug("release recvParentCreationMtx")
f := zfs.NewDatasetPathForest()
@@ -679,7 +637,7 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
if v.Path.Equal(lp) {
return false
}
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, v.Path)
ph, err := zfs.ZFSGetFilesystemPlaceholderState(v.Path)
getLogger(ctx).
WithField("fs", v.Path.ToString()).
WithField("placeholder_state", fmt.Sprintf("%#v", ph)).
@@ -703,7 +661,7 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
}
l := getLogger(ctx).WithField("placeholder_fs", v.Path)
l.Debug("create placeholder filesystem")
err := zfs.ZFSCreatePlaceholderFilesystem(ctx, v.Path)
err := zfs.ZFSCreatePlaceholderFilesystem(v.Path)
if err != nil {
l.WithError(err).Error("cannot create placeholder filesystem")
visitErr = err
@@ -720,31 +678,22 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
return nil, visitErr
}
log := getLogger(ctx).WithField("proto_fs", req.GetFilesystem()).WithField("local_fs", lp.ToString())
// determine whether we need to rollback the filesystem / change its placeholder state
var clearPlaceholderProperty bool
var recvOpts zfs.RecvOptions
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, lp)
if err != nil {
return nil, errors.Wrap(err, "cannot get placeholder state")
}
log.WithField("placeholder_state", fmt.Sprintf("%#v", ph)).Debug("placeholder state")
if ph.FSExists && ph.IsPlaceholder {
ph, err := zfs.ZFSGetFilesystemPlaceholderState(lp)
if err == nil && ph.FSExists && ph.IsPlaceholder {
recvOpts.RollbackAndForceRecv = true
clearPlaceholderProperty = true
}
if clearPlaceholderProperty {
log.Info("clearing placeholder property")
if err := zfs.ZFSSetPlaceholder(ctx, lp, false); err != nil {
if err := zfs.ZFSSetPlaceholder(lp, false); err != nil {
return nil, fmt.Errorf("cannot clear placeholder property for forced receive: %s", err)
}
}
if req.ClearResumeToken && ph.FSExists {
log.Info("clearing resume token")
if err := zfs.ZFSRecvClearResumeToken(ctx, lp.ToString()); err != nil {
if err := zfs.ZFSRecvClearResumeToken(lp.ToString()); err != nil {
return nil, errors.Wrap(err, "cannot clear resume token")
}
}
@@ -754,7 +703,7 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
return nil, errors.Wrap(err, "cannot determine whether we can use resumable send & recv")
}
log.Debug("acquire concurrent recv semaphore")
getLogger(ctx).Debug("acquire concurrent recv semaphore")
// TODO use try-acquire and fail with resource-exhaustion rpc status
// => would require handling on the client-side
// => this is a dataconn endpoint, doesn't have the status code semantics of gRPC
@@ -764,104 +713,28 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
}
defer guard.Release()
var peek bytes.Buffer
var MaxPeek = envconst.Int64("ZREPL_ENDPOINT_RECV_PEEK_SIZE", 1<<20)
log.WithField("max_peek_bytes", MaxPeek).Info("peeking incoming stream")
if _, err := io.Copy(&peek, io.LimitReader(receive, MaxPeek)); err != nil {
log.WithError(err).Error("cannot read peek-buffer from send stream")
}
var peekCopy bytes.Buffer
if n, err := peekCopy.Write(peek.Bytes()); err != nil || n != peek.Len() {
panic(peek.Len())
}
log.WithField("opts", fmt.Sprintf("%#v", recvOpts)).Debug("start receive command")
getLogger(ctx).WithField("opts", fmt.Sprintf("%#v", recvOpts)).Debug("start receive command")
snapFullPath := to.FullPath(lp.ToString())
if err := zfs.ZFSRecv(ctx, lp.ToString(), to, chainedio.NewChainedReader(&peek, receive), recvOpts); err != nil {
// best-effort rollback of placeholder state if the recv didn't start
_, resumableStatePresent := err.(*zfs.RecvFailedWithResumeTokenErr)
disablePlaceholderRestoration := envconst.Bool("ZREPL_ENDPOINT_DISABLE_PLACEHOLDER_RESTORATION", false)
placeholderRestored := !ph.IsPlaceholder
if !disablePlaceholderRestoration && !resumableStatePresent && recvOpts.RollbackAndForceRecv && ph.FSExists && ph.IsPlaceholder && clearPlaceholderProperty {
log.Info("restoring placeholder property")
if phErr := zfs.ZFSSetPlaceholder(ctx, lp, true); phErr != nil {
log.WithError(phErr).Error("cannot restore placeholder property after failed receive, subsequent replications will likely fail with a different error")
// fallthrough
} else {
placeholderRestored = true
}
// fallthrough
}
// deal with failing initial encrypted send & recv
if _, ok := err.(*zfs.RecvDestroyOrOverwriteEncryptedErr); ok && ph.IsPlaceholder && placeholderRestored {
msg := `cannot automatically replace placeholder filesystem with incoming send stream - please see receive-side log for details`
err := errors.New(msg)
log.Error(msg)
log.Error(`zrepl creates placeholder filesystems on the receiving side of a replication to match the sending side's dataset hierarchy`)
log.Error(`zrepl uses zfs receive -F to replace those placeholders with incoming full sends`)
log.Error(`OpenZFS native encryption prohibits zfs receive -F for encrypted filesystems`)
log.Error(`the current zrepl placeholder filesystem concept is thus incompatible with OpenZFS native encryption`)
tempStartFullRecvFS := lp.Copy().ToString() + ".zrepl.initial-recv"
tempStartFullRecvFSDP, dpErr := zfs.NewDatasetPath(tempStartFullRecvFS)
if dpErr != nil {
log.WithError(dpErr).Error("cannot determine temporary filesystem name for initial encrypted recv workaround")
return nil, err // yes, err, not dpErr
}
log := log.WithField("temp_recv_fs", tempStartFullRecvFS)
log.Error(`as a workaround, zrepl will now attempt to re-receive the beginning of the stream into a temporary filesystem temp_recv_fs`)
log.Error(`if that step succeeds: shut down zrepl and use 'zfs rename' to swap temp_recv_fs with local_fs, then restart zrepl`)
log.Error(`replication will then resume using resumable send+recv`)
tempPH, phErr := zfs.ZFSGetFilesystemPlaceholderState(ctx, tempStartFullRecvFSDP)
if phErr != nil {
log.WithError(phErr).Error("cannot determine placeholder state of temp_recv_fs")
return nil, err // yes, err, not dpErr
}
if tempPH.FSExists {
log.Error("temp_recv_fs already exists, assuming a (partial) initial recv to that filesystem has already been done")
return nil, err
}
recvOpts.RollbackAndForceRecv = false
recvOpts.SavePartialRecvState = true
rerecvErr := zfs.ZFSRecv(ctx, tempStartFullRecvFS, to, chainedio.NewChainedReader(&peekCopy), recvOpts)
if _, isResumable := rerecvErr.(*zfs.RecvFailedWithResumeTokenErr); rerecvErr == nil || isResumable {
log.Error("completed re-receive into temporary filesystem temp_recv_fs, now shut down zrepl and use zfs rename to swap temp_recv_fs with local_fs")
} else {
log.WithError(rerecvErr).Error("failed to receive the beginning of the stream into temporary filesystem temp_recv_fs")
log.Error("we advise you to collect the error log and current configuration, open an issue on GitHub, and revert to your previous configuration in the meantime")
}
log.Error(`if you would like to see improvements to this situation, please open an issue on GitHub`)
return nil, err
}
log.
if err := zfs.ZFSRecv(ctx, lp.ToString(), to, receive, recvOpts); err != nil {
getLogger(ctx).
WithError(err).
WithField("opts", fmt.Sprintf("%#v", recvOpts)).
WithField("opts", recvOpts).
Error("zfs receive failed")
return nil, err
}
// validate that we actually received what the sender claimed
toRecvd, err := to.ValidateExistsAndGetVersion(ctx, lp.ToString())
if err != nil {
if err := to.ValidateExists(ctx, lp.ToString()); err != nil {
msg := "receive request's `To` version does not match what we received in the stream"
log.WithError(err).WithField("snap", snapFullPath).Error(msg)
log.Error("aborting recv request, but keeping received snapshot for inspection")
getLogger(ctx).WithError(err).WithField("snap", snapFullPath).Error(msg)
getLogger(ctx).Error("aborting recv request, but keeping received snapshot for inspection")
return nil, errors.Wrap(err, msg)
}
if s.conf.UpdateLastReceivedHold {
log.Debug("move last-received-hold")
if err := MoveLastReceivedHold(ctx, lp.ToString(), toRecvd, s.conf.JobID); err != nil {
getLogger(ctx).Debug("move last-received-hold")
if err := MoveLastReceivedHold(ctx, lp.ToString(), *to, s.conf.JobID); err != nil {
return nil, errors.Wrap(err, "cannot move last-received-hold")
}
}
@@ -870,8 +743,6 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
}
func (s *Receiver) DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshotsReq) (*pdu.DestroySnapshotsRes, error) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
root := s.clientRootFromCtx(ctx)
lp, err := subroot{root}.MapToLocal(req.Filesystem)
if err != nil {
@@ -880,9 +751,16 @@ func (s *Receiver) DestroySnapshots(ctx context.Context, req *pdu.DestroySnapsho
return doDestroySnapshots(ctx, lp, req.Snapshots)
}
func (p *Receiver) SendCompleted(ctx context.Context, _ *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
func (p *Receiver) HintMostRecentCommonAncestor(ctx context.Context, r *pdu.HintMostRecentCommonAncestorReq) (*pdu.HintMostRecentCommonAncestorRes, error) {
// we don't move last-received-hold as part of this hint
// because that wouldn't give us any benefit wrt resumability.
//
// Other reason: the replication logic that issues this RPC would require refactoring
// to include the receiver's FilesystemVersion in the request)
return &pdu.HintMostRecentCommonAncestorRes{}, nil
}
func (p *Receiver) SendCompleted(context.Context, *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error) {
return &pdu.SendCompletedRes{}, nil
}
@@ -904,7 +782,7 @@ func doDestroySnapshots(ctx context.Context, lp *zfs.DatasetPath, snaps []*pdu.F
ErrOut: &errs[i],
}
}
zfs.ZFSDestroyFilesystemVersions(ctx, reqs)
zfs.ZFSDestroyFilesystemVersions(reqs)
for i := range reqs {
if errs[i] != nil {
if de, ok := errs[i].(*zfs.DestroySnapshotsError); ok && len(de.Reason) == 1 {
-7
View File
@@ -1,7 +0,0 @@
package endpoint
import "github.com/prometheus/client_golang/prometheus"
func RegisterMetrics(r prometheus.Registerer) {
r.MustRegister(sendAbstractionsCacheMetrics.count)
}
@@ -1,214 +0,0 @@
package endpoint
import (
"context"
"fmt"
"sync"
"github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/util/chainlock"
)
var sendAbstractionsCacheMetrics struct {
count prometheus.Gauge
}
func init() {
sendAbstractionsCacheMetrics.count = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "zrepl",
Subsystem: "endpoint",
Name: "send_abstractions_cache_entry_count",
Help: "number of send abstractions tracked in the sendAbstractionsCache data structure",
})
}
var sendAbstractionsCacheSingleton = newSendAbstractionsCache()
func SendAbstractionsCacheInvalidate(fs string) {
sendAbstractionsCacheSingleton.InvalidateFSCache(fs)
}
type sendAbstractionsCacheDidLoadFSState int
const (
sendAbstractionsCacheDidLoadFSStateNo sendAbstractionsCacheDidLoadFSState = iota // 0-value has meaning
sendAbstractionsCacheDidLoadFSStateInProgress
sendAbstractionsCacheDidLoadFSStateDone
)
type sendAbstractionsCache struct {
mtx chainlock.L
abstractions []Abstraction
didLoadFS map[string]sendAbstractionsCacheDidLoadFSState
didLoadFSChanged *sync.Cond
}
func newSendAbstractionsCache() *sendAbstractionsCache {
c := &sendAbstractionsCache{
didLoadFS: make(map[string]sendAbstractionsCacheDidLoadFSState),
}
c.didLoadFSChanged = c.mtx.NewCond()
return c
}
func (s *sendAbstractionsCache) Put(a Abstraction) {
defer s.mtx.Lock().Unlock()
var zeroJobId JobID
if a.GetJobID() == nil {
panic("abstraction must not have nil job id")
} else if *a.GetJobID() == zeroJobId {
panic(fmt.Sprintf("abstraction must not have zero-value job id: %s", a))
}
s.abstractions = append(s.abstractions, a)
sendAbstractionsCacheMetrics.count.Set(float64(len(s.abstractions)))
}
func (s *sendAbstractionsCache) InvalidateFSCache(fs string) {
// FIXME: O(n)
newAbs := make([]Abstraction, 0, len(s.abstractions))
for _, a := range s.abstractions {
if a.GetFS() != fs {
newAbs = append(newAbs, a)
}
}
s.abstractions = newAbs
sendAbstractionsCacheMetrics.count.Set(float64(len(s.abstractions)))
s.didLoadFS[fs] = sendAbstractionsCacheDidLoadFSStateNo
s.didLoadFSChanged.Broadcast()
}
// - logs errors in getting on-disk abstractions
// - only fetches on-disk abstractions once, but every time from the in-memory store
//
// That means that for precise results, all abstractions created by the endpoint must be .Put into this cache.
func (s *sendAbstractionsCache) GetAndDeleteByJobIDAndFS(ctx context.Context, jobID JobID, fs string) (ret []Abstraction) {
defer s.mtx.Lock().Unlock()
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
var zeroJobId JobID
if jobID == zeroJobId {
panic("must not pass zero-value job id")
}
if fs == "" {
panic("must not pass zero-value fs")
}
s.tryLoadOnDiskSendAbstractions(ctx, fs)
// FIXME O(n)
var remaining []Abstraction
for _, a := range s.abstractions {
aJobId := *a.GetJobID()
aFS := a.GetFS()
if aJobId == jobID && aFS == fs {
ret = append(ret, a)
} else {
remaining = append(remaining, a)
}
}
s.abstractions = remaining
sendAbstractionsCacheMetrics.count.Set(float64(len(s.abstractions)))
return ret
}
// caller must hold s.mtx
func (s *sendAbstractionsCache) tryLoadOnDiskSendAbstractions(ctx context.Context, fs string) {
for s.didLoadFS[fs] != sendAbstractionsCacheDidLoadFSStateDone {
if s.didLoadFS[fs] == sendAbstractionsCacheDidLoadFSStateInProgress {
s.didLoadFSChanged.Wait()
continue
}
if s.didLoadFS[fs] != sendAbstractionsCacheDidLoadFSStateNo {
panic(fmt.Sprintf("unreachable: %v", s.didLoadFS[fs]))
}
s.didLoadFS[fs] = sendAbstractionsCacheDidLoadFSStateInProgress
defer s.didLoadFSChanged.Broadcast()
var onDiskAbs []Abstraction
var err error
s.mtx.DropWhile(func() {
onDiskAbs, err = s.tryLoadOnDiskSendAbstractionsImpl(ctx, fs) // no shadow
})
if err != nil {
s.didLoadFS[fs] = sendAbstractionsCacheDidLoadFSStateNo
getLogger(ctx).WithField("fs", fs).WithError(err).Error("cannot list send step abstractions for filesystem")
} else {
s.didLoadFS[fs] = sendAbstractionsCacheDidLoadFSStateDone
s.abstractions = append(s.abstractions, onDiskAbs...)
getLogger(ctx).WithField("fs", fs).WithField("abstractions", onDiskAbs).Debug("loaded step abstractions for filesystem")
}
return
}
}
// caller should _not hold s.mtx
func (s *sendAbstractionsCache) tryLoadOnDiskSendAbstractionsImpl(ctx context.Context, fs string) ([]Abstraction, error) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
q := ListZFSHoldsAndBookmarksQuery{
FS: ListZFSHoldsAndBookmarksQueryFilesystemFilter{
FS: &fs,
},
JobID: nil,
What: AbstractionTypeSet{
AbstractionStepHold: true,
AbstractionStepBookmark: true,
AbstractionReplicationCursorBookmarkV2: true,
},
Concurrency: 1,
}
abs, absErrs, err := ListAbstractions(ctx, q)
if err != nil {
return nil, err
}
// safe to ignore absErrs here, this is best-effort cleanup
if len(absErrs) > 0 {
return nil, ListAbstractionsErrors(absErrs)
}
return abs, nil
}
func (s *sendAbstractionsCache) TryBatchDestroy(ctx context.Context, jobId JobID, fs string, keep func(a Abstraction) bool, check func(willDestroy []Abstraction)) {
// no s.mtx, we only use the public interface in this function
defer trace.WithSpanFromStackUpdateCtx(&ctx)()
allSendStepAbstractions := s.GetAndDeleteByJobIDAndFS(ctx, jobId, fs)
var obsoleteAbs []Abstraction
for _, a := range allSendStepAbstractions {
if !keep(a) {
obsoleteAbs = append(obsoleteAbs, a)
}
}
if check != nil {
check(obsoleteAbs)
}
hadErr := false
for res := range BatchDestroy(ctx, obsoleteAbs) {
if res.DestroyErr != nil {
hadErr = true
getLogger(ctx).
WithField("abstraction", res.Abstraction).
WithError(res.DestroyErr).
Error("cannot destroy stale send step abstraction")
} else {
getLogger(ctx).
WithField("abstraction", res.Abstraction).
Info("destroyed stale send step abstraction")
}
}
if hadErr {
s.InvalidateFSCache(fs)
}
}
+503
View File
@@ -0,0 +1,503 @@
package endpoint
import (
"context"
"fmt"
"regexp"
"sort"
"github.com/kr/pretty"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/zfs"
)
var stepHoldTagRE = regexp.MustCompile("^zrepl_STEP_J_(.+)")
func StepHoldTag(jobid JobID) (string, error) {
return stepHoldTagImpl(jobid.String())
}
func stepHoldTagImpl(jobid string) (string, error) {
t := fmt.Sprintf("zrepl_STEP_J_%s", jobid)
if err := zfs.ValidHoldTag(t); err != nil {
return "", err
}
return t, nil
}
// err != nil always means that the bookmark is not a step bookmark
func ParseStepHoldTag(tag string) (JobID, error) {
match := stepHoldTagRE.FindStringSubmatch(tag)
if match == nil {
return JobID{}, fmt.Errorf("parse hold tag: match regex %q", stepHoldTagRE)
}
jobID, err := MakeJobID(match[1])
if err != nil {
return JobID{}, errors.Wrap(err, "parse hold tag: invalid job id field")
}
return jobID, nil
}
const stepBookmarkNamePrefix = "zrepl_STEP"
// v must be validated by caller
func StepBookmarkName(fs string, guid uint64, id JobID) (string, error) {
return stepBookmarkNameImpl(fs, guid, id.String())
}
func stepBookmarkNameImpl(fs string, guid uint64, jobid string) (string, error) {
return makeJobAndGuidBookmarkName(stepBookmarkNamePrefix, fs, guid, jobid)
}
// name is the full bookmark name, including dataset path
//
// err != nil always means that the bookmark is not a step bookmark
func ParseStepBookmarkName(fullname string) (guid uint64, jobID JobID, err error) {
guid, jobID, err = parseJobAndGuidBookmarkName(fullname, stepBookmarkNamePrefix)
if err != nil {
err = errors.Wrap(err, "parse step bookmark name") // no shadow!
}
return guid, jobID, err
}
const replicationCursorBookmarkNamePrefix = "zrepl_CURSOR"
func ReplicationCursorBookmarkName(fs string, guid uint64, id JobID) (string, error) {
return replicationCursorBookmarkNameImpl(fs, guid, id.String())
}
func replicationCursorBookmarkNameImpl(fs string, guid uint64, jobid string) (string, error) {
return makeJobAndGuidBookmarkName(replicationCursorBookmarkNamePrefix, fs, guid, jobid)
}
var ErrV1ReplicationCursor = fmt.Errorf("bookmark name is a v1-replication cursor")
//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) {
// check for legacy cursors
{
if err := zfs.EntityNamecheck(fullname, zfs.EntityTypeBookmark); err != nil {
return 0, JobID{}, errors.Wrap(err, "parse replication cursor bookmark name")
}
_, _, name, err := zfs.DecomposeVersionString(fullname)
if err != nil {
return 0, JobID{}, errors.Wrap(err, "parse replication cursor bookmark name: decompose version string")
}
const V1ReplicationCursorBookmarkName = "zrepl_replication_cursor"
if name == V1ReplicationCursorBookmarkName {
return 0, JobID{}, ErrV1ReplicationCursor
}
}
guid, jobID, err := parseJobAndGuidBookmarkName(fullname, replicationCursorBookmarkNamePrefix)
if err != nil {
err = errors.Wrap(err, "parse replication cursor bookmark name") // no shadow
}
return guid, jobID, err
}
// may return nil for both values, indicating there is no cursor
func GetMostRecentReplicationCursorOfJob(ctx context.Context, fs string, jobID JobID) (*zfs.FilesystemVersion, error) {
fsp, err := zfs.NewDatasetPath(fs)
if err != nil {
return nil, err
}
candidates, err := GetReplicationCursors(ctx, fsp, jobID)
if err != nil || len(candidates) == 0 {
return nil, err
}
sort.Slice(candidates, func(i, j int) bool {
return candidates[i].CreateTXG < candidates[j].CreateTXG
})
mostRecent := candidates[len(candidates)-1]
return &mostRecent, nil
}
func GetReplicationCursors(ctx context.Context, fs *zfs.DatasetPath, jobID JobID) ([]zfs.FilesystemVersion, error) {
listOut := &ListHoldsAndBookmarksOutput{}
if err := listZFSHoldsAndBookmarksImplFS(ctx, listOut, fs); err != nil {
return nil, errors.Wrap(err, "get replication cursor: list bookmarks and holds")
}
if len(listOut.V1ReplicationCursors) > 0 {
getLogger(ctx).WithField("bookmark", pretty.Sprint(listOut.V1ReplicationCursors)).
Warn("found v1-replication cursor bookmarks, consider running migration 'replication-cursor:v1-v2' after successful replication with this zrepl version")
}
candidates := make([]zfs.FilesystemVersion, 0)
for _, v := range listOut.ReplicationCursorBookmarks {
zv := zfs.ZFSSendArgVersion{
RelName: "#" + v.Name,
GUID: v.Guid,
}
if err := zv.ValidateExists(ctx, v.FS); err != nil {
getLogger(ctx).WithError(err).WithField("bookmark", zv.FullPath(v.FS)).
Error("found invalid replication cursor bookmark")
continue
}
candidates = append(candidates, v.v)
}
return candidates, nil
}
// `target` is validated before replication cursor is set. if validation fails, the cursor is not moved.
//
// returns ErrBookmarkCloningNotSupported if version is a bookmark and bookmarking bookmarks is not supported by ZFS
func MoveReplicationCursor(ctx context.Context, fs string, target *zfs.ZFSSendArgVersion, jobID JobID) (destroyedCursors []zfs.FilesystemVersion, err error) {
if !target.IsSnapshot() {
return nil, zfs.ErrBookmarkCloningNotSupported
}
snapProps, err := target.ValidateExistsAndGetCheckedProps(ctx, fs)
if err != nil {
return nil, errors.Wrapf(err, "invalid replication cursor target %q (guid=%v)", target.RelName, target.GUID)
}
bookmarkname, err := ReplicationCursorBookmarkName(fs, snapProps.Guid, jobID)
if err != nil {
return nil, errors.Wrap(err, "determine replication cursor name")
}
// idempotently create bookmark (guid is encoded in it, hence we'll most likely add a new one
// cleanup the old one afterwards
err = zfs.ZFSBookmark(fs, *target, bookmarkname)
if err != nil {
if err == zfs.ErrBookmarkCloningNotSupported {
return nil, err // TODO go1.13 use wrapping
}
return nil, errors.Wrapf(err, "cannot create bookmark")
}
destroyedCursors, err = DestroyObsoleteReplicationCursors(ctx, fs, target, jobID)
if err != nil {
return nil, errors.Wrap(err, "destroy obsolete replication cursors")
}
return destroyedCursors, nil
}
func DestroyObsoleteReplicationCursors(ctx context.Context, fs string, target *zfs.ZFSSendArgVersion, jobID JobID) (destroyed []zfs.FilesystemVersion, err error) {
return destroyBookmarksOlderThan(ctx, fs, target, jobID, func(shortname string) (accept bool) {
_, parsedID, err := ParseReplicationCursorBookmarkName(fs + "#" + shortname)
return err == nil && parsedID == jobID
})
}
// idempotently hold / step-bookmark `version`
//
// returns ErrBookmarkCloningNotSupported if version is a bookmark and bookmarking bookmarks is not supported by ZFS
func HoldStep(ctx context.Context, fs string, v *zfs.ZFSSendArgVersion, jobID JobID) error {
if err := v.ValidateExists(ctx, fs); err != nil {
return err
}
if v.IsSnapshot() {
tag, err := StepHoldTag(jobID)
if err != nil {
return errors.Wrap(err, "step hold tag")
}
if err := zfs.ZFSHold(ctx, fs, *v, tag); err != nil {
return errors.Wrap(err, "step hold: zfs")
}
return nil
}
v.MustBeBookmark()
bmname, err := StepBookmarkName(fs, v.GUID, jobID)
if err != nil {
return errors.Wrap(err, "create step bookmark: determine bookmark name")
}
// idempotently create bookmark
err = zfs.ZFSBookmark(fs, *v, bmname)
if err != nil {
if err == zfs.ErrBookmarkCloningNotSupported {
// TODO we could actually try to find a local snapshot that has the requested GUID
// however, the replication algorithm prefers snapshots anyways, so this quest
// is most likely not going to be successful. Also, there's the possibility that
// the caller might want to filter what snapshots are eligibile, and this would
// complicate things even further.
return err // TODO go1.13 use wrapping
}
return errors.Wrap(err, "create step bookmark: zfs")
}
return nil
}
// idempotently release the step-hold on v if v is a snapshot
// or idempotently destroy the step-bookmark of v if v is a bookmark
//
// note that this operation leaves v itself untouched, unless v is the step-bookmark itself, in which case v is destroyed
//
// returns an instance of *zfs.DatasetDoesNotExist if `v` does not exist
func ReleaseStep(ctx context.Context, fs string, v *zfs.ZFSSendArgVersion, jobID JobID) error {
if err := v.ValidateExists(ctx, fs); err != nil {
return err
}
if v.IsSnapshot() {
tag, err := StepHoldTag(jobID)
if err != nil {
return errors.Wrap(err, "step release tag")
}
if err := zfs.ZFSRelease(ctx, tag, v.FullPath(fs)); err != nil {
return errors.Wrap(err, "step release: zfs")
}
return nil
}
v.MustBeBookmark()
bmname, err := StepBookmarkName(fs, v.GUID, jobID)
if err != nil {
return errors.Wrap(err, "step release: determine bookmark name")
}
// idempotently destroy bookmark
if err := zfs.ZFSDestroyIdempotent(bmname); err != nil {
return errors.Wrap(err, "step release: bookmark destroy: zfs")
}
return nil
}
// release {step holds, step bookmarks} earlier and including `mostRecent`
func ReleaseStepAll(ctx context.Context, fs string, mostRecent *zfs.ZFSSendArgVersion, jobID JobID) error {
if err := mostRecent.ValidateInMemory(fs); err != nil {
return err
}
tag, err := StepHoldTag(jobID)
if err != nil {
return errors.Wrap(err, "step release all: tag")
}
err = zfs.ZFSReleaseAllOlderAndIncludingGUID(ctx, fs, mostRecent.GUID, tag)
if err != nil {
return errors.Wrapf(err, "step release all: release holds older and including %q", mostRecent.FullPath(fs))
}
_, err = destroyBookmarksOlderThan(ctx, fs, mostRecent, jobID, func(shortname string) bool {
_, parsedId, parseErr := ParseStepBookmarkName(fs + "#" + shortname)
return parseErr == nil && parsedId == jobID
})
if err != nil {
return errors.Wrapf(err, "step release all: destroy bookmarks older than %q", mostRecent.FullPath(fs))
}
return nil
}
var lastReceivedHoldTagRE = regexp.MustCompile("^zrepl_last_received_J_(.+)$")
// err != nil always means that the bookmark is not a step bookmark
func ParseLastReceivedHoldTag(tag string) (JobID, error) {
match := lastReceivedHoldTagRE.FindStringSubmatch(tag)
if match == nil {
return JobID{}, errors.Errorf("parse last-received-hold tag: does not match regex %s", lastReceivedHoldTagRE.String())
}
jobId, err := MakeJobID(match[1])
if err != nil {
return JobID{}, errors.Wrap(err, "parse last-received-hold tag: invalid job id field")
}
return jobId, nil
}
func LastReceivedHoldTag(jobID JobID) (string, error) {
return lastReceivedHoldImpl(jobID.String())
}
func lastReceivedHoldImpl(jobid string) (string, error) {
tag := fmt.Sprintf("zrepl_last_received_J_%s", jobid)
if err := zfs.ValidHoldTag(tag); err != nil {
return "", err
}
return tag, nil
}
func MoveLastReceivedHold(ctx context.Context, fs string, to zfs.ZFSSendArgVersion, jobID JobID) error {
if err := to.ValidateExists(ctx, fs); err != nil {
return err
}
if err := zfs.EntityNamecheck(to.FullPath(fs), zfs.EntityTypeSnapshot); err != nil {
return err
}
tag, err := LastReceivedHoldTag(jobID)
if err != nil {
return errors.Wrap(err, "last-received-hold: hold tag")
}
// we never want to be without a hold
// => hold new one before releasing old hold
err = zfs.ZFSHold(ctx, fs, to, tag)
if err != nil {
return errors.Wrap(err, "last-received-hold: hold newly received")
}
err = zfs.ZFSReleaseAllOlderThanGUID(ctx, fs, to.GUID, tag)
if err != nil {
return errors.Wrap(err, "last-received-hold: release older holds")
}
return nil
}
type ListHoldsAndBookmarksOutputBookmarkV1ReplicationCursor struct {
FS string
Name string
}
type ListHoldsAndBookmarksOutput struct {
StepBookmarks []*ListHoldsAndBookmarksOutputBookmark
StepHolds []*ListHoldsAndBookmarksOutputHold
ReplicationCursorBookmarks []*ListHoldsAndBookmarksOutputBookmark
V1ReplicationCursors []*ListHoldsAndBookmarksOutputBookmarkV1ReplicationCursor
LastReceivedHolds []*ListHoldsAndBookmarksOutputHold
}
type ListHoldsAndBookmarksOutputBookmark struct {
FS, Name string
Guid uint64
JobID JobID
v zfs.FilesystemVersion
}
type ListHoldsAndBookmarksOutputHold struct {
FS string
Snap string
SnapGuid uint64
SnapCreateTXG uint64
Tag string
JobID JobID
}
// List all holds and bookmarks managed by endpoint
func ListZFSHoldsAndBookmarks(ctx context.Context, fsfilter zfs.DatasetFilter) (*ListHoldsAndBookmarksOutput, error) {
// initialize all fields so that JSON serializion of output looks pretty (see client/holds.go)
// however, listZFSHoldsAndBookmarksImplFS shouldn't rely on it
out := &ListHoldsAndBookmarksOutput{
StepBookmarks: make([]*ListHoldsAndBookmarksOutputBookmark, 0),
StepHolds: make([]*ListHoldsAndBookmarksOutputHold, 0),
ReplicationCursorBookmarks: make([]*ListHoldsAndBookmarksOutputBookmark, 0),
V1ReplicationCursors: make([]*ListHoldsAndBookmarksOutputBookmarkV1ReplicationCursor, 0),
LastReceivedHolds: make([]*ListHoldsAndBookmarksOutputHold, 0),
}
fss, err := zfs.ZFSListMapping(ctx, fsfilter)
if err != nil {
return nil, errors.Wrap(err, "list filesystems")
}
for _, fs := range fss {
err := listZFSHoldsAndBookmarksImplFS(ctx, out, fs)
if err != nil {
return nil, errors.Wrapf(err, "list holds and bookmarks on %q", fs.ToString())
}
}
return out, nil
}
func listZFSHoldsAndBookmarksImplFS(ctx context.Context, out *ListHoldsAndBookmarksOutput, fs *zfs.DatasetPath) error {
fsvs, err := zfs.ZFSListFilesystemVersions(fs, nil)
if err != nil {
return errors.Wrapf(err, "list filesystem versions of %q", fs)
}
for _, v := range fsvs {
switch v.Type {
case zfs.Bookmark:
listZFSHoldsAndBookmarksImplTryParseBookmark(ctx, out, fs, v)
case zfs.Snapshot:
holds, err := zfs.ZFSHolds(ctx, fs.ToString(), v.Name)
if err != nil {
return errors.Wrapf(err, "get holds of %q", v.ToAbsPath(fs))
}
for _, tag := range holds {
listZFSHoldsAndBookmarksImplSnapshotTryParseHold(ctx, out, fs, v, tag)
}
default:
continue
}
}
return nil
}
// pure function, err != nil always indicates parsing error
func listZFSHoldsAndBookmarksImplTryParseBookmark(ctx context.Context, out *ListHoldsAndBookmarksOutput, fs *zfs.DatasetPath, v zfs.FilesystemVersion) {
var err error
if v.Type != zfs.Bookmark {
panic("impl error")
}
fullname := v.ToAbsPath(fs)
bm := &ListHoldsAndBookmarksOutputBookmark{
FS: fs.ToString(), Name: v.Name, v: v,
}
bm.Guid, bm.JobID, err = ParseStepBookmarkName(fullname)
if err == nil {
out.StepBookmarks = append(out.StepBookmarks, bm)
return
}
bm.Guid, bm.JobID, err = ParseReplicationCursorBookmarkName(fullname)
if err == nil {
out.ReplicationCursorBookmarks = append(out.ReplicationCursorBookmarks, bm)
return
} else if err == ErrV1ReplicationCursor {
v1rc := &ListHoldsAndBookmarksOutputBookmarkV1ReplicationCursor{
FS: fs.ToString(), Name: v.Name,
}
out.V1ReplicationCursors = append(out.V1ReplicationCursors, v1rc)
return
}
}
// pure function, err != nil always indicates parsing error
func listZFSHoldsAndBookmarksImplSnapshotTryParseHold(ctx context.Context, out *ListHoldsAndBookmarksOutput, fs *zfs.DatasetPath, v zfs.FilesystemVersion, holdTag string) {
var err error
if v.Type != zfs.Snapshot {
panic("impl error")
}
hold := &ListHoldsAndBookmarksOutputHold{
FS: fs.ToString(),
Snap: v.Name,
SnapGuid: v.Guid,
SnapCreateTXG: v.CreateTXG,
Tag: holdTag,
}
hold.JobID, err = ParseStepHoldTag(holdTag)
if err == nil {
out.StepHolds = append(out.StepHolds, hold)
return
}
hold.JobID, err = ParseLastReceivedHoldTag(holdTag)
if err == nil {
out.LastReceivedHolds = append(out.LastReceivedHolds, hold)
return
}
}
-864
View File
@@ -1,864 +0,0 @@
package endpoint
import (
"context"
"encoding/json"
"fmt"
"math"
"sort"
"strings"
"sync"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/util/envconst"
"github.com/zrepl/zrepl/util/semaphore"
"github.com/zrepl/zrepl/zfs"
)
type AbstractionType string
// Implementation note:
// There are a lot of exhaustive switches on AbstractionType in the code base.
// When adding a new abstraction type, make sure to search and update them!
const (
AbstractionStepBookmark AbstractionType = "step-bookmark"
AbstractionStepHold AbstractionType = "step-hold"
AbstractionLastReceivedHold AbstractionType = "last-received-hold"
AbstractionReplicationCursorBookmarkV1 AbstractionType = "replication-cursor-bookmark-v1"
AbstractionReplicationCursorBookmarkV2 AbstractionType = "replication-cursor-bookmark-v2"
)
var AbstractionTypesAll = map[AbstractionType]bool{
AbstractionStepBookmark: true,
AbstractionStepHold: true,
AbstractionLastReceivedHold: true,
AbstractionReplicationCursorBookmarkV1: true,
AbstractionReplicationCursorBookmarkV2: true,
}
// Implementation Note:
// Whenever you add a new accessor, adjust AbstractionJSON.MarshalJSON accordingly
type Abstraction interface {
GetType() AbstractionType
GetFS() string
GetName() string
GetFullPath() string
GetJobID() *JobID // may return nil if the abstraction does not have a JobID
GetCreateTXG() uint64
GetFilesystemVersion() zfs.FilesystemVersion
String() string
// destroy the abstraction: either releases the hold or destroys the bookmark
Destroy(context.Context) error
json.Marshaler
}
func AbstractionEquals(a, b Abstraction) bool {
if (a != nil) != (b != nil) {
return false
}
if a == nil && b == nil {
return true
}
var aJobId, bJobId JobID
if aJid := a.GetJobID(); aJid != nil {
aJobId = *aJid
}
if bJid := b.GetJobID(); bJid != nil {
bJobId = *bJid
}
return a.GetType() == b.GetType() &&
a.GetFS() == b.GetFS() &&
a.GetName() == b.GetName() &&
a.GetFullPath() == b.GetFullPath() &&
aJobId == bJobId &&
a.GetCreateTXG() == b.GetCreateTXG() &&
zfs.FilesystemVersionEqualIdentity(a.GetFilesystemVersion(), b.GetFilesystemVersion()) &&
a.String() == b.String()
}
func (t AbstractionType) Validate() error {
switch t {
case AbstractionStepBookmark:
return nil
case AbstractionStepHold:
return nil
case AbstractionLastReceivedHold:
return nil
case AbstractionReplicationCursorBookmarkV1:
return nil
case AbstractionReplicationCursorBookmarkV2:
return nil
default:
return errors.Errorf("unknown abstraction type %q", t)
}
}
func (t AbstractionType) MustValidate() error {
if err := t.Validate(); err != nil {
panic(err)
}
return nil
}
type AbstractionJSON struct{ Abstraction }
var _ json.Marshaler = (*AbstractionJSON)(nil)
func (a AbstractionJSON) MarshalJSON() ([]byte, error) {
type S struct {
Type AbstractionType
FS string
Name string
FullPath string
JobID *JobID // may return nil if the abstraction does not have a JobID
CreateTXG uint64
FilesystemVersion zfs.FilesystemVersion
String string
}
v := S{
Type: a.Abstraction.GetType(),
FS: a.Abstraction.GetFS(),
Name: a.Abstraction.GetName(),
FullPath: a.Abstraction.GetFullPath(),
JobID: a.Abstraction.GetJobID(),
CreateTXG: a.Abstraction.GetCreateTXG(),
FilesystemVersion: a.Abstraction.GetFilesystemVersion(),
String: a.Abstraction.String(),
}
return json.Marshal(v)
}
type AbstractionTypeSet map[AbstractionType]bool
func AbstractionTypeSetFromStrings(sts []string) (AbstractionTypeSet, error) {
ats := make(map[AbstractionType]bool, len(sts))
for i, t := range sts {
at := AbstractionType(t)
if err := at.Validate(); err != nil {
return nil, errors.Wrapf(err, "invalid abstraction type #%d %q", i+1, t)
}
ats[at] = true
}
return ats, nil
}
func (s AbstractionTypeSet) ContainsAll(q AbstractionTypeSet) bool {
for k := range q {
if _, ok := s[k]; !ok {
return false
}
}
return true
}
func (s AbstractionTypeSet) ContainsAnyOf(q AbstractionTypeSet) bool {
for k := range q {
if _, ok := s[k]; ok {
return true
}
}
return false
}
func (s AbstractionTypeSet) String() string {
sts := make([]string, 0, len(s))
for i := range s {
sts = append(sts, string(i))
}
sts = sort.StringSlice(sts)
return strings.Join(sts, ",")
}
func (s AbstractionTypeSet) Validate() error {
for k := range s {
if err := k.Validate(); err != nil {
return err
}
}
return nil
}
type BookmarkExtractor func(fs *zfs.DatasetPath, v zfs.FilesystemVersion) Abstraction
// returns nil if the abstraction type is not bookmark-based
func (t AbstractionType) BookmarkExtractor() BookmarkExtractor {
switch t {
case AbstractionStepBookmark:
return StepBookmarkExtractor
case AbstractionReplicationCursorBookmarkV1:
return ReplicationCursorV1Extractor
case AbstractionReplicationCursorBookmarkV2:
return ReplicationCursorV2Extractor
case AbstractionStepHold:
return nil
case AbstractionLastReceivedHold:
return nil
default:
panic(fmt.Sprintf("unimpl: %q", t))
}
}
type HoldExtractor = func(fs *zfs.DatasetPath, v zfs.FilesystemVersion, tag string) Abstraction
// returns nil if the abstraction type is not hold-based
func (t AbstractionType) HoldExtractor() HoldExtractor {
switch t {
case AbstractionStepBookmark:
return nil
case AbstractionReplicationCursorBookmarkV1:
return nil
case AbstractionReplicationCursorBookmarkV2:
return nil
case AbstractionStepHold:
return StepHoldExtractor
case AbstractionLastReceivedHold:
return LastReceivedHoldExtractor
default:
panic(fmt.Sprintf("unimpl: %q", t))
}
}
type ListZFSHoldsAndBookmarksQuery struct {
FS ListZFSHoldsAndBookmarksQueryFilesystemFilter
// What abstraction types should match (any contained in the set)
What AbstractionTypeSet
// The output for the query must satisfy _all_ (AND) requirements of all fields in this query struct.
// if not nil: JobID of the hold or bookmark in question must be equal
// else: JobID of the hold or bookmark can be any value
JobID *JobID
// zero-value means any CreateTXG is acceptable
CreateTXG CreateTXGRange
// Number of concurrently queried filesystems. Must be >= 1
Concurrency int64
}
type CreateTXGRangeBound struct {
CreateTXG uint64
Inclusive *zfs.NilBool // must not be nil
}
// A non-empty range of CreateTXGs
//
// If both Since and Until are nil, any CreateTXG is acceptable
type CreateTXGRange struct {
// if not nil: The hold's snapshot or the bookmark's createtxg must be greater than (or equal) Since
// else: CreateTXG of the hold or bookmark can be any value accepted by Until
Since *CreateTXGRangeBound
// if not nil: The hold's snapshot or the bookmark's createtxg must be less than (or equal) Until
// else: CreateTXG of the hold or bookmark can be any value accepted by Since
Until *CreateTXGRangeBound
}
// FS == nil XOR Filter == nil
type ListZFSHoldsAndBookmarksQueryFilesystemFilter struct {
FS *string
Filter zfs.DatasetFilter
}
func (q *ListZFSHoldsAndBookmarksQuery) Validate() error {
if err := q.FS.Validate(); err != nil {
return errors.Wrap(err, "FS")
}
if q.JobID != nil {
q.JobID.MustValidate() // FIXME
}
if err := q.CreateTXG.Validate(); err != nil {
return errors.Wrap(err, "CreateTXGRange")
}
if err := q.What.Validate(); err != nil {
return err
}
if q.Concurrency < 1 {
return errors.New("Concurrency must be >= 1")
}
return nil
}
var createTXGRangeBoundAllowCreateTXG0 = envconst.Bool("ZREPL_ENDPOINT_LIST_ABSTRACTIONS_QUERY_CREATETXG_RANGE_BOUND_ALLOW_0", false)
func (i *CreateTXGRangeBound) Validate() error {
if err := i.Inclusive.Validate(); err != nil {
return errors.Wrap(err, "Inclusive")
}
if i.CreateTXG == 0 && !createTXGRangeBoundAllowCreateTXG0 {
return errors.New("CreateTXG must be non-zero")
}
return nil
}
func (f *ListZFSHoldsAndBookmarksQueryFilesystemFilter) Validate() error {
if f == nil {
return nil
}
fsSet := f.FS != nil
filterSet := f.Filter != nil
if fsSet && filterSet || !fsSet && !filterSet {
return fmt.Errorf("must set FS or Filter field, but fsIsSet=%v and filterIsSet=%v", fsSet, filterSet)
}
if fsSet {
if err := zfs.EntityNamecheck(*f.FS, zfs.EntityTypeFilesystem); err != nil {
return errors.Wrap(err, "FS invalid")
}
}
return nil
}
func (f *ListZFSHoldsAndBookmarksQueryFilesystemFilter) Filesystems(ctx context.Context) ([]string, error) {
if err := f.Validate(); err != nil {
panic(err)
}
if f.FS != nil {
return []string{*f.FS}, nil
}
if f.Filter != nil {
dps, err := zfs.ZFSListMapping(ctx, f.Filter)
if err != nil {
return nil, err
}
fss := make([]string, len(dps))
for i, dp := range dps {
fss[i] = dp.ToString()
}
return fss, nil
}
panic("unreachable")
}
func (r *CreateTXGRange) Validate() error {
if r.Since != nil {
if err := r.Since.Validate(); err != nil {
return errors.Wrap(err, "Since")
}
}
if r.Until != nil {
if err := r.Until.Validate(); err != nil {
return errors.Wrap(err, "Until")
}
}
if _, err := r.effectiveBounds(); err != nil {
return errors.Wrapf(err, "specified range %s is semantically invalid", r)
}
return nil
}
// inclusive-inclusive bounds
type effectiveBounds struct {
sinceInclusive uint64
sinceUnbounded bool
untilInclusive uint64
untilUnbounded bool
}
// callers must have validated r.Since and r.Until before calling this method
func (r *CreateTXGRange) effectiveBounds() (bounds effectiveBounds, err error) {
bounds.sinceUnbounded = r.Since == nil
bounds.untilUnbounded = r.Until == nil
if r.Since == nil && r.Until == nil {
return bounds, nil
}
if r.Since != nil {
bounds.sinceInclusive = r.Since.CreateTXG
if !r.Since.Inclusive.B {
if r.Since.CreateTXG == math.MaxUint64 {
return bounds, errors.Errorf("Since-exclusive (%v) must be less than math.MaxUint64 (%v)",
r.Since.CreateTXG, uint64(math.MaxUint64))
}
bounds.sinceInclusive++
}
}
if r.Until != nil {
bounds.untilInclusive = r.Until.CreateTXG
if !r.Until.Inclusive.B {
if r.Until.CreateTXG == 0 {
return bounds, errors.Errorf("Until-exclusive (%v) must be greater than 0", r.Until.CreateTXG)
}
bounds.untilInclusive--
}
}
if !bounds.sinceUnbounded && !bounds.untilUnbounded {
if bounds.sinceInclusive >= bounds.untilInclusive {
return bounds, errors.Errorf("effective range bounds are [%v,%v] which is empty or invalid", bounds.sinceInclusive, bounds.untilInclusive)
}
// fallthrough
}
return bounds, nil
}
func (r *CreateTXGRange) String() string {
var buf strings.Builder
if r.Since == nil {
fmt.Fprintf(&buf, "~")
} else {
if err := r.Since.Inclusive.Validate(); err != nil {
fmt.Fprintf(&buf, "?")
} else if r.Since.Inclusive.B {
fmt.Fprintf(&buf, "[")
} else {
fmt.Fprintf(&buf, "(")
}
fmt.Fprintf(&buf, "%d", r.Since.CreateTXG)
}
fmt.Fprintf(&buf, ",")
if r.Until == nil {
fmt.Fprintf(&buf, "~")
} else {
fmt.Fprintf(&buf, "%d", r.Until.CreateTXG)
if err := r.Until.Inclusive.Validate(); err != nil {
fmt.Fprintf(&buf, "?")
} else if r.Until.Inclusive.B {
fmt.Fprintf(&buf, "]")
} else {
fmt.Fprintf(&buf, ")")
}
}
return buf.String()
}
// panics if not .Validate()
func (r *CreateTXGRange) IsUnbounded() bool {
if err := r.Validate(); err != nil {
panic(err)
}
bounds, err := r.effectiveBounds()
if err != nil {
panic(err)
}
return bounds.sinceUnbounded && bounds.untilUnbounded
}
// panics if not .Validate()
func (r *CreateTXGRange) Contains(qCreateTxg uint64) bool {
if err := r.Validate(); err != nil {
panic(err)
}
bounds, err := r.effectiveBounds()
if err != nil {
panic(err)
}
sinceMatches := bounds.sinceUnbounded || bounds.sinceInclusive <= qCreateTxg
untilMatches := bounds.untilUnbounded || qCreateTxg <= bounds.untilInclusive
return sinceMatches && untilMatches
}
type ListAbstractionsError struct {
FS string
Snap string
What string
Err error
}
func (e ListAbstractionsError) Error() string {
if e.FS == "" {
return fmt.Sprintf("list endpoint abstractions: %s: %s", e.What, e.Err)
} else {
v := e.FS
if e.Snap != "" {
v = fmt.Sprintf("%s@%s", e.FS, e.Snap)
}
return fmt.Sprintf("list endpoint abstractions on %q: %s: %s", v, e.What, e.Err)
}
}
type putListAbstractionErr func(err error, fs string, what string)
type putListAbstraction func(a Abstraction)
type ListAbstractionsErrors []ListAbstractionsError
func (e ListAbstractionsErrors) Error() string {
if len(e) == 0 {
panic(e)
}
if len(e) == 1 {
return fmt.Sprintf("list endpoint abstractions: %s", e[0])
}
msgs := make([]string, len(e))
for i := range e {
msgs[i] = e[i].Error()
}
return fmt.Sprintf("list endpoint abstractions: multiple errors:\n%s", strings.Join(msgs, "\n"))
}
func ListAbstractions(ctx context.Context, query ListZFSHoldsAndBookmarksQuery) (out []Abstraction, outErrs []ListAbstractionsError, err error) {
outChan, outErrsChan, err := ListAbstractionsStreamed(ctx, query)
if err != nil {
return nil, nil, err
}
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for a := range outChan {
out = append(out, a)
}
}()
wg.Add(1)
go func() {
defer wg.Done()
for err := range outErrsChan {
outErrs = append(outErrs, err)
}
}()
wg.Wait()
return out, outErrs, nil
}
// 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) {
// 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")
}
fss, err := query.FS.Filesystems(ctx)
if err != nil {
return nil, nil, errors.Wrap(err, "list filesystems")
}
outErrs := make(chan ListAbstractionsError)
out := make(chan Abstraction)
errCb := func(err error, fs string, what string) {
outErrs <- ListAbstractionsError{Err: err, FS: fs, What: what}
}
emitAbstraction := func(a Abstraction) {
jobIdMatches := query.JobID == nil || a.GetJobID() == nil || *a.GetJobID() == *query.JobID
createTXGMatches := query.CreateTXG.Contains(a.GetCreateTXG())
if jobIdMatches && createTXGMatches {
out <- a
}
}
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)
_, add, wait := trace.WithTaskGroup(ctx, "list-abstractions-impl-fs")
defer wait()
for i := range fss {
add(func(ctx context.Context) {
g, err := sem.Acquire(ctx)
if err != nil {
errCb(err, fss[i], err.Error())
return
}
func() {
defer g.Release()
listAbstractionsImplFS(ctx, fss[i], &query, emitAbstraction, errCb)
}()
})
}
}()
return out, outErrs, nil
}
func listAbstractionsImplFS(ctx context.Context, fs string, query *ListZFSHoldsAndBookmarksQuery, emitCandidate putListAbstraction, errCb putListAbstractionErr) {
fsp, err := zfs.NewDatasetPath(fs)
if err != nil {
panic(err)
}
if len(query.What) == 0 {
return
}
whatTypes := zfs.VersionTypeSet{}
for what := range query.What {
if e := what.BookmarkExtractor(); e != nil {
whatTypes[zfs.Bookmark] = true
}
if e := what.HoldExtractor(); e != nil {
whatTypes[zfs.Snapshot] = true
}
}
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, fsp, zfs.ListFilesystemVersionsOptions{
Types: whatTypes,
})
if err != nil {
errCb(err, fs, "list filesystem versions")
return
}
for at := range query.What {
bmE := at.BookmarkExtractor()
holdE := at.HoldExtractor()
if bmE == nil && holdE == nil || bmE != nil && holdE != nil {
panic("implementation error: extractors misconfigured for " + at)
}
for _, v := range fsvs {
if v.Type == zfs.Bookmark && bmE != nil {
if a := bmE(fsp, v); a != nil {
emitCandidate(a)
}
}
if v.Type == zfs.Snapshot && holdE != nil && query.CreateTXG.Contains(v.GetCreateTXG()) && (!v.UserRefs.Valid || v.UserRefs.Value > 0) { // FIXME review v.UserRefsValid
holds, err := zfs.ZFSHolds(ctx, fsp.ToString(), v.Name)
if err != nil {
errCb(err, v.ToAbsPath(fsp), "get hold on snap")
continue
}
for _, tag := range holds {
if a := holdE(fsp, v, tag); a != nil {
emitCandidate(a)
}
}
}
}
}
}
type BatchDestroyResult struct {
Abstraction
DestroyErr error
}
var _ json.Marshaler = (*BatchDestroyResult)(nil)
func (r BatchDestroyResult) MarshalJSON() ([]byte, error) {
err := ""
if r.DestroyErr != nil {
err = r.DestroyErr.Error()
}
s := struct {
Abstraction AbstractionJSON
DestroyErr string
}{
AbstractionJSON{r.Abstraction},
err,
}
return json.Marshal(s)
}
func BatchDestroy(ctx context.Context, abs []Abstraction) <-chan BatchDestroyResult {
// hold-based batching: per snapshot
// bookmark-based batching: none possible via CLI
// => not worth the trouble for now, will be worth it once we start using channel programs
// => TODO: actual batching using channel programs
res := make(chan BatchDestroyResult, len(abs))
go func() {
for _, a := range abs {
res <- BatchDestroyResult{
a,
a.Destroy(ctx),
}
}
close(res)
}()
return res
}
type StalenessInfo struct {
ConstructedWithQuery ListZFSHoldsAndBookmarksQuery
Live []Abstraction
Stale []Abstraction
}
type fsAndJobId struct {
fs string
jobId JobID
}
type ListStaleQueryError struct {
error
}
// returns *ListStaleQueryError if the given query cannot be used for determining staleness info
func ListStale(ctx context.Context, q ListZFSHoldsAndBookmarksQuery) (*StalenessInfo, error) {
if !q.CreateTXG.IsUnbounded() {
// we must determine the most recent step per FS, can't allow that
return nil, &ListStaleQueryError{errors.New("ListStale cannot have Until != nil set on query")}
}
// if asking for step holds, must also as for step bookmarks (same kind of abstraction)
// as well as replication cursor bookmarks (for firstNotStale)
ifAnyThenAll := AbstractionTypeSet{
AbstractionStepHold: true,
AbstractionStepBookmark: true,
AbstractionReplicationCursorBookmarkV2: true,
}
if q.What.ContainsAnyOf(ifAnyThenAll) && !q.What.ContainsAll(ifAnyThenAll) {
return nil, &ListStaleQueryError{errors.Errorf("ListStale requires query to ask for all of %s", ifAnyThenAll.String())}
}
// ----------------- done validating query for listStaleFiltering -----------------------
qAbs, absErr, err := ListAbstractions(ctx, q)
if err != nil {
return nil, err
}
if len(absErr) > 0 {
// can't go on here because we can't determine the most recent step
return nil, ListAbstractionsErrors(absErr)
}
si := listStaleFiltering(qAbs, q.CreateTXG.Since)
si.ConstructedWithQuery = q
return si, nil
}
type fsAjobAtype struct {
fsAndJobId
Type AbstractionType
}
// For step holds and bookmarks, only those older than the most recent replication cursor
// of their (filesystem,job) is considered because younger ones cannot be stale by definition
// (if we destroy them, we might actually lose the hold on the `To` for an ongoing incremental replication)
//
// For replication cursors and last-received-holds, only the most recent one is kept.
//
// the returned StalenessInfo.ConstructedWithQuery is not set
func listStaleFiltering(abs []Abstraction, sinceBound *CreateTXGRangeBound) *StalenessInfo {
var noJobId []Abstraction
by := make(map[fsAjobAtype][]Abstraction)
for _, a := range abs {
if a.GetJobID() == nil {
noJobId = append(noJobId, a)
continue
}
faj := fsAjobAtype{fsAndJobId{a.GetFS(), *a.GetJobID()}, a.GetType()}
l := by[faj]
l = append(l, a)
by[faj] = l
}
type stepFirstNotStaleCandidate struct {
cursor *Abstraction
step *Abstraction
}
stepFirstNotStaleCandidates := make(map[fsAndJobId]stepFirstNotStaleCandidate) // empty map => will always return nil
for _, a := range abs {
if a.GetJobID() == nil {
continue // already put those into always-live list noJobId in above loop
}
key := fsAndJobId{a.GetFS(), *a.GetJobID()}
c := stepFirstNotStaleCandidates[key]
switch a.GetType() {
// stepFirstNotStaleCandidate.cursor
case AbstractionReplicationCursorBookmarkV2:
if c.cursor == nil || (*c.cursor).GetCreateTXG() < a.GetCreateTXG() {
a := a
c.cursor = &a
}
// stepFirstNotStaleCandidate.step
case AbstractionStepBookmark:
fallthrough
case AbstractionStepHold:
if c.step == nil || (*c.step).GetCreateTXG() < a.GetCreateTXG() {
a := a
c.step = &a
}
// not interested in the others
default:
continue // not relevant
}
stepFirstNotStaleCandidates[key] = c
}
ret := &StalenessInfo{
Live: noJobId,
Stale: []Abstraction{},
}
for k := range by {
l := by[k]
if k.Type == AbstractionStepHold || k.Type == AbstractionStepBookmark {
// all older than the most recent cursor are stale, others are always live
// if we don't have a replication cursor yet, use untilBound = nil
// to consider all steps stale (...at first)
var untilBound *CreateTXGRangeBound
{
sfnsc := stepFirstNotStaleCandidates[k.fsAndJobId]
// if there's a replication cursor, use it as a cutoff between live and stale
// if there's none, we are in initial replication and only need to keep
// the most recent step hold live, since that's what our initial replication strategy
// uses (both initially and on resume)
// (FIXME hardcoded replication strategy)
if sfnsc.cursor != nil {
untilBound = &CreateTXGRangeBound{
CreateTXG: (*sfnsc.cursor).GetCreateTXG(),
// if we have a cursor, can throw away step hold on both From and To
Inclusive: &zfs.NilBool{B: true},
}
} else if sfnsc.step != nil {
untilBound = &CreateTXGRangeBound{
CreateTXG: (*sfnsc.step).GetCreateTXG(),
// if we don't have a cursor, the step most recent step hold is our
// initial replication cursor and it's possibly still live (interrupted initial replication)
Inclusive: &zfs.NilBool{B: false},
}
} else {
untilBound = nil // consider everything stale
}
}
staleRange := CreateTXGRange{
Since: sinceBound,
Until: untilBound,
}
// partition by staleRange
for _, a := range l {
if staleRange.Contains(a.GetCreateTXG()) {
ret.Stale = append(ret.Stale, a)
} else {
ret.Live = append(ret.Live, a)
}
}
} else if k.Type == AbstractionReplicationCursorBookmarkV2 || k.Type == AbstractionLastReceivedHold {
// all but the most recent are stale by definition (we always _move_ them)
// NOTE: must not use firstNotStale in this branch, not computed for these types
// sort descending (highest createtxg first), then cut off
sort.Slice(l, func(i, j int) bool {
return l[i].GetCreateTXG() > l[j].GetCreateTXG()
})
if len(l) > 0 {
ret.Live = append(ret.Live, l[0])
ret.Stale = append(ret.Stale, l[1:]...)
}
} else {
ret.Live = append(ret.Live, l...)
}
}
return ret
}
@@ -1,355 +0,0 @@
package endpoint
import (
"context"
"encoding/json"
"fmt"
"regexp"
"sort"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/util/errorarray"
"github.com/zrepl/zrepl/zfs"
)
const replicationCursorBookmarkNamePrefix = "zrepl_CURSOR"
func ReplicationCursorBookmarkName(fs string, guid uint64, id JobID) (string, error) {
return replicationCursorBookmarkNameImpl(fs, guid, id.String())
}
func replicationCursorBookmarkNameImpl(fs string, guid uint64, jobid string) (string, error) {
return makeJobAndGuidBookmarkName(replicationCursorBookmarkNamePrefix, fs, guid, jobid)
}
var ErrV1ReplicationCursor = fmt.Errorf("bookmark name is a v1-replication cursor")
//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) {
// check for legacy cursors
{
if err := zfs.EntityNamecheck(fullname, zfs.EntityTypeBookmark); err != nil {
return 0, JobID{}, errors.Wrap(err, "parse replication cursor bookmark name")
}
_, _, name, err := zfs.DecomposeVersionString(fullname)
if err != nil {
return 0, JobID{}, errors.Wrap(err, "parse replication cursor bookmark name: decompose version string")
}
const V1ReplicationCursorBookmarkName = "zrepl_replication_cursor"
if name == V1ReplicationCursorBookmarkName {
return 0, JobID{}, ErrV1ReplicationCursor
}
// fallthrough to main parser
}
guid, jobID, err := parseJobAndGuidBookmarkName(fullname, replicationCursorBookmarkNamePrefix)
if err != nil {
err = errors.Wrap(err, "parse replication cursor bookmark name") // no shadow
}
return guid, jobID, err
}
// may return nil for both values, indicating there is no cursor
func GetMostRecentReplicationCursorOfJob(ctx context.Context, fs string, jobID JobID) (*zfs.FilesystemVersion, error) {
fsp, err := zfs.NewDatasetPath(fs)
if err != nil {
return nil, err
}
candidates, err := GetReplicationCursors(ctx, fsp, jobID)
if err != nil || len(candidates) == 0 {
return nil, err
}
sort.Slice(candidates, func(i, j int) bool {
return candidates[i].CreateTXG < candidates[j].CreateTXG
})
mostRecent := candidates[len(candidates)-1]
return &mostRecent, nil
}
func GetReplicationCursors(ctx context.Context, dp *zfs.DatasetPath, jobID JobID) ([]zfs.FilesystemVersion, error) {
fs := dp.ToString()
q := ListZFSHoldsAndBookmarksQuery{
FS: ListZFSHoldsAndBookmarksQueryFilesystemFilter{FS: &fs},
What: map[AbstractionType]bool{
AbstractionReplicationCursorBookmarkV1: true,
AbstractionReplicationCursorBookmarkV2: true,
},
JobID: &jobID,
CreateTXG: CreateTXGRange{},
Concurrency: 1,
}
abs, absErr, err := ListAbstractions(ctx, q)
if err != nil {
return nil, errors.Wrap(err, "get replication cursor: list bookmarks and holds")
}
if len(absErr) > 0 {
return nil, ListAbstractionsErrors(absErr)
}
var v1, v2 []Abstraction
for _, a := range abs {
switch a.GetType() {
case AbstractionReplicationCursorBookmarkV1:
v1 = append(v1, a)
case AbstractionReplicationCursorBookmarkV2:
v2 = append(v2, a)
default:
panic("unexpected abstraction: " + a.GetType())
}
}
if len(v1) > 0 {
getLogger(ctx).WithField("bookmark", v1).
Warn("found v1-replication cursor bookmarks, consider running migration 'replication-cursor:v1-v2' after successful replication with this zrepl version")
}
candidates := make([]zfs.FilesystemVersion, 0)
for _, v := range v2 {
candidates = append(candidates, v.GetFilesystemVersion())
}
return candidates, nil
}
// idempotently create a replication cursor targeting `target`
//
// returns ErrBookmarkCloningNotSupported if version is a bookmark and bookmarking bookmarks is not supported by ZFS
func CreateReplicationCursor(ctx context.Context, fs string, target zfs.FilesystemVersion, jobID JobID) (a Abstraction, err error) {
bookmarkname, err := ReplicationCursorBookmarkName(fs, target.GetGuid(), jobID)
if err != nil {
return nil, errors.Wrap(err, "determine replication cursor name")
}
if target.IsBookmark() && target.GetName() == bookmarkname {
return &bookmarkBasedAbstraction{
Type: AbstractionReplicationCursorBookmarkV2,
FS: fs,
FilesystemVersion: target,
JobID: jobID,
}, nil
}
if !target.IsSnapshot() {
return nil, zfs.ErrBookmarkCloningNotSupported
}
// idempotently create bookmark (guid is encoded in it)
cursorBookmark, err := zfs.ZFSBookmark(ctx, fs, target, bookmarkname)
if err != nil {
if err == zfs.ErrBookmarkCloningNotSupported {
return nil, err // TODO go1.13 use wrapping
}
return nil, errors.Wrapf(err, "cannot create bookmark")
}
return &bookmarkBasedAbstraction{
Type: AbstractionReplicationCursorBookmarkV2,
FS: fs,
FilesystemVersion: cursorBookmark,
JobID: jobID,
}, nil
}
const (
ReplicationCursorBookmarkNamePrefix = "zrepl_last_received_J_"
)
var lastReceivedHoldTagRE = regexp.MustCompile("^zrepl_last_received_J_(.+)$")
// err != nil always means that the bookmark is not a step bookmark
func ParseLastReceivedHoldTag(tag string) (JobID, error) {
match := lastReceivedHoldTagRE.FindStringSubmatch(tag)
if match == nil {
return JobID{}, errors.Errorf("parse last-received-hold tag: does not match regex %s", lastReceivedHoldTagRE.String())
}
jobId, err := MakeJobID(match[1])
if err != nil {
return JobID{}, errors.Wrap(err, "parse last-received-hold tag: invalid job id field")
}
return jobId, nil
}
func LastReceivedHoldTag(jobID JobID) (string, error) {
return lastReceivedHoldImpl(jobID.String())
}
func lastReceivedHoldImpl(jobid string) (string, error) {
tag := fmt.Sprintf("%s%s", ReplicationCursorBookmarkNamePrefix, jobid)
if err := zfs.ValidHoldTag(tag); err != nil {
return "", err
}
return tag, nil
}
func CreateLastReceivedHold(ctx context.Context, fs string, to zfs.FilesystemVersion, jobID JobID) (Abstraction, error) {
if !to.IsSnapshot() {
return nil, errors.Errorf("last-received-hold: target must be a snapshot: %s", to.FullPath(fs))
}
tag, err := LastReceivedHoldTag(jobID)
if err != nil {
return nil, errors.Wrap(err, "last-received-hold: hold tag")
}
// we never want to be without a hold
// => hold new one before releasing old hold
err = zfs.ZFSHold(ctx, fs, to, tag)
if err != nil {
return nil, errors.Wrap(err, "last-received-hold: hold newly received")
}
return &holdBasedAbstraction{
Type: AbstractionLastReceivedHold,
FS: fs,
FilesystemVersion: to,
JobID: jobID,
Tag: tag,
}, nil
}
func MoveLastReceivedHold(ctx context.Context, fs string, to zfs.FilesystemVersion, jobID JobID) error {
_, err := CreateLastReceivedHold(ctx, fs, to, jobID)
if err != nil {
return err
}
q := ListZFSHoldsAndBookmarksQuery{
What: AbstractionTypeSet{
AbstractionLastReceivedHold: true,
},
FS: ListZFSHoldsAndBookmarksQueryFilesystemFilter{
FS: &fs,
},
JobID: &jobID,
CreateTXG: CreateTXGRange{
Since: nil,
Until: &CreateTXGRangeBound{
CreateTXG: to.GetCreateTXG(),
Inclusive: &zfs.NilBool{B: false},
},
},
Concurrency: 1,
}
abs, absErrs, err := ListAbstractions(ctx, q)
if err != nil {
return errors.Wrap(err, "last-received-hold: list")
}
if len(absErrs) > 0 {
return errors.Wrap(ListAbstractionsErrors(absErrs), "last-received-hold: list")
}
getLogger(ctx).WithField("last-received-holds", fmt.Sprintf("%s", abs)).Debug("releasing last-received-holds")
var errs []error
for res := range BatchDestroy(ctx, abs) {
log := getLogger(ctx).
WithField("last-received-hold", res.Abstraction)
if res.DestroyErr != nil {
errs = append(errs, res.DestroyErr)
log.WithError(err).
Error("cannot release last-received-hold")
} else {
log.Info("released last-received-hold")
}
}
if len(errs) == 0 {
return nil
} else {
return errorarray.Wrap(errs, "last-received-hold: release")
}
}
func ReplicationCursorV2Extractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion) (_ Abstraction) {
if v.Type != zfs.Bookmark {
panic("impl error")
}
fullname := v.ToAbsPath(fs)
guid, jobid, err := ParseReplicationCursorBookmarkName(fullname)
if err == nil {
if guid != v.Guid {
// TODO log this possibly tinkered-with bookmark
return nil
}
return &bookmarkBasedAbstraction{
Type: AbstractionReplicationCursorBookmarkV2,
FS: fs.ToString(),
FilesystemVersion: v,
JobID: jobid,
}
}
return nil
}
func ReplicationCursorV1Extractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion) (_ Abstraction) {
if v.Type != zfs.Bookmark {
panic("impl error")
}
fullname := v.ToAbsPath(fs)
_, _, err := ParseReplicationCursorBookmarkName(fullname)
if err == ErrV1ReplicationCursor {
return &ReplicationCursorV1{
Type: AbstractionReplicationCursorBookmarkV1,
FS: fs.ToString(),
FilesystemVersion: v,
}
}
return nil
}
var _ HoldExtractor = LastReceivedHoldExtractor
func LastReceivedHoldExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion, holdTag string) Abstraction {
var err error
if v.Type != zfs.Snapshot {
panic("impl error")
}
jobID, err := ParseLastReceivedHoldTag(holdTag)
if err == nil {
return &holdBasedAbstraction{
Type: AbstractionLastReceivedHold,
FS: fs.ToString(),
FilesystemVersion: v,
Tag: holdTag,
JobID: jobID,
}
}
return nil
}
type ReplicationCursorV1 struct {
Type AbstractionType
FS string
zfs.FilesystemVersion
}
func (c ReplicationCursorV1) GetType() AbstractionType { return c.Type }
func (c ReplicationCursorV1) GetFS() string { return c.FS }
func (c ReplicationCursorV1) GetFullPath() string { return fmt.Sprintf("%s#%s", c.FS, c.GetName()) }
func (c ReplicationCursorV1) GetJobID() *JobID { return nil }
func (c ReplicationCursorV1) GetFilesystemVersion() zfs.FilesystemVersion { return c.FilesystemVersion }
func (c ReplicationCursorV1) MarshalJSON() ([]byte, error) {
return json.Marshal(AbstractionJSON{c})
}
func (c ReplicationCursorV1) String() string {
return fmt.Sprintf("%s %s", c.Type, c.GetFullPath())
}
func (c ReplicationCursorV1) Destroy(ctx context.Context) error {
if err := zfs.ZFSDestroyIdempotent(ctx, c.GetFullPath()); err != nil {
return errors.Wrapf(err, "destroy %s %s: zfs", c.Type, c.GetFullPath())
}
return nil
}
-159
View File
@@ -1,159 +0,0 @@
package endpoint
import (
"context"
"fmt"
"regexp"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/zfs"
)
var stepHoldTagRE = regexp.MustCompile("^zrepl_STEP_J_(.+)")
func StepHoldTag(jobid JobID) (string, error) {
return stepHoldTagImpl(jobid.String())
}
func stepHoldTagImpl(jobid string) (string, error) {
t := fmt.Sprintf("zrepl_STEP_J_%s", jobid)
if err := zfs.ValidHoldTag(t); err != nil {
return "", err
}
return t, nil
}
// err != nil always means that the bookmark is not a step bookmark
func ParseStepHoldTag(tag string) (JobID, error) {
match := stepHoldTagRE.FindStringSubmatch(tag)
if match == nil {
return JobID{}, fmt.Errorf("parse hold tag: match regex %q", stepHoldTagRE)
}
jobID, err := MakeJobID(match[1])
if err != nil {
return JobID{}, errors.Wrap(err, "parse hold tag: invalid job id field")
}
return jobID, nil
}
const stepBookmarkNamePrefix = "zrepl_STEP"
// v must be validated by caller
func StepBookmarkName(fs string, guid uint64, id JobID) (string, error) {
return stepBookmarkNameImpl(fs, guid, id.String())
}
func stepBookmarkNameImpl(fs string, guid uint64, jobid string) (string, error) {
return makeJobAndGuidBookmarkName(stepBookmarkNamePrefix, fs, guid, jobid)
}
// name is the full bookmark name, including dataset path
//
// err != nil always means that the bookmark is not a step bookmark
func ParseStepBookmarkName(fullname string) (guid uint64, jobID JobID, err error) {
guid, jobID, err = parseJobAndGuidBookmarkName(fullname, stepBookmarkNamePrefix)
if err != nil {
err = errors.Wrap(err, "parse step bookmark name") // no shadow!
}
return guid, jobID, err
}
// idempotently hold / step-bookmark `version`
//
// returns ErrBookmarkCloningNotSupported if version is a bookmark and bookmarking bookmarks is not supported by ZFS
func HoldStep(ctx context.Context, fs string, v zfs.FilesystemVersion, jobID JobID) (Abstraction, error) {
if v.IsSnapshot() {
tag, err := StepHoldTag(jobID)
if err != nil {
return nil, errors.Wrap(err, "step hold tag")
}
if err := zfs.ZFSHold(ctx, fs, v, tag); err != nil {
return nil, errors.Wrap(err, "step hold: zfs")
}
return &holdBasedAbstraction{
Type: AbstractionStepHold,
FS: fs,
Tag: tag,
JobID: jobID,
FilesystemVersion: v,
}, nil
}
if !v.IsBookmark() {
panic(fmt.Sprintf("version must bei either snapshot or bookmark, got %#v", v))
}
bmname, err := StepBookmarkName(fs, v.Guid, jobID)
if err != nil {
return nil, errors.Wrap(err, "create step bookmark: determine bookmark name")
}
// idempotently create bookmark
stepBookmark, err := zfs.ZFSBookmark(ctx, fs, v, bmname)
if err != nil {
if err == zfs.ErrBookmarkCloningNotSupported {
// TODO we could actually try to find a local snapshot that has the requested GUID
// however, the replication algorithm prefers snapshots anyways, so this quest
// is most likely not going to be successful. Also, there's the possibility that
// the caller might want to filter what snapshots are eligibile, and this would
// complicate things even further.
return nil, err // TODO go1.13 use wrapping
}
return nil, errors.Wrap(err, "create step bookmark: zfs")
}
return &bookmarkBasedAbstraction{
Type: AbstractionStepBookmark,
FS: fs,
FilesystemVersion: stepBookmark,
JobID: jobID,
}, nil
}
var _ BookmarkExtractor = StepBookmarkExtractor
func StepBookmarkExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion) (_ Abstraction) {
if v.Type != zfs.Bookmark {
panic("impl error")
}
fullname := v.ToAbsPath(fs)
guid, jobid, err := ParseStepBookmarkName(fullname)
if guid != v.Guid {
// TODO log this possibly tinkered-with bookmark
return nil
}
if err == nil {
bm := &bookmarkBasedAbstraction{
Type: AbstractionStepBookmark,
FS: fs.ToString(),
FilesystemVersion: v,
JobID: jobid,
}
return bm
}
return nil
}
var _ HoldExtractor = StepHoldExtractor
func StepHoldExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion, holdTag string) Abstraction {
if v.Type != zfs.Snapshot {
panic("impl error")
}
jobID, err := ParseStepHoldTag(holdTag)
if err == nil {
return &holdBasedAbstraction{
Type: AbstractionStepHold,
FS: fs.ToString(),
Tag: holdTag,
FilesystemVersion: v,
JobID: jobID,
}
}
return nil
}
-234
View File
@@ -1,234 +0,0 @@
package endpoint
import (
"fmt"
"math"
"runtime/debug"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zrepl/zrepl/zfs"
)
func TestCreateTXGRange(t *testing.T) {
type testCaseExpectation struct {
input uint64
expect bool
}
type testCase struct {
name string
config *CreateTXGRange
configAllowZeroCreateTXG bool
expectInvalid bool
expectString string
expect []testCaseExpectation
}
tcs := []testCase{
{
name: "unbounded",
expectInvalid: false,
config: &CreateTXGRange{
Since: nil,
Until: nil,
},
expectString: "~,~",
expect: []testCaseExpectation{
{0, true},
{math.MaxUint64, true},
{1, true},
{math.MaxUint64 - 1, true},
},
},
{
name: "wrong order obvious",
expectInvalid: true,
config: &CreateTXGRange{
Since: &CreateTXGRangeBound{23, &zfs.NilBool{B: true}},
Until: &CreateTXGRangeBound{20, &zfs.NilBool{B: true}},
},
expectString: "[23,20]",
},
{
name: "wrong order edge-case could also be empty",
expectInvalid: true,
config: &CreateTXGRange{
Since: &CreateTXGRangeBound{23, &zfs.NilBool{B: false}},
Until: &CreateTXGRangeBound{22, &zfs.NilBool{B: true}},
},
expectString: "(23,22]",
},
{
name: "empty",
expectInvalid: true,
config: &CreateTXGRange{
Since: &CreateTXGRangeBound{2, &zfs.NilBool{B: false}},
Until: &CreateTXGRangeBound{2, &zfs.NilBool{B: false}},
},
expectString: "(2,2)",
},
{
name: "inclusive-since-exclusive-until",
expectInvalid: false,
config: &CreateTXGRange{
Since: &CreateTXGRangeBound{2, &zfs.NilBool{B: true}},
Until: &CreateTXGRangeBound{5, &zfs.NilBool{B: false}},
},
expectString: "[2,5)",
expect: []testCaseExpectation{
{0, false},
{1, false},
{2, true},
{3, true},
{4, true},
{5, false},
{6, false},
},
},
{
name: "exclusive-since-inclusive-until",
expectInvalid: false,
config: &CreateTXGRange{
Since: &CreateTXGRangeBound{2, &zfs.NilBool{B: false}},
Until: &CreateTXGRangeBound{5, &zfs.NilBool{B: true}},
},
expectString: "(2,5]",
expect: []testCaseExpectation{
{0, false},
{1, false},
{2, false},
{3, true},
{4, true},
{5, true},
{6, false},
},
},
{
name: "zero-createtxg-not-allowed-because-likely-programmer-error",
expectInvalid: true,
config: &CreateTXGRange{
Since: nil,
Until: &CreateTXGRangeBound{0, &zfs.NilBool{B: true}},
},
expectString: "~,0]",
},
{
name: "half-open-no-until",
expectInvalid: false,
config: &CreateTXGRange{
Since: &CreateTXGRangeBound{2, &zfs.NilBool{B: false}},
Until: nil,
},
expectString: "(2,~",
expect: []testCaseExpectation{
{0, false},
{1, false},
{2, false},
{3, true},
{4, true},
{5, true},
{6, true},
},
},
{
name: "half-open-no-since",
expectInvalid: false,
config: &CreateTXGRange{
Since: nil,
Until: &CreateTXGRangeBound{4, &zfs.NilBool{B: true}},
},
expectString: "~,4]",
expect: []testCaseExpectation{
{0, true},
{1, true},
{2, true},
{3, true},
{4, true},
{5, false},
},
},
{
name: "edgeSince",
expectInvalid: false,
config: &CreateTXGRange{
Since: &CreateTXGRangeBound{math.MaxUint64, &zfs.NilBool{B: true}},
Until: nil,
},
expectString: "[18446744073709551615,~",
expect: []testCaseExpectation{
{math.MaxUint64, true},
{math.MaxUint64 - 1, false},
{0, false},
{1, false},
},
},
{
name: "edgeSinceNegative",
expectInvalid: true,
config: &CreateTXGRange{
Since: &CreateTXGRangeBound{math.MaxUint64, &zfs.NilBool{B: false}},
Until: nil,
},
expectString: "(18446744073709551615,~",
},
{
name: "edgeUntil",
expectInvalid: false,
config: &CreateTXGRange{
Until: &CreateTXGRangeBound{0, &zfs.NilBool{B: true}},
},
configAllowZeroCreateTXG: true,
expectString: "~,0]",
expect: []testCaseExpectation{
{0, true},
{math.MaxUint64, false},
{1, false},
},
},
{
name: "edgeUntilNegative",
expectInvalid: true,
configAllowZeroCreateTXG: true,
config: &CreateTXGRange{
Until: &CreateTXGRangeBound{0, &zfs.NilBool{B: false}},
},
expectString: "~,0)",
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
require.True(t, tc.expectInvalid != (len(tc.expect) > 0), "invalid test config: must either expect invalid or have expectations: %s", tc.name)
require.NotEmpty(t, tc.expectString)
assert.Equal(t, tc.expectString, tc.config.String())
save := createTXGRangeBoundAllowCreateTXG0
createTXGRangeBoundAllowCreateTXG0 = tc.configAllowZeroCreateTXG
defer func() {
createTXGRangeBoundAllowCreateTXG0 = save
}()
if tc.expectInvalid {
t.Run(tc.name, func(t *testing.T) {
assert.Error(t, tc.config.Validate())
})
} else {
for i, e := range tc.expect {
t.Run(fmt.Sprint(i), func(t *testing.T) {
defer func() {
v := recover()
if v != nil {
t.Fatalf("should not panic: %T %v\n%s", v, v, debug.Stack())
}
}()
assert.Equal(t, e.expect, tc.config.Contains(e.input))
})
}
}
})
}
}
@@ -1,6 +1,7 @@
package endpoint
import (
"context"
"fmt"
"regexp"
"strconv"
@@ -56,3 +57,53 @@ func parseJobAndGuidBookmarkName(fullname string, prefix string) (guid uint64, j
return guid, jobID, nil
}
func destroyBookmarksOlderThan(ctx context.Context, fs string, mostRecent *zfs.ZFSSendArgVersion, jobID JobID, filter func(shortname string) (accept bool)) (destroyed []zfs.FilesystemVersion, err error) {
if filter == nil {
panic(filter)
}
fsp, err := zfs.NewDatasetPath(fs)
if err != nil {
return nil, errors.Wrap(err, "invalid filesystem path")
}
mostRecentProps, err := mostRecent.ValidateExistsAndGetCheckedProps(ctx, fs)
if err != nil {
return nil, errors.Wrap(err, "validate most recent version argument")
}
stepBookmarks, err := zfs.ZFSListFilesystemVersions(fsp, zfs.FilterFromClosure(
func(t zfs.VersionType, name string) (accept bool, err error) {
if t != zfs.Bookmark {
return false, nil
}
return filter(name), nil
}))
if err != nil {
return nil, errors.Wrap(err, "list bookmarks")
}
// cut off all bookmarks prior to mostRecent's CreateTXG
var destroy []zfs.FilesystemVersion
for _, v := range stepBookmarks {
if v.Type != zfs.Bookmark {
panic("implementation error")
}
if !filter(v.Name) {
panic("inconsistent filter result")
}
if v.CreateTXG < mostRecentProps.CreateTXG {
destroy = append(destroy, v)
}
}
// FIXME use batch destroy, must adopt code to handle bookmarks
for _, v := range destroy {
if err := zfs.ZFSDestroyIdempotent(v.ToAbsPath(fsp)); err != nil {
return nil, errors.Wrap(err, "destroy bookmark")
}
}
return destroy, nil
}
-74
View File
@@ -1,74 +0,0 @@
package endpoint
import (
"context"
"encoding/json"
"fmt"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/zfs"
)
type bookmarkBasedAbstraction struct {
Type AbstractionType
FS string
zfs.FilesystemVersion
JobID JobID
}
func (b bookmarkBasedAbstraction) GetType() AbstractionType { return b.Type }
func (b bookmarkBasedAbstraction) GetFS() string { return b.FS }
func (b bookmarkBasedAbstraction) GetJobID() *JobID { return &b.JobID }
func (b bookmarkBasedAbstraction) GetFullPath() string {
return fmt.Sprintf("%s#%s", b.FS, b.Name) // TODO use zfs.FilesystemVersion.ToAbsPath
}
func (b bookmarkBasedAbstraction) MarshalJSON() ([]byte, error) {
return json.Marshal(AbstractionJSON{b})
}
func (b bookmarkBasedAbstraction) String() string {
return fmt.Sprintf("%s %s", b.Type, b.GetFullPath())
}
func (b bookmarkBasedAbstraction) GetFilesystemVersion() zfs.FilesystemVersion {
return b.FilesystemVersion
}
func (b bookmarkBasedAbstraction) Destroy(ctx context.Context) error {
if err := zfs.ZFSDestroyIdempotent(ctx, b.GetFullPath()); err != nil {
return errors.Wrapf(err, "destroy %s: zfs", b)
}
return nil
}
type holdBasedAbstraction struct {
Type AbstractionType
FS string
zfs.FilesystemVersion
Tag string
JobID JobID
}
func (h holdBasedAbstraction) GetType() AbstractionType { return h.Type }
func (h holdBasedAbstraction) GetFS() string { return h.FS }
func (h holdBasedAbstraction) GetJobID() *JobID { return &h.JobID }
func (h holdBasedAbstraction) GetFullPath() string {
return fmt.Sprintf("%s@%s", h.FS, h.GetName()) // TODO use zfs.FilesystemVersion.ToAbsPath
}
func (h holdBasedAbstraction) MarshalJSON() ([]byte, error) {
return json.Marshal(AbstractionJSON{h})
}
func (h holdBasedAbstraction) String() string {
return fmt.Sprintf("%s %q on %s", h.Type, h.Tag, h.GetFullPath())
}
func (h holdBasedAbstraction) GetFilesystemVersion() zfs.FilesystemVersion {
return h.FilesystemVersion
}
func (h holdBasedAbstraction) Destroy(ctx context.Context) error {
if err := zfs.ZFSRelease(ctx, h.Tag, h.GetFullPath()); err != nil {
return errors.Wrapf(err, "release %s: zfs", h)
}
return nil
}
+5 -5
View File
@@ -9,8 +9,8 @@ import (
"github.com/zrepl/zrepl/zfs"
)
// JobID instances returned by MakeJobID() guarantee their JobID.String()
// can be used in ZFS dataset names and hold tags.
// An instance of this type returned by MakeJobID guarantees
// that that instance's JobID.String() can be used in a ZFS dataset name and hold tag.
type JobID struct {
jid string
}
@@ -21,7 +21,7 @@ func MakeJobID(s string) (JobID, error) {
}
if err := zfs.ComponentNamecheck(s); err != nil {
return JobID{}, errors.Wrap(err, "must be usable as a dataset path component")
return JobID{}, errors.Wrap(err, "muse be usable as a dataset path component")
}
if _, err := stepBookmarkNameImpl("pool/ds", 0xface601d, s); err != nil {
@@ -34,7 +34,7 @@ func MakeJobID(s string) (JobID, error) {
}
if _, err := lastReceivedHoldImpl(s); err != nil {
return JobID{}, errors.Wrap(err, "must be usable as a last-received-hold tag")
return JobID{}, errors.Wrap(err, "must be usabel as a last-recieved-hold tag")
}
// FIXME replication cursor bookmark name
@@ -57,7 +57,7 @@ func MustMakeJobID(s string) JobID {
func (j JobID) expectInitialized() {
if j.jid == "" {
panic("use of uninitialized JobID")
panic("use of unitialized JobID")
}
}
+1 -6
View File
@@ -5,7 +5,6 @@ go 1.12
require (
github.com/fatih/color v1.7.0
github.com/gdamore/tcell v1.2.0
github.com/gitchander/permutation v0.0.0-20181107151852-9e56b92e9909
github.com/go-logfmt/logfmt v0.4.0
github.com/go-sql-driver/mysql v1.4.1-0.20190907122137-b2c03bcae3d4
github.com/golang/protobuf v1.3.2
@@ -22,21 +21,17 @@ require (
github.com/onsi/gomega v1.7.0 // indirect
github.com/pkg/errors v0.8.1
github.com/pkg/profile v1.2.1
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7
github.com/prometheus/client_golang v1.2.1
github.com/prometheus/common v0.7.0
github.com/sergi/go-diff v1.0.1-0.20180205163309-da645544ed44 // go1.12 thinks it needs this
github.com/spf13/cobra v0.0.2
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.4.0
github.com/willf/bitset v1.1.10
github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // go1.12 thinks it needs this
github.com/zrepl/yaml-config v0.0.0-20191220194647-cbb6b0cf4bdd
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980
golang.org/x/sync v0.0.0-20190423024810-112230192c58
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e
gonum.org/v1/gonum v0.7.0 // indirect
google.golang.org/grpc v1.17.0
)
-30
View File
@@ -5,12 +5,9 @@ github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q
github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alvaroloes/enumer v1.1.1/go.mod h1:FxrjvuXoDAx9isTJrv4c+T410zFi0DtXIT0m65DJ+Wo=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
@@ -31,7 +28,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ftrvxmtrx/fd v0.0.0-20150925145434-c6d800382fff h1:zk1wwii7uXmI0znwU+lqg+wFL9G5+vm5I+9rv2let60=
@@ -40,8 +36,6 @@ github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdk
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.2.0 h1:ikixzsxc8K8o3V2/CEmyoEW8mJZaNYQQ3NP3VIQdUe4=
github.com/gdamore/tcell v1.2.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
github.com/gitchander/permutation v0.0.0-20181107151852-9e56b92e9909 h1:9NC8seTx6/zRmMTAdsHj/uOMi0EGHGQtjyLafBjk77Q=
github.com/gitchander/permutation v0.0.0-20181107151852-9e56b92e9909/go.mod h1:lP+DW8LR6Rw3ru9Vo2/y/3iiLaLWmofYql/va+7zJOk=
github.com/go-critic/go-critic v0.3.4/go.mod h1:AHR42Lk/E/aOznsrYdMYeIQS5RH10HZHSqP+rD6AJrc=
github.com/go-critic/go-critic v0.3.5-0.20190526074819-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@@ -76,7 +70,6 @@ github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
@@ -129,7 +122,6 @@ github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8/go.mod h1:yL958EeXv8
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
@@ -212,8 +204,6 @@ github.com/problame/go-netssh v0.0.0-20191026123024-f34099f4f6b1 h1:HH8yzlaZq/A8
github.com/problame/go-netssh v0.0.0-20191026123024-f34099f4f6b1/go.mod h1:JRs3nyBjvOv/9YHC+Vlw9z1Jfzl5s5t0BNnTzmclj1c=
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7 h1:Oik8tLrLhoMq4fduDRuNNOAQ+q0M0dXkjAYLUf4+mAc=
github.com/problame/go-netssh v0.0.0-20191209123953-18d8aa6923c7/go.mod h1:Ba6RaFpK1+IHWs1wLZ6sCBuXkt4iAVLeOG5GinXHusM=
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5 h1:1eSrO2WAeSXjMol8WRKW9LekL1252VIMaKQwWkmEdvs=
github.com/problame/go-netssh v0.0.0-20200601114649-26439f9f0dc5/go.mod h1:Ba6RaFpK1+IHWs1wLZ6sCBuXkt4iAVLeOG5GinXHusM=
github.com/prometheus/client_golang v0.0.0-20180410130117-e11c6ff8170b/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
@@ -248,7 +238,6 @@ github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOms
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE=
github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
@@ -270,7 +259,6 @@ github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7Sr
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -284,8 +272,6 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC
github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d h1:yJIizrfO599ot2kQ6Af1enICnwBD3XoxgX3MrMwot2M=
github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
@@ -303,11 +289,6 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2 h1:y102fOLFqhV41b+4GPiJoa0k/x+pJcEi2/HB1Y5T6fU=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -347,25 +328,17 @@ golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181205014116-22934f0fdb62/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e h1:Io7mpb+aUAGF0MKxbyQ7HQl1VgB+cL6ZJZUFaFNqVV4=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190213192042-740235f6c0d8/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524210228-3d17549cdc6b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.7.0 h1:Hdks0L0hgznZLG9nzXb8vZ0rRvqNvAcgAp84y7Mwkgw=
gonum.org/v1/gonum v0.7.0/go.mod h1:L02bwd0sqlsvRv41G7wGWFCsVNZFv/k1xzGIxeANHGM=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
@@ -376,9 +349,7 @@ google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9M
google.golang.org/grpc v1.17.0 h1:TRJYBgMclJvGYn2rIMjj+h9KtMt5r1Ij7ODVRIZkwhk=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -397,6 +368,5 @@ mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIa
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe/go.mod h1:BnhuWBAqxH3+J5bDybdxgw5ZfS+DsVd4iylsKQePN8o=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
sourcegraph.com/sqs/pbtypes v1.0.0/go.mod h1:3AciMUv4qUuRHRHhOG4TZOB+72GdPVz5k+c648qsFS4=
+1 -2
View File
@@ -36,10 +36,9 @@ godep() {
go build -v -mod=readonly -o "$GOPATH/bin/enumer" github.com/alvaroloes/enumer
go build -v -mod=readonly -o "$GOPATH/bin/goimports" golang.org/x/tools/cmd/goimports
go build -v -mod=readonly -o "$GOPATH/bin/golangci-lint" github.com/golangci/golangci-lint/cmd/golangci-lint
go build -v -mod=readonly -o "$GOPATH/bin/gocovmerge" github.com/wadey/gocovmerge
set +x
popd
if ! type stringer || ! type protoc-gen-go || ! type enumer || ! type goimports || ! type golangci-lint || ! type gocovmerge; then
if ! type stringer || ! type protoc-gen-go || ! type enumer || ! type goimports || ! type golangci-lint; then
echo "Installed dependencies but can't find them in \$PATH, adjust it to contain \$GOPATH/bin" 1>&2
exit 1
fi
+1 -1
View File
@@ -17,7 +17,7 @@ func init() {
cli.AddSubcommand(client.PprofCmd)
cli.AddSubcommand(client.TestCmd)
cli.AddSubcommand(client.MigrateCmd)
cli.AddSubcommand(client.ZFSAbstractionsCmd)
cli.AddSubcommand(client.HoldsCmd)
}
func main() {
+25 -31
View File
@@ -11,7 +11,6 @@ import (
"github.com/fatih/color"
"github.com/pkg/errors"
"github.com/zrepl/zrepl/daemon/logging/trace"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/logging"
@@ -24,36 +23,33 @@ var bold = color.New(color.Bold)
var boldRed = color.New(color.Bold, color.FgHiRed)
var boldGreen = color.New(color.Bold, color.FgHiGreen)
const DefaultPoolImageSize = 200 * (1 << 20)
var args struct {
createArgs platformtest.ZpoolCreateArgs
stopAndKeepPoolOnFail bool
run string
runRE *regexp.Regexp
}
func main() {
var args HarnessArgs
flag.StringVar(&args.CreateArgs.PoolName, "poolname", "", "")
flag.StringVar(&args.CreateArgs.ImagePath, "imagepath", "", "")
flag.Int64Var(&args.CreateArgs.ImageSize, "imagesize", DefaultPoolImageSize, "")
flag.StringVar(&args.CreateArgs.Mountpoint, "mountpoint", "", "")
flag.BoolVar(&args.StopAndKeepPoolOnFail, "failure.stop-and-keep-pool", false, "if a test case fails, stop test execution and keep pool as it was when the test failed")
flag.StringVar(&args.Run, "run", "", "")
flag.Parse()
if err := HarnessRun(args); err != nil {
if err := doMain(); err != nil {
os.Exit(1)
}
}
var exitWithErr = fmt.Errorf("exit with error")
type HarnessArgs struct {
CreateArgs platformtest.ZpoolCreateArgs
StopAndKeepPoolOnFail bool
Run string
}
func doMain() error {
func HarnessRun(args HarnessArgs) error {
flag.StringVar(&args.createArgs.PoolName, "poolname", "", "")
flag.StringVar(&args.createArgs.ImagePath, "imagepath", "", "")
flag.Int64Var(&args.createArgs.ImageSize, "imagesize", 200*(1<<20), "")
flag.StringVar(&args.createArgs.Mountpoint, "mountpoint", "", "")
flag.BoolVar(&args.stopAndKeepPoolOnFail, "failure.stop-and-keep-pool", false, "if a test case fails, stop test execution and keep pool as it was when the test failed")
flag.StringVar(&args.run, "run", "", "")
flag.Parse()
runRE := regexp.MustCompile(args.Run)
args.runRE = regexp.MustCompile(args.run)
outlets := logger.NewOutlets()
outlet, level, err := logging.ParseOutlet(config.LoggingOutletEnum{Ret: &config.StdoutLoggingOutlet{
@@ -68,13 +64,11 @@ func HarnessRun(args HarnessArgs) error {
outlets.Add(outlet, level)
logger := logger.NewLogger(outlets, 1*time.Second)
if err := args.CreateArgs.Validate(); err != nil {
if err := args.createArgs.Validate(); err != nil {
logger.Error(err.Error())
panic(err)
}
ctx := context.Background()
defer trace.WithTaskFromStackUpdateCtx(&ctx)()
ctx = logging.WithLoggers(ctx, logging.SubsystemLoggersWithUniversalLogger(logger))
ctx := platformtest.WithLogger(context.Background(), logger)
ex := platformtest.NewEx(logger)
type invocation struct {
@@ -84,7 +78,7 @@ func HarnessRun(args HarnessArgs) error {
invocations := make([]*invocation, 0, len(tests.Cases))
for _, c := range tests.Cases {
if runRE.MatchString(c.String()) {
if args.runRE.MatchString(c.String()) {
invocations = append(invocations, &invocation{runFunc: c})
}
}
@@ -93,7 +87,7 @@ func HarnessRun(args HarnessArgs) error {
bold.Printf("BEGIN TEST CASE %s\n", inv.runFunc.String())
pool, err := platformtest.CreateOrReplaceZpool(ctx, ex, args.CreateArgs)
pool, err := platformtest.CreateOrReplaceZpool(ctx, ex, args.createArgs)
if err != nil {
panic(errors.Wrap(err, "create test pool"))
}
@@ -109,7 +103,7 @@ func HarnessRun(args HarnessArgs) error {
fmt.Printf("%+v\n", res.failedStack) // print with stack trace
}
if res.failed && args.StopAndKeepPoolOnFail {
if res.failed && args.stopAndKeepPoolOnFail {
boldRed.Printf("STOPPING TEST RUN AT FAILING TEST PER USER REQUEST\n")
return exitWithErr
}
@@ -178,21 +172,21 @@ type testCaseResult struct {
func runTestCase(ctx *platformtest.Context, ex platformtest.Execer, c tests.Case) *testCaseResult {
// run case
var panicked = false
var paniced = false
var panicValue interface{} = nil
var panicStack error
func() {
defer func() {
if item := recover(); item != nil {
panicValue = item
panicked = true
paniced = true
panicStack = errors.Errorf("panic while running test: %v", panicValue)
}
}()
c(ctx)
}()
if panicked {
if paniced {
switch panicValue {
case platformtest.SkipNowSentinel:
return &testCaseResult{skipped: true}
+4 -12
View File
@@ -1,7 +1,6 @@
package main
import (
"fmt"
"os"
"strings"
"testing"
@@ -22,29 +21,22 @@ import (
// https://github.com/wadey/gocovmerge
func TestMain(t *testing.T) {
fmt.Println("incoming args: ", os.Args)
var (
args []string
run bool
startCaptureArgs bool
args []string
run bool
)
for i, arg := range os.Args {
for _, arg := range os.Args {
switch {
case arg == "__DEVEL--i-heard-you-like-tests":
run = true
startCaptureArgs = true
case strings.HasPrefix(arg, "-test"):
case strings.HasPrefix(arg, "__DEVEL"):
case i == 0:
args = append(args, arg)
case startCaptureArgs:
default:
args = append(args, arg)
}
}
os.Args = args
fmt.Println("using args: ", os.Args)
if run {
main()
-1
View File
@@ -27,7 +27,6 @@ func (c *Context) Logf(format string, args ...interface{}) {
func (c *Context) Errorf(format string, args ...interface{}) {
GetLog(c).Printf(format, args...)
c.FailNow()
}
func (c *Context) FailNow() {
+13 -3
View File
@@ -3,12 +3,22 @@ package platformtest
import (
"context"
"github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/logger"
)
type Logger = logger.Logger
func GetLog(ctx context.Context) Logger {
return logging.GetLogger(ctx, logging.SubsysPlatformtest)
type contextKey int
const (
contextKeyLogger contextKey = iota
)
func WithLogger(ctx context.Context, logger Logger) context.Context {
ctx = context.WithValue(ctx, contextKeyLogger, logger)
return ctx
}
func GetLog(ctx context.Context) Logger {
return ctx.Value(contextKeyLogger).(Logger)
}

Some files were not shown because too many files have changed in this diff Show More