Compare commits

..

6 Commits

Author SHA1 Message Date
Christian Schwarz 0ea8f96db6 replication/driver: fs.debug() helper that automatically prefixes with fs name 2020-04-07 23:43:06 +02:00
Christian Schwarz 3cc0acd7ec replication/driver: rename receiver variable (fs *fs) to (f *fs) 2020-04-07 23:41:28 +02:00
Christian Schwarz 632acb6305 replication/driver: envconst for experimental parallel replication
refs #140
refs #302
2020-04-07 23:35:42 +02:00
Christian Schwarz 4376346d2c endpoint: log %#v recv options 2020-04-07 23:33:59 +02:00
Christian Schwarz 5a00b35d6f build: Makefile: GO_EXTRA_BUILDFLAGS 2020-04-07 23:33:42 +02:00
Christian Schwarz 98def6e940 endpoint: refactor, fix stale holds on initial replication failure, holds release subcmds, more efficient ZFS queries
The motivation for this recatoring are based on two independent issues:

- @JMoVS found that the changes merged as part of #259 slowed his OS X
  based installation down significantly.
  Analysis of the zfs command logging introduced in #296 showed that
  `zfs holds` took most of the execution time, and they pointed out
  that not all of those `zfs holds` invocations were actually necessary.
  I.e.: zrepl was inefficient about retrieving information from ZFS.

- @InsanePrawn found that failures on initial replication would lead
  to step holds accumulating on the sending side, i.e. they would never
  be cleaned up in the HintMostRecentCommonAncestor RPC handler.
  That was because we only sent that RPC if there was a most recent
  common ancestor detected during replication planning.
  @InsanePrawn prototyped an implementation of a `zrepl holds release`
  command to mitigate the situation.
  As part of that development work and back-and-forth with @problame,
  it became evident that the abstractions that #259 built on top of
  zfs in package endpoint (step holds, replication cursor,
  last-received-hold), were not well-represented for re-use in the
  `zrepl holds release` subocommand.

This commit refactors package endpoint to address both of these issues:

- endpoint abstractions now share an interface `Abstraction` that, among
  other things, provides a uniform `Destroy()` method.
  However, that method should not be destroyed directly but instead
  the package-level `BatchDestroy` function should be used in order
  to allow for a migration to zfs channel programs in the future.

- endpoint now has a query facitilty (`ListAbstractions`) which is
  used to find on-disk
    - step holds and bookmarks
    - replication cursors (v1, v2)
    - last-received-holds
  By describing the query in a struct, we can centralized the retrieval
  of information via the ZFS CLI and only have to be clever once.
  We are "clever" in the following ways:
  - When asking for hold-based abstractions, we only run `zfs holds` on
    snapshot that have `userrefs` > 0
    - To support this functionality, add field `UserRefs` to zfs.FilesystemVersion
      and retrieve it anywhere we retrieve zfs.FilesystemVersion from ZFS.
  - When asking only for bookmark-based abstractions, we only run
    `zfs list -t bookmark`, not with snapshots.
  - Currently unused (except for CLI) per-filesystem concurrent lookup
  - Option to only include abstractions with CreateTXG in a specified range

- refactor `endpoint`'s various ZFS info  retrieval methods to use
  `ListAbstractions`

- change the `zrepl holds list` command to consume endpoint.ListAbstractions

- Add a `ListStale` method which, given a query template,
  lists stale holds and bookmarks.
  - it uses replication cursor has different modes
- the new `zrepl holds release-{all,stale}` commands can be used
  to remove abstractions of package endpoint

- Adjust HintMostRecentCommonAncestor RPC for stale-holds cleanup:
    - send it also if no most recent common ancestor exists between sender and receiver
    - have the sender clean up its abstractions when it receives the RPC
      with no most recent common ancestor, using `ListStale`
    - Due to changed semantics, bump the protocol version.

- Adjust HintMostRecentCommonAncestor RPC for performance problems
  encountered by @JMoVS
    - by default, per (job,fs)-combination, only consider cleaning
      step holds in the createtxg range
      `[last replication cursor,conservatively-estimated-receive-side-version)`
    - this behavior ensures resumability at cost proportional to the
      time that replication was donw
    - however, as explained in a comment, we might leak holds if
      the zrepl daemon stops running
    - that  trade-off is acceptable because in the presumably rare
      this might happen the user has two tools at their hand:
    - Tool 1: run `zrepl holds release-stale`
    - Tool 2: use env var `ZREPL_ENDPOINT_SENDER_HINT_MOST_RECENT_STEP_HOLD_CLEANUP_MODE`
      to adjust the lower bound of the createtxg range (search for it in the code).
      The env var can also be used to disable hold-cleanup on the
      send-side entirely.

supersedes closes #293
supersedes closes #282
fixes #280
fixes #278

Additionaly, we fixed a couple of bugs:

- zfs: fix half-nil error reporting of dataset-does-not-exist for ZFSListChan and ZFSBookmark

- endpoint: Sender's `HintMostRecentCommonAncestor` handler would not
  check whether access to the specified filesystem was allowed.
