From 30cdc1430ee5e1684b98e0a483be5ed4417f4eaf Mon Sep 17 00:00:00 2001 From: Christian Schwarz Date: Sat, 27 Jun 2020 23:53:33 +0200 Subject: [PATCH] replication + endpoint: replication guarantees: guarantee_{resumability,incremental,nothing} This commit - adds a configuration in which no step holds, replication cursors, etc. are created - removes the send.step_holds.disable_incremental setting - creates a new config option `replication` for active-side jobs - adds the replication.protection.{initial,incremental} settings, each of which can have values - `guarantee_resumability` - `guarantee_incremental` - `guarantee_nothing` (refer to docs/configuration/replication.rst for semantics) The `replication` config from an active side is sent to both endpoint.Sender and endpoint.Receiver for each replication step. Sender and Receiver then act accordingly. For `guarantee_incremental`, we add the new `tentative-replication-cursor` abstraction. The necessity for that abstraction is outlined in https://github.com/zrepl/zrepl/issues/340. fixes https://github.com/zrepl/zrepl/issues/340 --- config/config.go | 47 +-- config/config_send_test.go | 6 +- .../quickstart_backup_to_external_disk.yml | 15 +- daemon/job/active.go | 44 +-- daemon/job/build_jobs_sendrecvoptions.go | 55 +++ daemon/job/passive.go | 28 +- daemon/job/snapjob.go | 2 - daemon/snapper/snapper.go | 7 +- daemon/snapper/snapper_all.go | 4 +- docs/changelog.rst | 19 +- docs/configuration.rst | 1 + docs/configuration/overview.rst | 56 +-- docs/configuration/replication.rst | 47 +++ docs/configuration/sendrecvoptions.rst | 20 -- docs/quickstart/backup_to_external_disk.rst | 2 +- endpoint/endpoint.go | 181 +++++----- ...ache.go => endpoint_abstractions_cache.go} | 85 ++--- endpoint/endpoint_guarantees.go | 213 ++++++++++++ endpoint/endpoint_metrics.go | 2 +- endpoint/endpoint_zfs_abstraction.go | 57 ++-- ...oint_zfs_abstraction_last_received_hold.go | 91 +++++ ...int_zfs_abstraction_replication_cursor.go} | 193 +++-------- endpoint/endpoint_zfs_abstraction_step.go | 159 --------- .../endpoint_zfs_abstraction_step_hold.go | 83 +++++ endpoint/jobid.go | 4 +- endpoint/replicationguaranteekind_enumer.go | 80 +++++ platformtest/tests/generated_cases.go | 7 +- platformtest/tests/replication.go | 302 ++++++++++++++--- platformtest/tests/replicationCursor.go | 7 +- replication/logic/pdu/pdu.pb.go | 318 +++++++++++++----- replication/logic/pdu/pdu.proto | 21 ++ replication/logic/pdu/pdu_extras.go | 7 + replication/logic/replication_logic.go | 24 +- replication/logic/replication_logic_policy.go | 42 +++ rpc/versionhandshake/versionhandshake.go | 2 +- zfs/zfs.go | 29 +- 36 files changed, 1503 insertions(+), 757 deletions(-) create mode 100644 daemon/job/build_jobs_sendrecvoptions.go create mode 100644 docs/configuration/replication.rst rename endpoint/{endpoint_send_abstractions_cache.go => endpoint_abstractions_cache.go} (53%) create mode 100644 endpoint/endpoint_guarantees.go create mode 100644 endpoint/endpoint_zfs_abstraction_last_received_hold.go rename endpoint/{endpoint_zfs_abstraction_cursor_and_last_received_hold.go => endpoint_zfs_abstraction_replication_cursor.go} (62%) delete mode 100644 endpoint/endpoint_zfs_abstraction_step.go create mode 100644 endpoint/endpoint_zfs_abstraction_step_hold.go create mode 100644 endpoint/replicationguaranteekind_enumer.go create mode 100644 replication/logic/replication_logic_policy.go diff --git a/config/config.go b/config/config.go index 6ff2718..117f711 100644 --- a/config/config.go +++ b/config/config.go @@ -52,11 +52,12 @@ func (j JobEnum) Name() string { } type ActiveJob struct { - Type string `yaml:"type"` - Name string `yaml:"name"` - Connect ConnectEnum `yaml:"connect"` - Pruning PruningSenderReceiver `yaml:"pruning"` - Debug JobDebugSettings `yaml:"debug,optional"` + Type string `yaml:"type"` + Name string `yaml:"name"` + Connect ConnectEnum `yaml:"connect"` + Pruning PruningSenderReceiver `yaml:"pruning"` + Debug JobDebugSettings `yaml:"debug,optional"` + Replication *Replication `yaml:"replication,optional,fromdefaults"` } type PassiveJob struct { @@ -76,18 +77,7 @@ type SnapJob struct { } type SendOptions struct { - Encrypted bool `yaml:"encrypted"` - StepHolds SendOptionsStepHolds `yaml:"step_holds,optional"` -} - -type SendOptionsStepHolds struct { - DisableIncremental bool `yaml:"disable_incremental,optional"` -} - -var _ yaml.Defaulter = (*SendOptions)(nil) - -func (l *SendOptions) SetDefault() { - *l = SendOptions{Encrypted: false} + Encrypted bool `yaml:"encrypted,optional,default=false"` } type RecvOptions struct { @@ -98,10 +88,13 @@ type RecvOptions struct { // Reencrypt bool `yaml:"reencrypt"` } -var _ yaml.Defaulter = (*RecvOptions)(nil) +type Replication struct { + Protection *ReplicationOptionsProtection `yaml:"protection,optional,fromdefaults"` +} -func (l *RecvOptions) SetDefault() { - *l = RecvOptions{} +type ReplicationOptionsProtection struct { + Initial string `yaml:"initial,optional,default=guarantee_resumability"` + Incremental string `yaml:"incremental,optional,default=guarantee_resumability"` } type PushJob struct { @@ -111,6 +104,9 @@ type PushJob struct { Send *SendOptions `yaml:"send,fromdefaults,optional"` } +func (j *PushJob) GetFilesystems() FilesystemsFilter { return j.Filesystems } +func (j *PushJob) GetSendOptions() *SendOptions { return j.Send } + type PullJob struct { ActiveJob `yaml:",inline"` RootFS string `yaml:"root_fs"` @@ -118,6 +114,10 @@ type PullJob struct { Recv *RecvOptions `yaml:"recv,fromdefaults,optional"` } +func (j *PullJob) GetRootFS() string { return j.RootFS } +func (j *PullJob) GetAppendClientIdentity() bool { return false } +func (j *PullJob) GetRecvOptions() *RecvOptions { return j.Recv } + type PositiveDurationOrManual struct { Interval time.Duration Manual bool @@ -155,6 +155,10 @@ type SinkJob struct { Recv *RecvOptions `yaml:"recv,optional,fromdefaults"` } +func (j *SinkJob) GetRootFS() string { return j.RootFS } +func (j *SinkJob) GetAppendClientIdentity() bool { return true } +func (j *SinkJob) GetRecvOptions() *RecvOptions { return j.Recv } + type SourceJob struct { PassiveJob `yaml:",inline"` Snapshotting SnapshottingEnum `yaml:"snapshotting"` @@ -162,6 +166,9 @@ type SourceJob struct { Send *SendOptions `yaml:"send,optional,fromdefaults"` } +func (j *SourceJob) GetFilesystems() FilesystemsFilter { return j.Filesystems } +func (j *SourceJob) GetSendOptions() *SendOptions { return j.Send } + type FilesystemsFilter map[string]bool type SnapshottingEnum struct { diff --git a/config/config_send_test.go b/config/config_send_test.go index 17b6c99..b708789 100644 --- a/config/config_send_test.go +++ b/config/config_send_test.go @@ -60,9 +60,9 @@ jobs: }) t.Run("encrypted_unspecified", func(t *testing.T) { - c, err := testConfig(t, fill(encrypted_unspecified)) - assert.Error(t, err) - assert.Nil(t, c) + c = testValidConfig(t, fill(encrypted_unspecified)) + encrypted := c.Jobs[0].Ret.(*PushJob).Send.Encrypted + assert.Equal(t, false, encrypted) }) t.Run("send_not_specified", func(t *testing.T) { diff --git a/config/samples/quickstart_backup_to_external_disk.yml b/config/samples/quickstart_backup_to_external_disk.yml index 57ad8df..9d7c151 100644 --- a/config/samples/quickstart_backup_to_external_disk.yml +++ b/config/samples/quickstart_backup_to_external_disk.yml @@ -52,12 +52,15 @@ jobs: } send: encrypted: true - # disable incremental step holds so that - # - we can yank out the backup drive during replication - # - thereby sacrificing resumability - # - in exchange for the replicating snapshot not sticking around until we reconnect the backup drive - step_holds: - disable_incremental: true + replication: + protection: + initial: guarantee_resumability + # Downgrade protection to guarantee_incremental which uses zfs bookmarks instead of zfs holds. + # Thus, when we yank out the backup drive during replication + # - we might not be able to resume the interrupted replication step because the partially received `to` snapshot of a `from`->`to` step may be pruned any time + # - but in exchange we get back the disk space allocated by `to` when we prune it + # - and because we still have the bookmarks created by `guarantee_incremental`, we can still do incremental replication of `from`->`to2` in the future + incremental: guarantee_incremental snapshotting: type: manual pruning: diff --git a/daemon/job/active.go b/daemon/job/active.go index fb66f94..bd8daa9 100644 --- a/daemon/job/active.go +++ b/daemon/job/active.go @@ -12,7 +12,6 @@ import ( "github.com/zrepl/zrepl/daemon/logging/trace" "github.com/zrepl/zrepl/config" - "github.com/zrepl/zrepl/daemon/filters" "github.com/zrepl/zrepl/daemon/job/reset" "github.com/zrepl/zrepl/daemon/job/wakeup" "github.com/zrepl/zrepl/daemon/pruner" @@ -145,23 +144,24 @@ func (m *modePush) ResetConnectBackoff() { func modePushFromConfig(g *config.Global, in *config.PushJob, jobID endpoint.JobID) (*modePush, error) { m := &modePush{} + var err error - fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems) + m.senderConfig, err = buildSenderConfig(in, jobID) if err != nil { - return nil, errors.Wrap(err, "cannot build filesystem filter") + return nil, errors.Wrap(err, "sender config") } - m.senderConfig = &endpoint.SenderConfig{ - FSF: fsf, - Encrypt: &zfs.NilBool{B: in.Send.Encrypted}, - DisableIncrementalStepHolds: in.Send.StepHolds.DisableIncremental, - JobID: jobID, + replicationConfig, err := logic.ReplicationConfigFromConfig(in.Replication) + if err != nil { + return nil, errors.Wrap(err, "field `replication`") } + m.plannerPolicy = &logic.PlannerPolicy{ - EncryptedSend: logic.TriFromBool(in.Send.Encrypted), + EncryptedSend: logic.TriFromBool(in.Send.Encrypted), + ReplicationConfig: *replicationConfig, } - if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil { + if m.snapper, err = snapper.FromConfig(g, m.senderConfig.FSF, in.Snapshotting); err != nil { return nil, errors.Wrap(err, "cannot build snapper") } @@ -173,7 +173,6 @@ type modePull struct { receiver *endpoint.Receiver receiverConfig endpoint.ReceiverConfig sender *rpc.Client - rootFS *zfs.DatasetPath plannerPolicy *logic.PlannerPolicy interval config.PositiveDurationOrManual } @@ -247,26 +246,19 @@ func modePullFromConfig(g *config.Global, in *config.PullJob, jobID endpoint.Job m = &modePull{} m.interval = in.Interval - m.rootFS, err = zfs.NewDatasetPath(in.RootFS) + replicationConfig, err := logic.ReplicationConfigFromConfig(in.Replication) if err != nil { - return nil, errors.New("RootFS is not a valid zfs filesystem path") - } - if m.rootFS.Length() <= 0 { - return nil, errors.New("RootFS must not be empty") // duplicates error check of receiver + return nil, errors.Wrap(err, "field `replication`") } m.plannerPolicy = &logic.PlannerPolicy{ - EncryptedSend: logic.DontCare, + EncryptedSend: logic.DontCare, + ReplicationConfig: *replicationConfig, } - m.receiverConfig = endpoint.ReceiverConfig{ - JobID: jobID, - RootWithoutClientComponent: m.rootFS, - AppendClientIdentity: false, // ! - UpdateLastReceivedHold: true, - } - if err := m.receiverConfig.Validate(); err != nil { - return nil, errors.Wrap(err, "cannot build receiver config") + m.receiverConfig, err = buildReceiverConfig(in, jobID) + if err != nil { + return nil, err } return m, nil @@ -365,7 +357,7 @@ func (j *ActiveSide) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) { _ = j.mode.(*modePush) // make sure we didn't introduce a new job type return nil, false } - return pull.rootFS.Copy(), true + return pull.receiverConfig.RootWithoutClientComponent.Copy(), true } func (j *ActiveSide) SenderConfig() *endpoint.SenderConfig { diff --git a/daemon/job/build_jobs_sendrecvoptions.go b/daemon/job/build_jobs_sendrecvoptions.go new file mode 100644 index 0000000..0db9553 --- /dev/null +++ b/daemon/job/build_jobs_sendrecvoptions.go @@ -0,0 +1,55 @@ +package job + +import ( + "github.com/pkg/errors" + "github.com/zrepl/zrepl/config" + "github.com/zrepl/zrepl/daemon/filters" + "github.com/zrepl/zrepl/endpoint" + "github.com/zrepl/zrepl/zfs" +) + +type SendingJobConfig interface { + GetFilesystems() config.FilesystemsFilter + GetSendOptions() *config.SendOptions // must not be nil +} + +func buildSenderConfig(in SendingJobConfig, jobID endpoint.JobID) (*endpoint.SenderConfig, error) { + + fsf, err := filters.DatasetMapFilterFromConfig(in.GetFilesystems()) + if err != nil { + return nil, errors.Wrap(err, "cannot build filesystem filter") + } + + return &endpoint.SenderConfig{ + FSF: fsf, + Encrypt: &zfs.NilBool{B: in.GetSendOptions().Encrypted}, + JobID: jobID, + }, nil +} + +type ReceivingJobConfig interface { + GetRootFS() string + GetAppendClientIdentity() bool + GetRecvOptions() *config.RecvOptions +} + +func buildReceiverConfig(in ReceivingJobConfig, jobID endpoint.JobID) (rc endpoint.ReceiverConfig, err error) { + rootFs, err := zfs.NewDatasetPath(in.GetRootFS()) + if err != nil { + return rc, errors.New("root_fs is not a valid zfs filesystem path") + } + if rootFs.Length() <= 0 { + return rc, errors.New("root_fs must not be empty") // duplicates error check of receiver + } + + rc = endpoint.ReceiverConfig{ + JobID: jobID, + RootWithoutClientComponent: rootFs, + AppendClientIdentity: in.GetAppendClientIdentity(), + } + if err := rc.Validate(); err != nil { + return rc, errors.Wrap(err, "cannot build receiver config") + } + + return rc, nil +} diff --git a/daemon/job/passive.go b/daemon/job/passive.go index 3c1d32e..a75d0d5 100644 --- a/daemon/job/passive.go +++ b/daemon/job/passive.go @@ -9,7 +9,6 @@ import ( "github.com/zrepl/zrepl/daemon/logging/trace" "github.com/zrepl/zrepl/config" - "github.com/zrepl/zrepl/daemon/filters" "github.com/zrepl/zrepl/daemon/logging" "github.com/zrepl/zrepl/daemon/snapper" "github.com/zrepl/zrepl/endpoint" @@ -48,19 +47,9 @@ func (m *modeSink) SnapperReport() *snapper.Report { return nil } func modeSinkFromConfig(g *config.Global, in *config.SinkJob, jobID endpoint.JobID) (m *modeSink, err error) { m = &modeSink{} - rootDataset, err := zfs.NewDatasetPath(in.RootFS) + m.receiverConfig, err = buildReceiverConfig(in, jobID) if err != nil { - return nil, errors.New("root dataset is not a valid zfs filesystem path") - } - - m.receiverConfig = endpoint.ReceiverConfig{ - JobID: jobID, - RootWithoutClientComponent: rootDataset, - AppendClientIdentity: true, // ! - UpdateLastReceivedHold: true, - } - if err := m.receiverConfig.Validate(); err != nil { - return nil, errors.Wrap(err, "cannot build receiver config") + return nil, err } return m, nil @@ -74,18 +63,13 @@ type modeSource struct { func modeSourceFromConfig(g *config.Global, in *config.SourceJob, jobID endpoint.JobID) (m *modeSource, err error) { // FIXME exact dedup of modePush m = &modeSource{} - fsf, err := filters.DatasetMapFilterFromConfig(in.Filesystems) + + m.senderConfig, err = buildSenderConfig(in, jobID) if err != nil { - return nil, errors.Wrap(err, "cannot build filesystem filter") - } - m.senderConfig = &endpoint.SenderConfig{ - FSF: fsf, - Encrypt: &zfs.NilBool{B: in.Send.Encrypted}, - DisableIncrementalStepHolds: in.Send.StepHolds.DisableIncremental, - JobID: jobID, + return nil, errors.Wrap(err, "send options") } - if m.snapper, err = snapper.FromConfig(g, fsf, in.Snapshotting); err != nil { + if m.snapper, err = snapper.FromConfig(g, m.senderConfig.FSF, in.Snapshotting); err != nil { return nil, errors.Wrap(err, "cannot build snapper") } diff --git a/daemon/job/snapjob.go b/daemon/job/snapjob.go index 24f6863..3ce7ee4 100644 --- a/daemon/job/snapjob.go +++ b/daemon/job/snapjob.go @@ -175,8 +175,6 @@ func (j *SnapJob) doPrune(ctx context.Context) { FSF: j.fsfilter, // FIXME encryption setting is irrelevant for SnapJob because the endpoint is only used as pruner.Target Encrypt: &zfs.NilBool{B: true}, - // FIXME DisableIncrementalStepHolds setting is irrelevant for SnapJob because the endpoint is only used as pruner.Target - DisableIncrementalStepHolds: false, }) j.pruner = j.prunerFactory.BuildLocalPruner(ctx, sender, alwaysUpToDateReplicationCursorHistory{sender}) log.Info("start pruning") diff --git a/daemon/snapper/snapper.go b/daemon/snapper/snapper.go index 35433d5..b2460de 100644 --- a/daemon/snapper/snapper.go +++ b/daemon/snapper/snapper.go @@ -11,7 +11,6 @@ import ( "github.com/zrepl/zrepl/daemon/logging/trace" "github.com/zrepl/zrepl/config" - "github.com/zrepl/zrepl/daemon/filters" "github.com/zrepl/zrepl/daemon/hooks" "github.com/zrepl/zrepl/daemon/logging" "github.com/zrepl/zrepl/logger" @@ -49,7 +48,7 @@ type args struct { ctx context.Context prefix string interval time.Duration - fsf *filters.DatasetMapFilter + fsf zfs.DatasetFilter snapshotsTaken chan<- struct{} hooks *hooks.List dryRun bool @@ -109,7 +108,7 @@ func getLogger(ctx context.Context) Logger { return logging.GetLogger(ctx, logging.SubsysSnapshot) } -func PeriodicFromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in *config.SnapshottingPeriodic) (*Snapper, error) { +func PeriodicFromConfig(g *config.Global, fsf zfs.DatasetFilter, in *config.SnapshottingPeriodic) (*Snapper, error) { if in.Prefix == "" { return nil, errors.New("prefix must not be empty") } @@ -383,7 +382,7 @@ func wait(a args, u updater) state { } } -func listFSes(ctx context.Context, mf *filters.DatasetMapFilter) (fss []*zfs.DatasetPath, err error) { +func listFSes(ctx context.Context, mf zfs.DatasetFilter) (fss []*zfs.DatasetPath, err error) { return zfs.ZFSListMapping(ctx, mf) } diff --git a/daemon/snapper/snapper_all.go b/daemon/snapper/snapper_all.go index 51eda9e..3141069 100644 --- a/daemon/snapper/snapper_all.go +++ b/daemon/snapper/snapper_all.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/zrepl/zrepl/config" - "github.com/zrepl/zrepl/daemon/filters" + "github.com/zrepl/zrepl/zfs" ) // FIXME: properly abstract snapshotting: @@ -32,7 +32,7 @@ func (s *PeriodicOrManual) Report() *Report { return nil } -func FromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in config.SnapshottingEnum) (*PeriodicOrManual, error) { +func FromConfig(g *config.Global, fsf zfs.DatasetFilter, in config.SnapshottingEnum) (*PeriodicOrManual, error) { switch v := in.Ret.(type) { case *config.SnapshottingPeriodic: snapper, err := PeriodicFromConfig(g, fsf, v) diff --git a/docs/changelog.rst b/docs/changelog.rst index 51673ea..dc55bbc 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -34,27 +34,30 @@ This is a big one! Headlining features: * **Resumable Send & Recv Support** No knobs required, automatically used where supported. -* **Hold-Protected Send & Recv** - Automatic ZFS holds to ensure that we can always use resumable send&recv for a replication step. * **Encrypted Send & Recv Support** for OpenZFS native encryption, :ref:`configurable ` at the job level, i.e., for all filesystems a job is responsible for. -* **Receive-side hold on last received dataset** - The counterpart to the replication cursor bookmark on the send-side. - Ensures that incremental replication will always be possible between a sender and receiver. +* **Replication Guarantees** + Automatic use of ZFS holds and bookmarks to protect a replicated filesystem from losing synchronization between sender and receiver. + By default, zrepl guarantees that incremental replication will always be possible and interrupted steps will always be resumable. .. TIP:: - We highly recommend studying the :ref:`overview section of the configuration chapter ` to understand how replication works. + We highly recommend studying the updated :ref:`overview section of the configuration chapter ` to understand how replication works. + +Quick-start guides: + +* We have added :ref:`another quick-start guide for a typical workstation use case for zrepl `. + Check it out to learn how you can use zrepl to back up your workstation's OpenZFS natively-encrypted root filesystem to an external disk. Additional changelog: * |break| |break_config| **more restrictive job names than in prior zrepl versions** - Starting with this version, job names are going to be embedded into ZFS holds and bookmark names (see :ref:`here` and :ref:`here`). + Starting with this version, job names are going to be embedded into ZFS holds and bookmark names (see :ref:`this section for details `). Therefore you might need to adjust your job names. **Note that jobs** cannot be renamed easily **once you start using zrepl 0.3.** * |break| |mig| replication cursor representation changed - * zrepl now manages the :ref:`replication cursor bookmark ` per job-filesystem tuple instead of a single replication cursor per filesystem. + * zrepl now manages the :ref:`replication cursor bookmark ` per job-filesystem tuple instead of a single replication cursor per filesystem. In the future, this will permit multiple sending jobs to send from the same filesystems. * ZFS does not allow bookmark renaming, thus we cannot migrate the old replication cursors. * zrepl 0.3 will automatically create cursors in the new format for new replications, and warn if it still finds ones in the old format. diff --git a/docs/configuration.rst b/docs/configuration.rst index bd12502..51fd5c4 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -12,6 +12,7 @@ Configuration configuration/transports configuration/filter_syntax configuration/sendrecvoptions + configuration/replication configuration/snapshotting configuration/prune configuration/logging diff --git a/docs/configuration/overview.rst b/docs/configuration/overview.rst index 86e8e82..a996e34 100644 --- a/docs/configuration/overview.rst +++ b/docs/configuration/overview.rst @@ -132,23 +132,10 @@ The following high-level steps take place during replication and can be monitore The idea behind the execution order of replication steps is that if the sender snapshots all filesystems simultaneously at fixed intervals, the receiver will have all filesystems snapshotted at time ``T1`` before the first snapshot at ``T2 = T1 + $interval`` is replicated. -Placeholder Filesystems -^^^^^^^^^^^^^^^^^^^^^^^ -.. _replication-placeholder-property: - -**Placeholder filesystems** on the receiving side are regular ZFS filesystems with the placeholder property ``zrepl:placeholder=on``. -Placeholders allow the receiving side to mirror the sender's ZFS dataset hierarchy without replicating every filesystem at every intermediary dataset path component. -Consider the following example: ``S/H/J`` shall be replicated to ``R/sink/job/S/H/J``, but neither ``S/H`` nor ``S`` shall be replicated. -ZFS requires the existence of ``R/sink/job/S`` and ``R/sink/job/S/H`` in order to receive into ``R/sink/job/S/H/J``. -Thus, zrepl creates the parent filesystems as placeholders on the receiving side. -If at some point ``S/H`` and ``S`` shall be replicated, the receiving side invalidates the placeholder flag automatically. -The ``zrepl test placeholder`` command can be used to check whether a filesystem is a placeholder. - ZFS Background Knowledge ^^^^^^^^^^^^^^^^^^^^^^^^ - -This section gives some background knowledge about ZFS features that zrepl uses to guarantee that -**incremental replication is always possible and that started replication steps can always be resumed if they are interrupted.** +This section gives some background knowledge about ZFS features that zrepl uses to provide guarantees for a replication filesystem. +Specifically, zrepl guarantees by default that **incremental replication is always possible and that started replication steps can always be resumed if they are interrupted.** **ZFS Send Modes & Bookmarks** ZFS supports full sends (``zfs send fs@to``) and incremental sends (``zfs send -i @from fs@to``). @@ -166,41 +153,56 @@ An incremental send can only be resumed if ``@to`` still exists *and* either ``@ **ZFS Holds** ZFS holds prevent a snapshot from being deleted through ``zfs destroy``, letting the destroy fail with a ``datset is busy`` error. -Holds are created and referred to by a user-defined *tag*. They can be thought of as a named, persistent lock on the snapshot. +Holds are created and referred to by a *tag*. They can be thought of as a named, persistent lock on the snapshot. + + +.. _zrepl-zfs-abstractions: + +ZFS Abstractions Managed By zrepl +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +With the background knowledge from the previous paragraph, we now summarize the different on-disk ZFS objects that zrepl manages to provide its functionality. + +.. _replication-placeholder-property: + +**Placeholder filesystems** on the receiving side are regular ZFS filesystems with the placeholder property ``zrepl:placeholder=on``. +Placeholders allow the receiving side to mirror the sender's ZFS dataset hierarchy without replicating every filesystem at every intermediary dataset path component. +Consider the following example: ``S/H/J`` shall be replicated to ``R/sink/job/S/H/J``, but neither ``S/H`` nor ``S`` shall be replicated. +ZFS requires the existence of ``R/sink/job/S`` and ``R/sink/job/S/H`` in order to receive into ``R/sink/job/S/H/J``. +Thus, zrepl creates the parent filesystems as placeholders on the receiving side. +If at some point ``S/H`` and ``S`` shall be replicated, the receiving side invalidates the placeholder flag automatically. +The ``zrepl test placeholder`` command can be used to check whether a filesystem is a placeholder. .. _replication-cursor-and-last-received-hold: -Guaranteeing That Incremental Sends Are Always Possible -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -**Replication cursor** bookmark and **last-received-hold** are managed by zrepl to ensure that future replications can always be done incrementally. +The **replication cursor** bookmark and **last-received-hold** are managed by zrepl to ensure that future replications can always be done incrementally. The replication cursor is a send-side bookmark of the most recent successfully replicated snapshot, and the last-received-hold is a hold of that snapshot on the receiving side. -Both are moved aomically after the receiving side has confirmed that a replication step is complete. +Both are moved atomically after the receiving side has confirmed that a replication step is complete. The replication cursor has the format ``#zrepl_CUSOR_G__J_``. The last-received-hold tag has the format ``zrepl_last_received_J_``. Encoding the job name in the names ensures that multiple sending jobs can replicate the same filesystem to different receivers without interference. +.. _tentative-replication-cursor-bookmarks: + +**Tentative replication cursor bookmarks** are short-lived boomkarks that protect the atomic moving-forward of the replication cursor and last-received-hold (see :issue:`this issue <340>`). +They are only necessary if step holds are not used as per the :ref:`replication.protection ` setting. +The tentative replication cursor has the format ``#zrepl_CUSORTENTATIVE_G__J_``. The ``zrepl zfs-abstraction list`` command provides a listing of all bookmarks and holds managed by zrepl. -.. _step-holds-and-bookmarks: - -Guaranteeing That Sends Are Always Resumable -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. _step-holds: **Step holds** are zfs holds managed by zrepl to ensure that a replication step can always be resumed if it is interrupted, e.g., due to network outage. zrepl creates step holds before it attempts a replication step and releases them after the receiver confirms that the replication step is complete. For an initial replication ``full @initial_snap``, zrepl puts a zfs hold on ``@initial_snap``. For an incremental send ``@from -> @to``, zrepl puts a zfs hold on both ``@from`` and ``@to``. Note that ``@from`` is not strictly necessary for resumability -- a bookmark on the sending side would be sufficient --, but size-estimation in currently used OpenZFS versions only works if ``@from`` is a snapshot. - The hold tag has the format ``zrepl_STEP_J_``. A job only ever has one active send per filesystem. Thus, there are never more than two step holds for a given pair of ``(job,filesystem)``. **Step bookmarks** are zrepl's equivalent for holds on bookmarks (ZFS does not support putting holds on bookmarks). -They are intended for a situation where a replication step uses a bookmark ``#bm`` as incremental ``from`` that is not managed by zrepl. +They are intended for a situation where a replication step uses a bookmark ``#bm`` as incremental ``from`` where ``#bm`` is not managed by zrepl. To ensure resumability, zrepl copies ``#bm`` to step bookmark ``#zrepl_STEP_G__J_``. If the replication is interrupted and ``#bm`` is deleted by the user, the step bookmark remains as an incremental source for the resumable send. Note that zrepl does not yet support creating step bookmarks because the `corresponding ZFS feature for copying bookmarks `_ is not yet widely available . diff --git a/docs/configuration/replication.rst b/docs/configuration/replication.rst new file mode 100644 index 0000000..03f8e6d --- /dev/null +++ b/docs/configuration/replication.rst @@ -0,0 +1,47 @@ +.. include:: ../global.rst.inc + + +Replication Options +=================== + + +:: + + jobs: + - type: push + filesystems: ... + replication: + protection: + initial: guarantee_resumability # guarantee_{resumability,incremental,nothing} + incremental: guarantee_resumability # guarantee_{resumability,incremental,nothing} + ... + +.. _replication-option-protection: + +``protection`` option +-------------------------- + +The ``protection`` variable controls the degree to which a replicated filesystem is protected from getting out of sync through a zrepl pruner or external tools that destroy snapshots. +zrepl can guarantee :ref:`resumability ` or just :ref:`incremental replication `. + +``guarantee_resumability`` is the **default** value and guarantees that a replication step is always resumable and that incremental replication will always be possible. +The implementation uses replication cursors, last-received-hold and step holds. + +``guarantee_incremental`` only guarantees that incremental replication will always be possible. +If a step ``from -> to`` is interrupted and its `to` snapshot is destroyed, zrepl will remove the half-received ``to``'s resume state and start a new step ``from -> to2``. +The implementation uses replication cursors, tentative replication cursors and last-received-hold. + +``guarantee_nothing`` does not make any guarantees with regards to keeping sending and receiving side in sync. +No bookmarks or holds are created to protect sender and receiver from diverging. + +**Tradeoffs** + +Using ``guarantee_incremental`` instead of ``guarantee_resumability`` obviously removes the resumability guarantee. +This means that replication progress is no longer monotonic which might lead to a replication setup that never makes progress if mid-step interruptions are too frequent (e.g. frequent network outages). +However, the advantage and :issue:`reason for existence <288>` of the ``incremental`` mode is that it allows the pruner to delete snapshots of interrupted replication steps +which is useful if replication happens so rarely (or fails so frequently) that the amount of disk space exclusively referenced by the step's snapshots becomes intolerable. + +.. NOTE:: + + When changing this flag, obsoleted zrepl-managed bookmarks and holds will be destroyed on the next replication step that is attempted for each filesystem. + diff --git a/docs/configuration/sendrecvoptions.rst b/docs/configuration/sendrecvoptions.rst index a9e197a..3e75d72 100644 --- a/docs/configuration/sendrecvoptions.rst +++ b/docs/configuration/sendrecvoptions.rst @@ -16,8 +16,6 @@ Send Options filesystems: ... send: encrypted: true - step_holds: - disable_incremental: false ... :ref:`Source` and :ref:`push` jobs have an optional ``send`` configuration section. @@ -36,24 +34,6 @@ Filesystems matched by ``filesystems`` that are not encrypted are not sent and w If ``encryption=false``, zrepl expects that filesystems matching ``filesystems`` are not encrypted or have loaded encryption keys. -.. _job-send-option-step-holds-disable-incremental: - -``step_holds.disable_incremental`` option ------------------------------------------ - -The ``step_holds.disable_incremental`` variable controls whether the creation of :ref:`step holds ` should be disabled for incremental replication. -The default value is ``false``. - -Disabling step holds has the disadvantage that steps :ref:`might not be resumable ` if interrupted. -Non-resumability means that replication progress is no longer monotonic which might result in a replication setup that never makes progress if mid-step interruptions are too frequent (e.g. frequent network outages). - -However, the advantage and :issue:`reason for existence <288>` of this flag is that it allows the pruner to delete snapshots of interrupted replication steps -which is useful if replication happens so rarely (or fails so frequently) that the amount of disk space exclusively referenced by the step's snapshots becomes intolerable. - -.. NOTE:: - - When setting this flag to ``true``, existing step holds for the job will be destroyed on the next replication attempt. - .. _job-recv-options: Recv Options diff --git a/docs/quickstart/backup_to_external_disk.rst b/docs/quickstart/backup_to_external_disk.rst index 1506122..3e7af5c 100644 --- a/docs/quickstart/backup_to_external_disk.rst +++ b/docs/quickstart/backup_to_external_disk.rst @@ -25,7 +25,7 @@ A few additional requirements: * We want to be able to put off the backups for more than three weeks, i.e., longer than the lifetime of the automatically created snapshots on our workstation. **zrepl should use bookmarks and holds to achieve this goal**. * When we yank out the drive during replication and go on a long vacation, we do *not* want the partially replicated snapshot to stick around as it would hold on to too much disk space over time. - Therefore, we want zrepl to deviate from its :ref:`default step-hold behavior ` and sacrifice resumability, but nonetheless retain the ability to do incremental replication once we return from our vacation. + Therefore, we want zrepl to deviate from its :ref:`default behavior ` and sacrifice resumability, but nonetheless retain the ability to do incremental replication once we return from our vacation. **zrepl should provide an easy config knob to disable step holds for incremental replication**. The following config snippet implements the setup described above. diff --git a/endpoint/endpoint.go b/endpoint/endpoint.go index e06bfc3..45b2bd5 100644 --- a/endpoint/endpoint.go +++ b/endpoint/endpoint.go @@ -21,10 +21,9 @@ import ( ) type SenderConfig struct { - FSF zfs.DatasetFilter - Encrypt *zfs.NilBool - DisableIncrementalStepHolds bool - JobID JobID + FSF zfs.DatasetFilter + Encrypt *zfs.NilBool + JobID JobID } func (c *SenderConfig) Validate() error { @@ -40,10 +39,9 @@ func (c *SenderConfig) Validate() error { // Sender implements replication.ReplicationEndpoint for a sending side type Sender struct { - FSFilter zfs.DatasetFilter - encrypt *zfs.NilBool - disableIncrementalStepHolds bool - jobId JobID + FSFilter zfs.DatasetFilter + encrypt *zfs.NilBool + jobId JobID } func NewSender(conf SenderConfig) *Sender { @@ -51,10 +49,9 @@ func NewSender(conf SenderConfig) *Sender { panic("invalid config" + err.Error()) } return &Sender{ - FSFilter: conf.FSF, - encrypt: conf.Encrypt, - disableIncrementalStepHolds: conf.DisableIncrementalStepHolds, - jobId: conf.JobID, + FSFilter: conf.FSF, + encrypt: conf.Encrypt, + jobId: conf.JobID, } } @@ -211,37 +208,27 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea return res, nil, nil } - // create a replication cursor for `From` (usually an idempotent no-op because SendCompleted already created it before) - var fromReplicationCursor Abstraction - if sendArgs.From != nil { - // For all but the first replication, this should always be a no-op because SendCompleted already moved the cursor - fromReplicationCursor, err = CreateReplicationCursor(ctx, sendArgs.FS, *sendArgs.FromVersion, s.jobId) // no shadow - if err == zfs.ErrBookmarkCloningNotSupported { - getLogger(ctx).Debug("not creating replication cursor from bookmark because ZFS does not support it") - // fallthrough - } else if err != nil { - return nil, nil, errors.Wrap(err, "cannot set replication cursor to `from` version before starting send") - } + // create holds or bookmarks of `From` and `To` to guarantee one of the following: + // - that the replication step can always be resumed (`holds`), + // - that the replication step can be interrupted and a future replication + // step with same or different `To` but same `From` is still possible (`bookmarks`) + // - nothing (`none`) + // + // ... + // + // ... actually create the abstractions + replicationGuaranteeOptions, err := replicationGuaranteeOptionsFromPDU(r.GetReplicationConfig().Protection) + if err != nil { + return nil, nil, err } - - takeStepHolds := sendArgs.FromVersion == nil || !s.disableIncrementalStepHolds - - var fromHold, toHold Abstraction - // make sure `From` doesn't go away in order to make this step resumable - if sendArgs.From != nil && takeStepHolds { - fromHold, err = HoldStep(ctx, sendArgs.FS, *sendArgs.FromVersion, s.jobId) // no shadow - if err == zfs.ErrBookmarkCloningNotSupported { - getLogger(ctx).Debug("not creating step bookmark because ZFS does not support it") - // fallthrough - } else if err != nil { - return nil, nil, errors.Wrapf(err, "cannot hold `from` version %q before starting send", *sendArgs.FromVersion) - } + replicationGuaranteeStrategy := replicationGuaranteeOptions.Strategy(sendArgs.From != nil) + liveAbs, err := replicationGuaranteeStrategy.SenderPreSend(ctx, s.jobId, &sendArgs) + if err != nil { + return nil, nil, err } - if takeStepHolds { - // make sure `To` doesn't go away in order to make this step resumable - toHold, err = HoldStep(ctx, sendArgs.FS, sendArgs.ToVersion, s.jobId) - if err != nil { - return nil, nil, errors.Wrapf(err, "cannot hold `to` version %q before starting send", sendArgs.ToVersion) + for _, a := range liveAbs { + if a != nil { + abstractionsCacheSingleton.Put(a) } } @@ -261,7 +248,6 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea // // Note further that a resuming send, due to the idempotent nature of func CreateReplicationCursor and HoldStep, // will never lose its step holds because we just (idempotently re-)created them above, before attempting the cleanup. - liveAbs := []Abstraction{fromHold, toHold, fromReplicationCursor} func() { ctx, endSpan := trace.WithSpan(ctx, "cleanup-stale-abstractions") defer endSpan() @@ -283,22 +269,29 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea for _, staleVersion := range obsoleteAbs { for _, mustLiveVersion := range mustLiveVersions { isSendArg := zfs.FilesystemVersionEqualIdentity(mustLiveVersion, staleVersion.GetFilesystemVersion()) - isStepHoldWeMightHaveCreatedWithCurrentValueOf_takeStepHolds := - takeStepHolds && staleVersion.GetType() == AbstractionStepHold - if isSendArg && isStepHoldWeMightHaveCreatedWithCurrentValueOf_takeStepHolds { + stepHoldBasedGuaranteeStrategy := false + k := replicationGuaranteeStrategy.Kind() + switch k { + case ReplicationGuaranteeKindResumability: + stepHoldBasedGuaranteeStrategy = true + case ReplicationGuaranteeKindIncremental: + case ReplicationGuaranteeKindNone: + default: + panic(fmt.Sprintf("this is supposed to be an exhaustive match, got %v", k)) + } + isSnapshot := mustLiveVersion.IsSnapshot() + if isSendArg && (!isSnapshot || stepHoldBasedGuaranteeStrategy) { panic(fmt.Sprintf("impl error: %q would be destroyed because it is considered stale but it is part of of sendArgs=%s", mustLiveVersion.String(), pretty.Sprint(sendArgs))) } } } } - sendAbstractionsCacheSingleton.TryBatchDestroy(ctx, s.jobId, sendArgs.FS, keep, check) - }() - // now add the newly created abstractions to the cleaned-up cache - for _, a := range liveAbs { - if a != nil { - sendAbstractionsCacheSingleton.Put(a) + destroyTypes := AbstractionTypeSet{ + AbstractionStepHold: true, + AbstractionTentativeReplicationCursorBookmark: true, } - } + abstractionsCacheSingleton.TryBatchDestroy(ctx, s.jobId, sendArgs.FS, destroyTypes, keep, check) + }() sendStream, err := zfs.ZFSSend(ctx, sendArgs) if err != nil { @@ -332,36 +325,32 @@ func (p *Sender) SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*p return nil, errors.Wrap(err, "validate `to` exists") } - log := func(ctx context.Context) Logger { - log := getLogger(ctx).WithField("to_guid", to.Guid). - WithField("fs", fs). - WithField("to", to.RelName) - if from != nil { - log = log.WithField("from", from.RelName).WithField("from_guid", from.Guid) - } - return log - } - - toReplicationCursor, err := CreateReplicationCursor(ctx, fs, to, p.jobId) + replicationGuaranteeOptions, err := replicationGuaranteeOptionsFromPDU(orig.GetReplicationConfig().Protection) if err != nil { - if err == zfs.ErrBookmarkCloningNotSupported { - log(ctx).Debug("not setting replication cursor, bookmark cloning not supported") - } else { - msg := "cannot move replication cursor, keeping hold on `to` until successful" - log(ctx).WithError(err).Error(msg) - err = errors.Wrap(err, msg) - // it is correct to not destroy from and to step holds if we can't move the cursor! - return &pdu.SendCompletedRes{}, err + return nil, err + } + liveAbs, err := replicationGuaranteeOptions.Strategy(from != nil).SenderPostRecvConfirmed(ctx, p.jobId, fs, to) + if err != nil { + return nil, err + } + for _, a := range liveAbs { + if a != nil { + abstractionsCacheSingleton.Put(a) } - } else { - sendAbstractionsCacheSingleton.Put(toReplicationCursor) - log(ctx).WithField("to_cursor", toReplicationCursor.String()).Info("successfully created `to` replication cursor") } - - keep := func(a Abstraction) bool { - return AbstractionEquals(a, toReplicationCursor) + keep := func(a Abstraction) (keep bool) { + keep = false + for _, k := range liveAbs { + keep = keep || AbstractionEquals(a, k) + } + return keep } - sendAbstractionsCacheSingleton.TryBatchDestroy(ctx, p.jobId, fs, keep, nil) + destroyTypes := AbstractionTypeSet{ + AbstractionStepHold: true, + AbstractionTentativeReplicationCursorBookmark: true, + AbstractionReplicationCursorBookmarkV2: true, + } + abstractionsCacheSingleton.TryBatchDestroy(ctx, p.jobId, fs, destroyTypes, keep, nil) return &pdu.SendCompletedRes{}, nil @@ -437,8 +426,6 @@ type ReceiverConfig struct { RootWithoutClientComponent *zfs.DatasetPath // TODO use AppendClientIdentity bool - - UpdateLastReceivedHold bool } func (c *ReceiverConfig) copyIn() { @@ -863,12 +850,38 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io. return nil, errors.Wrap(err, msg) } - if s.conf.UpdateLastReceivedHold { - log.Debug("move last-received-hold") - if err := MoveLastReceivedHold(ctx, lp.ToString(), toRecvd, s.conf.JobID); err != nil { - return nil, errors.Wrap(err, "cannot move last-received-hold") + replicationGuaranteeOptions, err := replicationGuaranteeOptionsFromPDU(req.GetReplicationConfig().Protection) + if err != nil { + return nil, err + } + replicationGuaranteeStrategy := replicationGuaranteeOptions.Strategy(ph.FSExists) + liveAbs, err := replicationGuaranteeStrategy.ReceiverPostRecv(ctx, s.conf.JobID, lp.ToString(), toRecvd) + if err != nil { + return nil, err + } + for _, a := range liveAbs { + if a != nil { + abstractionsCacheSingleton.Put(a) } } + keep := func(a Abstraction) (keep bool) { + keep = false + for _, k := range liveAbs { + keep = keep || AbstractionEquals(a, k) + } + return keep + } + check := func(obsoleteAbs []Abstraction) { + for _, abs := range obsoleteAbs { + if zfs.FilesystemVersionEqualIdentity(abs.GetFilesystemVersion(), toRecvd) { + panic(fmt.Sprintf("would destroy endpoint abstraction around the filesystem version we just received %s", abs)) + } + } + } + destroyTypes := AbstractionTypeSet{ + AbstractionLastReceivedHold: true, + } + abstractionsCacheSingleton.TryBatchDestroy(ctx, s.conf.JobID, lp.ToString(), destroyTypes, keep, check) return &pdu.ReceiveRes{}, nil } diff --git a/endpoint/endpoint_send_abstractions_cache.go b/endpoint/endpoint_abstractions_cache.go similarity index 53% rename from endpoint/endpoint_send_abstractions_cache.go rename to endpoint/endpoint_abstractions_cache.go index 94f4695..19058cf 100644 --- a/endpoint/endpoint_send_abstractions_cache.go +++ b/endpoint/endpoint_abstractions_cache.go @@ -10,49 +10,49 @@ import ( "github.com/zrepl/zrepl/util/chainlock" ) -var sendAbstractionsCacheMetrics struct { +var abstractionsCacheMetrics struct { count prometheus.Gauge } func init() { - sendAbstractionsCacheMetrics.count = prometheus.NewGauge(prometheus.GaugeOpts{ + abstractionsCacheMetrics.count = prometheus.NewGauge(prometheus.GaugeOpts{ Namespace: "zrepl", Subsystem: "endpoint", - Name: "send_abstractions_cache_entry_count", - Help: "number of send abstractions tracked in the sendAbstractionsCache data structure", + Name: "abstractions_cache_entry_count", + Help: "number of abstractions tracked in the abstractionsCache data structure", }) } -var sendAbstractionsCacheSingleton = newSendAbstractionsCache() +var abstractionsCacheSingleton = newAbstractionsCache() -func SendAbstractionsCacheInvalidate(fs string) { - sendAbstractionsCacheSingleton.InvalidateFSCache(fs) +func AbstractionsCacheInvalidate(fs string) { + abstractionsCacheSingleton.InvalidateFSCache(fs) } -type sendAbstractionsCacheDidLoadFSState int +type abstractionsCacheDidLoadFSState int const ( - sendAbstractionsCacheDidLoadFSStateNo sendAbstractionsCacheDidLoadFSState = iota // 0-value has meaning - sendAbstractionsCacheDidLoadFSStateInProgress - sendAbstractionsCacheDidLoadFSStateDone + abstractionsCacheDidLoadFSStateNo abstractionsCacheDidLoadFSState = iota // 0-value has meaning + abstractionsCacheDidLoadFSStateInProgress + abstractionsCacheDidLoadFSStateDone ) -type sendAbstractionsCache struct { +type abstractionsCache struct { mtx chainlock.L abstractions []Abstraction - didLoadFS map[string]sendAbstractionsCacheDidLoadFSState + didLoadFS map[string]abstractionsCacheDidLoadFSState didLoadFSChanged *sync.Cond } -func newSendAbstractionsCache() *sendAbstractionsCache { - c := &sendAbstractionsCache{ - didLoadFS: make(map[string]sendAbstractionsCacheDidLoadFSState), +func newAbstractionsCache() *abstractionsCache { + c := &abstractionsCache{ + didLoadFS: make(map[string]abstractionsCacheDidLoadFSState), } c.didLoadFSChanged = c.mtx.NewCond() return c } -func (s *sendAbstractionsCache) Put(a Abstraction) { +func (s *abstractionsCache) Put(a Abstraction) { defer s.mtx.Lock().Unlock() var zeroJobId JobID @@ -63,10 +63,10 @@ func (s *sendAbstractionsCache) Put(a Abstraction) { } s.abstractions = append(s.abstractions, a) - sendAbstractionsCacheMetrics.count.Set(float64(len(s.abstractions))) + abstractionsCacheMetrics.count.Set(float64(len(s.abstractions))) } -func (s *sendAbstractionsCache) InvalidateFSCache(fs string) { +func (s *abstractionsCache) InvalidateFSCache(fs string) { // FIXME: O(n) newAbs := make([]Abstraction, 0, len(s.abstractions)) for _, a := range s.abstractions { @@ -75,9 +75,9 @@ func (s *sendAbstractionsCache) InvalidateFSCache(fs string) { } } s.abstractions = newAbs - sendAbstractionsCacheMetrics.count.Set(float64(len(s.abstractions))) + abstractionsCacheMetrics.count.Set(float64(len(s.abstractions))) - s.didLoadFS[fs] = sendAbstractionsCacheDidLoadFSStateNo + s.didLoadFS[fs] = abstractionsCacheDidLoadFSStateNo s.didLoadFSChanged.Broadcast() } @@ -86,7 +86,7 @@ func (s *sendAbstractionsCache) InvalidateFSCache(fs string) { // - only fetches on-disk abstractions once, but every time from the in-memory store // // That means that for precise results, all abstractions created by the endpoint must be .Put into this cache. -func (s *sendAbstractionsCache) GetAndDeleteByJobIDAndFS(ctx context.Context, jobID JobID, fs string, keep func(a Abstraction) bool) (ret []Abstraction) { +func (s *abstractionsCache) GetAndDeleteByJobIDAndFS(ctx context.Context, jobID JobID, fs string, types AbstractionTypeSet, keep func(a Abstraction) bool) (ret []Abstraction) { defer s.mtx.Lock().Unlock() defer trace.WithSpanFromStackUpdateCtx(&ctx)() var zeroJobId JobID @@ -97,50 +97,50 @@ func (s *sendAbstractionsCache) GetAndDeleteByJobIDAndFS(ctx context.Context, jo panic("must not pass zero-value fs") } - s.tryLoadOnDiskSendAbstractions(ctx, fs) + s.tryLoadOnDiskAbstractions(ctx, fs) // FIXME O(n) var remaining []Abstraction for _, a := range s.abstractions { aJobId := *a.GetJobID() aFS := a.GetFS() - if aJobId == jobID && aFS == fs && !keep(a) { + if aJobId == jobID && aFS == fs && types[a.GetType()] && !keep(a) { ret = append(ret, a) } else { remaining = append(remaining, a) } } s.abstractions = remaining - sendAbstractionsCacheMetrics.count.Set(float64(len(s.abstractions))) + abstractionsCacheMetrics.count.Set(float64(len(s.abstractions))) return ret } // caller must hold s.mtx -func (s *sendAbstractionsCache) tryLoadOnDiskSendAbstractions(ctx context.Context, fs string) { - for s.didLoadFS[fs] != sendAbstractionsCacheDidLoadFSStateDone { - if s.didLoadFS[fs] == sendAbstractionsCacheDidLoadFSStateInProgress { +func (s *abstractionsCache) tryLoadOnDiskAbstractions(ctx context.Context, fs string) { + for s.didLoadFS[fs] != abstractionsCacheDidLoadFSStateDone { + if s.didLoadFS[fs] == abstractionsCacheDidLoadFSStateInProgress { s.didLoadFSChanged.Wait() continue } - if s.didLoadFS[fs] != sendAbstractionsCacheDidLoadFSStateNo { + if s.didLoadFS[fs] != abstractionsCacheDidLoadFSStateNo { panic(fmt.Sprintf("unreachable: %v", s.didLoadFS[fs])) } - s.didLoadFS[fs] = sendAbstractionsCacheDidLoadFSStateInProgress + s.didLoadFS[fs] = abstractionsCacheDidLoadFSStateInProgress defer s.didLoadFSChanged.Broadcast() var onDiskAbs []Abstraction var err error s.mtx.DropWhile(func() { - onDiskAbs, err = s.tryLoadOnDiskSendAbstractionsImpl(ctx, fs) // no shadow + onDiskAbs, err = s.tryLoadOnDiskAbstractionsImpl(ctx, fs) // no shadow }) if err != nil { - s.didLoadFS[fs] = sendAbstractionsCacheDidLoadFSStateNo - getLogger(ctx).WithField("fs", fs).WithError(err).Error("cannot list send step abstractions for filesystem") + s.didLoadFS[fs] = abstractionsCacheDidLoadFSStateNo + getLogger(ctx).WithField("fs", fs).WithError(err).Error("cannot list abstractions for filesystem") } else { - s.didLoadFS[fs] = sendAbstractionsCacheDidLoadFSStateDone + s.didLoadFS[fs] = abstractionsCacheDidLoadFSStateDone s.abstractions = append(s.abstractions, onDiskAbs...) getLogger(ctx).WithField("fs", fs).WithField("abstractions", onDiskAbs).Debug("loaded step abstractions for filesystem") } @@ -149,7 +149,7 @@ func (s *sendAbstractionsCache) tryLoadOnDiskSendAbstractions(ctx context.Contex } // caller should _not hold s.mtx -func (s *sendAbstractionsCache) tryLoadOnDiskSendAbstractionsImpl(ctx context.Context, fs string) ([]Abstraction, error) { +func (s *abstractionsCache) tryLoadOnDiskAbstractionsImpl(ctx context.Context, fs string) ([]Abstraction, error) { defer trace.WithSpanFromStackUpdateCtx(&ctx)() q := ListZFSHoldsAndBookmarksQuery{ @@ -158,9 +158,10 @@ func (s *sendAbstractionsCache) tryLoadOnDiskSendAbstractionsImpl(ctx context.Co }, JobID: nil, What: AbstractionTypeSet{ - AbstractionStepHold: true, - AbstractionStepBookmark: true, - AbstractionReplicationCursorBookmarkV2: true, + AbstractionStepHold: true, + AbstractionTentativeReplicationCursorBookmark: true, + AbstractionReplicationCursorBookmarkV2: true, + AbstractionLastReceivedHold: true, }, Concurrency: 1, } @@ -175,12 +176,12 @@ func (s *sendAbstractionsCache) tryLoadOnDiskSendAbstractionsImpl(ctx context.Co return abs, nil } -func (s *sendAbstractionsCache) TryBatchDestroy(ctx context.Context, jobId JobID, fs string, keep func(a Abstraction) bool, check func(willDestroy []Abstraction)) { +func (s *abstractionsCache) TryBatchDestroy(ctx context.Context, jobId JobID, fs string, types AbstractionTypeSet, keep func(a Abstraction) bool, check func(willDestroy []Abstraction)) { // no s.mtx, we only use the public interface in this function defer trace.WithSpanFromStackUpdateCtx(&ctx)() - obsoleteAbs := s.GetAndDeleteByJobIDAndFS(ctx, jobId, fs, keep) + obsoleteAbs := s.GetAndDeleteByJobIDAndFS(ctx, jobId, fs, types, keep) if check != nil { check(obsoleteAbs) @@ -193,11 +194,11 @@ func (s *sendAbstractionsCache) TryBatchDestroy(ctx context.Context, jobId JobID getLogger(ctx). WithField("abstraction", res.Abstraction). WithError(res.DestroyErr). - Error("cannot destroy stale send step abstraction") + Error("cannot destroy abstraction") } else { getLogger(ctx). WithField("abstraction", res.Abstraction). - Info("destroyed stale send step abstraction") + Info("destroyed abstraction") } } if hadErr { diff --git a/endpoint/endpoint_guarantees.go b/endpoint/endpoint_guarantees.go new file mode 100644 index 0000000..1b94c28 --- /dev/null +++ b/endpoint/endpoint_guarantees.go @@ -0,0 +1,213 @@ +package endpoint + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + "github.com/zrepl/zrepl/replication/logic/pdu" + "github.com/zrepl/zrepl/zfs" +) + +type ReplicationGuaranteeOptions struct { + Initial ReplicationGuaranteeKind + Incremental ReplicationGuaranteeKind +} + +func replicationGuaranteeOptionsFromPDU(in *pdu.ReplicationConfigProtection) (o ReplicationGuaranteeOptions, _ error) { + if in == nil { + return o, errors.New("pdu.ReplicationConfigProtection must not be nil") + } + initial, err := replicationGuaranteeKindFromPDU(in.GetInitial()) + if err != nil { + return o, errors.Wrap(err, "pdu.ReplicationConfigProtection: field Initial") + } + incremental, err := replicationGuaranteeKindFromPDU(in.GetIncremental()) + if err != nil { + return o, errors.Wrap(err, "pdu.ReplicationConfigProtection: field Incremental") + } + o = ReplicationGuaranteeOptions{ + Initial: initial, + Incremental: incremental, + } + return o, nil +} + +func replicationGuaranteeKindFromPDU(in pdu.ReplicationGuaranteeKind) (k ReplicationGuaranteeKind, _ error) { + switch in { + case pdu.ReplicationGuaranteeKind_GuaranteeNothing: + return ReplicationGuaranteeKindNone, nil + case pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication: + return ReplicationGuaranteeKindIncremental, nil + case pdu.ReplicationGuaranteeKind_GuaranteeResumability: + return ReplicationGuaranteeKindResumability, nil + + case pdu.ReplicationGuaranteeKind_GuaranteeInvalid: + fallthrough + default: + return k, errors.Errorf("%q", in.String()) + } +} + +func (o ReplicationGuaranteeOptions) Strategy(incremental bool) ReplicationGuaranteeStrategy { + g := o.Initial + if incremental { + g = o.Incremental + } + return ReplicationGuaranteeFromKind(g) +} + +//go:generate enumer -type=ReplicationGuaranteeKind -json -transform=snake -trimprefix=ReplicationGuaranteeKind +type ReplicationGuaranteeKind int + +const ( + ReplicationGuaranteeKindResumability ReplicationGuaranteeKind = 1 << iota + ReplicationGuaranteeKindIncremental + ReplicationGuaranteeKindNone +) + +type ReplicationGuaranteeStrategy interface { + Kind() ReplicationGuaranteeKind + SenderPreSend(ctx context.Context, jid JobID, sendArgs *zfs.ZFSSendArgsValidated) (keep []Abstraction, err error) + ReceiverPostRecv(ctx context.Context, jid JobID, fs string, toRecvd zfs.FilesystemVersion) (keep []Abstraction, err error) + SenderPostRecvConfirmed(ctx context.Context, jid JobID, fs string, to zfs.FilesystemVersion) (keep []Abstraction, err error) +} + +func ReplicationGuaranteeFromKind(k ReplicationGuaranteeKind) ReplicationGuaranteeStrategy { + switch k { + case ReplicationGuaranteeKindNone: + return ReplicationGuaranteeNone{} + case ReplicationGuaranteeKindIncremental: + return ReplicationGuaranteeIncremental{} + case ReplicationGuaranteeKindResumability: + return ReplicationGuaranteeResumability{} + default: + panic(fmt.Sprintf("unreachable: %q %T", k, k)) + } +} + +type ReplicationGuaranteeNone struct{} + +func (g ReplicationGuaranteeNone) Kind() ReplicationGuaranteeKind { + return ReplicationGuaranteeKindNone +} + +func (g ReplicationGuaranteeNone) SenderPreSend(ctx context.Context, jid JobID, sendArgs *zfs.ZFSSendArgsValidated) (keep []Abstraction, err error) { + return nil, nil +} + +func (g ReplicationGuaranteeNone) ReceiverPostRecv(ctx context.Context, jid JobID, fs string, toRecvd zfs.FilesystemVersion) (keep []Abstraction, err error) { + return nil, nil +} + +func (g ReplicationGuaranteeNone) SenderPostRecvConfirmed(ctx context.Context, jid JobID, fs string, to zfs.FilesystemVersion) (keep []Abstraction, err error) { + return nil, nil +} + +type ReplicationGuaranteeIncremental struct{} + +func (g ReplicationGuaranteeIncremental) Kind() ReplicationGuaranteeKind { + return ReplicationGuaranteeKindIncremental +} + +func (g ReplicationGuaranteeIncremental) SenderPreSend(ctx context.Context, jid JobID, sendArgs *zfs.ZFSSendArgsValidated) (keep []Abstraction, err error) { + if sendArgs.FromVersion != nil { + from, err := CreateTentativeReplicationCursor(ctx, sendArgs.FS, *sendArgs.FromVersion, jid) + if err != nil { + if err == zfs.ErrBookmarkCloningNotSupported { + getLogger(ctx).WithField("replication_guarantee", g). + WithField("bookmark", sendArgs.From.FullPath(sendArgs.FS)). + Info("bookmark cloning is not supported, speculating that `from` will not be destroyed until step is done") + } else { + return nil, err + } + } + keep = append(keep, from) + } + to, err := CreateTentativeReplicationCursor(ctx, sendArgs.FS, sendArgs.ToVersion, jid) + if err != nil { + return nil, err + } + keep = append(keep, to) + + return keep, nil +} + +func (g ReplicationGuaranteeIncremental) ReceiverPostRecv(ctx context.Context, jid JobID, fs string, toRecvd zfs.FilesystemVersion) (keep []Abstraction, err error) { + return receiverPostRecvCommon(ctx, jid, fs, toRecvd) +} + +func (g ReplicationGuaranteeIncremental) SenderPostRecvConfirmed(ctx context.Context, jid JobID, fs string, to zfs.FilesystemVersion) (keep []Abstraction, err error) { + return senderPostRecvConfirmedCommon(ctx, jid, fs, to) +} + +type ReplicationGuaranteeResumability struct{} + +func (g ReplicationGuaranteeResumability) Kind() ReplicationGuaranteeKind { + return ReplicationGuaranteeKindResumability +} + +func (g ReplicationGuaranteeResumability) SenderPreSend(ctx context.Context, jid JobID, sendArgs *zfs.ZFSSendArgsValidated) (keep []Abstraction, err error) { + // try to hold the FromVersion + if sendArgs.FromVersion != nil { + if sendArgs.FromVersion.Type == zfs.Bookmark { + getLogger(ctx).WithField("replication_guarantee", g).WithField("fromVersion", sendArgs.FromVersion.FullPath(sendArgs.FS)). + Debug("cannot hold a bookmark, speculating that `from` will not be destroyed until step is done") + } else { + from, err := HoldStep(ctx, sendArgs.FS, *sendArgs.FromVersion, jid) + if err != nil { + return nil, err + } + keep = append(keep, from) + } + // fallthrough + } + + to, err := HoldStep(ctx, sendArgs.FS, sendArgs.ToVersion, jid) + if err != nil { + return nil, err + } + keep = append(keep, to) + + return keep, nil +} + +func (g ReplicationGuaranteeResumability) ReceiverPostRecv(ctx context.Context, jid JobID, fs string, toRecvd zfs.FilesystemVersion) (keep []Abstraction, err error) { + return receiverPostRecvCommon(ctx, jid, fs, toRecvd) +} + +func (g ReplicationGuaranteeResumability) SenderPostRecvConfirmed(ctx context.Context, jid JobID, fs string, to zfs.FilesystemVersion) (keep []Abstraction, err error) { + return senderPostRecvConfirmedCommon(ctx, jid, fs, to) +} + +// helper function used by multiple strategies +func senderPostRecvConfirmedCommon(ctx context.Context, jid JobID, fs string, to zfs.FilesystemVersion) (keep []Abstraction, err error) { + + log := getLogger(ctx).WithField("toVersion", to.FullPath(fs)) + + toReplicationCursor, err := CreateReplicationCursor(ctx, fs, to, jid) + if err != nil { + if err == zfs.ErrBookmarkCloningNotSupported { + log.Debug("not setting replication cursor, bookmark cloning not supported") + } else { + msg := "cannot move replication cursor, keeping hold on `to` until successful" + log.WithError(err).Error(msg) + err = errors.Wrap(err, msg) + return nil, err + } + } else { + log.WithField("to_cursor", toReplicationCursor.String()).Info("successfully created `to` replication cursor") + } + + return []Abstraction{toReplicationCursor}, nil +} + +// helper function used by multiple strategies +func receiverPostRecvCommon(ctx context.Context, jid JobID, fs string, toRecvd zfs.FilesystemVersion) (keep []Abstraction, err error) { + getLogger(ctx).Debug("create new last-received-hold") + lrh, err := CreateLastReceivedHold(ctx, fs, toRecvd, jid) + if err != nil { + return nil, err + } + return []Abstraction{lrh}, nil +} diff --git a/endpoint/endpoint_metrics.go b/endpoint/endpoint_metrics.go index 15471fd..9975679 100644 --- a/endpoint/endpoint_metrics.go +++ b/endpoint/endpoint_metrics.go @@ -3,5 +3,5 @@ package endpoint import "github.com/prometheus/client_golang/prometheus" func RegisterMetrics(r prometheus.Registerer) { - r.MustRegister(sendAbstractionsCacheMetrics.count) + r.MustRegister(abstractionsCacheMetrics.count) } diff --git a/endpoint/endpoint_zfs_abstraction.go b/endpoint/endpoint_zfs_abstraction.go index 0662f9a..616803b 100644 --- a/endpoint/endpoint_zfs_abstraction.go +++ b/endpoint/endpoint_zfs_abstraction.go @@ -23,19 +23,19 @@ type AbstractionType string // There are a lot of exhaustive switches on AbstractionType in the code base. // When adding a new abstraction type, make sure to search and update them! const ( - AbstractionStepBookmark AbstractionType = "step-bookmark" - AbstractionStepHold AbstractionType = "step-hold" - AbstractionLastReceivedHold AbstractionType = "last-received-hold" - AbstractionReplicationCursorBookmarkV1 AbstractionType = "replication-cursor-bookmark-v1" - AbstractionReplicationCursorBookmarkV2 AbstractionType = "replication-cursor-bookmark-v2" + AbstractionStepHold AbstractionType = "step-hold" + AbstractionLastReceivedHold AbstractionType = "last-received-hold" + AbstractionTentativeReplicationCursorBookmark AbstractionType = "tentative-replication-cursor-bookmark-v2" + AbstractionReplicationCursorBookmarkV1 AbstractionType = "replication-cursor-bookmark-v1" + AbstractionReplicationCursorBookmarkV2 AbstractionType = "replication-cursor-bookmark-v2" ) var AbstractionTypesAll = map[AbstractionType]bool{ - AbstractionStepBookmark: true, - AbstractionStepHold: true, - AbstractionLastReceivedHold: true, - AbstractionReplicationCursorBookmarkV1: true, - AbstractionReplicationCursorBookmarkV2: true, + AbstractionStepHold: true, + AbstractionLastReceivedHold: true, + AbstractionTentativeReplicationCursorBookmark: true, + AbstractionReplicationCursorBookmarkV1: true, + AbstractionReplicationCursorBookmarkV2: true, } // Implementation Note: @@ -80,12 +80,12 @@ func AbstractionEquals(a, b Abstraction) bool { func (t AbstractionType) Validate() error { switch t { - case AbstractionStepBookmark: - return nil case AbstractionStepHold: return nil case AbstractionLastReceivedHold: return nil + case AbstractionTentativeReplicationCursorBookmark: + return nil case AbstractionReplicationCursorBookmarkV1: return nil case AbstractionReplicationCursorBookmarkV2: @@ -185,8 +185,8 @@ type BookmarkExtractor func(fs *zfs.DatasetPath, v zfs.FilesystemVersion) Abstra // returns nil if the abstraction type is not bookmark-based func (t AbstractionType) BookmarkExtractor() BookmarkExtractor { switch t { - case AbstractionStepBookmark: - return StepBookmarkExtractor + case AbstractionTentativeReplicationCursorBookmark: + return TentativeReplicationCursorExtractor case AbstractionReplicationCursorBookmarkV1: return ReplicationCursorV1Extractor case AbstractionReplicationCursorBookmarkV2: @@ -205,7 +205,7 @@ type HoldExtractor = func(fs *zfs.DatasetPath, v zfs.FilesystemVersion, tag stri // returns nil if the abstraction type is not hold-based func (t AbstractionType) HoldExtractor() HoldExtractor { switch t { - case AbstractionStepBookmark: + case AbstractionTentativeReplicationCursorBookmark: return nil case AbstractionReplicationCursorBookmarkV1: return nil @@ -220,6 +220,23 @@ func (t AbstractionType) HoldExtractor() HoldExtractor { } } +func (t AbstractionType) BookmarkNamer() func(fs string, guid uint64, jobId JobID) (string, error) { + switch t { + case AbstractionTentativeReplicationCursorBookmark: + return TentativeReplicationCursorBookmarkName + case AbstractionReplicationCursorBookmarkV1: + panic("shouldn't be creating new ones") + case AbstractionReplicationCursorBookmarkV2: + return ReplicationCursorBookmarkName + case AbstractionStepHold: + return nil + case AbstractionLastReceivedHold: + return nil + default: + panic(fmt.Sprintf("unimpl: %q", t)) + } +} + type ListZFSHoldsAndBookmarksQuery struct { FS ListZFSHoldsAndBookmarksQueryFilesystemFilter // What abstraction types should match (any contained in the set) @@ -697,11 +714,9 @@ func ListStale(ctx context.Context, q ListZFSHoldsAndBookmarksQuery) (*Staleness return nil, &ListStaleQueryError{errors.New("ListStale cannot have Until != nil set on query")} } - // if asking for step holds, must also as for step bookmarks (same kind of abstraction) - // as well as replication cursor bookmarks (for firstNotStale) + // if asking for step holds must also ask for replication cursor bookmarks (for firstNotStale) ifAnyThenAll := AbstractionTypeSet{ AbstractionStepHold: true, - AbstractionStepBookmark: true, AbstractionReplicationCursorBookmarkV2: true, } if q.What.ContainsAnyOf(ifAnyThenAll) && !q.What.ContainsAll(ifAnyThenAll) { @@ -730,7 +745,7 @@ type fsAjobAtype struct { } // For step holds and bookmarks, only those older than the most recent replication cursor -// of their (filesystem,job) is considered because younger ones cannot be stale by definition +// of their (filesystem,job) are considered because younger ones cannot be stale by definition // (if we destroy them, we might actually lose the hold on the `To` for an ongoing incremental replication) // // For replication cursors and last-received-holds, only the most recent one is kept. @@ -772,8 +787,6 @@ func listStaleFiltering(abs []Abstraction, sinceBound *CreateTXGRangeBound) *Sta } // stepFirstNotStaleCandidate.step - case AbstractionStepBookmark: - fallthrough case AbstractionStepHold: if c.step == nil || (*c.step).GetCreateTXG() < a.GetCreateTXG() { a := a @@ -797,7 +810,7 @@ func listStaleFiltering(abs []Abstraction, sinceBound *CreateTXGRangeBound) *Sta for k := range by { l := by[k] - if k.Type == AbstractionStepHold || k.Type == AbstractionStepBookmark { + if k.Type == AbstractionStepHold { // all older than the most recent cursor are stale, others are always live // if we don't have a replication cursor yet, use untilBound = nil diff --git a/endpoint/endpoint_zfs_abstraction_last_received_hold.go b/endpoint/endpoint_zfs_abstraction_last_received_hold.go new file mode 100644 index 0000000..93fdcae --- /dev/null +++ b/endpoint/endpoint_zfs_abstraction_last_received_hold.go @@ -0,0 +1,91 @@ +package endpoint + +import ( + "context" + "fmt" + "regexp" + + "github.com/pkg/errors" + "github.com/zrepl/zrepl/zfs" +) + +const ( + LastReceivedHoldTagNamePrefix = "zrepl_last_received_J_" +) + +var lastReceivedHoldTagRE = regexp.MustCompile("^zrepl_last_received_J_(.+)$") + +var _ HoldExtractor = LastReceivedHoldExtractor + +func LastReceivedHoldExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion, holdTag string) Abstraction { + var err error + + if v.Type != zfs.Snapshot { + panic("impl error") + } + + jobID, err := ParseLastReceivedHoldTag(holdTag) + if err == nil { + return &holdBasedAbstraction{ + Type: AbstractionLastReceivedHold, + FS: fs.ToString(), + FilesystemVersion: v, + Tag: holdTag, + JobID: jobID, + } + } + return nil +} + +// err != nil always means that the bookmark is not a step bookmark +func ParseLastReceivedHoldTag(tag string) (JobID, error) { + match := lastReceivedHoldTagRE.FindStringSubmatch(tag) + if match == nil { + return JobID{}, errors.Errorf("parse last-received-hold tag: does not match regex %s", lastReceivedHoldTagRE.String()) + } + jobId, err := MakeJobID(match[1]) + if err != nil { + return JobID{}, errors.Wrap(err, "parse last-received-hold tag: invalid job id field") + } + return jobId, nil +} + +func LastReceivedHoldTag(jobID JobID) (string, error) { + return lastReceivedHoldImpl(jobID.String()) +} + +func lastReceivedHoldImpl(jobid string) (string, error) { + tag := fmt.Sprintf("%s%s", LastReceivedHoldTagNamePrefix, jobid) + if err := zfs.ValidHoldTag(tag); err != nil { + return "", err + } + return tag, nil +} + +func CreateLastReceivedHold(ctx context.Context, fs string, to zfs.FilesystemVersion, jobID JobID) (Abstraction, error) { + + if !to.IsSnapshot() { + return nil, errors.Errorf("last-received-hold: target must be a snapshot: %s", to.FullPath(fs)) + } + + tag, err := LastReceivedHoldTag(jobID) + if err != nil { + return nil, errors.Wrap(err, "last-received-hold: hold tag") + } + + // we never want to be without a hold + // => hold new one before releasing old hold + + err = zfs.ZFSHold(ctx, fs, to, tag) + if err != nil { + return nil, errors.Wrap(err, "last-received-hold: hold newly received") + } + + return &holdBasedAbstraction{ + Type: AbstractionLastReceivedHold, + FS: fs, + FilesystemVersion: to, + JobID: jobID, + Tag: tag, + }, nil +} diff --git a/endpoint/endpoint_zfs_abstraction_cursor_and_last_received_hold.go b/endpoint/endpoint_zfs_abstraction_replication_cursor.go similarity index 62% rename from endpoint/endpoint_zfs_abstraction_cursor_and_last_received_hold.go rename to endpoint/endpoint_zfs_abstraction_replication_cursor.go index ec21301..f58f78a 100644 --- a/endpoint/endpoint_zfs_abstraction_cursor_and_last_received_hold.go +++ b/endpoint/endpoint_zfs_abstraction_replication_cursor.go @@ -4,12 +4,9 @@ import ( "context" "encoding/json" "fmt" - "regexp" "sort" "github.com/pkg/errors" - - "github.com/zrepl/zrepl/util/errorarray" "github.com/zrepl/zrepl/zfs" ) @@ -53,6 +50,28 @@ func ParseReplicationCursorBookmarkName(fullname string) (uint64, JobID, error) return guid, jobID, err } +const tentativeReplicationCursorBookmarkNamePrefix = "zrepl_CURSORTENTATIVE_" + +// v must be validated by caller +func TentativeReplicationCursorBookmarkName(fs string, guid uint64, id JobID) (string, error) { + return tentativeReplicationCursorBookmarkNameImpl(fs, guid, id.String()) +} + +func tentativeReplicationCursorBookmarkNameImpl(fs string, guid uint64, jobid string) (string, error) { + return makeJobAndGuidBookmarkName(tentativeReplicationCursorBookmarkNamePrefix, fs, guid, jobid) +} + +// name is the full bookmark name, including dataset path +// +// err != nil always means that the bookmark is not a step bookmark +func ParseTentativeReplicationCursorBookmarkName(fullname string) (guid uint64, jobID JobID, err error) { + guid, jobID, err = parseJobAndGuidBookmarkName(fullname, tentativeReplicationCursorBookmarkNamePrefix) + if err != nil { + err = errors.Wrap(err, "parse step bookmark name") // no shadow! + } + return guid, jobID, err +} + // may return nil for both values, indicating there is no cursor func GetMostRecentReplicationCursorOfJob(ctx context.Context, fs string, jobID JobID) (*zfs.FilesystemVersion, error) { fsp, err := zfs.NewDatasetPath(fs) @@ -122,23 +141,23 @@ func GetReplicationCursors(ctx context.Context, dp *zfs.DatasetPath, jobID JobID // // returns ErrBookmarkCloningNotSupported if version is a bookmark and bookmarking bookmarks is not supported by ZFS func CreateReplicationCursor(ctx context.Context, fs string, target zfs.FilesystemVersion, jobID JobID) (a Abstraction, err error) { + return createBookmarkAbstraction(ctx, AbstractionReplicationCursorBookmarkV2, fs, target, jobID) +} - bookmarkname, err := ReplicationCursorBookmarkName(fs, target.GetGuid(), jobID) +func CreateTentativeReplicationCursor(ctx context.Context, fs string, target zfs.FilesystemVersion, jobID JobID) (a Abstraction, err error) { + return createBookmarkAbstraction(ctx, AbstractionTentativeReplicationCursorBookmark, fs, target, jobID) +} + +func createBookmarkAbstraction(ctx context.Context, abstractionType AbstractionType, fs string, target zfs.FilesystemVersion, jobID JobID) (a Abstraction, err error) { + + bookmarkNamer := abstractionType.BookmarkNamer() + if bookmarkNamer == nil { + panic(abstractionType) + } + + bookmarkname, err := bookmarkNamer(fs, target.GetGuid(), jobID) if err != nil { - return nil, errors.Wrap(err, "determine replication cursor name") - } - - if target.IsBookmark() && target.GetName() == bookmarkname { - return &bookmarkBasedAbstraction{ - Type: AbstractionReplicationCursorBookmarkV2, - FS: fs, - FilesystemVersion: target, - JobID: jobID, - }, nil - } - - if !target.IsSnapshot() { - return nil, zfs.ErrBookmarkCloningNotSupported + return nil, errors.Wrapf(err, "determine %s name", abstractionType) } // idempotently create bookmark (guid is encoded in it) @@ -152,125 +171,13 @@ func CreateReplicationCursor(ctx context.Context, fs string, target zfs.Filesyst } return &bookmarkBasedAbstraction{ - Type: AbstractionReplicationCursorBookmarkV2, + Type: abstractionType, FS: fs, FilesystemVersion: cursorBookmark, JobID: jobID, }, nil } -const ( - ReplicationCursorBookmarkNamePrefix = "zrepl_last_received_J_" -) - -var lastReceivedHoldTagRE = regexp.MustCompile("^zrepl_last_received_J_(.+)$") - -// err != nil always means that the bookmark is not a step bookmark -func ParseLastReceivedHoldTag(tag string) (JobID, error) { - match := lastReceivedHoldTagRE.FindStringSubmatch(tag) - if match == nil { - return JobID{}, errors.Errorf("parse last-received-hold tag: does not match regex %s", lastReceivedHoldTagRE.String()) - } - jobId, err := MakeJobID(match[1]) - if err != nil { - return JobID{}, errors.Wrap(err, "parse last-received-hold tag: invalid job id field") - } - return jobId, nil -} - -func LastReceivedHoldTag(jobID JobID) (string, error) { - return lastReceivedHoldImpl(jobID.String()) -} - -func lastReceivedHoldImpl(jobid string) (string, error) { - tag := fmt.Sprintf("%s%s", ReplicationCursorBookmarkNamePrefix, jobid) - if err := zfs.ValidHoldTag(tag); err != nil { - return "", err - } - return tag, nil -} - -func CreateLastReceivedHold(ctx context.Context, fs string, to zfs.FilesystemVersion, jobID JobID) (Abstraction, error) { - - if !to.IsSnapshot() { - return nil, errors.Errorf("last-received-hold: target must be a snapshot: %s", to.FullPath(fs)) - } - - tag, err := LastReceivedHoldTag(jobID) - if err != nil { - return nil, errors.Wrap(err, "last-received-hold: hold tag") - } - - // we never want to be without a hold - // => hold new one before releasing old hold - - err = zfs.ZFSHold(ctx, fs, to, tag) - if err != nil { - return nil, errors.Wrap(err, "last-received-hold: hold newly received") - } - - return &holdBasedAbstraction{ - Type: AbstractionLastReceivedHold, - FS: fs, - FilesystemVersion: to, - JobID: jobID, - Tag: tag, - }, nil -} - -func MoveLastReceivedHold(ctx context.Context, fs string, to zfs.FilesystemVersion, jobID JobID) error { - - _, err := CreateLastReceivedHold(ctx, fs, to, jobID) - if err != nil { - return err - } - - q := ListZFSHoldsAndBookmarksQuery{ - What: AbstractionTypeSet{ - AbstractionLastReceivedHold: true, - }, - FS: ListZFSHoldsAndBookmarksQueryFilesystemFilter{ - FS: &fs, - }, - JobID: &jobID, - CreateTXG: CreateTXGRange{ - Since: nil, - Until: &CreateTXGRangeBound{ - CreateTXG: to.GetCreateTXG(), - Inclusive: &zfs.NilBool{B: false}, - }, - }, - Concurrency: 1, - } - abs, absErrs, err := ListAbstractions(ctx, q) - if err != nil { - return errors.Wrap(err, "last-received-hold: list") - } - if len(absErrs) > 0 { - return errors.Wrap(ListAbstractionsErrors(absErrs), "last-received-hold: list") - } - - getLogger(ctx).WithField("last-received-holds", fmt.Sprintf("%s", abs)).Debug("releasing last-received-holds") - - var errs []error - for res := range BatchDestroy(ctx, abs) { - log := getLogger(ctx). - WithField("last-received-hold", res.Abstraction) - if res.DestroyErr != nil { - errs = append(errs, res.DestroyErr) - log.WithError(err). - Error("cannot release last-received-hold") - } else { - log.Info("released last-received-hold") - } - } - if len(errs) == 0 { - return nil - } else { - return errorarray.Wrap(errs, "last-received-hold: release") - } -} - func ReplicationCursorV2Extractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion) (_ Abstraction) { if v.Type != zfs.Bookmark { panic("impl error") @@ -308,24 +215,28 @@ func ReplicationCursorV1Extractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion) return nil } -var _ HoldExtractor = LastReceivedHoldExtractor +var _ BookmarkExtractor = TentativeReplicationCursorExtractor -func LastReceivedHoldExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion, holdTag string) Abstraction { - var err error - - if v.Type != zfs.Snapshot { +func TentativeReplicationCursorExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion) (_ Abstraction) { + if v.Type != zfs.Bookmark { panic("impl error") } - jobID, err := ParseLastReceivedHoldTag(holdTag) + fullname := v.ToAbsPath(fs) + + guid, jobid, err := ParseTentativeReplicationCursorBookmarkName(fullname) + if guid != v.Guid { + // TODO log this possibly tinkered-with bookmark + return nil + } if err == nil { - return &holdBasedAbstraction{ - Type: AbstractionLastReceivedHold, + bm := &bookmarkBasedAbstraction{ + Type: AbstractionTentativeReplicationCursorBookmark, FS: fs.ToString(), FilesystemVersion: v, - Tag: holdTag, - JobID: jobID, + JobID: jobid, } + return bm } return nil } diff --git a/endpoint/endpoint_zfs_abstraction_step.go b/endpoint/endpoint_zfs_abstraction_step.go deleted file mode 100644 index 78f7b78..0000000 --- a/endpoint/endpoint_zfs_abstraction_step.go +++ /dev/null @@ -1,159 +0,0 @@ -package endpoint - -import ( - "context" - "fmt" - "regexp" - - "github.com/pkg/errors" - - "github.com/zrepl/zrepl/zfs" -) - -var stepHoldTagRE = regexp.MustCompile("^zrepl_STEP_J_(.+)") - -func StepHoldTag(jobid JobID) (string, error) { - return stepHoldTagImpl(jobid.String()) -} - -func stepHoldTagImpl(jobid string) (string, error) { - t := fmt.Sprintf("zrepl_STEP_J_%s", jobid) - if err := zfs.ValidHoldTag(t); err != nil { - return "", err - } - return t, nil -} - -// err != nil always means that the bookmark is not a step bookmark -func ParseStepHoldTag(tag string) (JobID, error) { - match := stepHoldTagRE.FindStringSubmatch(tag) - if match == nil { - return JobID{}, fmt.Errorf("parse hold tag: match regex %q", stepHoldTagRE) - } - jobID, err := MakeJobID(match[1]) - if err != nil { - return JobID{}, errors.Wrap(err, "parse hold tag: invalid job id field") - } - return jobID, nil -} - -const stepBookmarkNamePrefix = "zrepl_STEP" - -// v must be validated by caller -func StepBookmarkName(fs string, guid uint64, id JobID) (string, error) { - return stepBookmarkNameImpl(fs, guid, id.String()) -} - -func stepBookmarkNameImpl(fs string, guid uint64, jobid string) (string, error) { - return makeJobAndGuidBookmarkName(stepBookmarkNamePrefix, fs, guid, jobid) -} - -// name is the full bookmark name, including dataset path -// -// err != nil always means that the bookmark is not a step bookmark -func ParseStepBookmarkName(fullname string) (guid uint64, jobID JobID, err error) { - guid, jobID, err = parseJobAndGuidBookmarkName(fullname, stepBookmarkNamePrefix) - if err != nil { - err = errors.Wrap(err, "parse step bookmark name") // no shadow! - } - return guid, jobID, err -} - -// idempotently hold / step-bookmark `version` -// -// returns ErrBookmarkCloningNotSupported if version is a bookmark and bookmarking bookmarks is not supported by ZFS -func HoldStep(ctx context.Context, fs string, v zfs.FilesystemVersion, jobID JobID) (Abstraction, error) { - if v.IsSnapshot() { - - tag, err := StepHoldTag(jobID) - if err != nil { - return nil, errors.Wrap(err, "step hold tag") - } - - if err := zfs.ZFSHold(ctx, fs, v, tag); err != nil { - return nil, errors.Wrap(err, "step hold: zfs") - } - - return &holdBasedAbstraction{ - Type: AbstractionStepHold, - FS: fs, - Tag: tag, - JobID: jobID, - FilesystemVersion: v, - }, nil - } - - if !v.IsBookmark() { - panic(fmt.Sprintf("version must bei either snapshot or bookmark, got %#v", v)) - } - - bmname, err := StepBookmarkName(fs, v.Guid, jobID) - if err != nil { - return nil, errors.Wrap(err, "create step bookmark: determine bookmark name") - } - // idempotently create bookmark - stepBookmark, err := zfs.ZFSBookmark(ctx, fs, v, bmname) - if err != nil { - if err == zfs.ErrBookmarkCloningNotSupported { - // TODO we could actually try to find a local snapshot that has the requested GUID - // however, the replication algorithm prefers snapshots anyways, so this quest - // is most likely not going to be successful. Also, there's the possibility that - // the caller might want to filter what snapshots are eligibile, and this would - // complicate things even further. - return nil, err // TODO go1.13 use wrapping - } - return nil, errors.Wrap(err, "create step bookmark: zfs") - } - return &bookmarkBasedAbstraction{ - Type: AbstractionStepBookmark, - FS: fs, - FilesystemVersion: stepBookmark, - JobID: jobID, - }, nil -} - -var _ BookmarkExtractor = StepBookmarkExtractor - -func StepBookmarkExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion) (_ Abstraction) { - if v.Type != zfs.Bookmark { - panic("impl error") - } - - fullname := v.ToAbsPath(fs) - - guid, jobid, err := ParseStepBookmarkName(fullname) - if guid != v.Guid { - // TODO log this possibly tinkered-with bookmark - return nil - } - if err == nil { - bm := &bookmarkBasedAbstraction{ - Type: AbstractionStepBookmark, - FS: fs.ToString(), - FilesystemVersion: v, - JobID: jobid, - } - return bm - } - return nil -} - -var _ HoldExtractor = StepHoldExtractor - -func StepHoldExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion, holdTag string) Abstraction { - if v.Type != zfs.Snapshot { - panic("impl error") - } - - jobID, err := ParseStepHoldTag(holdTag) - if err == nil { - return &holdBasedAbstraction{ - Type: AbstractionStepHold, - FS: fs.ToString(), - Tag: holdTag, - FilesystemVersion: v, - JobID: jobID, - } - } - return nil -} diff --git a/endpoint/endpoint_zfs_abstraction_step_hold.go b/endpoint/endpoint_zfs_abstraction_step_hold.go new file mode 100644 index 0000000..2a83b78 --- /dev/null +++ b/endpoint/endpoint_zfs_abstraction_step_hold.go @@ -0,0 +1,83 @@ +package endpoint + +import ( + "context" + "fmt" + "regexp" + + "github.com/pkg/errors" + + "github.com/zrepl/zrepl/zfs" +) + +var stepHoldTagRE = regexp.MustCompile("^zrepl_STEP_J_(.+)") + +func StepHoldTag(jobid JobID) (string, error) { + return stepHoldTagImpl(jobid.String()) +} + +func stepHoldTagImpl(jobid string) (string, error) { + t := fmt.Sprintf("zrepl_STEP_J_%s", jobid) + if err := zfs.ValidHoldTag(t); err != nil { + return "", err + } + return t, nil +} + +// err != nil always means that the bookmark is not a step bookmark +func ParseStepHoldTag(tag string) (JobID, error) { + match := stepHoldTagRE.FindStringSubmatch(tag) + if match == nil { + return JobID{}, fmt.Errorf("parse hold tag: match regex %q", stepHoldTagRE) + } + jobID, err := MakeJobID(match[1]) + if err != nil { + return JobID{}, errors.Wrap(err, "parse hold tag: invalid job id field") + } + return jobID, nil +} + +// idempotently hold `version` +func HoldStep(ctx context.Context, fs string, v zfs.FilesystemVersion, jobID JobID) (Abstraction, error) { + if !v.IsSnapshot() { + panic(fmt.Sprintf("version must be a snapshot got %#v", v)) + } + + tag, err := StepHoldTag(jobID) + if err != nil { + return nil, errors.Wrap(err, "step hold tag") + } + + if err := zfs.ZFSHold(ctx, fs, v, tag); err != nil { + return nil, errors.Wrap(err, "step hold: zfs") + } + + return &holdBasedAbstraction{ + Type: AbstractionStepHold, + FS: fs, + Tag: tag, + JobID: jobID, + FilesystemVersion: v, + }, nil + +} + +var _ HoldExtractor = StepHoldExtractor + +func StepHoldExtractor(fs *zfs.DatasetPath, v zfs.FilesystemVersion, holdTag string) Abstraction { + if v.Type != zfs.Snapshot { + panic("impl error") + } + + jobID, err := ParseStepHoldTag(holdTag) + if err == nil { + return &holdBasedAbstraction{ + Type: AbstractionStepHold, + FS: fs.ToString(), + Tag: holdTag, + FilesystemVersion: v, + JobID: jobID, + } + } + return nil +} diff --git a/endpoint/jobid.go b/endpoint/jobid.go index 372b6f9..0cba6f9 100644 --- a/endpoint/jobid.go +++ b/endpoint/jobid.go @@ -24,9 +24,9 @@ func MakeJobID(s string) (JobID, error) { return JobID{}, errors.Wrap(err, "must be usable as a dataset path component") } - if _, err := stepBookmarkNameImpl("pool/ds", 0xface601d, s); err != nil { + if _, err := tentativeReplicationCursorBookmarkNameImpl("pool/ds", 0xface601d, s); err != nil { // note that this might still fail due to total maximum name length, but we can't enforce that - return JobID{}, errors.Wrap(err, "must be usable for a step bookmark") + return JobID{}, errors.Wrap(err, "must be usable for a tentative replication cursor bookmark") } if _, err := stepHoldTagImpl(s); err != nil { diff --git a/endpoint/replicationguaranteekind_enumer.go b/endpoint/replicationguaranteekind_enumer.go new file mode 100644 index 0000000..33bc061 --- /dev/null +++ b/endpoint/replicationguaranteekind_enumer.go @@ -0,0 +1,80 @@ +// Code generated by "enumer -type=ReplicationGuaranteeKind -json -transform=snake -trimprefix=ReplicationGuaranteeKind"; DO NOT EDIT. + +// +package endpoint + +import ( + "encoding/json" + "fmt" +) + +const ( + _ReplicationGuaranteeKindName_0 = "resumabilityincremental" + _ReplicationGuaranteeKindName_1 = "none" +) + +var ( + _ReplicationGuaranteeKindIndex_0 = [...]uint8{0, 12, 23} + _ReplicationGuaranteeKindIndex_1 = [...]uint8{0, 4} +) + +func (i ReplicationGuaranteeKind) String() string { + switch { + case 1 <= i && i <= 2: + i -= 1 + return _ReplicationGuaranteeKindName_0[_ReplicationGuaranteeKindIndex_0[i]:_ReplicationGuaranteeKindIndex_0[i+1]] + case i == 4: + return _ReplicationGuaranteeKindName_1 + default: + return fmt.Sprintf("ReplicationGuaranteeKind(%d)", i) + } +} + +var _ReplicationGuaranteeKindValues = []ReplicationGuaranteeKind{1, 2, 4} + +var _ReplicationGuaranteeKindNameToValueMap = map[string]ReplicationGuaranteeKind{ + _ReplicationGuaranteeKindName_0[0:12]: 1, + _ReplicationGuaranteeKindName_0[12:23]: 2, + _ReplicationGuaranteeKindName_1[0:4]: 4, +} + +// ReplicationGuaranteeKindString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func ReplicationGuaranteeKindString(s string) (ReplicationGuaranteeKind, error) { + if val, ok := _ReplicationGuaranteeKindNameToValueMap[s]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to ReplicationGuaranteeKind values", s) +} + +// ReplicationGuaranteeKindValues returns all values of the enum +func ReplicationGuaranteeKindValues() []ReplicationGuaranteeKind { + return _ReplicationGuaranteeKindValues +} + +// IsAReplicationGuaranteeKind returns "true" if the value is listed in the enum definition. "false" otherwise +func (i ReplicationGuaranteeKind) IsAReplicationGuaranteeKind() bool { + for _, v := range _ReplicationGuaranteeKindValues { + if i == v { + return true + } + } + return false +} + +// MarshalJSON implements the json.Marshaler interface for ReplicationGuaranteeKind +func (i ReplicationGuaranteeKind) MarshalJSON() ([]byte, error) { + return json.Marshal(i.String()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface for ReplicationGuaranteeKind +func (i *ReplicationGuaranteeKind) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return fmt.Errorf("ReplicationGuaranteeKind should be a string, got %s", data) + } + + var err error + *i, err = ReplicationGuaranteeKindString(s) + return err +} diff --git a/platformtest/tests/generated_cases.go b/platformtest/tests/generated_cases.go index 3a4396d..73c5b4e 100644 --- a/platformtest/tests/generated_cases.go +++ b/platformtest/tests/generated_cases.go @@ -20,8 +20,11 @@ var Cases = []Case{BatchDestroy, ReplicationIncrementalCleansUpStaleAbstractionsWithoutCacheOnSecondReplication, ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledButStepHoldsExist, ReplicationIncrementalIsPossibleIfCommonSnapshotIsDestroyed, - ReplicationIsResumableFullSend__DisableIncrementalStepHolds_False, - ReplicationIsResumableFullSend__DisableIncrementalStepHolds_True, + ReplicationIsResumableFullSend__both_GuaranteeResumability, + ReplicationIsResumableFullSend__initial_GuaranteeIncrementalReplication_incremental_GuaranteeIncrementalReplication, + ReplicationIsResumableFullSend__initial_GuaranteeResumability_incremental_GuaranteeIncrementalReplication, + ReplicationStepCompletedLostBehavior__GuaranteeIncrementalReplication, + ReplicationStepCompletedLostBehavior__GuaranteeResumability, ResumableRecvAndTokenHandling, ResumeTokenParsing, SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden, diff --git a/platformtest/tests/replication.go b/platformtest/tests/replication.go index 23cb4a4..7cd0faa 100644 --- a/platformtest/tests/replication.go +++ b/platformtest/tests/replication.go @@ -27,11 +27,11 @@ import ( // of a new sender and receiver instance and one blocking invocation // of the replication engine without encryption type replicationInvocation struct { - sjid, rjid endpoint.JobID - sfs string - rfsRoot string - interceptSender func(e *endpoint.Sender) logic.Sender - disableIncrementalStepHolds bool + sjid, rjid endpoint.JobID + sfs string + rfsRoot string + interceptSender func(e *endpoint.Sender) logic.Sender + guarantee pdu.ReplicationConfigProtection } func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report { @@ -44,19 +44,20 @@ func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report { err := sfilter.Add(i.sfs, "ok") require.NoError(ctx, err) sender := i.interceptSender(endpoint.NewSender(endpoint.SenderConfig{ - FSF: sfilter.AsFilter(), - Encrypt: &zfs.NilBool{B: false}, - DisableIncrementalStepHolds: i.disableIncrementalStepHolds, - JobID: i.sjid, + FSF: sfilter.AsFilter(), + Encrypt: &zfs.NilBool{B: false}, + JobID: i.sjid, })) receiver := endpoint.NewReceiver(endpoint.ReceiverConfig{ JobID: i.rjid, AppendClientIdentity: false, RootWithoutClientComponent: mustDatasetPath(i.rfsRoot), - UpdateLastReceivedHold: true, }) plannerPolicy := logic.PlannerPolicy{ EncryptedSend: logic.TriFromBool(false), + ReplicationConfig: pdu.ReplicationConfig{ + Protection: &i.guarantee, + }, } report, wait := replication.Do( @@ -89,11 +90,11 @@ func ReplicationIncrementalIsPossibleIfCommonSnapshotIsDestroyed(ctx *platformte snap1 := fsversion(ctx, sfs, "@1") rep := replicationInvocation{ - sjid: sjid, - rjid: rjid, - sfs: sfs, - rfsRoot: rfsRoot, - disableIncrementalStepHolds: false, + sjid: sjid, + rjid: rjid, + sfs: sfs, + rfsRoot: rfsRoot, + guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability), } rfs := rep.ReceiveSideFilesystem() @@ -153,11 +154,11 @@ func implReplicationIncrementalCleansUpStaleAbstractions(ctx *platformtest.Conte rfsRoot := ctx.RootDataset + "/receiver" rep := replicationInvocation{ - sjid: sjid, - rjid: rjid, - sfs: sfs, - rfsRoot: rfsRoot, - disableIncrementalStepHolds: false, + sjid: sjid, + rjid: rjid, + sfs: sfs, + rfsRoot: rfsRoot, + guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability), } rfs := rep.ReceiveSideFilesystem() @@ -207,7 +208,7 @@ func implReplicationIncrementalCleansUpStaleAbstractions(ctx *platformtest.Conte snap5 := fsversion(ctx, sfs, "@5") if invalidateCacheBeforeSecondReplication { - endpoint.SendAbstractionsCacheInvalidate(sfs) + endpoint.AbstractionsCacheInvalidate(sfs) } // do another replication @@ -327,15 +328,59 @@ func (s *PartialSender) Send(ctx context.Context, r *pdu.SendReq) (r1 *pdu.SendR return r1, r2, r3 } -func ReplicationIsResumableFullSend__DisableIncrementalStepHolds_False(ctx *platformtest.Context) { - implReplicationIsResumableFullSend(ctx, false) +func ReplicationIsResumableFullSend__both_GuaranteeResumability(ctx *platformtest.Context) { + + setup := replicationIsResumableFullSendSetup{ + protection: pdu.ReplicationConfigProtection{ + Initial: pdu.ReplicationGuaranteeKind_GuaranteeResumability, + Incremental: pdu.ReplicationGuaranteeKind_GuaranteeResumability, + }, + expectDatasetIsBusyErrorWhenDestroySnapshotWhilePartiallyReplicated: true, + expectAllThreeSnapshotsToThreeBePresentAfterLoop: true, + expectNoSnapshotsOnReceiverAfterLoop: false, + } + + implReplicationIsResumableFullSend(ctx, setup) } -func ReplicationIsResumableFullSend__DisableIncrementalStepHolds_True(ctx *platformtest.Context) { - implReplicationIsResumableFullSend(ctx, true) +func ReplicationIsResumableFullSend__initial_GuaranteeResumability_incremental_GuaranteeIncrementalReplication(ctx *platformtest.Context) { + + setup := replicationIsResumableFullSendSetup{ + protection: pdu.ReplicationConfigProtection{ + Initial: pdu.ReplicationGuaranteeKind_GuaranteeResumability, + Incremental: pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication, + }, + expectDatasetIsBusyErrorWhenDestroySnapshotWhilePartiallyReplicated: true, + expectAllThreeSnapshotsToThreeBePresentAfterLoop: true, + expectNoSnapshotsOnReceiverAfterLoop: false, + } + + implReplicationIsResumableFullSend(ctx, setup) } -func implReplicationIsResumableFullSend(ctx *platformtest.Context, disableIncrementalStepHolds bool) { +func ReplicationIsResumableFullSend__initial_GuaranteeIncrementalReplication_incremental_GuaranteeIncrementalReplication(ctx *platformtest.Context) { + + setup := replicationIsResumableFullSendSetup{ + protection: pdu.ReplicationConfigProtection{ + Initial: pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication, + Incremental: pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication, + }, + expectDatasetIsBusyErrorWhenDestroySnapshotWhilePartiallyReplicated: false, + expectAllThreeSnapshotsToThreeBePresentAfterLoop: false, + expectNoSnapshotsOnReceiverAfterLoop: true, + } + + implReplicationIsResumableFullSend(ctx, setup) +} + +type replicationIsResumableFullSendSetup struct { + protection pdu.ReplicationConfigProtection + expectDatasetIsBusyErrorWhenDestroySnapshotWhilePartiallyReplicated bool + expectAllThreeSnapshotsToThreeBePresentAfterLoop bool + expectNoSnapshotsOnReceiverAfterLoop bool +} + +func implReplicationIsResumableFullSend(ctx *platformtest.Context, setup replicationIsResumableFullSendSetup) { platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, ` CREATEROOT @@ -366,8 +411,9 @@ func implReplicationIsResumableFullSend(ctx *platformtest.Context, disableIncrem interceptSender: func(e *endpoint.Sender) logic.Sender { return &PartialSender{Sender: e, failAfterByteCount: 1 << 20} }, - disableIncrementalStepHolds: disableIncrementalStepHolds, + guarantee: setup.protection, } + rfs := rep.ReceiveSideFilesystem() for i := 2; i < 10; i++ { @@ -381,8 +427,11 @@ func implReplicationIsResumableFullSend(ctx *platformtest.Context, disableIncrem // and we wrote dummy data 1<<22 bytes, thus at least // for the first 4 times this should not be possible // due to step holds - require.Error(ctx, err) - require.Contains(ctx, err.Error(), "dataset is busy") + if setup.expectDatasetIsBusyErrorWhenDestroySnapshotWhilePartiallyReplicated { + ctx.Logf("i=%v", i) + require.Error(ctx, err) + require.Contains(ctx, err.Error(), "dataset is busy") + } } // and create some additional snapshots that could @@ -401,11 +450,19 @@ func implReplicationIsResumableFullSend(ctx *platformtest.Context, disableIncrem } } - // make sure all the filesystem versions we created - // were replicated by the replication loop - _ = fsversion(ctx, rfs, "@1") - _ = fsversion(ctx, rfs, "@2") - _ = fsversion(ctx, rfs, "@3") + if setup.expectAllThreeSnapshotsToThreeBePresentAfterLoop { + // make sure all the filesystem versions we created + // were replicated by the replication loop + _ = fsversion(ctx, rfs, "@1") + _ = fsversion(ctx, rfs, "@2") + _ = fsversion(ctx, rfs, "@3") + } + + if setup.expectNoSnapshotsOnReceiverAfterLoop { + versions, err := zfs.ZFSListFilesystemVersions(ctx, mustDatasetPath(rfs), zfs.ListFilesystemVersionsOptions{}) + require.NoError(ctx, err) + require.Empty(ctx, versions) + } } @@ -428,11 +485,11 @@ func ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledBu { mustSnapshot(ctx, sfs+"@1") rep := replicationInvocation{ - sjid: sjid, - rjid: rjid, - sfs: sfs, - rfsRoot: rfsRoot, - disableIncrementalStepHolds: false, + sjid: sjid, + rjid: rjid, + sfs: sfs, + rfsRoot: rfsRoot, + guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability), } rfs := rep.ReceiveSideFilesystem() report := rep.Do(ctx) @@ -455,11 +512,11 @@ func ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledBu // to effect a step-holds situation { rep := replicationInvocation{ - sjid: sjid, - rjid: rjid, - sfs: sfs, - rfsRoot: rfsRoot, - disableIncrementalStepHolds: false, // ! + sjid: sjid, + rjid: rjid, + sfs: sfs, + rfsRoot: rfsRoot, + guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeResumability), // ! interceptSender: func(e *endpoint.Sender) logic.Sender { return &PartialSender{Sender: e, failAfterByteCount: 1 << 20} }, @@ -495,17 +552,17 @@ func ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledBu // end of test setup // - // retry replication with incremental step holds disabled + // retry replication with incremental step holds disabled (set to bookmarks-only in this case) // - replication should not fail due to holds-related stuff // - replication should fail intermittently due to partial sender being fully read // - the partial sender is 1/4th the length of the stream, thus expect // successful replication after 5 more attempts rep := replicationInvocation{ - sjid: sjid, - rjid: rjid, - sfs: sfs, - rfsRoot: rfsRoot, - disableIncrementalStepHolds: true, // ! + sjid: sjid, + rjid: rjid, + sfs: sfs, + rfsRoot: rfsRoot, + guarantee: *pdu.ReplicationConfigProtectionWithKind(pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication), // ! interceptSender: func(e *endpoint.Sender) logic.Sender { return &PartialSender{Sender: e, failAfterByteCount: 1 << 20} }, @@ -551,3 +608,148 @@ func ReplicationIncrementalDestroysStepHoldsIffIncrementalStepHoldsAreDisabledBu require.Len(ctx, abs, 1) require.True(ctx, zfs.FilesystemVersionEqualIdentity(abs[0].GetFilesystemVersion(), snap2sfs)) } + +func ReplicationStepCompletedLostBehavior__GuaranteeResumability(ctx *platformtest.Context) { + scenario := replicationStepCompletedLostBehavior_impl(ctx, pdu.ReplicationGuaranteeKind_GuaranteeResumability) + + require.Error(ctx, scenario.deleteSfs1Err, "protected by holds") + require.Contains(ctx, scenario.deleteSfs1Err.Error(), "dataset is busy") + + require.Error(ctx, scenario.deleteSfs2Err, "protected by holds") + require.Contains(ctx, scenario.deleteSfs2Err.Error(), "dataset is busy") + + require.Nil(ctx, scenario.finalReport.Error()) + _ = fsversion(ctx, scenario.rfs, "@3") // @3 ade it to the other side +} + +func ReplicationStepCompletedLostBehavior__GuaranteeIncrementalReplication(ctx *platformtest.Context) { + scenario := replicationStepCompletedLostBehavior_impl(ctx, pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication) + + require.NoError(ctx, scenario.deleteSfs1Err, "not protected by holds") + require.NoError(ctx, scenario.deleteSfs2Err, "not protected by holds") + + // step bookmarks should protect against loss of StepCompleted message + require.Nil(ctx, scenario.finalReport.Error()) + _ = fsversion(ctx, scenario.rfs, "@3") // @3 ade it to the other side +} + +type FailSendCompletedSender struct { + *endpoint.Sender +} + +var _ logic.Sender = (*FailSendCompletedSender)(nil) + +func (p *FailSendCompletedSender) SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error) { + return nil, fmt.Errorf("[mock] SendCompleted not delivered to actual endpoint") +} + +type replicationStepCompletedLost_scenario struct { + rfs string + deleteSfs1Err, deleteSfs2Err error + finalReport *report.FilesystemReport +} + +func replicationStepCompletedLostBehavior_impl(ctx *platformtest.Context, guaranteeKind pdu.ReplicationGuaranteeKind) *replicationStepCompletedLost_scenario { + + platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, ` + CREATEROOT + + "sender" + + "receiver" + R zfs create -p "${ROOTDS}/receiver/${ROOTDS}" + `) + + sjid := endpoint.MustMakeJobID("sender-job") + rjid := endpoint.MustMakeJobID("receiver-job") + + sfs := ctx.RootDataset + "/sender" + rfsRoot := ctx.RootDataset + "/receiver" + + // fully replicate snapshots @1 + { + mustSnapshot(ctx, sfs+"@1") + rep := replicationInvocation{ + sjid: sjid, + rjid: rjid, + sfs: sfs, + rfsRoot: rfsRoot, + guarantee: *pdu.ReplicationConfigProtectionWithKind(guaranteeKind), + } + rfs := rep.ReceiveSideFilesystem() + report := rep.Do(ctx) + ctx.Logf("\n%s", pretty.Sprint(report)) + // assert this worked (not the main subject of the test) + _ = fsversion(ctx, rfs, "@1") + } + + // create a second snapshot @2 + mustSnapshot(ctx, sfs+"@2") + + // fake loss of stepcompleted message + rep := replicationInvocation{ + sjid: sjid, + rjid: rjid, + sfs: sfs, + rfsRoot: rfsRoot, + guarantee: *pdu.ReplicationConfigProtectionWithKind(guaranteeKind), + interceptSender: func(e *endpoint.Sender) logic.Sender { + return &FailSendCompletedSender{e} + }, + } + rfs := rep.ReceiveSideFilesystem() + report := rep.Do(ctx) + ctx.Logf("\n%s", pretty.Sprint(report)) + + // assert the replication worked + _ = fsversion(ctx, rfs, "@2") + // and that we hold it using a last-received-hold + abs, absErrs, err := endpoint.ListAbstractions(ctx, endpoint.ListZFSHoldsAndBookmarksQuery{ + FS: endpoint.ListZFSHoldsAndBookmarksQueryFilesystemFilter{ + FS: &rfs, + }, + Concurrency: 1, + JobID: &rjid, + What: endpoint.AbstractionTypeSet{endpoint.AbstractionLastReceivedHold: true}, + }) + require.NoError(ctx, err) + require.Empty(ctx, absErrs) + require.Len(ctx, abs, 1) + require.True(ctx, zfs.FilesystemVersionEqualIdentity(abs[0].GetFilesystemVersion(), fsversion(ctx, rfs, "@2"))) + + // now try to delete @2 on the sender, this should work because don't have step holds on it + deleteSfs2Err := zfs.ZFSDestroy(ctx, sfs+"@2") + // defer check to caller + + // and create a new snapshot on the sender + mustSnapshot(ctx, sfs+"@3") + + // now we have: sender @1, @3 + // recver @1, @2 + + // delete @1 on both sides to demonstrate that, if we didn't have bookmarks, we would be out of sync + deleteSfs1Err := zfs.ZFSDestroy(ctx, sfs+"@1") + // defer check to caller + err = zfs.ZFSDestroy(ctx, rfs+"@1") + require.NoError(ctx, err) + + // attempt replication and return the filesystem report report + { + rep := replicationInvocation{ + sjid: sjid, + rjid: rjid, + sfs: sfs, + rfsRoot: rfsRoot, + guarantee: *pdu.ReplicationConfigProtectionWithKind(guaranteeKind), + } + report := rep.Do(ctx) + ctx.Logf("expecting failure:\n%s", pretty.Sprint(report)) + require.Len(ctx, report.Attempts, 1) + require.Len(ctx, report.Attempts[0].Filesystems, 1) + return &replicationStepCompletedLost_scenario{ + rfs: rfs, + deleteSfs1Err: deleteSfs1Err, + deleteSfs2Err: deleteSfs2Err, + finalReport: report.Attempts[0].Filesystems[0], + } + } + +} diff --git a/platformtest/tests/replicationCursor.go b/platformtest/tests/replicationCursor.go index 72e3e5c..2eb04af 100644 --- a/platformtest/tests/replicationCursor.go +++ b/platformtest/tests/replicationCursor.go @@ -22,6 +22,7 @@ func CreateReplicationCursor(ctx *platformtest.Context) { R zfs bookmark "${ROOTDS}/foo bar@2 with space" "${ROOTDS}/foo bar#2 with space" + "foo bar@3 with space" R zfs bookmark "${ROOTDS}/foo bar@3 with space" "${ROOTDS}/foo bar#3 with space" + - "foo bar@3 with space" `) jobid := endpoint.MustMakeJobID("zreplplatformtest") @@ -42,6 +43,7 @@ func CreateReplicationCursor(ctx *platformtest.Context) { snap := fsversion(ctx, fs, "@1 with space") book := fsversion(ctx, fs, "#1 with space") + book3 := fsversion(ctx, fs, "#3 with space") // create first cursor cursorOfSnap, err := endpoint.CreateReplicationCursor(ctx, fs, snap, jobid) @@ -49,8 +51,11 @@ func CreateReplicationCursor(ctx *platformtest.Context) { // check CreateReplicationCursor is idempotent (for snapshot target) cursorOfSnapIdemp, err := endpoint.CreateReplicationCursor(ctx, fs, snap, jobid) checkCreateCursor(err, cursorOfSnap, snap) + // check CreateReplicationCursor is idempotent (for bookmark target of snapshot) + cursorOfBook, err := endpoint.CreateReplicationCursor(ctx, fs, book, jobid) + checkCreateCursor(err, cursorOfBook, snap) // ... for target = non-cursor bookmark - _, err = endpoint.CreateReplicationCursor(ctx, fs, book, jobid) + _, err = endpoint.CreateReplicationCursor(ctx, fs, book3, jobid) assert.Equal(ctx, zfs.ErrBookmarkCloningNotSupported, err) // ... for target = replication cursor bookmark to be created cursorOfCursor, err := endpoint.CreateReplicationCursor(ctx, fs, cursorOfSnapIdemp.GetFilesystemVersion(), jobid) diff --git a/replication/logic/pdu/pdu.pb.go b/replication/logic/pdu/pdu.pb.go index cdb85bc..22d0fdd 100644 --- a/replication/logic/pdu/pdu.pb.go +++ b/replication/logic/pdu/pdu.pb.go @@ -46,7 +46,36 @@ func (x Tri) String() string { return proto.EnumName(Tri_name, int32(x)) } func (Tri) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_pdu_483c6918b7b3d747, []int{0} + return fileDescriptor_pdu_616c27178643eca4, []int{0} +} + +type ReplicationGuaranteeKind int32 + +const ( + ReplicationGuaranteeKind_GuaranteeInvalid ReplicationGuaranteeKind = 0 + ReplicationGuaranteeKind_GuaranteeResumability ReplicationGuaranteeKind = 1 + ReplicationGuaranteeKind_GuaranteeIncrementalReplication ReplicationGuaranteeKind = 2 + ReplicationGuaranteeKind_GuaranteeNothing ReplicationGuaranteeKind = 3 +) + +var ReplicationGuaranteeKind_name = map[int32]string{ + 0: "GuaranteeInvalid", + 1: "GuaranteeResumability", + 2: "GuaranteeIncrementalReplication", + 3: "GuaranteeNothing", +} +var ReplicationGuaranteeKind_value = map[string]int32{ + "GuaranteeInvalid": 0, + "GuaranteeResumability": 1, + "GuaranteeIncrementalReplication": 2, + "GuaranteeNothing": 3, +} + +func (x ReplicationGuaranteeKind) String() string { + return proto.EnumName(ReplicationGuaranteeKind_name, int32(x)) +} +func (ReplicationGuaranteeKind) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_pdu_616c27178643eca4, []int{1} } type FilesystemVersion_VersionType int32 @@ -69,7 +98,7 @@ func (x FilesystemVersion_VersionType) String() string { return proto.EnumName(FilesystemVersion_VersionType_name, int32(x)) } func (FilesystemVersion_VersionType) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_pdu_483c6918b7b3d747, []int{5, 0} + return fileDescriptor_pdu_616c27178643eca4, []int{5, 0} } type ListFilesystemReq struct { @@ -82,7 +111,7 @@ func (m *ListFilesystemReq) Reset() { *m = ListFilesystemReq{} } func (m *ListFilesystemReq) String() string { return proto.CompactTextString(m) } func (*ListFilesystemReq) ProtoMessage() {} func (*ListFilesystemReq) Descriptor() ([]byte, []int) { - return fileDescriptor_pdu_483c6918b7b3d747, []int{0} + return fileDescriptor_pdu_616c27178643eca4, []int{0} } func (m *ListFilesystemReq) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ListFilesystemReq.Unmarshal(m, b) @@ -113,7 +142,7 @@ func (m *ListFilesystemRes) Reset() { *m = ListFilesystemRes{} } func (m *ListFilesystemRes) String() string { return proto.CompactTextString(m) } func (*ListFilesystemRes) ProtoMessage() {} func (*ListFilesystemRes) Descriptor() ([]byte, []int) { - return fileDescriptor_pdu_483c6918b7b3d747, []int{1} + return fileDescriptor_pdu_616c27178643eca4, []int{1} } func (m *ListFilesystemRes) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ListFilesystemRes.Unmarshal(m, b) @@ -154,7 +183,7 @@ func (m *Filesystem) Reset() { *m = Filesystem{} } func (m *Filesystem) String() string { return proto.CompactTextString(m) } func (*Filesystem) ProtoMessage() {} func (*Filesystem) Descriptor() ([]byte, []int) { - return fileDescriptor_pdu_483c6918b7b3d747, []int{2} + return fileDescriptor_pdu_616c27178643eca4, []int{2} } func (m *Filesystem) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Filesystem.Unmarshal(m, b) @@ -213,7 +242,7 @@ func (m *ListFilesystemVersionsReq) Reset() { *m = ListFilesystemVersion func (m *ListFilesystemVersionsReq) String() string { return proto.CompactTextString(m) } func (*ListFilesystemVersionsReq) ProtoMessage() {} func (*ListFilesystemVersionsReq) Descriptor() ([]byte, []int) { - return fileDescriptor_pdu_483c6918b7b3d747, []int{3} + return fileDescriptor_pdu_616c27178643eca4, []int{3} } func (m *ListFilesystemVersionsReq) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ListFilesystemVersionsReq.Unmarshal(m, b) @@ -251,7 +280,7 @@ func (m *ListFilesystemVersionsRes) Reset() { *m = ListFilesystemVersion func (m *ListFilesystemVersionsRes) String() string { return proto.CompactTextString(m) } func (*ListFilesystemVersionsRes) ProtoMessage() {} func (*ListFilesystemVersionsRes) Descriptor() ([]byte, []int) { - return fileDescriptor_pdu_483c6918b7b3d747, []int{4} + return fileDescriptor_pdu_616c27178643eca4, []int{4} } func (m *ListFilesystemVersionsRes) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ListFilesystemVersionsRes.Unmarshal(m, b) @@ -293,7 +322,7 @@ func (m *FilesystemVersion) Reset() { *m = FilesystemVersion{} } func (m *FilesystemVersion) String() string { return proto.CompactTextString(m) } func (*FilesystemVersion) ProtoMessage() {} func (*FilesystemVersion) Descriptor() ([]byte, []int) { - return fileDescriptor_pdu_483c6918b7b3d747, []int{5} + return fileDescriptor_pdu_616c27178643eca4, []int{5} } func (m *FilesystemVersion) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_FilesystemVersion.Unmarshal(m, b) @@ -359,19 +388,20 @@ type SendReq struct { // SHOULD clear the resume token on their side and use From and To instead If // ResumeToken is not empty, the GUIDs of From and To MUST correspond to those // encoded in the ResumeToken. Otherwise, the Sender MUST return an error. - ResumeToken string `protobuf:"bytes,4,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"` - Encrypted Tri `protobuf:"varint,5,opt,name=Encrypted,proto3,enum=Tri" json:"Encrypted,omitempty"` - DryRun bool `protobuf:"varint,6,opt,name=DryRun,proto3" json:"DryRun,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + ResumeToken string `protobuf:"bytes,4,opt,name=ResumeToken,proto3" json:"ResumeToken,omitempty"` + Encrypted Tri `protobuf:"varint,5,opt,name=Encrypted,proto3,enum=Tri" json:"Encrypted,omitempty"` + DryRun bool `protobuf:"varint,6,opt,name=DryRun,proto3" json:"DryRun,omitempty"` + ReplicationConfig *ReplicationConfig `protobuf:"bytes,7,opt,name=ReplicationConfig,proto3" json:"ReplicationConfig,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *SendReq) Reset() { *m = SendReq{} } func (m *SendReq) String() string { return proto.CompactTextString(m) } func (*SendReq) ProtoMessage() {} func (*SendReq) Descriptor() ([]byte, []int) { - return fileDescriptor_pdu_483c6918b7b3d747, []int{6} + return fileDescriptor_pdu_616c27178643eca4, []int{6} } func (m *SendReq) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_SendReq.Unmarshal(m, b) @@ -433,6 +463,97 @@ func (m *SendReq) GetDryRun() bool { return false } +func (m *SendReq) GetReplicationConfig() *ReplicationConfig { + if m != nil { + return m.ReplicationConfig + } + return nil +} + +type ReplicationConfig struct { + Protection *ReplicationConfigProtection `protobuf:"bytes,1,opt,name=protection,proto3" json:"protection,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ReplicationConfig) Reset() { *m = ReplicationConfig{} } +func (m *ReplicationConfig) String() string { return proto.CompactTextString(m) } +func (*ReplicationConfig) ProtoMessage() {} +func (*ReplicationConfig) Descriptor() ([]byte, []int) { + return fileDescriptor_pdu_616c27178643eca4, []int{7} +} +func (m *ReplicationConfig) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ReplicationConfig.Unmarshal(m, b) +} +func (m *ReplicationConfig) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ReplicationConfig.Marshal(b, m, deterministic) +} +func (dst *ReplicationConfig) XXX_Merge(src proto.Message) { + xxx_messageInfo_ReplicationConfig.Merge(dst, src) +} +func (m *ReplicationConfig) XXX_Size() int { + return xxx_messageInfo_ReplicationConfig.Size(m) +} +func (m *ReplicationConfig) XXX_DiscardUnknown() { + xxx_messageInfo_ReplicationConfig.DiscardUnknown(m) +} + +var xxx_messageInfo_ReplicationConfig proto.InternalMessageInfo + +func (m *ReplicationConfig) GetProtection() *ReplicationConfigProtection { + if m != nil { + return m.Protection + } + return nil +} + +type ReplicationConfigProtection struct { + Initial ReplicationGuaranteeKind `protobuf:"varint,1,opt,name=Initial,proto3,enum=ReplicationGuaranteeKind" json:"Initial,omitempty"` + Incremental ReplicationGuaranteeKind `protobuf:"varint,2,opt,name=Incremental,proto3,enum=ReplicationGuaranteeKind" json:"Incremental,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ReplicationConfigProtection) Reset() { *m = ReplicationConfigProtection{} } +func (m *ReplicationConfigProtection) String() string { return proto.CompactTextString(m) } +func (*ReplicationConfigProtection) ProtoMessage() {} +func (*ReplicationConfigProtection) Descriptor() ([]byte, []int) { + return fileDescriptor_pdu_616c27178643eca4, []int{8} +} +func (m *ReplicationConfigProtection) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ReplicationConfigProtection.Unmarshal(m, b) +} +func (m *ReplicationConfigProtection) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ReplicationConfigProtection.Marshal(b, m, deterministic) +} +func (dst *ReplicationConfigProtection) XXX_Merge(src proto.Message) { + xxx_messageInfo_ReplicationConfigProtection.Merge(dst, src) +} +func (m *ReplicationConfigProtection) XXX_Size() int { + return xxx_messageInfo_ReplicationConfigProtection.Size(m) +} +func (m *ReplicationConfigProtection) XXX_DiscardUnknown() { + xxx_messageInfo_ReplicationConfigProtection.DiscardUnknown(m) +} + +var xxx_messageInfo_ReplicationConfigProtection proto.InternalMessageInfo + +func (m *ReplicationConfigProtection) GetInitial() ReplicationGuaranteeKind { + if m != nil { + return m.Initial + } + return ReplicationGuaranteeKind_GuaranteeInvalid +} + +func (m *ReplicationConfigProtection) GetIncremental() ReplicationGuaranteeKind { + if m != nil { + return m.Incremental + } + return ReplicationGuaranteeKind_GuaranteeInvalid +} + type Property struct { Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,omitempty"` Value string `protobuf:"bytes,2,opt,name=Value,proto3" json:"Value,omitempty"` @@ -445,7 +566,7 @@ func (m *Property) Reset() { *m = Property{} } func (m *Property) String() string { return proto.CompactTextString(m) } func (*Property) ProtoMessage() {} func (*Property) Descriptor() ([]byte, []int) { - return fileDescriptor_pdu_483c6918b7b3d747, []int{7} + return fileDescriptor_pdu_616c27178643eca4, []int{9} } func (m *Property) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Property.Unmarshal(m, b) @@ -496,7 +617,7 @@ func (m *SendRes) Reset() { *m = SendRes{} } func (m *SendRes) String() string { return proto.CompactTextString(m) } func (*SendRes) ProtoMessage() {} func (*SendRes) Descriptor() ([]byte, []int) { - return fileDescriptor_pdu_483c6918b7b3d747, []int{8} + return fileDescriptor_pdu_616c27178643eca4, []int{10} } func (m *SendRes) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_SendRes.Unmarshal(m, b) @@ -548,7 +669,7 @@ func (m *SendCompletedReq) Reset() { *m = SendCompletedReq{} } func (m *SendCompletedReq) String() string { return proto.CompactTextString(m) } func (*SendCompletedReq) ProtoMessage() {} func (*SendCompletedReq) Descriptor() ([]byte, []int) { - return fileDescriptor_pdu_483c6918b7b3d747, []int{9} + return fileDescriptor_pdu_616c27178643eca4, []int{11} } func (m *SendCompletedReq) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_SendCompletedReq.Unmarshal(m, b) @@ -585,7 +706,7 @@ func (m *SendCompletedRes) Reset() { *m = SendCompletedRes{} } func (m *SendCompletedRes) String() string { return proto.CompactTextString(m) } func (*SendCompletedRes) ProtoMessage() {} func (*SendCompletedRes) Descriptor() ([]byte, []int) { - return fileDescriptor_pdu_483c6918b7b3d747, []int{10} + return fileDescriptor_pdu_616c27178643eca4, []int{12} } func (m *SendCompletedRes) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_SendCompletedRes.Unmarshal(m, b) @@ -610,17 +731,18 @@ type ReceiveReq struct { To *FilesystemVersion `protobuf:"bytes,2,opt,name=To,proto3" json:"To,omitempty"` // If true, the receiver should clear the resume token before performing the // zfs recv of the stream in the request - ClearResumeToken bool `protobuf:"varint,3,opt,name=ClearResumeToken,proto3" json:"ClearResumeToken,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + ClearResumeToken bool `protobuf:"varint,3,opt,name=ClearResumeToken,proto3" json:"ClearResumeToken,omitempty"` + ReplicationConfig *ReplicationConfig `protobuf:"bytes,4,opt,name=ReplicationConfig,proto3" json:"ReplicationConfig,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *ReceiveReq) Reset() { *m = ReceiveReq{} } func (m *ReceiveReq) String() string { return proto.CompactTextString(m) } func (*ReceiveReq) ProtoMessage() {} func (*ReceiveReq) Descriptor() ([]byte, []int) { - return fileDescriptor_pdu_483c6918b7b3d747, []int{11} + return fileDescriptor_pdu_616c27178643eca4, []int{13} } func (m *ReceiveReq) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ReceiveReq.Unmarshal(m, b) @@ -661,6 +783,13 @@ func (m *ReceiveReq) GetClearResumeToken() bool { return false } +func (m *ReceiveReq) GetReplicationConfig() *ReplicationConfig { + if m != nil { + return m.ReplicationConfig + } + return nil +} + type ReceiveRes struct { XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` @@ -671,7 +800,7 @@ func (m *ReceiveRes) Reset() { *m = ReceiveRes{} } func (m *ReceiveRes) String() string { return proto.CompactTextString(m) } func (*ReceiveRes) ProtoMessage() {} func (*ReceiveRes) Descriptor() ([]byte, []int) { - return fileDescriptor_pdu_483c6918b7b3d747, []int{12} + return fileDescriptor_pdu_616c27178643eca4, []int{14} } func (m *ReceiveRes) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ReceiveRes.Unmarshal(m, b) @@ -704,7 +833,7 @@ func (m *DestroySnapshotsReq) Reset() { *m = DestroySnapshotsReq{} } func (m *DestroySnapshotsReq) String() string { return proto.CompactTextString(m) } func (*DestroySnapshotsReq) ProtoMessage() {} func (*DestroySnapshotsReq) Descriptor() ([]byte, []int) { - return fileDescriptor_pdu_483c6918b7b3d747, []int{13} + return fileDescriptor_pdu_616c27178643eca4, []int{15} } func (m *DestroySnapshotsReq) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_DestroySnapshotsReq.Unmarshal(m, b) @@ -750,7 +879,7 @@ func (m *DestroySnapshotRes) Reset() { *m = DestroySnapshotRes{} } func (m *DestroySnapshotRes) String() string { return proto.CompactTextString(m) } func (*DestroySnapshotRes) ProtoMessage() {} func (*DestroySnapshotRes) Descriptor() ([]byte, []int) { - return fileDescriptor_pdu_483c6918b7b3d747, []int{14} + return fileDescriptor_pdu_616c27178643eca4, []int{16} } func (m *DestroySnapshotRes) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_DestroySnapshotRes.Unmarshal(m, b) @@ -795,7 +924,7 @@ func (m *DestroySnapshotsRes) Reset() { *m = DestroySnapshotsRes{} } func (m *DestroySnapshotsRes) String() string { return proto.CompactTextString(m) } func (*DestroySnapshotsRes) ProtoMessage() {} func (*DestroySnapshotsRes) Descriptor() ([]byte, []int) { - return fileDescriptor_pdu_483c6918b7b3d747, []int{15} + return fileDescriptor_pdu_616c27178643eca4, []int{17} } func (m *DestroySnapshotsRes) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_DestroySnapshotsRes.Unmarshal(m, b) @@ -833,7 +962,7 @@ func (m *ReplicationCursorReq) Reset() { *m = ReplicationCursorReq{} } func (m *ReplicationCursorReq) String() string { return proto.CompactTextString(m) } func (*ReplicationCursorReq) ProtoMessage() {} func (*ReplicationCursorReq) Descriptor() ([]byte, []int) { - return fileDescriptor_pdu_483c6918b7b3d747, []int{16} + return fileDescriptor_pdu_616c27178643eca4, []int{18} } func (m *ReplicationCursorReq) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ReplicationCursorReq.Unmarshal(m, b) @@ -874,7 +1003,7 @@ func (m *ReplicationCursorRes) Reset() { *m = ReplicationCursorRes{} } func (m *ReplicationCursorRes) String() string { return proto.CompactTextString(m) } func (*ReplicationCursorRes) ProtoMessage() {} func (*ReplicationCursorRes) Descriptor() ([]byte, []int) { - return fileDescriptor_pdu_483c6918b7b3d747, []int{17} + return fileDescriptor_pdu_616c27178643eca4, []int{19} } func (m *ReplicationCursorRes) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_ReplicationCursorRes.Unmarshal(m, b) @@ -1010,7 +1139,7 @@ func (m *PingReq) Reset() { *m = PingReq{} } func (m *PingReq) String() string { return proto.CompactTextString(m) } func (*PingReq) ProtoMessage() {} func (*PingReq) Descriptor() ([]byte, []int) { - return fileDescriptor_pdu_483c6918b7b3d747, []int{18} + return fileDescriptor_pdu_616c27178643eca4, []int{20} } func (m *PingReq) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_PingReq.Unmarshal(m, b) @@ -1049,7 +1178,7 @@ func (m *PingRes) Reset() { *m = PingRes{} } func (m *PingRes) String() string { return proto.CompactTextString(m) } func (*PingRes) ProtoMessage() {} func (*PingRes) Descriptor() ([]byte, []int) { - return fileDescriptor_pdu_483c6918b7b3d747, []int{19} + return fileDescriptor_pdu_616c27178643eca4, []int{21} } func (m *PingRes) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_PingRes.Unmarshal(m, b) @@ -1084,6 +1213,8 @@ func init() { proto.RegisterType((*ListFilesystemVersionsRes)(nil), "ListFilesystemVersionsRes") proto.RegisterType((*FilesystemVersion)(nil), "FilesystemVersion") proto.RegisterType((*SendReq)(nil), "SendReq") + proto.RegisterType((*ReplicationConfig)(nil), "ReplicationConfig") + proto.RegisterType((*ReplicationConfigProtection)(nil), "ReplicationConfigProtection") proto.RegisterType((*Property)(nil), "Property") proto.RegisterType((*SendRes)(nil), "SendRes") proto.RegisterType((*SendCompletedReq)(nil), "SendCompletedReq") @@ -1098,6 +1229,7 @@ func init() { proto.RegisterType((*PingReq)(nil), "PingReq") proto.RegisterType((*PingRes)(nil), "PingRes") proto.RegisterEnum("Tri", Tri_name, Tri_value) + proto.RegisterEnum("ReplicationGuaranteeKind", ReplicationGuaranteeKind_name, ReplicationGuaranteeKind_value) proto.RegisterEnum("FilesystemVersion_VersionType", FilesystemVersion_VersionType_name, FilesystemVersion_VersionType_value) } @@ -1338,61 +1470,71 @@ var _Replication_serviceDesc = grpc.ServiceDesc{ Metadata: "pdu.proto", } -func init() { proto.RegisterFile("pdu.proto", fileDescriptor_pdu_483c6918b7b3d747) } +func init() { proto.RegisterFile("pdu.proto", fileDescriptor_pdu_616c27178643eca4) } -var fileDescriptor_pdu_483c6918b7b3d747 = []byte{ - // 833 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x56, 0x5f, 0x6f, 0xe3, 0x44, - 0x10, 0xaf, 0x13, 0xa7, 0x75, 0x26, 0x3d, 0x2e, 0x9d, 0x96, 0x93, 0xb1, 0xe0, 0x54, 0x2d, 0x08, - 0xe5, 0x2a, 0x61, 0xa1, 0xf2, 0x47, 0x42, 0x48, 0x27, 0xd1, 0xb4, 0xbd, 0x3b, 0x01, 0x47, 0xb4, - 0x35, 0x27, 0x74, 0x6f, 0x26, 0x19, 0xb5, 0x56, 0x1d, 0xaf, 0xbb, 0xe3, 0xa0, 0x0b, 0xe2, 0x89, - 0x47, 0xbe, 0x1e, 0x7c, 0x10, 0x3e, 0x02, 0xf2, 0xc6, 0x4e, 0x9c, 0xd8, 0x41, 0x79, 0xca, 0xce, - 0x6f, 0x66, 0x77, 0x67, 0x7f, 0xf3, 0x9b, 0x71, 0xa0, 0x9b, 0x4e, 0x66, 0x7e, 0xaa, 0x55, 0xa6, - 0xc4, 0x31, 0x1c, 0xfd, 0x10, 0x71, 0x76, 0x1d, 0xc5, 0xc4, 0x73, 0xce, 0x68, 0x2a, 0xe9, 0x41, - 0x5c, 0xd4, 0x41, 0xc6, 0xcf, 0xa0, 0xb7, 0x02, 0xd8, 0xb5, 0x4e, 0xdb, 0x83, 0xde, 0x79, 0xcf, - 0xaf, 0x04, 0x55, 0xfd, 0xe2, 0x2f, 0x0b, 0x60, 0x65, 0x23, 0x82, 0x3d, 0x0a, 0xb3, 0x3b, 0xd7, - 0x3a, 0xb5, 0x06, 0x5d, 0x69, 0xd6, 0x78, 0x0a, 0x3d, 0x49, 0x3c, 0x9b, 0x52, 0xa0, 0xee, 0x29, - 0x71, 0x5b, 0xc6, 0x55, 0x85, 0xf0, 0x13, 0x78, 0xf4, 0x8a, 0x47, 0x71, 0x38, 0xa6, 0x3b, 0x15, - 0x4f, 0x48, 0xbb, 0xed, 0x53, 0x6b, 0xe0, 0xc8, 0x75, 0x30, 0x3f, 0xe7, 0x15, 0x5f, 0x25, 0x63, - 0x3d, 0x4f, 0x33, 0x9a, 0xb8, 0xb6, 0x89, 0xa9, 0x42, 0xe2, 0x5b, 0xf8, 0x60, 0xfd, 0x41, 0x6f, - 0x48, 0x73, 0xa4, 0x12, 0x96, 0xf4, 0x80, 0x4f, 0xab, 0x89, 0x16, 0x09, 0x56, 0x10, 0xf1, 0xfd, - 0xf6, 0xcd, 0x8c, 0x3e, 0x38, 0xa5, 0x59, 0x50, 0x82, 0x7e, 0x2d, 0x52, 0x2e, 0x63, 0xc4, 0x3f, - 0x16, 0x1c, 0xd5, 0xfc, 0x78, 0x0e, 0x76, 0x30, 0x4f, 0xc9, 0x5c, 0xfe, 0xde, 0xf9, 0xd3, 0xfa, - 0x09, 0x7e, 0xf1, 0x9b, 0x47, 0x49, 0x13, 0x9b, 0x33, 0xfa, 0x3a, 0x9c, 0x52, 0x41, 0x9b, 0x59, - 0xe7, 0xd8, 0x8b, 0x59, 0x34, 0x31, 0x34, 0xd9, 0xd2, 0xac, 0xf1, 0x43, 0xe8, 0x0e, 0x35, 0x85, - 0x19, 0x05, 0xbf, 0xbc, 0x30, 0xdc, 0xd8, 0x72, 0x05, 0xa0, 0x07, 0x8e, 0x31, 0x22, 0x95, 0xb8, - 0x1d, 0x73, 0xd2, 0xd2, 0x16, 0xcf, 0xa0, 0x57, 0xb9, 0x16, 0x0f, 0xc1, 0xb9, 0x49, 0xc2, 0x94, - 0xef, 0x54, 0xd6, 0xdf, 0xcb, 0xad, 0x0b, 0xa5, 0xee, 0xa7, 0xa1, 0xbe, 0xef, 0x5b, 0xe2, 0x6f, - 0x0b, 0x0e, 0x6e, 0x28, 0x99, 0xec, 0xc0, 0x27, 0x7e, 0x0a, 0xf6, 0xb5, 0x56, 0x53, 0x93, 0x78, - 0x33, 0x5d, 0xc6, 0x8f, 0x02, 0x5a, 0x81, 0x32, 0x4f, 0x69, 0x8e, 0x6a, 0x05, 0x6a, 0x53, 0x42, - 0x76, 0x5d, 0x42, 0x02, 0xba, 0x2b, 0x69, 0x74, 0x0c, 0xbf, 0xb6, 0x1f, 0xe8, 0x48, 0xae, 0x60, - 0x7c, 0x02, 0xfb, 0x97, 0x7a, 0x2e, 0x67, 0x89, 0xbb, 0x6f, 0xb4, 0x53, 0x58, 0xe2, 0x4b, 0x70, - 0x46, 0x5a, 0xa5, 0xa4, 0xb3, 0xf9, 0x92, 0x6e, 0xab, 0x42, 0xf7, 0x09, 0x74, 0xde, 0x84, 0xf1, - 0xac, 0xac, 0xc1, 0xc2, 0x10, 0x7f, 0x2e, 0xb9, 0x60, 0x1c, 0xc0, 0xe3, 0x9f, 0x99, 0x26, 0x9b, - 0x32, 0x77, 0xe4, 0x26, 0x8c, 0x02, 0x0e, 0xaf, 0xde, 0xa5, 0x34, 0xce, 0x68, 0x72, 0x13, 0xfd, - 0x4e, 0xe6, 0xdd, 0x6d, 0xb9, 0x86, 0xe1, 0x33, 0x80, 0x22, 0x9f, 0x88, 0xd8, 0xb5, 0x8d, 0xdc, - 0xba, 0x7e, 0x99, 0xa2, 0xac, 0x38, 0xc5, 0x73, 0xe8, 0xe7, 0x39, 0x0c, 0xd5, 0x34, 0x8d, 0x29, - 0x23, 0x53, 0x98, 0x33, 0xe8, 0xfd, 0xa4, 0xa3, 0xdb, 0x28, 0x09, 0x63, 0x49, 0x0f, 0x05, 0xff, - 0x8e, 0x5f, 0xd4, 0x4d, 0x56, 0x9d, 0x02, 0x6b, 0xfb, 0x59, 0xfc, 0x01, 0x20, 0x69, 0x4c, 0xd1, - 0x6f, 0xb4, 0x4b, 0x99, 0x17, 0xe5, 0x6b, 0xfd, 0x6f, 0xf9, 0xce, 0xa0, 0x3f, 0x8c, 0x29, 0xd4, - 0x55, 0x7e, 0x16, 0x2d, 0x5e, 0xc3, 0xc5, 0x61, 0xe5, 0x76, 0x16, 0xb7, 0x70, 0x7c, 0x49, 0x9c, - 0x69, 0x35, 0x2f, 0x35, 0xb9, 0x4b, 0x2f, 0xe3, 0xe7, 0xd0, 0x5d, 0xc6, 0xbb, 0xad, 0xad, 0xfd, - 0xba, 0x0a, 0x12, 0x6f, 0x01, 0x37, 0x2e, 0x2a, 0xda, 0xbe, 0x34, 0xcd, 0x2d, 0x5b, 0xda, 0xbe, - 0x8c, 0xc9, 0x95, 0x72, 0xa5, 0xb5, 0xd2, 0xa5, 0x52, 0x8c, 0x21, 0x2e, 0x9b, 0x1e, 0x91, 0x4f, - 0xda, 0x83, 0xfc, 0xe1, 0x71, 0x56, 0x8e, 0x94, 0x63, 0xbf, 0x9e, 0x82, 0x2c, 0x63, 0xc4, 0xd7, - 0x70, 0x22, 0x29, 0x8d, 0xa3, 0xb1, 0xe9, 0xda, 0xe1, 0x4c, 0xb3, 0xd2, 0xbb, 0xcc, 0xb5, 0xa0, - 0x71, 0x1f, 0xe3, 0x49, 0x31, 0x44, 0xf2, 0x1d, 0xf6, 0xcb, 0xbd, 0xe5, 0x18, 0x71, 0x5e, 0xab, - 0x8c, 0xde, 0x45, 0x9c, 0x2d, 0x24, 0xfc, 0x72, 0x4f, 0x2e, 0x91, 0x0b, 0x07, 0xf6, 0x17, 0xe9, - 0x88, 0x8f, 0xe1, 0x60, 0x14, 0x25, 0xb7, 0x79, 0x02, 0x2e, 0x1c, 0xfc, 0x48, 0xcc, 0xe1, 0x6d, - 0xd9, 0x35, 0xa5, 0x29, 0x3e, 0x2a, 0x83, 0x38, 0xef, 0xab, 0xab, 0xf1, 0x9d, 0x2a, 0xfb, 0x2a, - 0x5f, 0x9f, 0x0d, 0xa0, 0x1d, 0xe8, 0x28, 0x1f, 0x31, 0x97, 0x2a, 0xc9, 0x86, 0xa1, 0xa6, 0xfe, - 0x1e, 0x76, 0xa1, 0x73, 0x1d, 0xc6, 0x4c, 0x7d, 0x0b, 0x1d, 0xb0, 0x03, 0x3d, 0xa3, 0x7e, 0xeb, - 0xfc, 0xdf, 0x56, 0x3e, 0x00, 0x96, 0x8f, 0x40, 0x0f, 0xec, 0xfc, 0x60, 0x74, 0xfc, 0x22, 0x09, - 0xaf, 0x5c, 0x31, 0x7e, 0x03, 0x8f, 0xd7, 0xe7, 0x38, 0x23, 0xfa, 0xb5, 0x8f, 0x9f, 0x57, 0xc7, - 0x18, 0x47, 0xf0, 0xa4, 0xf9, 0x13, 0x80, 0x9e, 0xbf, 0xf5, 0xc3, 0xe2, 0x6d, 0xf7, 0x31, 0x3e, - 0x87, 0xfe, 0x66, 0xe9, 0xf1, 0xc4, 0x6f, 0x90, 0xb4, 0xd7, 0x84, 0x32, 0x7e, 0x07, 0x47, 0xb5, - 0xe2, 0xe1, 0xfb, 0x7e, 0x93, 0x10, 0xbc, 0x46, 0x98, 0xf1, 0x2b, 0x78, 0xb4, 0xd6, 0xe2, 0x78, - 0xe4, 0x6f, 0x8e, 0x0c, 0xaf, 0x06, 0xf1, 0x45, 0xe7, 0x6d, 0x3b, 0x9d, 0xcc, 0x7e, 0xdd, 0x37, - 0xff, 0x1f, 0xbe, 0xf8, 0x2f, 0x00, 0x00, 0xff, 0xff, 0x27, 0x95, 0xc1, 0x78, 0x4c, 0x08, 0x00, - 0x00, +var fileDescriptor_pdu_616c27178643eca4 = []byte{ + // 995 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x56, 0xcf, 0x6e, 0xdb, 0xc6, + 0x13, 0x36, 0x25, 0xda, 0xa2, 0x46, 0xce, 0x2f, 0xf4, 0xd8, 0x09, 0x68, 0xfd, 0xd2, 0xd4, 0xd8, + 0x14, 0x85, 0x63, 0xa0, 0x44, 0xe1, 0xb4, 0x05, 0x8a, 0x14, 0x41, 0xeb, 0xbf, 0x31, 0xd2, 0xba, + 0xea, 0x5a, 0x0d, 0x8a, 0xdc, 0x18, 0x69, 0x2a, 0x2f, 0x4c, 0x71, 0xe9, 0x5d, 0x2a, 0x88, 0x7a, + 0xec, 0xa1, 0x87, 0x5e, 0x7a, 0xea, 0xeb, 0xf4, 0x29, 0xfa, 0x20, 0x7d, 0x84, 0x82, 0x6b, 0x92, + 0xa2, 0x44, 0xca, 0x70, 0x4f, 0xda, 0xf9, 0xe6, 0xdb, 0xd9, 0xd9, 0xd1, 0x37, 0xb3, 0x84, 0x76, + 0x3c, 0x9c, 0xf8, 0xb1, 0x92, 0x89, 0x64, 0x9b, 0xb0, 0xf1, 0xad, 0xd0, 0xc9, 0x89, 0x08, 0x49, + 0x4f, 0x75, 0x42, 0x63, 0x4e, 0xd7, 0xec, 0xa0, 0x0a, 0x6a, 0xfc, 0x04, 0x3a, 0x33, 0x40, 0x7b, + 0xd6, 0x4e, 0x73, 0xb7, 0xb3, 0xdf, 0xf1, 0x4b, 0xa4, 0xb2, 0x9f, 0xfd, 0x6e, 0x01, 0xcc, 0x6c, + 0x44, 0xb0, 0x7b, 0x41, 0x72, 0xe9, 0x59, 0x3b, 0xd6, 0x6e, 0x9b, 0x9b, 0x35, 0xee, 0x40, 0x87, + 0x93, 0x9e, 0x8c, 0xa9, 0x2f, 0xaf, 0x28, 0xf2, 0x1a, 0xc6, 0x55, 0x86, 0xf0, 0x23, 0xb8, 0x77, + 0xa6, 0x7b, 0x61, 0x30, 0xa0, 0x4b, 0x19, 0x0e, 0x49, 0x79, 0xcd, 0x1d, 0x6b, 0xd7, 0xe1, 0xf3, + 0x60, 0x1a, 0xe7, 0x4c, 0x1f, 0x47, 0x03, 0x35, 0x8d, 0x13, 0x1a, 0x7a, 0xb6, 0xe1, 0x94, 0x21, + 0xf6, 0x1c, 0xb6, 0xe7, 0x2f, 0xf4, 0x9a, 0x94, 0x16, 0x32, 0xd2, 0x9c, 0xae, 0xf1, 0x71, 0x39, + 0xd1, 0x2c, 0xc1, 0x12, 0xc2, 0x5e, 0x2d, 0xdf, 0xac, 0xd1, 0x07, 0x27, 0x37, 0xb3, 0x92, 0xa0, + 0x5f, 0x61, 0xf2, 0x82, 0xc3, 0xfe, 0xb6, 0x60, 0xa3, 0xe2, 0xc7, 0x7d, 0xb0, 0xfb, 0xd3, 0x98, + 0xcc, 0xe1, 0xff, 0xdb, 0x7f, 0x5c, 0x8d, 0xe0, 0x67, 0xbf, 0x29, 0x8b, 0x1b, 0x6e, 0x5a, 0xd1, + 0xf3, 0x60, 0x4c, 0x59, 0xd9, 0xcc, 0x3a, 0xc5, 0x4e, 0x27, 0x62, 0x68, 0xca, 0x64, 0x73, 0xb3, + 0xc6, 0x47, 0xd0, 0x3e, 0x54, 0x14, 0x24, 0xd4, 0xff, 0xe9, 0xd4, 0xd4, 0xc6, 0xe6, 0x33, 0x00, + 0xbb, 0xe0, 0x18, 0x43, 0xc8, 0xc8, 0x5b, 0x35, 0x91, 0x0a, 0x9b, 0x3d, 0x85, 0x4e, 0xe9, 0x58, + 0x5c, 0x07, 0xe7, 0x22, 0x0a, 0x62, 0x7d, 0x29, 0x13, 0x77, 0x25, 0xb5, 0x0e, 0xa4, 0xbc, 0x1a, + 0x07, 0xea, 0xca, 0xb5, 0xd8, 0x9f, 0x0d, 0x68, 0x5d, 0x50, 0x34, 0xbc, 0x43, 0x3d, 0xf1, 0x63, + 0xb0, 0x4f, 0x94, 0x1c, 0x9b, 0xc4, 0xeb, 0xcb, 0x65, 0xfc, 0xc8, 0xa0, 0xd1, 0x97, 0xe6, 0x2a, + 0xf5, 0xac, 0x46, 0x5f, 0x2e, 0x4a, 0xc8, 0xae, 0x4a, 0x88, 0x41, 0x7b, 0x26, 0x8d, 0x55, 0x53, + 0x5f, 0xdb, 0xef, 0x2b, 0xc1, 0x67, 0x30, 0x3e, 0x84, 0xb5, 0x23, 0x35, 0xe5, 0x93, 0xc8, 0x5b, + 0x33, 0xda, 0xc9, 0x2c, 0xfc, 0x1a, 0x36, 0x38, 0xc5, 0xa1, 0x18, 0x98, 0x7a, 0x1c, 0xca, 0xe8, + 0x67, 0x31, 0xf2, 0x5a, 0x59, 0x42, 0x15, 0x0f, 0xaf, 0x92, 0xd9, 0x0f, 0x35, 0x11, 0xf0, 0x2b, + 0x80, 0xb4, 0xf9, 0x68, 0x60, 0xaa, 0x6e, 0x99, 0x78, 0x8f, 0xaa, 0xf1, 0x7a, 0x05, 0x87, 0x97, + 0xf8, 0xec, 0x0f, 0x0b, 0xfe, 0x7f, 0x0b, 0x17, 0x9f, 0x41, 0xeb, 0x2c, 0x12, 0x89, 0x08, 0xc2, + 0x4c, 0x4e, 0xdb, 0xe5, 0xd0, 0xa7, 0x93, 0x40, 0x05, 0x51, 0x42, 0xf4, 0x4a, 0x44, 0x43, 0x9e, + 0x33, 0xf1, 0x39, 0x74, 0xce, 0xa2, 0x81, 0xa2, 0x31, 0x45, 0x49, 0x10, 0x9a, 0xbf, 0xe6, 0xd6, + 0x8d, 0x65, 0x36, 0xfb, 0x0c, 0x9c, 0x9e, 0x92, 0x31, 0xa9, 0x64, 0x5a, 0xa8, 0xd2, 0x2a, 0xa9, + 0x72, 0x0b, 0x56, 0x5f, 0x07, 0xe1, 0x24, 0x97, 0xea, 0x8d, 0xc1, 0x7e, 0xb5, 0x72, 0xc9, 0x68, + 0xdc, 0x85, 0xfb, 0x3f, 0x6a, 0x1a, 0x2e, 0x4e, 0x03, 0x87, 0x2f, 0xc2, 0xc8, 0x60, 0xfd, 0xf8, + 0x7d, 0x4c, 0x83, 0x84, 0x86, 0x17, 0xe2, 0x17, 0x32, 0xf2, 0x68, 0xf2, 0x39, 0x0c, 0x9f, 0x02, + 0x64, 0xf9, 0x08, 0xd2, 0x9e, 0x6d, 0xba, 0xb2, 0xed, 0xe7, 0x29, 0xf2, 0x92, 0x93, 0xbd, 0x00, + 0x37, 0xcd, 0xe1, 0x50, 0x8e, 0xe3, 0x90, 0x12, 0x32, 0xfa, 0xdd, 0x83, 0xce, 0xf7, 0x4a, 0x8c, + 0x44, 0x14, 0x84, 0x9c, 0xae, 0x33, 0x99, 0x3a, 0x7e, 0x26, 0x6f, 0x5e, 0x76, 0x32, 0xac, 0xec, + 0xd7, 0xec, 0x2f, 0x0b, 0x80, 0xd3, 0x80, 0xc4, 0x3b, 0xba, 0x4b, 0x3b, 0xdc, 0xc8, 0xbc, 0x71, + 0xab, 0xcc, 0xf7, 0xc0, 0x3d, 0x0c, 0x29, 0x50, 0xe5, 0x02, 0xdd, 0x8c, 0xc2, 0x0a, 0x5e, 0x2f, + 0x5a, 0xfb, 0xbf, 0x88, 0x76, 0xbd, 0x94, 0xbf, 0x66, 0x23, 0xd8, 0x3c, 0x22, 0x9d, 0x28, 0x39, + 0xcd, 0xbb, 0xff, 0x2e, 0x53, 0x13, 0x3f, 0x85, 0x76, 0xc1, 0xf7, 0x1a, 0x4b, 0x27, 0xe3, 0x8c, + 0xc4, 0xde, 0x00, 0x2e, 0x1c, 0x94, 0x0d, 0xd8, 0xdc, 0xcc, 0x5a, 0xa5, 0x76, 0xc0, 0xe6, 0x9c, + 0x54, 0x6c, 0xc7, 0x4a, 0x49, 0x95, 0x8b, 0xcd, 0x18, 0xec, 0xa8, 0xee, 0x12, 0xe9, 0x9b, 0xd6, + 0x4a, 0x4b, 0x17, 0x26, 0xf9, 0xf0, 0xde, 0xf4, 0xab, 0x29, 0xf0, 0x9c, 0xc3, 0xbe, 0x80, 0xad, + 0x72, 0xb5, 0x26, 0x4a, 0x4b, 0x75, 0x97, 0x17, 0xa4, 0x5f, 0xbb, 0x4f, 0xe3, 0x56, 0x36, 0xae, + 0xd3, 0x1d, 0xf6, 0xcb, 0x95, 0x62, 0x60, 0x3b, 0xe7, 0x32, 0xa1, 0xf7, 0x42, 0x27, 0x37, 0x5d, + 0xf0, 0x72, 0x85, 0x17, 0xc8, 0x81, 0x03, 0x6b, 0x37, 0xe9, 0xb0, 0x27, 0xd0, 0xea, 0x89, 0x68, + 0x94, 0x26, 0xe0, 0x41, 0xeb, 0x3b, 0xd2, 0x3a, 0x18, 0xe5, 0x8d, 0x97, 0x9b, 0xec, 0x83, 0x9c, + 0xa4, 0xd3, 0xd6, 0x3c, 0x1e, 0x5c, 0xca, 0xbc, 0x35, 0xd3, 0xf5, 0xde, 0x2e, 0x34, 0xfb, 0x4a, + 0xa4, 0xc3, 0xfc, 0x48, 0x46, 0xc9, 0x61, 0xa0, 0xc8, 0x5d, 0xc1, 0x36, 0xac, 0x9e, 0x04, 0xa1, + 0x26, 0xd7, 0x42, 0x07, 0xec, 0xbe, 0x9a, 0x90, 0xdb, 0xd8, 0xfb, 0xcd, 0x02, 0x6f, 0xd9, 0x38, + 0xc0, 0x2d, 0x70, 0x0b, 0xe0, 0x2c, 0x7a, 0x17, 0x84, 0x62, 0xe8, 0xae, 0xe0, 0x36, 0x3c, 0x28, + 0x50, 0xa3, 0xd0, 0xe0, 0xad, 0x08, 0x45, 0x32, 0x75, 0x2d, 0x7c, 0x02, 0x1f, 0x96, 0x36, 0x14, + 0xa3, 0xa4, 0x74, 0x80, 0xdb, 0x98, 0x8b, 0x7a, 0x2e, 0x93, 0x4b, 0x11, 0x8d, 0xdc, 0xe6, 0xfe, + 0x3f, 0x8d, 0x74, 0xe6, 0x17, 0x3c, 0xec, 0x82, 0x9d, 0xde, 0x10, 0x1d, 0x3f, 0xab, 0x46, 0x37, + 0x5f, 0x69, 0xfc, 0x12, 0xee, 0xcf, 0x3f, 0xdd, 0x1a, 0xd1, 0xaf, 0x7c, 0xef, 0x74, 0xab, 0x98, + 0xc6, 0x1e, 0x3c, 0xac, 0x7f, 0xf5, 0xb1, 0xeb, 0x2f, 0xfd, 0x96, 0xe8, 0x2e, 0xf7, 0x69, 0x7c, + 0x01, 0xee, 0xa2, 0x06, 0x71, 0xcb, 0xaf, 0xe9, 0xad, 0x6e, 0x1d, 0xaa, 0xf1, 0x9b, 0xf9, 0xc6, + 0x36, 0x2a, 0xc2, 0x07, 0x7e, 0x9d, 0x22, 0xbb, 0xb5, 0xb0, 0xc6, 0xcf, 0xe1, 0xde, 0xdc, 0xb8, + 0xc2, 0x0d, 0x7f, 0x71, 0xfc, 0x75, 0x2b, 0x90, 0x3e, 0x58, 0x7d, 0xd3, 0x8c, 0x87, 0x93, 0xb7, + 0x6b, 0xe6, 0x93, 0xf1, 0xd9, 0xbf, 0x01, 0x00, 0x00, 0xff, 0xff, 0x7e, 0xa4, 0xfb, 0xca, 0x3f, + 0x0a, 0x00, 0x00, } diff --git a/replication/logic/pdu/pdu.proto b/replication/logic/pdu/pdu.proto index 983504e..b1733ed 100644 --- a/replication/logic/pdu/pdu.proto +++ b/replication/logic/pdu/pdu.proto @@ -61,6 +61,25 @@ message SendReq { Tri Encrypted = 5; bool DryRun = 6; + + ReplicationConfig ReplicationConfig = 7; +} + +message ReplicationConfig { + ReplicationConfigProtection protection = 1; +} + + +message ReplicationConfigProtection { + ReplicationGuaranteeKind Initial = 1; + ReplicationGuaranteeKind Incremental = 2; +} + +enum ReplicationGuaranteeKind { + GuaranteeInvalid = 0; + GuaranteeResumability = 1; + GuaranteeIncrementalReplication = 2; + GuaranteeNothing = 3; } message Property { @@ -93,6 +112,8 @@ message ReceiveReq { // If true, the receiver should clear the resume token before performing the // zfs recv of the stream in the request bool ClearResumeToken = 3; + + ReplicationConfig ReplicationConfig = 4; } message ReceiveRes {} diff --git a/replication/logic/pdu/pdu_extras.go b/replication/logic/pdu/pdu_extras.go index 2aba2fe..8995397 100644 --- a/replication/logic/pdu/pdu_extras.go +++ b/replication/logic/pdu/pdu_extras.go @@ -83,3 +83,10 @@ func (v *FilesystemVersion) ZFSFilesystemVersion() (*zfs.FilesystemVersion, erro Creation: ct, }, nil } + +func ReplicationConfigProtectionWithKind(both ReplicationGuaranteeKind) *ReplicationConfigProtection { + return &ReplicationConfigProtection{ + Initial: both, + Incremental: both, + } +} diff --git a/replication/logic/replication_logic.go b/replication/logic/replication_logic.go index 4a0b4c1..bb4bbde 100644 --- a/replication/logic/replication_logic.go +++ b/replication/logic/replication_logic.go @@ -52,10 +52,6 @@ type Receiver interface { Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.ReadCloser) (*pdu.ReceiveRes, error) } -type PlannerPolicy struct { - EncryptedSend tri // all sends must be encrypted (send -w, and encryption!=off) -} - type Planner struct { sender Sender receiver Receiver @@ -561,12 +557,13 @@ func (s *Step) updateSizeEstimate(ctx context.Context) error { func (s *Step) buildSendRequest(dryRun bool) (sr *pdu.SendReq) { fs := s.parent.Path sr = &pdu.SendReq{ - Filesystem: fs, - From: s.from, // may be nil - To: s.to, - Encrypted: s.encrypt.ToPDU(), - ResumeToken: s.resumeToken, - DryRun: dryRun, + Filesystem: fs, + From: s.from, // may be nil + To: s.to, + Encrypted: s.encrypt.ToPDU(), + ResumeToken: s.resumeToken, + DryRun: dryRun, + ReplicationConfig: &s.parent.policy.ReplicationConfig, } return sr } @@ -603,9 +600,10 @@ func (s *Step) doReplication(ctx context.Context) error { }() rr := &pdu.ReceiveReq{ - Filesystem: fs, - To: sr.GetTo(), - ClearResumeToken: !sres.UsedResumeToken, + Filesystem: fs, + To: sr.GetTo(), + ClearResumeToken: !sres.UsedResumeToken, + ReplicationConfig: &s.parent.policy.ReplicationConfig, } log.Debug("initiate receive request") _, err = s.receiver.Receive(ctx, rr, byteCountingStream) diff --git a/replication/logic/replication_logic_policy.go b/replication/logic/replication_logic_policy.go new file mode 100644 index 0000000..9024128 --- /dev/null +++ b/replication/logic/replication_logic_policy.go @@ -0,0 +1,42 @@ +package logic + +import ( + "github.com/pkg/errors" + "github.com/zrepl/zrepl/config" + "github.com/zrepl/zrepl/replication/logic/pdu" +) + +type PlannerPolicy struct { + EncryptedSend tri // all sends must be encrypted (send -w, and encryption!=off) + ReplicationConfig pdu.ReplicationConfig +} + +func ReplicationConfigFromConfig(in *config.Replication) (*pdu.ReplicationConfig, error) { + initial, err := pduReplicationGuaranteeKindFromConfig(in.Protection.Initial) + if err != nil { + return nil, errors.Wrap(err, "field 'initial'") + } + incremental, err := pduReplicationGuaranteeKindFromConfig(in.Protection.Incremental) + if err != nil { + return nil, errors.Wrap(err, "field 'incremental'") + } + return &pdu.ReplicationConfig{ + Protection: &pdu.ReplicationConfigProtection{ + Initial: initial, + Incremental: incremental, + }, + }, nil +} + +func pduReplicationGuaranteeKindFromConfig(in string) (k pdu.ReplicationGuaranteeKind, _ error) { + switch in { + case "guarantee_nothing": + return pdu.ReplicationGuaranteeKind_GuaranteeNothing, nil + case "guarantee_incremental": + return pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication, nil + case "guarantee_resumability": + return pdu.ReplicationGuaranteeKind_GuaranteeResumability, nil + default: + return k, errors.Errorf("%q is not in guarantee_{nothing,incremental,resumability}", in) + } +} diff --git a/rpc/versionhandshake/versionhandshake.go b/rpc/versionhandshake/versionhandshake.go index 222881d..749535a 100644 --- a/rpc/versionhandshake/versionhandshake.go +++ b/rpc/versionhandshake/versionhandshake.go @@ -152,7 +152,7 @@ func (m *HandshakeMessage) DecodeReader(r io.Reader, maxLen int) error { func DoHandshakeCurrentVersion(conn net.Conn, deadline time.Time) *HandshakeError { // current protocol version is hardcoded here - return DoHandshakeVersion(conn, deadline, 4) + return DoHandshakeVersion(conn, deadline, 5) } const HandshakeMessageMaxLen = 16 * 4096 diff --git a/zfs/zfs.go b/zfs/zfs.go index 1b99a0f..46e0359 100644 --- a/zfs/zfs.go +++ b/zfs/zfs.go @@ -1593,9 +1593,10 @@ var ErrBookmarkCloningNotSupported = fmt.Errorf("bookmark cloning feature is not // idempotently create bookmark of the given version v // -// v must be validated by the caller +// if `v` is a bookmark, returns ErrBookmarkCloningNotSupported +// unless a bookmark with the name `bookmark` exists and has the same idenitty (zfs.FilesystemVersionEqualIdentity) // -// does not destroy an existing bookmark, returns +// v must be validated by the caller // func ZFSBookmark(ctx context.Context, fs string, v FilesystemVersion, bookmark string) (bm FilesystemVersion, err error) { @@ -1612,7 +1613,21 @@ func ZFSBookmark(ctx context.Context, fs string, v FilesystemVersion, bookmark s promTimer := prometheus.NewTimer(prom.ZFSBookmarkDuration.WithLabelValues(fs)) defer promTimer.ObserveDuration() - if !v.IsSnapshot() { + bookmarkname := fmt.Sprintf("%s#%s", fs, bookmark) + if err := EntityNamecheck(bookmarkname, EntityTypeBookmark); err != nil { + return bm, err + } + + if v.IsBookmark() { + existingBm, err := ZFSGetFilesystemVersion(ctx, bookmarkname) + if _, ok := err.(*DatasetDoesNotExist); ok { + return bm, ErrBookmarkCloningNotSupported + } else if err != nil { + return bm, errors.Wrap(err, "bookmark: idempotency check for bookmark cloning") + } + if FilesystemVersionEqualIdentity(bm, existingBm) { + return existingBm, nil + } return bm, ErrBookmarkCloningNotSupported // TODO This is work in progress: https://github.com/zfsonlinux/zfs/pull/9571 } @@ -1620,12 +1635,6 @@ func ZFSBookmark(ctx context.Context, fs string, v FilesystemVersion, bookmark s if err := EntityNamecheck(snapname, EntityTypeSnapshot); err != nil { return bm, err } - bookmarkname := fmt.Sprintf("%s#%s", fs, bookmark) - if err := EntityNamecheck(bookmarkname, EntityTypeBookmark); err != nil { - return bm, err - } - - debug("bookmark: %q %q", snapname, bookmarkname) cmd := zfscmd.CommandContext(ctx, ZFS_BINARY, "bookmark", snapname, bookmarkname) stdio, err := cmd.CombinedOutput() @@ -1637,7 +1646,7 @@ func ZFSBookmark(ctx context.Context, fs string, v FilesystemVersion, bookmark s // check if this was idempotent bookGuid, err := ZFSGetGUID(ctx, fs, "#"+bookmark) if err != nil { - return bm, errors.Wrap(err, "bookmark idempotency check") // guid error expressive enough + return bm, errors.Wrap(err, "bookmark: idempotency check for bookmark creation") // guid error expressive enough } if v.Guid == bookGuid {