Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b8abba6f55 | |||
| 0eb7032735 |
+3
-45
@@ -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
|
||||
#
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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 > $@
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
@@ -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,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{}) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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...)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
@@ -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())
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
-----
|
||||
|
||||
|
||||
@@ -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``
|
||||
|
||||
@@ -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>` ).
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
@@ -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!
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
+177
-288
@@ -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
|
||||
@@ -188,7 +198,11 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer guard.Release()
|
||||
defer func(guardp **semaphore.AcquireGuard) {
|
||||
if *guardp != nil {
|
||||
(*guardp).Release()
|
||||
}
|
||||
}(&guard)
|
||||
|
||||
si, err := zfs.ZFSSendDry(ctx, sendArgs)
|
||||
if err != nil {
|
||||
@@ -196,7 +210,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 +225,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 +237,129 @@ 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
|
||||
// defer releasing guard until streamCopier is closed
|
||||
streamCopier.SetPostCloseCallback(func(_ error) {
|
||||
guard.Release()
|
||||
})
|
||||
guard = 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 +368,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 +375,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 +398,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 +513,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 +522,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 +563,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 +583,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 +590,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 +621,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 +636,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 +648,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 +672,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 +689,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 +714,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 +724,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 +754,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 +762,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 +793,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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user