2020-04-07 18:57:55 +02:00
43 changed files with 588 additions and 750 deletions
+3 -8
View File
@@ -66,7 +66,6 @@ wrapup-and-checksum:
-acf $(NOARCH_TARBALL) \ -acf $(NOARCH_TARBALL) \
$(ARTIFACTDIR)/docs/html \ $(ARTIFACTDIR)/docs/html \
$(ARTIFACTDIR)/bash_completion \ $(ARTIFACTDIR)/bash_completion \
$(ARTIFACTDIR)/_zrepl.zsh_completion \
$(ARTIFACTDIR)/go_env.txt \ $(ARTIFACTDIR)/go_env.txt \
dist \ dist \
config/samples config/samples
@@ -161,7 +160,7 @@ platformtest: # do not track dependency on platformtest-bin to allow build of pl
$(ZREPL_PLATFORMTEST_ARGS) $(ZREPL_PLATFORMTEST_ARGS)
##################### NOARCH ##################### ##################### 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): $(ARTIFACTDIR):
@@ -169,16 +168,12 @@ $(ARTIFACTDIR):
$(ARTIFACTDIR)/docs: $(ARTIFACTDIR) $(ARTIFACTDIR)/docs: $(ARTIFACTDIR)
mkdir -p "$@" 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 # pass
$(ARTIFACTDIR)/bash_completion: $(ARTIFACTDIR)/bash_completion:
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) $(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) gencompletion bash "$@" artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) bashcomp "$@"
$(ARTIFACTDIR)/_zrepl.zsh_completion:
$(MAKE) zrepl-bin GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH)
artifacts/zrepl-$(GOHOSTOS)-$(GOHOSTARCH) gencompletion zsh "$@"
$(ARTIFACTDIR)/go_env.txt: $(ARTIFACTDIR)/go_env.txt:
$(GO_ENV_VARS) $(GO) env > $@ $(GO_ENV_VARS) $(GO) env > $@
+19 -45
View File
@@ -19,55 +19,29 @@ var rootCmd = &cobra.Command{
Short: "One-stop ZFS replication solution", Short: "One-stop ZFS replication solution",
} }
func init() { var bashcompCmd = &cobra.Command{
rootCmd.PersistentFlags().StringVar(&rootArgs.configPath, "config", "", "config file path") Use: "bashcomp path/to/out/file",
} Short: "generate bash completions",
Run: func(cmd *cobra.Command, args []string) {
var genCompletionCmd = &cobra.Command{ if len(args) != 1 {
Use: "gencompletion", fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n")
Short: "generate shell auto-completions", err := cmd.Usage()
} if err != nil {
panic(err)
type completionCmdInfo struct { }
genFunc func(outpath string) error os.Exit(1)
help string }
} if err := rootCmd.GenBashCompletionFile(args[0]); err != nil {
fmt.Fprintf(os.Stderr, "error generating bash completion: %s", err)
var completionCmdMap = map[string]completionCmdInfo{ os.Exit(1)
"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",
}, },
Hidden: true,
} }
func init() { func init() {
for sh, info := range completionCmdMap { rootCmd.PersistentFlags().StringVar(&rootArgs.configPath, "config", "", "config file path")
sh, info := sh, info rootCmd.AddCommand(bashcompCmd)
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)
} }
type Subcommand struct { type Subcommand struct {
@@ -12,28 +12,28 @@ import (
"github.com/zrepl/zrepl/zfs" "github.com/zrepl/zrepl/zfs"
) )
var zabsCreateStepHoldFlags struct { var holdsCreateStepHoldFlags struct {
target string target string
jobid JobIDFlag jobid JobIDFlag
} }
var zabsCmdCreateStepHold = &cli.Subcommand{ var holdsCmdCreateStepHold = &cli.Subcommand{
Use: "step", Use: "step",
Run: doZabsCreateStep, Run: doHoldsCreateStep,
NoRequireConfig: true, NoRequireConfig: true,
Short: `create a step hold or bookmark`, Short: `create a step hold or bookmark`,
SetupFlags: func(f *pflag.FlagSet) { SetupFlags: func(f *pflag.FlagSet) {
f.StringVarP(&zabsCreateStepHoldFlags.target, "target", "t", "", "snapshot to be held / bookmark to be held") f.StringVarP(&holdsCreateStepHoldFlags.target, "target", "t", "", "snapshot to be held / bookmark to be held")
f.VarP(&zabsCreateStepHoldFlags.jobid, "jobid", "j", "jobid for which the hold is installed") f.VarP(&holdsCreateStepHoldFlags.jobid, "jobid", "j", "jobid for which the hold is installed")
}, },
} }
func doZabsCreateStep(sc *cli.Subcommand, args []string) error { func doHoldsCreateStep(sc *cli.Subcommand, args []string) error {
if len(args) > 0 { if len(args) > 0 {
return errors.New("subcommand takes no arguments") return errors.New("subcommand takes no arguments")
} }
f := &zabsCreateStepHoldFlags f := &holdsCreateStepHoldFlags
fs, _, _, err := zfs.DecomposeVersionString(f.target) fs, _, _, err := zfs.DecomposeVersionString(f.target)
if err != nil { if err != nil {
+10 -10
View File
@@ -14,15 +14,15 @@ import (
) )
var ( var (
ZFSAbstractionsCmd = &cli.Subcommand{ HoldsCmd = &cli.Subcommand{
Use: "zfs-abstraction", Use: "holds",
Short: "manage abstractions that zrepl builds on top of ZFS", Short: "manage holds & step bookmarks",
SetupSubcommands: func() []*cli.Subcommand { SetupSubcommands: func() []*cli.Subcommand {
return []*cli.Subcommand{ return []*cli.Subcommand{
zabsCmdList, holdsCmdList,
zabsCmdReleaseAll, holdsCmdReleaseAll,
zabsCmdReleaseStale, holdsCmdReleaseStale,
zabsCmdCreate, holdsCmdCreate,
} }
}, },
} }
@@ -30,7 +30,7 @@ var (
// a common set of CLI flags that map to the fields of an // a common set of CLI flags that map to the fields of an
// endpoint.ListZFSHoldsAndBookmarksQuery // endpoint.ListZFSHoldsAndBookmarksQuery
type zabsFilterFlags struct { type holdsFilterFlags struct {
Filesystems FilesystemsFilterFlag Filesystems FilesystemsFilterFlag
Job JobIDFlag Job JobIDFlag
Types AbstractionTypesFlag Types AbstractionTypesFlag
@@ -38,7 +38,7 @@ type zabsFilterFlags struct {
} }
// produce a query from the CLI flags // produce a query from the CLI flags
func (f zabsFilterFlags) Query() (endpoint.ListZFSHoldsAndBookmarksQuery, error) { func (f holdsFilterFlags) Query() (endpoint.ListZFSHoldsAndBookmarksQuery, error) {
q := endpoint.ListZFSHoldsAndBookmarksQuery{ q := endpoint.ListZFSHoldsAndBookmarksQuery{
FS: f.Filesystems.FlagValue(), FS: f.Filesystems.FlagValue(),
What: f.Types.FlagValue(), What: f.Types.FlagValue(),
@@ -48,7 +48,7 @@ func (f zabsFilterFlags) Query() (endpoint.ListZFSHoldsAndBookmarksQuery, error)
return q, q.Validate() return q, q.Validate()
} }
func (f *zabsFilterFlags) registerZabsFilterFlags(s *pflag.FlagSet, verb string) { func (f *holdsFilterFlags) registerHoldsFilterFlags(s *pflag.FlagSet, verb string) {
// Note: the default value is defined in the .FlagValue methods // 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.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)) s.Var(&f.Job, "job", fmt.Sprintf("only %s holds created by the specified job [default: any job]", verb))
+14
View File
@@ -0,0 +1,14 @@
package client
import "github.com/zrepl/zrepl/cli"
var holdsCmdCreate = &cli.Subcommand{
Use: "create",
NoRequireConfig: true,
Short: `create zrepl-managed holds and boomkmarks (for debugging & development only!)`,
SetupSubcommands: func() []*cli.Subcommand {
return []*cli.Subcommand{
holdsCmdCreateStepHold,
}
},
}
@@ -16,23 +16,23 @@ import (
"github.com/zrepl/zrepl/util/chainlock" "github.com/zrepl/zrepl/util/chainlock"
) )
var zabsListFlags struct { var holdsListFlags struct {
Filter zabsFilterFlags Filter holdsFilterFlags
Json bool Json bool
} }
var zabsCmdList = &cli.Subcommand{ var holdsCmdList = &cli.Subcommand{
Use: "list", Use: "list",
Short: `list zrepl ZFS abstractions`, Run: doHoldsList,
Run: doZabsList,
NoRequireConfig: true, NoRequireConfig: true,
Short: "list holds and bookmarks",
SetupFlags: func(f *pflag.FlagSet) { SetupFlags: func(f *pflag.FlagSet) {
zabsListFlags.Filter.registerZabsFilterFlags(f, "list") holdsListFlags.Filter.registerHoldsFilterFlags(f, "list")
f.BoolVar(&zabsListFlags.Json, "json", false, "emit JSON") f.BoolVar(&holdsListFlags.Json, "json", false, "emit JSON")
}, },
} }
func doZabsList(sc *cli.Subcommand, args []string) error { func doHoldsList(sc *cli.Subcommand, args []string) error {
var err error var err error
ctx := context.Background() ctx := context.Background()
@@ -40,7 +40,7 @@ func doZabsList(sc *cli.Subcommand, args []string) error {
return errors.New("this subcommand takes no positional arguments") return errors.New("this subcommand takes no positional arguments")
} }
q, err := zabsListFlags.Filter.Query() q, err := holdsListFlags.Filter.Query()
if err != nil { if err != nil {
return errors.Wrap(err, "invalid filter specification on command line") return errors.Wrap(err, "invalid filter specification on command line")
} }
@@ -62,7 +62,7 @@ func doZabsList(sc *cli.Subcommand, args []string) error {
for a := range abstractions { for a := range abstractions {
func() { func() {
defer line.Lock().Unlock() defer line.Lock().Unlock()
if zabsListFlags.Json { if holdsListFlags.Json {
enc.SetIndent("", " ") enc.SetIndent("", " ")
if err := enc.Encode(abstractions); err != nil { if err := enc.Encode(abstractions); err != nil {
panic(err) panic(err)
@@ -15,35 +15,35 @@ import (
) )
// shared between release-all and release-step // shared between release-all and release-step
var zabsReleaseFlags struct { var holdsReleaseFlags struct {
Filter zabsFilterFlags Filter holdsFilterFlags
Json bool Json bool
DryRun bool DryRun bool
} }
func registerZabsReleaseFlags(s *pflag.FlagSet) { func registerHoldsReleaseFlags(s *pflag.FlagSet) {
zabsReleaseFlags.Filter.registerZabsFilterFlags(s, "release") holdsReleaseFlags.Filter.registerHoldsFilterFlags(s, "release")
s.BoolVar(&zabsReleaseFlags.Json, "json", false, "emit json instead of pretty-printed") s.BoolVar(&holdsReleaseFlags.Json, "json", false, "emit json instead of pretty-printed")
s.BoolVar(&zabsReleaseFlags.DryRun, "dry-run", false, "do a dry-run") s.BoolVar(&holdsReleaseFlags.DryRun, "dry-run", false, "do a dry-run")
} }
var zabsCmdReleaseAll = &cli.Subcommand{ var holdsCmdReleaseAll = &cli.Subcommand{
Use: "release-all", Use: "release-all",
Run: doZabsReleaseAll, Run: doHoldsReleaseAll,
NoRequireConfig: true, NoRequireConfig: true,
Short: `(DANGEROUS) release ALL zrepl ZFS abstractions (mostly useful for uninstalling zrepl completely or for "de-zrepl-ing" a filesystem)`, Short: `(DANGEROUS) release all zrepl-managed holds and bookmarks, mostly useful for uninstalling zrepl`,
SetupFlags: registerZabsReleaseFlags, SetupFlags: registerHoldsReleaseFlags,
} }
var zabsCmdReleaseStale = &cli.Subcommand{ var holdsCmdReleaseStale = &cli.Subcommand{
Use: "release-stale", Use: "release-stale",
Run: doZabsReleaseStale, Run: doHoldsReleaseStale,
NoRequireConfig: true, NoRequireConfig: true,
Short: `release stale zrepl ZFS abstractions (useful if zrepl has a bug and does not do it by itself)`, Short: `release stale zrepl-managed holds and boomkarks (useful if zrepl has a bug and doesn't do it by itself)`,
SetupFlags: registerZabsReleaseFlags, SetupFlags: registerHoldsReleaseFlags,
} }
func doZabsReleaseAll(sc *cli.Subcommand, args []string) error { func doHoldsReleaseAll(sc *cli.Subcommand, args []string) error {
var err error var err error
ctx := context.Background() ctx := context.Background()
@@ -51,7 +51,7 @@ func doZabsReleaseAll(sc *cli.Subcommand, args []string) error {
return errors.New("this subcommand takes no positional arguments") return errors.New("this subcommand takes no positional arguments")
} }
q, err := zabsReleaseFlags.Filter.Query() q, err := holdsReleaseFlags.Filter.Query()
if err != nil { if err != nil {
return errors.Wrap(err, "invalid filter specification on command line") return errors.Wrap(err, "invalid filter specification on command line")
} }
@@ -65,10 +65,10 @@ func doZabsReleaseAll(sc *cli.Subcommand, args []string) error {
// proceed anyways with rest of abstractions // proceed anyways with rest of abstractions
} }
return doZabsRelease_Common(ctx, abstractions) return doHoldsRelease_Common(ctx, abstractions)
} }
func doZabsReleaseStale(sc *cli.Subcommand, args []string) error { func doHoldsReleaseStale(sc *cli.Subcommand, args []string) error {
var err error var err error
ctx := context.Background() ctx := context.Background()
@@ -77,7 +77,7 @@ func doZabsReleaseStale(sc *cli.Subcommand, args []string) error {
return errors.New("this subcommand takes no positional arguments") return errors.New("this subcommand takes no positional arguments")
} }
q, err := zabsReleaseFlags.Filter.Query() q, err := holdsReleaseFlags.Filter.Query()
if err != nil { if err != nil {
return errors.Wrap(err, "invalid filter specification on command line") return errors.Wrap(err, "invalid filter specification on command line")
} }
@@ -87,13 +87,13 @@ func doZabsReleaseStale(sc *cli.Subcommand, args []string) error {
return err // context clear by invocation of command return err // context clear by invocation of command
} }
return doZabsRelease_Common(ctx, stalenessInfo.Stale) return doHoldsRelease_Common(ctx, stalenessInfo.Stale)
} }
func doZabsRelease_Common(ctx context.Context, destroy []endpoint.Abstraction) error { func doHoldsRelease_Common(ctx context.Context, destroy []endpoint.Abstraction) error {
if zabsReleaseFlags.DryRun { if holdsReleaseFlags.DryRun {
if zabsReleaseFlags.Json { if holdsReleaseFlags.Json {
m, err := json.MarshalIndent(destroy, "", " ") m, err := json.MarshalIndent(destroy, "", " ")
if err != nil { if err != nil {
panic(err) panic(err)
@@ -121,7 +121,7 @@ func doZabsRelease_Common(ctx context.Context, destroy []endpoint.Abstraction) e
for res := range outcome { for res := range outcome {
hadErr = hadErr || res.DestroyErr != nil hadErr = hadErr || res.DestroyErr != nil
if zabsReleaseFlags.Json { if holdsReleaseFlags.Json {
err := enc.Encode(res) err := enc.Encode(res)
if err != nil { if err != nil {
colorErr.Fprintf(os.Stderr, "cannot marshal there were errors in destroying the abstractions") colorErr.Fprintf(os.Stderr, "cannot marshal there were errors in destroying the abstractions")
+1 -1
View File
@@ -211,7 +211,7 @@ func doMigrateReplicationCursorFS(ctx context.Context, v1CursorJobs []job.Job, f
} }
fmt.Printf("identified owning job %q\n", owningJob.Name()) fmt.Printf("identified owning job %q\n", owningJob.Name())
bookmarks, err := zfs.ZFSListFilesystemVersions(ctx, fs, zfs.ListFilesystemVersionsOptions{ bookmarks, err := zfs.ZFSListFilesystemVersions(fs, zfs.ListFilesystemVersionsOptions{
Types: zfs.Bookmarks, Types: zfs.Bookmarks,
}) })
if err != nil { if err != nil {
-14
View File
@@ -1,14 +0,0 @@
package client
import "github.com/zrepl/zrepl/cli"
var zabsCmdCreate = &cli.Subcommand{
Use: "create",
NoRequireConfig: true,
Short: `create zrepl ZFS abstractions (mostly useful for debugging & development, users should not need to use this command)`,
SetupSubcommands: func() []*cli.Subcommand {
return []*cli.Subcommand{
zabsCmdCreateStepHold,
}
},
}
+1 -1
View File
@@ -485,7 +485,7 @@ var findSyncPointFSNoFilesystemVersionsErr = fmt.Errorf("no filesystem versions"
func findSyncPointFSNextOptimalSnapshotTime(l Logger, 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(context.TODO(), d, zfs.ListFilesystemVersionsOptions{ fsvs, err := zfs.ZFSListFilesystemVersions(d, zfs.ListFilesystemVersionsOptions{
Types: zfs.Snapshots, Types: zfs.Snapshots,
ShortnamePrefix: prefix, ShortnamePrefix: prefix,
}) })
-2
View File
@@ -62,9 +62,7 @@ Actual changelog:
* |bugfix| |docs| snapshotting: clarify sync-up behavior and warn about filesystems * |bugfix| |docs| snapshotting: clarify sync-up behavior and warn about filesystems
that will not be snapshotted until the sync-up phase is over that will not be snapshotted until the sync-up phase is over
* |docs| Document new replication features in the :ref:`config overview <overview-how-replication-works>` and :repomasterlink:`replication/design.md`. * |docs| Document new replication features in the :ref:`config overview <overview-how-replication-works>` and :repomasterlink:`replication/design.md`.
* |feature| documented subcommand to generate ``bash`` and ``zsh`` completions
* **[MAINTAINER NOTICE]** New platform tests in this version, please make sure you run them for your distro! * **[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.
0.2.1 0.2.1
----- -----
+2 -2
View File
@@ -38,8 +38,8 @@ CLI Overview
* - ``zrepl migrate`` * - ``zrepl migrate``
- | perform on-disk state / ZFS property migrations - | perform on-disk state / ZFS property migrations
| (see :ref:`changelog <changelog>` for details) | (see :ref:`changelog <changelog>` for details)
* - ``zrepl zfs-abstractions`` * - ``zrepl holds``
- 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>` ) - list and remove holds and step bookmarks created by zrepl (see :ref:`overview <replication-cursor-and-last-received-hold>` )
.. _usage-zrepl-daemon: .. _usage-zrepl-daemon:
+16 -103
View File
@@ -2,17 +2,14 @@
package endpoint package endpoint
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io"
"path" "path"
"sync" "sync"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/zrepl/zrepl/replication/logic/pdu" "github.com/zrepl/zrepl/replication/logic/pdu"
"github.com/zrepl/zrepl/util/chainedio"
"github.com/zrepl/zrepl/util/chainlock" "github.com/zrepl/zrepl/util/chainlock"
"github.com/zrepl/zrepl/util/envconst" "github.com/zrepl/zrepl/util/envconst"
"github.com/zrepl/zrepl/util/semaphore" "github.com/zrepl/zrepl/util/semaphore"
@@ -99,7 +96,7 @@ func (s *Sender) ListFilesystemVersions(ctx context.Context, r *pdu.ListFilesyst
if err != nil { if err != nil {
return nil, err return nil, err
} }
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, lp, zfs.ListFilesystemVersionsOptions{}) fsvs, err := zfs.ZFSListFilesystemVersions(lp, zfs.ListFilesystemVersionsOptions{})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -244,7 +241,7 @@ func sendArgsFromPDUAndValidateExistsAndGetVersion(ctx context.Context, fs strin
return version, nil return version, nil
} }
func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) { func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) {
_, err := s.filterCheckFS(r.Filesystem) _, err := s.filterCheckFS(r.Filesystem)
if err != nil { if err != nil {
@@ -342,11 +339,11 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
// step holds & replication cursor released / moved forward in s.SendCompleted => s.moveCursorAndReleaseSendHolds // step holds & replication cursor released / moved forward in s.SendCompleted => s.moveCursorAndReleaseSendHolds
sendStream, err := zfs.ZFSSend(ctx, sendArgs) streamCopier, err := zfs.ZFSSend(ctx, sendArgs)
if err != nil { if err != nil {
return nil, nil, errors.Wrap(err, "zfs send failed") return nil, nil, errors.Wrap(err, "zfs send failed")
} }
return res, sendStream, nil return res, streamCopier, nil
} }
func (p *Sender) SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error) { func (p *Sender) SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error) {
@@ -479,7 +476,7 @@ func (p *Sender) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCurs
return &pdu.ReplicationCursorRes{Result: &pdu.ReplicationCursorRes_Guid{Guid: cursor.Guid}}, nil 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()") return nil, fmt.Errorf("sender does not implement Receive()")
} }
@@ -651,7 +648,7 @@ func (s *Receiver) ListFilesystemVersions(ctx context.Context, req *pdu.ListFile
} }
// TODO share following code with sender // TODO share following code with sender
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, lp, zfs.ListFilesystemVersionsOptions{}) fsvs, err := zfs.ZFSListFilesystemVersions(lp, zfs.ListFilesystemVersionsOptions{})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -683,13 +680,13 @@ func (s *Receiver) ReplicationCursor(context.Context, *pdu.ReplicationCursorReq)
return nil, fmt.Errorf("ReplicationCursor not implemented for Receiver") return nil, fmt.Errorf("ReplicationCursor not implemented for Receiver")
} }
func (s *Receiver) Send(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) { 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()") return nil, nil, fmt.Errorf("receiver does not implement Send()")
} }
var maxConcurrentZFSRecvSemaphore = semaphore.New(envconst.Int64("ZREPL_ENDPOINT_MAX_CONCURRENT_RECV", 10)) 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) { func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs.StreamCopier) (*pdu.ReceiveRes, error) {
getLogger(ctx).Debug("incoming Receive") getLogger(ctx).Debug("incoming Receive")
defer receive.Close() defer receive.Close()
@@ -768,30 +765,21 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
return nil, visitErr 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 // determine whether we need to rollback the filesystem / change its placeholder state
var clearPlaceholderProperty bool var clearPlaceholderProperty bool
var recvOpts zfs.RecvOptions var recvOpts zfs.RecvOptions
ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, lp) ph, err := zfs.ZFSGetFilesystemPlaceholderState(ctx, lp)
if err != nil { if err == nil && ph.FSExists && ph.IsPlaceholder {
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 {
recvOpts.RollbackAndForceRecv = true recvOpts.RollbackAndForceRecv = true
clearPlaceholderProperty = true clearPlaceholderProperty = true
} }
if clearPlaceholderProperty { if clearPlaceholderProperty {
log.Info("clearing placeholder property")
if err := zfs.ZFSSetPlaceholder(ctx, lp, false); err != nil { if err := zfs.ZFSSetPlaceholder(ctx, lp, false); err != nil {
return nil, fmt.Errorf("cannot clear placeholder property for forced receive: %s", err) return nil, fmt.Errorf("cannot clear placeholder property for forced receive: %s", err)
} }
} }
if req.ClearResumeToken && ph.FSExists { if req.ClearResumeToken && ph.FSExists {
log.Info("clearing resume token")
if err := zfs.ZFSRecvClearResumeToken(ctx, lp.ToString()); err != nil { if err := zfs.ZFSRecvClearResumeToken(ctx, lp.ToString()); err != nil {
return nil, errors.Wrap(err, "cannot clear resume token") return nil, errors.Wrap(err, "cannot clear resume token")
} }
@@ -802,7 +790,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") 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 // TODO use try-acquire and fail with resource-exhaustion rpc status
// => would require handling on the client-side // => would require handling on the client-side
// => this is a dataconn endpoint, doesn't have the status code semantics of gRPC // => this is a dataconn endpoint, doesn't have the status code semantics of gRPC
@@ -812,89 +800,14 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
} }
defer guard.Release() defer guard.Release()
log.Info("peeking 1M ahead") getLogger(ctx).WithField("opts", fmt.Sprintf("%#v", recvOpts)).Debug("start receive command")
var peek bytes.Buffer
var MaxPeek = envconst.Int64("ZREPL_ENDPOINT_RECV_PEEK_SIZE", 1<<20)
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")
snapFullPath := to.FullPath(lp.ToString()) snapFullPath := to.FullPath(lp.ToString())
if err := zfs.ZFSRecv(ctx, lp.ToString(), to, chainedio.NewChainedReader(&peek, receive), recvOpts); err != nil { if err := zfs.ZFSRecv(ctx, lp.ToString(), to, receive, recvOpts); err != nil {
getLogger(ctx).
// 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.
WithError(err). WithError(err).
WithField("opts", fmt.Sprintf("%#v", recvOpts)). WithField("opts", fmt.Sprintf("%#v", recvOpts)).
Error("zfs receive failed") Error("zfs receive failed")
return nil, err return nil, err
} }
@@ -902,13 +815,13 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
toRecvd, err := to.ValidateExistsAndGetVersion(ctx, lp.ToString()) toRecvd, err := to.ValidateExistsAndGetVersion(ctx, lp.ToString())
if err != nil { if err != nil {
msg := "receive request's `To` version does not match what we received in the stream" msg := "receive request's `To` version does not match what we received in the stream"
log.WithError(err).WithField("snap", snapFullPath).Error(msg) getLogger(ctx).WithError(err).WithField("snap", snapFullPath).Error(msg)
log.Error("aborting recv request, but keeping received snapshot for inspection") getLogger(ctx).Error("aborting recv request, but keeping received snapshot for inspection")
return nil, errors.Wrap(err, msg) return nil, errors.Wrap(err, msg)
} }
if s.conf.UpdateLastReceivedHold { if s.conf.UpdateLastReceivedHold {
log.Debug("move last-received-hold") getLogger(ctx).Debug("move last-received-hold")
if err := MoveLastReceivedHold(ctx, lp.ToString(), toRecvd, s.conf.JobID); err != nil { if err := MoveLastReceivedHold(ctx, lp.ToString(), toRecvd, s.conf.JobID); err != nil {
return nil, errors.Wrap(err, "cannot move last-received-hold") return nil, errors.Wrap(err, "cannot move last-received-hold")
} }
+1 -1
View File
@@ -573,7 +573,7 @@ func listAbstractionsImplFS(ctx context.Context, fs string, query *ListZFSHoldsA
whatTypes[zfs.Snapshot] = true whatTypes[zfs.Snapshot] = true
} }
} }
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, fsp, zfs.ListFilesystemVersionsOptions{ fsvs, err := zfs.ZFSListFilesystemVersions(fsp, zfs.ListFilesystemVersionsOptions{
Types: whatTypes, Types: whatTypes,
}) })
if err != nil { if err != nil {
+1 -1
View File
@@ -17,7 +17,7 @@ func init() {
cli.AddSubcommand(client.PprofCmd) cli.AddSubcommand(client.PprofCmd)
cli.AddSubcommand(client.TestCmd) cli.AddSubcommand(client.TestCmd)
cli.AddSubcommand(client.MigrateCmd) cli.AddSubcommand(client.MigrateCmd)
cli.AddSubcommand(client.ZFSAbstractionsCmd) cli.AddSubcommand(client.HoldsCmd)
} }
func main() { func main() {
+1 -1
View File
@@ -136,7 +136,7 @@ func makeResumeSituation(ctx *platformtest.Context, src dummySnapshotSituation,
return situation return situation
} }
limitedCopier := limitio.ReadCloser(copier, src.dummyDataLen/2) limitedCopier := zfs.NewReadCloserCopier(limitio.ReadCloser(copier, src.dummyDataLen/2))
defer limitedCopier.Close() defer limitedCopier.Close()
require.NotNil(ctx, sendArgs.To) require.NotNil(ctx, sendArgs.To)
+7 -7
View File
@@ -43,7 +43,7 @@ func ListFilesystemVersionsTypeFilteringAndPrefix(t *platformtest.Context) {
fs := fmt.Sprintf("%s/foo bar", t.RootDataset) fs := fmt.Sprintf("%s/foo bar", t.RootDataset)
// no options := all types // no options := all types
vs, err := zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{}) vs, err := zfs.ZFSListFilesystemVersions(mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{})
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, []string{ require.Equal(t, []string{
"#blup 1", "#bookfoo 1", "#bookfoo 2", "#foo 1", "#foo 2", "#blup 1", "#bookfoo 1", "#bookfoo 2", "#foo 1", "#foo 2",
@@ -51,21 +51,21 @@ func ListFilesystemVersionsTypeFilteringAndPrefix(t *platformtest.Context) {
}, versionRelnamesSorted(vs)) }, versionRelnamesSorted(vs))
// just snapshots // just snapshots
vs, err = zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{ vs, err = zfs.ZFSListFilesystemVersions(mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{
Types: zfs.Snapshots, Types: zfs.Snapshots,
}) })
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, []string{"@ foo with leading whitespace", "@blup 1", "@foo 1", "@foo 2"}, versionRelnamesSorted(vs)) require.Equal(t, []string{"@ foo with leading whitespace", "@blup 1", "@foo 1", "@foo 2"}, versionRelnamesSorted(vs))
// just bookmarks // just bookmarks
vs, err = zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{ vs, err = zfs.ZFSListFilesystemVersions(mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{
Types: zfs.Bookmarks, Types: zfs.Bookmarks,
}) })
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, []string{"#blup 1", "#bookfoo 1", "#bookfoo 2", "#foo 1", "#foo 2"}, versionRelnamesSorted(vs)) require.Equal(t, []string{"#blup 1", "#bookfoo 1", "#bookfoo 2", "#foo 1", "#foo 2"}, versionRelnamesSorted(vs))
// just with prefix foo // just with prefix foo
vs, err = zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{ vs, err = zfs.ZFSListFilesystemVersions(mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{
ShortnamePrefix: "foo", ShortnamePrefix: "foo",
}) })
require.NoError(t, err) require.NoError(t, err)
@@ -82,7 +82,7 @@ func ListFilesystemVersionsZeroExistIsNotAnError(t *platformtest.Context) {
fs := fmt.Sprintf("%s/foo bar", t.RootDataset) fs := fmt.Sprintf("%s/foo bar", t.RootDataset)
vs, err := zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{}) vs, err := zfs.ZFSListFilesystemVersions(mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{})
require.Empty(t, vs) require.Empty(t, vs)
require.NoError(t, err) require.NoError(t, err)
dsne, ok := err.(*zfs.DatasetDoesNotExist) dsne, ok := err.(*zfs.DatasetDoesNotExist)
@@ -98,7 +98,7 @@ func ListFilesystemVersionsFilesystemNotExist(t *platformtest.Context) {
nonexistentFS := fmt.Sprintf("%s/not existent", t.RootDataset) nonexistentFS := fmt.Sprintf("%s/not existent", t.RootDataset)
vs, err := zfs.ZFSListFilesystemVersions(t, mustDatasetPath(nonexistentFS), zfs.ListFilesystemVersionsOptions{}) vs, err := zfs.ZFSListFilesystemVersions(mustDatasetPath(nonexistentFS), zfs.ListFilesystemVersionsOptions{})
require.Empty(t, vs) require.Empty(t, vs)
require.Error(t, err) require.Error(t, err)
t.Logf("err = %T\n%s", err, err) t.Logf("err = %T\n%s", err, err)
@@ -141,7 +141,7 @@ func ListFilesystemVersionsUserrefs(t *platformtest.Context) {
fs := fmt.Sprintf("%s/foo bar", t.RootDataset) fs := fmt.Sprintf("%s/foo bar", t.RootDataset)
vs, err := zfs.ZFSListFilesystemVersions(t, mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{}) vs, err := zfs.ZFSListFilesystemVersions(mustDatasetPath(fs), zfs.ListFilesystemVersionsOptions{})
require.NoError(t, err) require.NoError(t, err)
type expectation struct { type expectation struct {
+1 -1
View File
@@ -35,7 +35,7 @@ func SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden(ctx *platformt
ResumeToken: "", ResumeToken: "",
}.Validate(ctx) }.Validate(ctx)
var stream *zfs.SendStream var stream *zfs.ReadCloserCopier
if err == nil { if err == nil {
stream, err = zfs.ZFSSend(ctx, sendArgs) // no shadow stream, err = zfs.ZFSSend(ctx, sendArgs) // no shadow
if err == nil { if err == nil {
+7 -131
View File
@@ -146,12 +146,6 @@ type fs struct {
l *chainlock.L l *chainlock.L
// ordering relationship that must be maintained for initial replication
initialRepOrd struct {
parents, children []*fs
parentDidUpdate chan struct{}
}
planning struct { planning struct {
done bool done bool
err *timedError err *timedError
@@ -302,7 +296,6 @@ func (a *attempt) do(ctx context.Context, prev *attempt) {
fs: pfs, fs: pfs,
l: a.l, l: a.l,
} }
fs.initialRepOrd.parentDidUpdate = make(chan struct{}, 1)
a.fss = append(a.fss, fs) a.fss = append(a.fss, fs)
} }
@@ -361,18 +354,6 @@ func (a *attempt) do(ctx context.Context, prev *attempt) {
} }
// invariant: prevs contains an entry for each unambiguous correspondence // invariant: prevs contains an entry for each unambiguous correspondence
// build up parent-child relationship (FIXME (O(n^2), but who's going to have that many filesystems...))
for _, f1 := range a.fss {
fs1 := f1.fs.ReportInfo().Name
for _, f2 := range a.fss {
fs2 := f2.fs.ReportInfo().Name
if strings.HasPrefix(fs1, fs2) && fs1 != fs2 {
f1.initialRepOrd.parents = append(f1.initialRepOrd.parents, f2)
f2.initialRepOrd.children = append(f2.initialRepOrd.children, f1)
}
}
}
stepQueue := newStepQueue() stepQueue := newStepQueue()
defer stepQueue.Start(envconst.Int("ZREPL_REPLICATION_EXPERIMENTAL_REPLICATION_CONCURRENCY", 1))() // TODO parallel replication defer stepQueue.Start(envconst.Int("ZREPL_REPLICATION_EXPERIMENTAL_REPLICATION_CONCURRENCY", 1))() // TODO parallel replication
var fssesDone sync.WaitGroup var fssesDone sync.WaitGroup
@@ -393,27 +374,9 @@ func (f *fs) debug(format string, args ...interface{}) {
debugPrefix("fs=%s", f.fs.ReportInfo().Name)(format, args...) debugPrefix("fs=%s", f.fs.ReportInfo().Name)(format, args...)
} }
// wake up children that watch for f.{planning.{err,done},planned.{step,stepErr}}
func (f *fs) initialRepOrdWakeupChildren() {
var children []string
for _, c := range f.initialRepOrd.children {
// no locking required, c.fs does not change
children = append(children, c.fs.ReportInfo().Name)
}
f.debug("wakeup children %s", children)
for _, child := range f.initialRepOrd.children {
select {
// no locking required, child.initialRepOrd does not change
case child.initialRepOrd.parentDidUpdate <- struct{}{}:
default:
}
}
}
func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) { func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
defer f.l.Lock().Unlock() defer f.l.Lock().Unlock()
defer f.initialRepOrdWakeupChildren()
// get planned steps from replication logic // get planned steps from replication logic
var psteps []Step var psteps []Step
@@ -427,6 +390,7 @@ func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
psteps, err = f.fs.PlanFS(ctx) // no shadow psteps, err = f.fs.PlanFS(ctx) // no shadow
errTime = time.Now() // no shadow errTime = time.Now() // no shadow
}) })
f.planning.done = true
if err != nil { if err != nil {
f.planning.err = newTimedError(err, errTime) f.planning.err = newTimedError(err, errTime)
return return
@@ -438,8 +402,6 @@ func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
} }
f.planned.steps = append(f.planned.steps, step) f.planned.steps = append(f.planned.steps, step)
} }
// we're not done planning yet, f.planned.steps might still be changed by next block
// => don't set f.planning.done just yet
f.debug("initial len(fs.planned.steps) = %d", len(f.planned.steps)) f.debug("initial len(fs.planned.steps) = %d", len(f.planned.steps))
// for not-first attempts, only allow fs.planned.steps // for not-first attempts, only allow fs.planned.steps
@@ -494,110 +456,24 @@ func (f *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
} }
f.debug("post-prev-merge len(fs.planned.steps) = %d", len(f.planned.steps)) f.debug("post-prev-merge len(fs.planned.steps) = %d", len(f.planned.steps))
// now we are done planning (f.planned.steps won't change from now on)
f.planning.done = true
// wait for parents' initial replication
var parents []string
for _, p := range f.initialRepOrd.parents {
parents = append(parents, p.fs.ReportInfo().Name)
}
f.debug("wait for parents %s", parents)
for {
var initialReplicatingParentsWithErrors []string
allParentsPresentOnReceiver := true
f.l.DropWhile(func() {
for _, p := range f.initialRepOrd.parents {
p.l.HoldWhile(func() {
// (get the preconditions that allow us to inspect p.planned)
parentHasPlanningDone := p.planning.done && p.planning.err == nil
if !parentHasPlanningDone {
// if the parent couldn't be planned, we cannot know whether it needs initial replication
// or incremental replication => be conservative and assume it was initial replication
allParentsPresentOnReceiver = false
if p.planning.err != nil {
initialReplicatingParentsWithErrors = append(initialReplicatingParentsWithErrors, p.fs.ReportInfo().Name)
}
return
}
// now allowed to inspect p.planned
// if there are no steps to be done, the filesystem must exist on the receiving side
// (otherwise we'd replicate it, and there would be a step for that)
// (FIXME hardcoded initial replication policy, assuming the policy will always do _some_ initial replication)
parentHasNoSteps := len(p.planned.steps) == 0
// OR if it has completed at least one step
// (remember that .step points to the next step to be done)
// (TODO technically, we could make this step ready in the moment the recv-side
// dataset exists, i.e. after the first few megabytes of transferred data, but we'd have to ask the receiver for that -> poll ListFilesystems RPC)
parentHasTakenAtLeastOneSuccessfulStep := !parentHasNoSteps && p.planned.step >= 1
parentFirstStepIsIncremental := // no need to lock for .report() because step.l == it's fs.l
len(p.planned.steps) > 0 && p.planned.steps[0].report().IsIncremental()
f.debug("parentHasNoSteps=%v parentFirstStepIsIncremental=%v parentHasTakenAtLeastOneSuccessfulStep=%v",
parentHasNoSteps, parentFirstStepIsIncremental, parentHasTakenAtLeastOneSuccessfulStep)
parentPresentOnReceiver := parentHasNoSteps || parentFirstStepIsIncremental || parentHasTakenAtLeastOneSuccessfulStep
allParentsPresentOnReceiver = allParentsPresentOnReceiver && parentPresentOnReceiver // no shadow
if !parentPresentOnReceiver && p.planned.stepErr != nil {
initialReplicatingParentsWithErrors = append(initialReplicatingParentsWithErrors, p.fs.ReportInfo().Name)
}
})
}
})
if len(initialReplicatingParentsWithErrors) > 0 {
f.planned.stepErr = newTimedError(fmt.Errorf("parent(s) failed during initial replication: %s", initialReplicatingParentsWithErrors), time.Now())
return
}
if allParentsPresentOnReceiver {
break // good to go
}
// wait for wakeups from parents, then check again
// lock must not be held while waiting in order for reporting to work
f.l.DropWhile(func() {
select {
case <-ctx.Done():
f.planned.stepErr = newTimedError(ctx.Err(), time.Now())
return
case <-f.initialRepOrd.parentDidUpdate:
// loop
}
})
if f.planned.stepErr != nil {
return
}
}
f.debug("all parents ready, start replication %s", parents)
// do our steps
for i, s := range f.planned.steps { for i, s := range f.planned.steps {
var (
err error
errTime time.Time
)
// lock must not be held while executing step in order for reporting to work // lock must not be held while executing step in order for reporting to work
f.l.DropWhile(func() { f.l.DropWhile(func() {
// wait for parallel replication
targetDate := s.step.TargetDate() targetDate := s.step.TargetDate()
defer pq.WaitReady(f, targetDate)() defer pq.WaitReady(f, targetDate)()
// do the step err = s.step.Step(ctx) // no shadow
err, errTime = s.step.Step(ctx), time.Now() // no shadow errTime = time.Now() // no shadow
}) })
if err != nil { if err != nil {
f.planned.stepErr = newTimedError(err, errTime) f.planned.stepErr = newTimedError(err, errTime)
break break
} }
f.planned.step = i + 1 // fs.planned.step must be == len(fs.planned.steps) if all went OK f.planned.step = i + 1 // fs.planned.step must be == len(fs.planned.steps) if all went OK
f.initialRepOrdWakeupChildren()
} }
} }
// caller must hold lock l // caller must hold lock l
@@ -26,6 +26,6 @@ type debugFunc func(format string, args ...interface{})
func debugPrefix(prefixFormat string, prefixFormatArgs ...interface{}) debugFunc { func debugPrefix(prefixFormat string, prefixFormatArgs ...interface{}) debugFunc {
prefix := fmt.Sprintf(prefixFormat, prefixFormatArgs...) prefix := fmt.Sprintf(prefixFormat, prefixFormatArgs...)
return func(format string, args ...interface{}) { return func(format string, args ...interface{}) {
debug("%s: %s", prefix, fmt.Sprintf(format, args...)) debug("%s: %s", prefix, fmt.Sprintf(format, args))
} }
} }
+7 -8
View File
@@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io"
"sync" "sync"
"time" "time"
@@ -39,7 +38,7 @@ type Sender interface {
// If a non-nil io.ReadCloser is returned, it is guaranteed to be closed before // If a non-nil io.ReadCloser is returned, it is guaranteed to be closed before
// any next call to the parent github.com/zrepl/zrepl/replication.Endpoint. // any next call to the parent github.com/zrepl/zrepl/replication.Endpoint.
// If the send request is for dry run the io.ReadCloser will be nil // If the send request is for dry run the io.ReadCloser will be nil
Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error)
SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error) SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error)
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error) ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
} }
@@ -48,7 +47,7 @@ type Receiver interface {
Endpoint Endpoint
// Receive sends r and sendStream (the latter containing a ZFS send stream) // Receive sends r and sendStream (the latter containing a ZFS send stream)
// to the parent github.com/zrepl/zrepl/replication.Endpoint. // to the parent github.com/zrepl/zrepl/replication.Endpoint.
Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.ReadCloser) (*pdu.ReceiveRes, error) Receive(ctx context.Context, req *pdu.ReceiveReq, receive zfs.StreamCopier) (*pdu.ReceiveRes, error)
} }
type PlannerPolicy struct { type PlannerPolicy struct {
@@ -163,7 +162,7 @@ type Step struct {
// byteCounter is nil initially, and set later in Step.doReplication // byteCounter is nil initially, and set later in Step.doReplication
// => concurrent read of that pointer from Step.ReportInfo must be protected // => concurrent read of that pointer from Step.ReportInfo must be protected
byteCounter bytecounter.ReadCloser byteCounter bytecounter.StreamCopier
byteCounterMtx chainlock.L byteCounterMtx chainlock.L
} }
@@ -607,19 +606,19 @@ func (s *Step) doReplication(ctx context.Context) error {
sr := s.buildSendRequest(false) sr := s.buildSendRequest(false)
log.Debug("initiate send request") log.Debug("initiate send request")
sres, stream, err := s.sender.Send(ctx, sr) sres, sstreamCopier, err := s.sender.Send(ctx, sr)
if err != nil { if err != nil {
log.WithError(err).Error("send request failed") log.WithError(err).Error("send request failed")
return err return err
} }
if stream == nil { if sstreamCopier == nil {
err := errors.New("send request did not return a stream, broken endpoint implementation") err := errors.New("send request did not return a stream, broken endpoint implementation")
return err return err
} }
defer stream.Close() defer sstreamCopier.Close()
// Install a byte counter to track progress + for status report // Install a byte counter to track progress + for status report
byteCountingStream := bytecounter.NewReadCloser(stream) byteCountingStream := bytecounter.NewStreamCopier(sstreamCopier)
s.byteCounterMtx.Lock() s.byteCounterMtx.Lock()
s.byteCounter = byteCountingStream s.byteCounter = byteCountingStream
s.byteCounterMtx.Unlock() s.byteCounterMtx.Unlock()
+10 -13
View File
@@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"io"
"strings" "strings"
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
@@ -12,6 +11,7 @@ import (
"github.com/zrepl/zrepl/replication/logic/pdu" "github.com/zrepl/zrepl/replication/logic/pdu"
"github.com/zrepl/zrepl/rpc/dataconn/stream" "github.com/zrepl/zrepl/rpc/dataconn/stream"
"github.com/zrepl/zrepl/transport" "github.com/zrepl/zrepl/transport"
"github.com/zrepl/zrepl/zfs"
) )
type Client struct { type Client struct {
@@ -26,7 +26,7 @@ func NewClient(connecter transport.Connecter, log Logger) *Client {
} }
} }
func (c *Client) send(ctx context.Context, conn *stream.Conn, endpoint string, req proto.Message, stream io.ReadCloser) error { func (c *Client) send(ctx context.Context, conn *stream.Conn, endpoint string, req proto.Message, streamCopier zfs.StreamCopier) error {
var buf bytes.Buffer var buf bytes.Buffer
_, memErr := buf.WriteString(endpoint) _, memErr := buf.WriteString(endpoint)
@@ -46,8 +46,8 @@ func (c *Client) send(ctx context.Context, conn *stream.Conn, endpoint string, r
return err return err
} }
if stream != nil { if streamCopier != nil {
return conn.SendStream(ctx, stream, ZFSStream) return conn.SendStream(ctx, streamCopier, ZFSStream)
} else { } else {
return nil return nil
} }
@@ -109,7 +109,7 @@ func (c *Client) putWire(conn *stream.Conn) {
} }
} }
func (c *Client) ReqSend(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) { func (c *Client) ReqSend(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) {
conn, err := c.getWire(ctx) conn, err := c.getWire(ctx)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -130,19 +130,16 @@ func (c *Client) ReqSend(ctx context.Context, req *pdu.SendReq) (*pdu.SendRes, i
return nil, nil, err return nil, nil, err
} }
var stream io.ReadCloser var copier zfs.StreamCopier = nil
if !req.DryRun { if !req.DryRun {
putWireOnReturn = false putWireOnReturn = false
stream, err = conn.ReadStream(ZFSStream, true) // no shadow copier = &streamCopier{streamConn: conn, closeStreamOnClose: true}
if err != nil {
return nil, nil, err
}
} }
return &res, stream, nil return &res, copier, nil
} }
func (c *Client) ReqRecv(ctx context.Context, req *pdu.ReceiveReq, stream io.ReadCloser) (*pdu.ReceiveRes, error) { func (c *Client) ReqRecv(ctx context.Context, req *pdu.ReceiveReq, streamCopier zfs.StreamCopier) (*pdu.ReceiveRes, error) {
defer c.log.Debug("ReqRecv returns") defer c.log.Debug("ReqRecv returns")
conn, err := c.getWire(ctx) conn, err := c.getWire(ctx)
@@ -169,7 +166,7 @@ func (c *Client) ReqRecv(ctx context.Context, req *pdu.ReceiveReq, stream io.Rea
sendErrChan := make(chan error) sendErrChan := make(chan error)
go func() { go func() {
if err := c.send(ctx, conn, EndpointRecv, req, stream); err != nil { if err := c.send(ctx, conn, EndpointRecv, req, streamCopier); err != nil {
sendErrChan <- err sendErrChan <- err
} else { } else {
sendErrChan <- nil sendErrChan <- nil
+5 -10
View File
@@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"io"
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
@@ -12,6 +11,7 @@ import (
"github.com/zrepl/zrepl/replication/logic/pdu" "github.com/zrepl/zrepl/replication/logic/pdu"
"github.com/zrepl/zrepl/rpc/dataconn/stream" "github.com/zrepl/zrepl/rpc/dataconn/stream"
"github.com/zrepl/zrepl/transport" "github.com/zrepl/zrepl/transport"
"github.com/zrepl/zrepl/zfs"
) )
// WireInterceptor has a chance to exchange the context and connection on each client connection. // WireInterceptor has a chance to exchange the context and connection on each client connection.
@@ -21,11 +21,11 @@ type WireInterceptor func(ctx context.Context, rawConn *transport.AuthConn) (con
type Handler interface { type Handler interface {
// Send handles a SendRequest. // Send handles a SendRequest.
// The returned io.ReadCloser is allowed to be nil, for example if the requested Send is a dry-run. // The returned io.ReadCloser is allowed to be nil, for example if the requested Send is a dry-run.
Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error)
// Receive handles a ReceiveRequest. // Receive handles a ReceiveRequest.
// It is guaranteed that Server calls Receive with a stream that holds the IdleConnTimeout // It is guaranteed that Server calls Receive with a stream that holds the IdleConnTimeout
// configured in ServerConfig.Shared.IdleConnTimeout. // configured in ServerConfig.Shared.IdleConnTimeout.
Receive(ctx context.Context, r *pdu.ReceiveReq, receive io.ReadCloser) (*pdu.ReceiveRes, error) Receive(ctx context.Context, r *pdu.ReceiveReq, receive zfs.StreamCopier) (*pdu.ReceiveRes, error)
// PingDataconn handles a PingReq // PingDataconn handles a PingReq
PingDataconn(ctx context.Context, r *pdu.PingReq) (*pdu.PingRes, error) PingDataconn(ctx context.Context, r *pdu.PingReq) (*pdu.PingRes, error)
} }
@@ -111,7 +111,7 @@ func (s *Server) serveConn(nc *transport.AuthConn) {
s.log.WithField("endpoint", endpoint).Debug("calling handler") s.log.WithField("endpoint", endpoint).Debug("calling handler")
var res proto.Message var res proto.Message
var sendStream io.ReadCloser var sendStream zfs.StreamCopier
var handlerErr error var handlerErr error
switch endpoint { switch endpoint {
case EndpointSend: case EndpointSend:
@@ -127,12 +127,7 @@ func (s *Server) serveConn(nc *transport.AuthConn) {
s.log.WithError(err).Error("cannot unmarshal receive request") s.log.WithError(err).Error("cannot unmarshal receive request")
return return
} }
stream, err := c.ReadStream(ZFSStream, false) res, handlerErr = s.h.Receive(ctx, &req, &streamCopier{streamConn: c, closeStreamOnClose: false}) // SHADOWING
if err != nil {
s.log.WithError(err).Error("cannot open stream in receive request")
return
}
res, handlerErr = s.h.Receive(ctx, &req, stream) // SHADOWING
case EndpointPing: case EndpointPing:
var req pdu.PingReq var req pdu.PingReq
if err := proto.Unmarshal(reqStructured, &req); err != nil { if err := proto.Unmarshal(reqStructured, &req); err != nil {
+35
View File
@@ -1,7 +1,12 @@
package dataconn package dataconn
import ( import (
"io"
"sync"
"time" "time"
"github.com/zrepl/zrepl/rpc/dataconn/stream"
"github.com/zrepl/zrepl/zfs"
) )
const ( const (
@@ -34,3 +39,33 @@ const (
responseHeaderHandlerOk = "HANDLER OK\n" responseHeaderHandlerOk = "HANDLER OK\n"
responseHeaderHandlerErrorPrefix = "HANDLER ERROR:\n" responseHeaderHandlerErrorPrefix = "HANDLER ERROR:\n"
) )
type streamCopier struct {
mtx sync.Mutex
used bool
streamConn *stream.Conn
closeStreamOnClose bool
}
// WriteStreamTo implements zfs.StreamCopier
func (s *streamCopier) WriteStreamTo(w io.Writer) zfs.StreamCopierError {
s.mtx.Lock()
defer s.mtx.Unlock()
if s.used {
panic("streamCopier used multiple times")
}
s.used = true
return s.streamConn.ReadStreamInto(w, ZFSStream)
}
// Close implements zfs.StreamCopier
func (s *streamCopier) Close() error {
// only record the close here, what we do actually depends on whether
// the streamCopier is instantiated server-side or client-side
s.mtx.Lock()
defer s.mtx.Unlock()
if s.closeStreamOnClose {
return s.streamConn.Close()
}
return nil
}
+19 -4
View File
@@ -29,6 +29,7 @@ import (
"github.com/zrepl/zrepl/rpc/dataconn/timeoutconn" "github.com/zrepl/zrepl/rpc/dataconn/timeoutconn"
"github.com/zrepl/zrepl/transport" "github.com/zrepl/zrepl/transport"
"github.com/zrepl/zrepl/util/devnoop" "github.com/zrepl/zrepl/util/devnoop"
"github.com/zrepl/zrepl/zfs"
) )
func orDie(err error) { func orDie(err error) {
@@ -41,9 +42,23 @@ type readerStreamCopier struct{ io.Reader }
func (readerStreamCopier) Close() error { return nil } func (readerStreamCopier) Close() error { return nil }
type readerStreamCopierErr struct {
error
}
func (readerStreamCopierErr) IsReadError() bool { return false }
func (readerStreamCopierErr) IsWriteError() bool { return true }
func (c readerStreamCopier) WriteStreamTo(w io.Writer) zfs.StreamCopierError {
var buf [1 << 21]byte
_, err := io.CopyBuffer(w, c.Reader, buf[:])
// always assume write error
return readerStreamCopierErr{err}
}
type devNullHandler struct{} type devNullHandler struct{}
func (devNullHandler) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) { func (devNullHandler) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) {
var res pdu.SendRes var res pdu.SendRes
if args.devnoopReader { if args.devnoopReader {
return &res, readerStreamCopier{devnoop.Get()}, nil return &res, readerStreamCopier{devnoop.Get()}, nil
@@ -52,12 +67,12 @@ func (devNullHandler) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, i
} }
} }
func (devNullHandler) Receive(ctx context.Context, r *pdu.ReceiveReq, stream io.ReadCloser) (*pdu.ReceiveRes, error) { func (devNullHandler) Receive(ctx context.Context, r *pdu.ReceiveReq, stream zfs.StreamCopier) (*pdu.ReceiveRes, error) {
var out io.Writer = os.Stdout var out io.Writer = os.Stdout
if args.devnoopWriter { if args.devnoopWriter {
out = devnoop.Get() out = devnoop.Get()
} }
_, err := io.Copy(out, stream) err := stream.WriteStreamTo(out)
var res pdu.ReceiveRes var res pdu.ReceiveRes
return &res, err return &res, err
} }
@@ -157,7 +172,7 @@ func client() {
req := pdu.SendReq{} req := pdu.SendReq{}
_, stream, err := client.ReqSend(ctx, &req) _, stream, err := client.ReqSend(ctx, &req)
orDie(err) orDie(err)
_, err = io.Copy(os.Stdout, stream) err = stream.WriteStreamTo(os.Stdout)
orDie(err) orDie(err)
case "recv": case "recv":
var r io.Reader = os.Stdin var r io.Reader = os.Stdin
+2 -1
View File
@@ -14,6 +14,7 @@ import (
"github.com/zrepl/zrepl/rpc/dataconn/base2bufpool" "github.com/zrepl/zrepl/rpc/dataconn/base2bufpool"
"github.com/zrepl/zrepl/rpc/dataconn/frameconn" "github.com/zrepl/zrepl/rpc/dataconn/frameconn"
"github.com/zrepl/zrepl/rpc/dataconn/heartbeatconn" "github.com/zrepl/zrepl/rpc/dataconn/heartbeatconn"
"github.com/zrepl/zrepl/zfs"
) )
type Logger = logger.Logger type Logger = logger.Logger
@@ -197,7 +198,7 @@ func (e ReadStreamError) Temporary() bool {
return false return false
} }
var _ net.Error = &ReadStreamError{} var _ zfs.StreamCopierError = &ReadStreamError{}
func (e ReadStreamError) IsReadError() bool { func (e ReadStreamError) IsReadError() bool {
return e.Kind != ReadStreamErrorKindWrite return e.Kind != ReadStreamErrorKindWrite
+73 -47
View File
@@ -14,6 +14,7 @@ import (
"github.com/zrepl/zrepl/rpc/dataconn/heartbeatconn" "github.com/zrepl/zrepl/rpc/dataconn/heartbeatconn"
"github.com/zrepl/zrepl/rpc/dataconn/timeoutconn" "github.com/zrepl/zrepl/rpc/dataconn/timeoutconn"
"github.com/zrepl/zrepl/zfs"
) )
type Conn struct { type Conn struct {
@@ -39,7 +40,15 @@ type Conn struct {
var readMessageSentinel = fmt.Errorf("read stream complete") var readMessageSentinel = fmt.Errorf("read stream complete")
var errWriteStreamToErrorUnknownState = fmt.Errorf("dataconn read stream: connection is in unknown state") type writeStreamToErrorUnknownState struct{}
func (e writeStreamToErrorUnknownState) Error() string {
return "dataconn read stream: connection is in unknown state"
}
func (e writeStreamToErrorUnknownState) IsReadError() bool { return true }
func (e writeStreamToErrorUnknownState) IsWriteError() bool { return false }
func Wrap(nc timeoutconn.Wire, sendHeartbeatInterval, peerTimeout time.Duration) *Conn { func Wrap(nc timeoutconn.Wire, sendHeartbeatInterval, peerTimeout time.Duration) *Conn {
hc := heartbeatconn.Wrap(nc, sendHeartbeatInterval, peerTimeout) hc := heartbeatconn.Wrap(nc, sendHeartbeatInterval, peerTimeout)
@@ -114,28 +123,14 @@ func (c *Conn) ReadStreamedMessage(ctx context.Context, maxSize uint32, frameTyp
} }
} }
type StreamReader struct {
*io.PipeReader
conn *Conn
closeConnOnClose bool
}
func (r *StreamReader) Close() error {
err := r.PipeReader.Close()
if r.closeConnOnClose {
r.conn.Close() // TODO error logging
}
return err
}
// WriteStreamTo reads a stream from Conn and writes it to w. // WriteStreamTo reads a stream from Conn and writes it to w.
func (c *Conn) ReadStream(frameType uint32, closeConnOnClose bool) (_ *StreamReader, err error) { func (c *Conn) ReadStreamInto(w io.Writer, frameType uint32) (err zfs.StreamCopierError) {
// if we are closed while writing, return that as an error // if we are closed while writing, return that as an error
if closeGuard, cse := c.closeState.RWEntry(); cse != nil { if closeGuard, cse := c.closeState.RWEntry(); cse != nil {
return nil, cse return cse
} else { } else {
defer func(err *error) { defer func(err *zfs.StreamCopierError) {
if closed := closeGuard.RWExit(); closed != nil { if closed := closeGuard.RWExit(); closed != nil {
*err = closed *err = closed
} }
@@ -143,23 +138,18 @@ func (c *Conn) ReadStream(frameType uint32, closeConnOnClose bool) (_ *StreamRea
} }
c.readMtx.Lock() c.readMtx.Lock()
defer c.readMtx.Unlock()
if !c.readClean { if !c.readClean {
return nil, errWriteStreamToErrorUnknownState return writeStreamToErrorUnknownState{}
} }
var rse *ReadStreamError = readStream(c.frameReads, c.hc, w, frameType)
c.readClean = isConnCleanAfterRead(rse)
r, w := io.Pipe() // https://golang.org/doc/faq#nil_error
go func() { if rse == nil {
defer c.readMtx.Unlock() return nil
var err *ReadStreamError = readStream(c.frameReads, c.hc, w, frameType) }
if err != nil { return rse
_ = w.CloseWithError(err) // doc guarantees that error will always be nil
} else {
w.Close()
}
c.readClean = isConnCleanAfterRead(err)
}()
return &StreamReader{PipeReader: r, conn: c, closeConnOnClose: closeConnOnClose}, nil
} }
func (c *Conn) WriteStreamedMessage(ctx context.Context, buf io.Reader, frameType uint32) (err error) { func (c *Conn) WriteStreamedMessage(ctx context.Context, buf io.Reader, frameType uint32) (err error) {
@@ -188,7 +178,7 @@ func (c *Conn) WriteStreamedMessage(ctx context.Context, buf io.Reader, frameTyp
return errConn return errConn
} }
func (c *Conn) SendStream(ctx context.Context, stream io.ReadCloser, frameType uint32) (err error) { func (c *Conn) SendStream(ctx context.Context, src zfs.StreamCopier, frameType uint32) (err error) {
// if we are closed while reading, return that as an error // if we are closed while reading, return that as an error
if closeGuard, cse := c.closeState.RWEntry(); cse != nil { if closeGuard, cse := c.closeState.RWEntry(); cse != nil {
@@ -207,17 +197,49 @@ func (c *Conn) SendStream(ctx context.Context, stream io.ReadCloser, frameType u
return fmt.Errorf("dataconn send stream: connection is in unknown state") return fmt.Errorf("dataconn send stream: connection is in unknown state")
} }
errStream, errConn := writeStream(ctx, c.hc, stream, frameType) // avoid io.Pipe if zfs.StreamCopier is an io.Reader
var r io.Reader
c.writeClean = isConnCleanAfterWrite(errConn) // TODO correct? var w *io.PipeWriter
streamCopierErrChan := make(chan zfs.StreamCopierError, 1)
if errStream != nil { if reader, ok := src.(io.Reader); ok {
return errStream r = reader
} else if errConn != nil { streamCopierErrChan <- nil
return errConn close(streamCopierErrChan)
} else {
r, w = io.Pipe()
go func() {
streamCopierErrChan <- src.WriteStreamTo(w)
w.Close()
}()
}
type writeStreamRes struct {
errStream, errConn error
}
writeStreamErrChan := make(chan writeStreamRes, 1)
go func() {
var res writeStreamRes
res.errStream, res.errConn = writeStream(ctx, c.hc, r, frameType)
if w != nil {
_ = w.CloseWithError(res.errStream) // always returns nil
}
writeStreamErrChan <- res
}()
writeRes := <-writeStreamErrChan
streamCopierErr := <-streamCopierErrChan
c.writeClean = isConnCleanAfterWrite(writeRes.errConn) // TODO correct?
if streamCopierErr != nil && streamCopierErr.IsReadError() {
return streamCopierErr // something on our side is bad
} else {
if writeRes.errStream != nil {
return writeRes.errStream
} else if writeRes.errConn != nil {
return writeRes.errConn
}
// TODO combined error?
return streamCopierErr
} }
// TODO combined error?
return nil
} }
type closeState struct { type closeState struct {
@@ -226,13 +248,17 @@ type closeState struct {
type closeStateErrConnectionClosed struct{} type closeStateErrConnectionClosed struct{}
var _ zfs.StreamCopierError = (*closeStateErrConnectionClosed)(nil)
var _ error = (*closeStateErrConnectionClosed)(nil)
var _ net.Error = (*closeStateErrConnectionClosed)(nil) var _ net.Error = (*closeStateErrConnectionClosed)(nil)
func (e *closeStateErrConnectionClosed) Error() string { func (e *closeStateErrConnectionClosed) Error() string {
return "connection closed" return "connection closed"
} }
func (e *closeStateErrConnectionClosed) Timeout() bool { return false } func (e *closeStateErrConnectionClosed) IsReadError() bool { return true }
func (e *closeStateErrConnectionClosed) Temporary() bool { return false } func (e *closeStateErrConnectionClosed) IsWriteError() bool { return true }
func (e *closeStateErrConnectionClosed) Timeout() bool { return false }
func (e *closeStateErrConnectionClosed) Temporary() bool { return false }
func (s *closeState) CloseEntry() error { func (s *closeState) CloseEntry() error {
firstCloser := atomic.AddUint32(&s.closeCount, 1) == 1 firstCloser := atomic.AddUint32(&s.closeCount, 1) == 1
@@ -247,7 +273,7 @@ type closeStateEntry struct {
entryCount uint32 entryCount uint32
} }
func (s *closeState) RWEntry() (e *closeStateEntry, err net.Error) { func (s *closeState) RWEntry() (e *closeStateEntry, err zfs.StreamCopierError) {
entry := &closeStateEntry{s, atomic.LoadUint32(&s.closeCount)} entry := &closeStateEntry{s, atomic.LoadUint32(&s.closeCount)}
if entry.entryCount > 0 { if entry.entryCount > 0 {
return nil, &closeStateErrConnectionClosed{} return nil, &closeStateErrConnectionClosed{}
@@ -255,7 +281,7 @@ func (s *closeState) RWEntry() (e *closeStateEntry, err net.Error) {
return entry, nil return entry, nil
} }
func (e *closeStateEntry) RWExit() net.Error { func (e *closeStateEntry) RWExit() zfs.StreamCopierError {
if atomic.LoadUint32(&e.entryCount) == e.entryCount { if atomic.LoadUint32(&e.entryCount) == e.entryCount {
// no calls to Close() while running rw operation // no calls to Close() while running rw operation
return nil return nil
+7 -7
View File
@@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io"
"net" "net"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -21,6 +20,7 @@ import (
"github.com/zrepl/zrepl/rpc/versionhandshake" "github.com/zrepl/zrepl/rpc/versionhandshake"
"github.com/zrepl/zrepl/transport" "github.com/zrepl/zrepl/transport"
"github.com/zrepl/zrepl/util/envconst" "github.com/zrepl/zrepl/util/envconst"
"github.com/zrepl/zrepl/zfs"
) )
// Client implements the active side of a replication setup. // Client implements the active side of a replication setup.
@@ -82,22 +82,22 @@ func (c *Client) Close() {
// callers must ensure that the returned io.ReadCloser is closed // callers must ensure that the returned io.ReadCloser is closed
// TODO expose dataClient interface to the outside world // TODO expose dataClient interface to the outside world
func (c *Client) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error) { func (c *Client) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, zfs.StreamCopier, error) {
// TODO the returned sendStream may return a read error created by the remote side // TODO the returned sendStream may return a read error created by the remote side
res, stream, err := c.dataClient.ReqSend(ctx, r) res, streamCopier, err := c.dataClient.ReqSend(ctx, r)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if stream == nil { if streamCopier == nil {
return res, nil, nil return res, nil, nil
} }
return res, stream, nil return res, streamCopier, nil
} }
func (c *Client) Receive(ctx context.Context, req *pdu.ReceiveReq, stream io.ReadCloser) (*pdu.ReceiveRes, error) { func (c *Client) Receive(ctx context.Context, req *pdu.ReceiveReq, streamCopier zfs.StreamCopier) (*pdu.ReceiveRes, error) {
return c.dataClient.ReqRecv(ctx, req, stream) return c.dataClient.ReqRecv(ctx, req, streamCopier)
} }
func (c *Client) ListFilesystems(ctx context.Context, in *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error) { func (c *Client) ListFilesystems(ctx context.Context, in *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error) {
@@ -1,39 +0,0 @@
package bytecounter
import (
"io"
"sync/atomic"
)
// ReadCloser wraps an io.ReadCloser, reimplementing
// its interface and counting the bytes written to during copying.
type ReadCloser interface {
io.ReadCloser
Count() int64
}
// NewReadCloser wraps rc.
func NewReadCloser(rc io.ReadCloser) ReadCloser {
return &readCloser{rc, 0}
}
type readCloser struct {
rc io.ReadCloser
count int64
}
func (r *readCloser) Count() int64 {
return atomic.LoadInt64(&r.count)
}
var _ io.ReadCloser = &readCloser{}
func (r *readCloser) Close() error {
return r.rc.Close()
}
func (r *readCloser) Read(p []byte) (int, error) {
n, err := r.rc.Read(p)
atomic.AddInt64(&r.count, int64(n))
return n, err
}
+49
View File
@@ -0,0 +1,49 @@
package bytecounter
import (
"io"
"sync/atomic"
"time"
)
type ByteCounterReader struct {
reader io.ReadCloser
// called & accessed synchronously during Read, no external access
cb func(full int64)
cbEvery time.Duration
lastCbAt time.Time
// set atomically because it may be read by multiple threads
bytes int64
}
func NewByteCounterReader(reader io.ReadCloser) *ByteCounterReader {
return &ByteCounterReader{
reader: reader,
}
}
func (b *ByteCounterReader) SetCallback(every time.Duration, cb func(full int64)) {
b.cbEvery = every
b.cb = cb
}
func (b *ByteCounterReader) Close() error {
return b.reader.Close()
}
func (b *ByteCounterReader) Read(p []byte) (n int, err error) {
n, err = b.reader.Read(p)
full := atomic.AddInt64(&b.bytes, int64(n))
now := time.Now()
if b.cb != nil && now.Sub(b.lastCbAt) > b.cbEvery {
b.cb(full)
b.lastCbAt = now
}
return n, err
}
func (b *ByteCounterReader) Bytes() int64 {
return atomic.LoadInt64(&b.bytes)
}
@@ -0,0 +1,71 @@
package bytecounter
import (
"io"
"sync/atomic"
"github.com/zrepl/zrepl/zfs"
)
// StreamCopier wraps a zfs.StreamCopier, reimplementing
// its interface and counting the bytes written to during copying.
type StreamCopier interface {
zfs.StreamCopier
Count() int64
}
// NewStreamCopier wraps sc into a StreamCopier.
// If sc is io.Reader, it is guaranteed that the returned StreamCopier
// implements that interface, too.
func NewStreamCopier(sc zfs.StreamCopier) StreamCopier {
bsc := &streamCopier{sc, 0}
if scr, ok := sc.(io.Reader); ok {
return streamCopierAndReader{bsc, scr}
} else {
return bsc
}
}
type streamCopier struct {
sc zfs.StreamCopier
count int64
}
// proxy writer used by streamCopier
type streamCopierWriter struct {
parent *streamCopier
w io.Writer
}
func (w streamCopierWriter) Write(p []byte) (n int, err error) {
n, err = w.w.Write(p)
atomic.AddInt64(&w.parent.count, int64(n))
return
}
func (s *streamCopier) Count() int64 {
return atomic.LoadInt64(&s.count)
}
var _ zfs.StreamCopier = &streamCopier{}
func (s streamCopier) Close() error {
return s.sc.Close()
}
func (s *streamCopier) WriteStreamTo(w io.Writer) zfs.StreamCopierError {
ww := streamCopierWriter{s, w}
return s.sc.WriteStreamTo(ww)
}
// a streamCopier whose underlying sc is an io.Reader
type streamCopierAndReader struct {
*streamCopier
asReader io.Reader
}
func (scr streamCopierAndReader) Read(p []byte) (int, error) {
n, err := scr.asReader.Read(p)
atomic.AddInt64(&scr.streamCopier.count, int64(n))
return n, err
}
@@ -0,0 +1,39 @@
package bytecounter
import (
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/zrepl/zrepl/zfs"
)
type mockStreamCopierAndReader struct {
zfs.StreamCopier // to satisfy interface
reads int
}
func (r *mockStreamCopierAndReader) Read(p []byte) (int, error) {
r.reads++
return len(p), nil
}
var _ io.Reader = &mockStreamCopierAndReader{}
func TestNewStreamCopierReexportsReader(t *testing.T) {
mock := &mockStreamCopierAndReader{}
x := NewStreamCopier(mock)
r, ok := x.(io.Reader)
if !ok {
t.Fatalf("%T does not implement io.Reader, hence reader cannout have been wrapped", x)
}
var buf [23]byte
n, err := r.Read(buf[:])
assert.True(t, mock.reads == 1)
assert.True(t, n == len(buf))
assert.NoError(t, err)
assert.True(t, x.Count() == 23)
}
-43
View File
@@ -1,43 +0,0 @@
package chainedio
import "io"
type ChainedReadCloser struct {
readers []io.Reader
curReader int
}
func NewChainedReader(reader ...io.Reader) *ChainedReadCloser {
return &ChainedReadCloser{
readers: reader,
curReader: 0,
}
}
func (c *ChainedReadCloser) Read(buf []byte) (n int, err error) {
n = 0
for c.curReader < len(c.readers) {
n, err = c.readers[c.curReader].Read(buf)
if err == io.EOF {
c.curReader++
continue
}
break
}
if c.curReader == len(c.readers) {
err = io.EOF // actually, there was no gap
}
return
}
func (c *ChainedReadCloser) Close() error {
for _, r := range c.readers {
if c, ok := r.(io.Closer); ok {
c.Close() // TODO debug log error?
}
}
return nil
}
+34
View File
@@ -0,0 +1,34 @@
package chainedio
import "io"
type ChainedReader struct {
Readers []io.Reader
curReader int
}
func NewChainedReader(reader ...io.Reader) *ChainedReader {
return &ChainedReader{
Readers: reader,
curReader: 0,
}
}
func (c *ChainedReader) Read(buf []byte) (n int, err error) {
n = 0
for c.curReader < len(c.Readers) {
n, err = c.Readers[c.curReader].Read(buf)
if err == io.EOF {
c.curReader++
continue
}
break
}
if c.curReader == len(c.Readers) {
err = io.EOF // actually, there was no gap
}
return
}
-5
View File
@@ -40,8 +40,3 @@ func (l *L) DropWhile(f func()) {
defer l.Unlock().Lock() defer l.Unlock().Lock()
f() f()
} }
func (l *L) HoldWhile(f func()) {
defer l.Lock().Unlock()
f()
}
+2 -2
View File
@@ -206,13 +206,13 @@ func (o *ListFilesystemVersionsOptions) matches(v FilesystemVersion) bool {
} }
// returned versions are sorted by createtxg FIXME drop sort by createtxg requirement // returned versions are sorted by createtxg FIXME drop sort by createtxg requirement
func ZFSListFilesystemVersions(ctx context.Context, fs *DatasetPath, options ListFilesystemVersionsOptions) (res []FilesystemVersion, err error) { func ZFSListFilesystemVersions(fs *DatasetPath, options ListFilesystemVersionsOptions) (res []FilesystemVersion, err error) {
listResults := make(chan ZFSListResult) listResults := make(chan ZFSListResult)
promTimer := prometheus.NewTimer(prom.ZFSListFilesystemVersionDuration.WithLabelValues(fs.ToString())) promTimer := prometheus.NewTimer(prom.ZFSListFilesystemVersionDuration.WithLabelValues(fs.ToString()))
defer promTimer.ObserveDuration() defer promTimer.ObserveDuration()
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
go ZFSListChan(ctx, listResults, go ZFSListChan(ctx, listResults,
[]string{"name", "guid", "createtxg", "creation", "userrefs"}, []string{"name", "guid", "createtxg", "creation", "userrefs"},
+92 -62
View File
@@ -354,6 +354,63 @@ func (a ZFSSendArgsUnvalidated) buildCommonSendArgs() ([]string, error) {
return args, nil return args, nil
} }
type ReadCloserCopier struct {
recorder readErrRecorder
}
type readErrRecorder struct {
io.ReadCloser
readErr error
}
type sendStreamCopierError struct {
isReadErr bool // if false, it's a write error
err error
}
func (e sendStreamCopierError) Error() string {
if e.isReadErr {
return fmt.Sprintf("stream: read error: %s", e.err)
} else {
return fmt.Sprintf("stream: writer error: %s", e.err)
}
}
func (e sendStreamCopierError) IsReadError() bool { return e.isReadErr }
func (e sendStreamCopierError) IsWriteError() bool { return !e.isReadErr }
func (r *readErrRecorder) Read(p []byte) (n int, err error) {
n, err = r.ReadCloser.Read(p)
r.readErr = err
return n, err
}
func NewReadCloserCopier(stream io.ReadCloser) *ReadCloserCopier {
return &ReadCloserCopier{recorder: readErrRecorder{stream, nil}}
}
func (c *ReadCloserCopier) WriteStreamTo(w io.Writer) StreamCopierError {
debug("sendStreamCopier.WriteStreamTo: begin")
_, err := io.Copy(w, &c.recorder)
debug("sendStreamCopier.WriteStreamTo: copy done")
if err != nil {
if c.recorder.readErr != nil {
return sendStreamCopierError{isReadErr: true, err: c.recorder.readErr}
} else {
return sendStreamCopierError{isReadErr: false, err: err}
}
}
return nil
}
func (c *ReadCloserCopier) Read(p []byte) (n int, err error) {
return c.recorder.Read(p)
}
func (c *ReadCloserCopier) Close() error {
return c.recorder.ReadCloser.Close()
}
func pipeWithCapacityHint(capacity int) (r, w *os.File, err error) { func pipeWithCapacityHint(capacity int) (r, w *os.File, err error) {
if capacity <= 0 { if capacity <= 0 {
panic(fmt.Sprintf("capacity must be positive %v", capacity)) panic(fmt.Sprintf("capacity must be positive %v", capacity))
@@ -366,7 +423,7 @@ func pipeWithCapacityHint(capacity int) (r, w *os.File, err error) {
return stdoutReader, stdoutWriter, nil return stdoutReader, stdoutWriter, nil
} }
type SendStream struct { type sendStream struct {
cmd *zfscmd.Cmd cmd *zfscmd.Cmd
kill context.CancelFunc kill context.CancelFunc
@@ -376,7 +433,7 @@ type SendStream struct {
opErr error opErr error
} }
func (s *SendStream) Read(p []byte) (n int, err error) { func (s *sendStream) Read(p []byte) (n int, err error) {
s.closeMtx.Lock() s.closeMtx.Lock()
opErr := s.opErr opErr := s.opErr
s.closeMtx.Unlock() s.closeMtx.Unlock()
@@ -397,12 +454,12 @@ func (s *SendStream) Read(p []byte) (n int, err error) {
return n, err return n, err
} }
func (s *SendStream) Close() error { func (s *sendStream) Close() error {
debug("sendStream: close called") debug("sendStream: close called")
return s.killAndWait(nil) return s.killAndWait(nil)
} }
func (s *SendStream) killAndWait(precedingReadErr error) error { func (s *sendStream) killAndWait(precedingReadErr error) error {
debug("sendStream: killAndWait enter") debug("sendStream: killAndWait enter")
defer debug("sendStream: killAndWait leave") defer debug("sendStream: killAndWait leave")
@@ -773,7 +830,7 @@ var ErrEncryptedSendNotSupported = fmt.Errorf("raw sends which are required for
// (if from is "" a full ZFS send is done) // (if from is "" a full ZFS send is done)
// //
// Returns ErrEncryptedSendNotSupported if encrypted send is requested but not supported by CLI // Returns ErrEncryptedSendNotSupported if encrypted send is requested but not supported by CLI
func ZFSSend(ctx context.Context, sendArgs ZFSSendArgsValidated) (*SendStream, error) { func ZFSSend(ctx context.Context, sendArgs ZFSSendArgsValidated) (*ReadCloserCopier, error) {
args := make([]string, 0) args := make([]string, 0)
args = append(args, "send") args = append(args, "send")
@@ -822,14 +879,14 @@ func ZFSSend(ctx context.Context, sendArgs ZFSSendArgsValidated) (*SendStream, e
// close our writing-end of the pipe so that we don't wait for ourselves when reading from the reading end // close our writing-end of the pipe so that we don't wait for ourselves when reading from the reading end
stdoutWriter.Close() stdoutWriter.Close()
stream := &SendStream{ stream := &sendStream{
cmd: cmd, cmd: cmd,
kill: cancel, kill: cancel,
stdoutReader: stdoutReader, stdoutReader: stdoutReader,
stderrBuf: stderrBuf, stderrBuf: stderrBuf,
} }
return stream, nil return NewReadCloserCopier(stream), nil
} }
type DrySendType string type DrySendType string
@@ -968,6 +1025,24 @@ func ZFSSendDry(ctx context.Context, sendArgs ZFSSendArgsValidated) (_ *DrySendI
return &si, nil return &si, nil
} }
type StreamCopierError interface {
error
IsReadError() bool
IsWriteError() bool
}
type StreamCopier interface {
// WriteStreamTo writes the stream represented by this StreamCopier
// to the given io.Writer.
WriteStreamTo(w io.Writer) StreamCopierError
// Close must be called as soon as it is clear that no more data will
// be read from the StreamCopier.
// If StreamCopier gets its data from a connection, it might hold
// a lock on the connection until Close is called. Only closing ensures
// that the connection can be used afterwards.
Close() error
}
type RecvOptions struct { type RecvOptions struct {
// Rollback to the oldest snapshot, destroy it, then perform `recv -F`. // Rollback to the oldest snapshot, destroy it, then perform `recv -F`.
// Note that this doesn't change property values, i.e. an existing local property value will be kept. // Note that this doesn't change property values, i.e. an existing local property value will be kept.
@@ -992,9 +1067,7 @@ func (e *ErrRecvResumeNotSupported) Error() string {
return buf.String() return buf.String()
} }
const RecvStderrBufSiz = 1 << 15 func ZFSRecv(ctx context.Context, fs string, v *ZFSSendArgVersion, streamCopier StreamCopier, opts RecvOptions) (err error) {
func ZFSRecv(ctx context.Context, fs string, v *ZFSSendArgVersion, stream io.ReadCloser, opts RecvOptions) (err error) {
if err := v.ValidateInMemory(fs); err != nil { if err := v.ValidateInMemory(fs); err != nil {
return errors.Wrap(err, "invalid version") return errors.Wrap(err, "invalid version")
@@ -1011,7 +1084,7 @@ func ZFSRecv(ctx context.Context, fs string, v *ZFSSendArgVersion, stream io.Rea
if opts.RollbackAndForceRecv { if opts.RollbackAndForceRecv {
// destroy all snapshots before `recv -F` because `recv -F` // destroy all snapshots before `recv -F` because `recv -F`
// does not perform a rollback unless `send -R` was used (which we assume hasn't been the case) // does not perform a rollback unless `send -R` was used (which we assume hasn't been the case)
snaps, err := ZFSListFilesystemVersions(ctx, fsdp, ListFilesystemVersionsOptions{ snaps, err := ZFSListFilesystemVersions(fsdp, ListFilesystemVersionsOptions{
Types: Snapshots, Types: Snapshots,
}) })
if err != nil { if err != nil {
@@ -1061,7 +1134,7 @@ func ZFSRecv(ctx context.Context, fs string, v *ZFSSendArgVersion, stream io.Rea
// cannot receive new filesystem stream: invalid backup stream // cannot receive new filesystem stream: invalid backup stream
stdout := bytes.NewBuffer(make([]byte, 0, 1024)) stdout := bytes.NewBuffer(make([]byte, 0, 1024))
stderr := bytes.NewBuffer(make([]byte, 0, RecvStderrBufSiz)) stderr := bytes.NewBuffer(make([]byte, 0, 1024))
stdin, stdinWriter, err := pipeWithCapacityHint(ZFSRecvPipeCapacityHint) stdin, stdinWriter, err := pipeWithCapacityHint(ZFSRecvPipeCapacityHint)
if err != nil { if err != nil {
@@ -1089,10 +1162,9 @@ func ZFSRecv(ctx context.Context, fs string, v *ZFSSendArgVersion, stream io.Rea
debug("started") debug("started")
copierErrChan := make(chan error) copierErrChan := make(chan StreamCopierError)
go func() { go func() {
_, err := io.Copy(stdinWriter, stream) copierErrChan <- streamCopier.WriteStreamTo(stdinWriter)
copierErrChan <- err
stdinWriter.Close() stdinWriter.Close()
}() }()
waitErrChan := make(chan error) waitErrChan := make(chan error)
@@ -1101,10 +1173,6 @@ func ZFSRecv(ctx context.Context, fs string, v *ZFSSendArgVersion, stream io.Rea
if err = cmd.Wait(); err != nil { if err = cmd.Wait(); err != nil {
if rtErr := tryRecvErrorWithResumeToken(ctx, stderr.String()); rtErr != nil { if rtErr := tryRecvErrorWithResumeToken(ctx, stderr.String()); rtErr != nil {
waitErrChan <- rtErr waitErrChan <- rtErr
} else if owErr := tryRecvDestroyOrOverwriteEncryptedErr(stderr.Bytes()); owErr != nil {
waitErrChan <- owErr
} else if readErr := tryRecvCannotReadFromStreamErr(stderr.Bytes()); readErr != nil {
waitErrChan <- readErr
} else { } else {
waitErrChan <- &ZFSError{ waitErrChan <- &ZFSError{
Stderr: stderr.Bytes(), Stderr: stderr.Bytes(),
@@ -1115,23 +1183,22 @@ func ZFSRecv(ctx context.Context, fs string, v *ZFSSendArgVersion, stream io.Rea
} }
}() }()
// streamCopier always fails before or simultaneously with Wait
// thus receive from it first
copierErr := <-copierErrChan copierErr := <-copierErrChan
debug("copierErr: %T %s", copierErr, copierErr) debug("copierErr: %T %s", copierErr, copierErr)
if copierErr != nil { if copierErr != nil {
debug("killing zfs recv command after copierErr")
cancelCmd() cancelCmd()
} }
waitErr := <-waitErrChan waitErr := <-waitErrChan
debug("waitErr: %T %s", waitErr, waitErr) debug("waitErr: %T %s", waitErr, waitErr)
if copierErr == nil && waitErr == nil { if copierErr == nil && waitErr == nil {
return nil return nil
} else if _, isReadErr := waitErr.(*RecvCannotReadFromStreamErr); isReadErr { } else if waitErr != nil && (copierErr == nil || copierErr.IsWriteError()) {
return copierErr // likely network error reading from stream return waitErr // has more interesting info in that case
} else {
return waitErr // almost always more interesting info. NOTE: do not wrap!
} }
return copierErr // if it's not a write error, the copier error is more interesting
} }
type RecvFailedWithResumeTokenErr struct { type RecvFailedWithResumeTokenErr struct {
@@ -1161,43 +1228,6 @@ func (e *RecvFailedWithResumeTokenErr) Error() string {
return fmt.Sprintf("receive failed, resume token available: %s\n%#v", e.ResumeTokenRaw, e.ResumeTokenParsed) return fmt.Sprintf("receive failed, resume token available: %s\n%#v", e.ResumeTokenRaw, e.ResumeTokenParsed)
} }
type RecvDestroyOrOverwriteEncryptedErr struct {
Msg string
}
func (e *RecvDestroyOrOverwriteEncryptedErr) Error() string {
return e.Msg
}
var recvDestroyOrOverwriteEncryptedErrRe = regexp.MustCompile(`^(cannot receive new filesystem stream: zfs receive -F cannot be used to destroy an encrypted filesystem or overwrite an unencrypted one with an encrypted one)`)
func tryRecvDestroyOrOverwriteEncryptedErr(stderr []byte) *RecvDestroyOrOverwriteEncryptedErr {
debug("tryRecvDestroyOrOverwriteEncryptedErr: %v", stderr)
m := recvDestroyOrOverwriteEncryptedErrRe.FindSubmatch(stderr)
if m == nil {
return nil
}
return &RecvDestroyOrOverwriteEncryptedErr{Msg: string(m[1])}
}
type RecvCannotReadFromStreamErr struct {
Msg string
}
func (e *RecvCannotReadFromStreamErr) Error() string {
return e.Msg
}
var reRecvCannotReadFromStreamErr = regexp.MustCompile(`^(cannot receive: failed to read from stream)$`)
func tryRecvCannotReadFromStreamErr(stderr []byte) *RecvCannotReadFromStreamErr {
m := reRecvCannotReadFromStreamErr.FindSubmatch(stderr)
if m == nil {
return nil
}
return &RecvCannotReadFromStreamErr{Msg: string(m[1])}
}
type ClearResumeTokenError struct { type ClearResumeTokenError struct {
ZFSOutput []byte ZFSOutput []byte
CmdError error CmdError error
-11
View File
@@ -2,11 +2,9 @@ package zfs
import ( import (
"context" "context"
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestZFSListHandlesProducesZFSErrorOnNonZeroExit(t *testing.T) { func TestZFSListHandlesProducesZFSErrorOnNonZeroExit(t *testing.T) {
@@ -261,12 +259,3 @@ size 10518512
}) })
} }
} }
func TestTryRecvDestroyOrOverwriteEncryptedErr(t *testing.T) {
msg := "cannot receive new filesystem stream: zfs receive -F cannot be used to destroy an encrypted filesystem or overwrite an unencrypted one with an encrypted one\n"
assert.GreaterOrEqual(t, RecvStderrBufSiz, len(msg))
err := tryRecvDestroyOrOverwriteEncryptedErr([]byte(msg))
require.NotNil(t, err)
assert.EqualError(t, err, strings.TrimSpace(msg))
}
+8 -54
View File
@@ -18,10 +18,10 @@ import (
) )
type Cmd struct { type Cmd struct {
cmd *exec.Cmd cmd *exec.Cmd
ctx context.Context ctx context.Context
mtx sync.RWMutex mtx sync.RWMutex
startedAt, waitStartedAt, waitReturnedAt time.Time startedAt, waitReturnedAt time.Time
} }
func CommandContext(ctx context.Context, name string, arg ...string) *Cmd { func CommandContext(ctx context.Context, name string, arg ...string) *Cmd {
@@ -119,65 +119,19 @@ func (c *Cmd) startPost(err error) {
} }
func (c *Cmd) waitPre() { func (c *Cmd) waitPre() {
now := time.Now() waitPreLogging(c, time.Now())
// ignore duplicate waits
c.mtx.Lock()
// ignore duplicate waits
if !c.waitStartedAt.IsZero() {
c.mtx.Unlock()
return
}
c.waitStartedAt = now
c.mtx.Unlock()
waitPreLogging(c, now)
}
type usage struct {
total_secs, system_secs, user_secs float64
} }
func (c *Cmd) waitPost(err error) { func (c *Cmd) waitPost(err error) {
now := time.Now() now := time.Now()
c.mtx.Lock() c.mtx.Lock()
// ignore duplicate waits
if !c.waitReturnedAt.IsZero() {
c.mtx.Unlock()
return
}
c.waitReturnedAt = now c.waitReturnedAt = now
c.mtx.Unlock() c.mtx.Unlock()
// build usage waitPostReport(c, now)
var u usage waitPostLogging(c, err, now)
{ waitPostPrometheus(c, err, now)
var s *os.ProcessState
if err == nil {
s = c.cmd.ProcessState
} else if ee, ok := err.(*exec.ExitError); ok {
s = ee.ProcessState
}
if s == nil {
u = usage{
total_secs: c.Runtime().Seconds(),
system_secs: -1,
user_secs: -1,
}
} else {
u = usage{
total_secs: c.Runtime().Seconds(),
system_secs: s.SystemTime().Seconds(),
user_secs: s.UserTime().Seconds(),
}
}
}
waitPostReport(c, u, now)
waitPostLogging(c, u, err, now)
waitPostPrometheus(c, u, err, now)
} }
// returns 0 if the command did not yet finish // returns 0 if the command did not yet finish
+4 -5
View File
@@ -27,12 +27,11 @@ func waitPreLogging(c *Cmd, now time.Time) {
c.log().Debug("start waiting") c.log().Debug("start waiting")
} }
func waitPostLogging(c *Cmd, u usage, err error, now time.Time) { func waitPostLogging(c *Cmd, err error, now time.Time) {
log := c.log(). log := c.log().
WithField("total_time_s", u.total_secs). WithField("total_time_s", c.Runtime().Seconds()).
WithField("systemtime_s", u.system_secs). WithField("systemtime_s", c.cmd.ProcessState.SystemTime().Seconds()).
WithField("usertime_s", u.user_secs) WithField("usertime_s", c.cmd.ProcessState.UserTime().Seconds())
if err == nil { if err == nil {
log.Info("command exited without error") log.Info("command exited without error")
-64
View File
@@ -1,18 +1,13 @@
package zfscmd package zfscmd
import ( import (
"bufio"
"bytes" "bytes"
"context"
"io" "io"
"os"
"os/exec" "os/exec"
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/zrepl/zrepl/util/circlog"
) )
const testBin = "./zfscmd_platform_test.bash" const testBin = "./zfscmd_platform_test.bash"
@@ -63,62 +58,3 @@ func TestCmdStderrBehaviorStdoutPipe(t *testing.T) {
require.True(t, ok) require.True(t, ok)
require.Empty(t, ee.Stderr) // !!!!! probably not what one would expect if we only redirect stdout require.Empty(t, ee.Stderr) // !!!!! probably not what one would expect if we only redirect stdout
} }
func TestCmdProcessState(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cmd := exec.CommandContext(ctx, "bash", "-c", "echo running; sleep 3600")
stdout, err := cmd.StdoutPipe()
require.NoError(t, err)
err = cmd.Start()
require.NoError(t, err)
r := bufio.NewReader(stdout)
line, err := r.ReadString('\n')
require.NoError(t, err)
require.Equal(t, "running\n", line)
// we know it's running and sleeping
cancel()
err = cmd.Wait()
t.Logf("wait err %T\n%s", err, err)
require.Error(t, err)
ee, ok := err.(*exec.ExitError)
require.True(t, ok)
require.NotNil(t, ee.ProcessState)
require.Contains(t, ee.Error(), "killed")
}
func TestSigpipe(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cmd := CommandContext(ctx, "bash", "-c", "sleep 5; echo invalid input; exit 23")
r, w, err := os.Pipe()
require.NoError(t, err)
output := circlog.MustNewCircularLog(1 << 20)
cmd.SetStdio(Stdio{
Stdin: r,
Stdout: output,
Stderr: output,
})
err = cmd.Start()
require.NoError(t, err)
err = r.Close()
require.NoError(t, err)
// the script doesn't read stdin, but this input is almost certainly smaller than the pipe buffer
const LargerThanPipeBuffer = 1 << 21
_, err = io.Copy(w, bytes.NewBuffer(bytes.Repeat([]byte("i"), LargerThanPipeBuffer)))
// => io.Copy is going to block because the pipe buffer is full and the
// script is not reading from it
// => the script is going to exit after 5s
// => we should expect a broken pipe error from the copier's perspective
t.Logf("copy err = %T: %s", err, err)
require.NotNil(t, err)
require.True(t, strings.Contains(err.Error(), "broken pipe"))
err = cmd.Wait()
require.EqualError(t, err, "exit status 23")
}
+4 -4
View File
@@ -46,7 +46,7 @@ func RegisterMetrics(r prometheus.Registerer) {
r.MustRegister(metrics.usertime) r.MustRegister(metrics.usertime)
} }
func waitPostPrometheus(c *Cmd, u usage, err error, now time.Time) { func waitPostPrometheus(c *Cmd, err error, now time.Time) {
if len(c.cmd.Args) < 2 { if len(c.cmd.Args) < 2 {
getLogger(c.ctx).WithField("args", c.cmd.Args). getLogger(c.ctx).WithField("args", c.cmd.Args).
@@ -64,10 +64,10 @@ func waitPostPrometheus(c *Cmd, u usage, err error, now time.Time) {
metrics.totaltime. metrics.totaltime.
WithLabelValues(labelValues...). WithLabelValues(labelValues...).
Observe(u.total_secs) Observe(c.Runtime().Seconds())
metrics.systemtime.WithLabelValues(labelValues...). metrics.systemtime.WithLabelValues(labelValues...).
Observe(u.system_secs) Observe(c.cmd.ProcessState.SystemTime().Seconds())
metrics.usertime.WithLabelValues(labelValues...). metrics.usertime.WithLabelValues(labelValues...).
Observe(u.user_secs) Observe(c.cmd.ProcessState.UserTime().Seconds())
} }
+1 -1
View File
@@ -57,7 +57,7 @@ func startPostReport(c *Cmd, err error, now time.Time) {
active.mtx.Unlock() active.mtx.Unlock()
} }
func waitPostReport(c *Cmd, _ usage, now time.Time) { func waitPostReport(c *Cmd, now time.Time) {
active.mtx.Lock() active.mtx.Lock()
defer active.mtx.Unlock() defer active.mtx.Unlock()
prev := active.cmds[c] prev := active.cmds[c]