From e49d52de4adecebf5f273adb6b7507ff84f10a8c Mon Sep 17 00:00:00 2001 From: Felix Matouschek Date: Sun, 14 Mar 2021 17:45:52 +0100 Subject: [PATCH 1/2] Use bookmarks created after the latest snapshot This commit changes syncoid's behavior so it is always looking for a matching snapshot and a matching bookmark. If the bookmark was created after the snapshot it is used instead. This allows replication when the latest snapshot replicated was deleted on the source, a common snapshot was found but rollback on the target is not allowed. The matching bookmark is used instead for replication. This fixes https://github.com/jimsalterjrs/sanoid/issues/602 Signed-off-by: Felix Matouschek --- syncoid | 49 ++++++++++----- .../run.sh | 62 +++++++++++++++++++ 2 files changed, 95 insertions(+), 16 deletions(-) create mode 100755 tests/syncoid/015_use_bookmarks_created_after_latest_snapshot/run.sh diff --git a/syncoid b/syncoid index 34e5a12..02ec2e1 100755 --- a/syncoid +++ b/syncoid @@ -440,6 +440,7 @@ sub syncdataset { my $newsyncsnap; my $matchingsnap; + my $usebookmark = 0; # skip snapshot checking/creation in case of resumed receive if (!defined($receivetoken)) { @@ -606,24 +607,26 @@ sub syncdataset { my $targetsize = getzfsvalue($targethost,$targetfs,$targetisroot,'-p used'); my %bookmark = (); + my $bookmarksnap = 0; + + # find most recent matching bookmark + my %bookmarks = getbookmarks($sourcehost,$sourcefs,$sourceisroot); + + # check for matching guid of source bookmark and target snapshot (newest first) + foreach my $snap ( sort { sortsnapshots($snaps{'target'}, $b, $a) } keys %{ $snaps{'target'} }) { + my $guid = $snaps{'target'}{$snap}{'guid'}; + + if (defined $bookmarks{$guid}) { + # found a match + %bookmark = %{ $bookmarks{$guid} }; + $bookmarksnap = $snap; + last; + } + } $matchingsnap = getmatchingsnapshot($sourcefs, $targetfs, \%snaps); if (! $matchingsnap) { # no matching snapshots, check for bookmarks as fallback - my %bookmarks = getbookmarks($sourcehost,$sourcefs,$sourceisroot); - - # check for matching guid of source bookmark and target snapshot (newest first) - foreach my $snap ( sort { sortsnapshots($snaps{'target'}, $b, $a) } keys %{ $snaps{'target'} }) { - my $guid = $snaps{'target'}{$snap}{'guid'}; - - if (defined $bookmarks{$guid}) { - # found a match - %bookmark = %{ $bookmarks{$guid} }; - $matchingsnap = $snap; - last; - } - } - if (! %bookmark) { # force delete is not possible for the root dataset if ($args{'force-delete'} && index($targetfs, '/') != -1) { @@ -678,6 +681,20 @@ sub syncdataset { # return false now in case more child datasets need replication. return 0; + } else { + # use bookmark as fallback + $matchingsnap = $bookmarksnap; + $usebookmark = 1; + } + } elsif (%bookmark) { + my $comparisonkey = 'creation'; + if (defined $snaps{'source'}{$matchingsnap}{'createtxg'} && defined $bookmark{'createtxg'}) { + $comparisonkey = 'createtxg'; + } + if ($bookmark{$comparisonkey} > $snaps{'source'}{$matchingsnap}{$comparisonkey}) { + writelog('DEBUG', "using bookmark $bookmark{'name'} because it was created after latest matching snapshot $matchingsnap"); + $matchingsnap = $bookmarksnap; + $usebookmark = 1; } } @@ -697,7 +714,7 @@ sub syncdataset { my $nextsnapshot = 0; - if (%bookmark) { + if ($usebookmark) { if (!defined $args{'no-stream'}) { # if intermediate snapshots are needed we need to find the next oldest snapshot, @@ -758,7 +775,7 @@ sub syncdataset { # do a normal replication if bookmarks aren't used or if previous # bookmark replication was only done to the next oldest snapshot # edge case: skip replication if bookmark replication used the latest snapshot - if ((!%bookmark || $nextsnapshot) && !($matchingsnap eq $newsyncsnap)) { + if ((!$usebookmark || $nextsnapshot) && !($matchingsnap eq $newsyncsnap)) { ($exit, $stdout) = syncincremental($sourcehost, $sourcefs, $targethost, $targetfs, $matchingsnap, $newsyncsnap, defined($args{'no-stream'})); diff --git a/tests/syncoid/015_use_bookmarks_created_after_latest_snapshot/run.sh b/tests/syncoid/015_use_bookmarks_created_after_latest_snapshot/run.sh new file mode 100755 index 0000000..5babee8 --- /dev/null +++ b/tests/syncoid/015_use_bookmarks_created_after_latest_snapshot/run.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# test using bookmark created after last snapshot + +set -x +set -e + +. ../../common/lib.sh + +POOL_IMAGE="/tmp/syncoid-test-015.zpool" +MOUNT_TARGET="/tmp/syncoid-test-015.mount" +POOL_SIZE="1000M" +POOL_NAME="syncoid-test-015" +TARGET_CHECKSUM="73d7271f58f0d79eea0dd69d5ee3f4fe3aeaa3cb8106f7fc88feded5be3ce04e -" + +truncate -s "${POOL_SIZE}" "${POOL_IMAGE}" + +zpool create -m "${MOUNT_TARGET}" -f "${POOL_NAME}" "${POOL_IMAGE}" + +function cleanUp { + zpool export "${POOL_NAME}" +} + +# export pool in any case +trap cleanUp EXIT + +zfs create "${POOL_NAME}"/a +zfs snapshot "${POOL_NAME}"/a@s0 + +# This fully replicates a to b +../../../syncoid --debug --no-sync-snap --no-rollback --create-bookmark "${POOL_NAME}"/a "${POOL_NAME}"/b + +echo "Test 1" > "${MOUNT_TARGET}"/a/file1 +zfs snapshot "${POOL_NAME}"/a@s1 + +# This incrementally replicates from a@s0 to a@s1 +../../../syncoid --debug --no-sync-snap --no-rollback --create-bookmark "${POOL_NAME}"/a "${POOL_NAME}"/b + +echo "Test 2" > "${MOUNT_TARGET}"/a/file2 +zfs snapshot "${POOL_NAME}"/a@s2 + +# Destroy latest common snap between a and b +zfs destroy "${POOL_NAME}"/a@s1 + +# This uses a#s1 as base snap although common but older snap a@s0 exists +../../../syncoid --debug --no-sync-snap --no-rollback --create-bookmark "${POOL_NAME}"/a "${POOL_NAME}"/b + +echo "Test 3" > "${MOUNT_TARGET}"/a/file3 +zfs snapshot "${POOL_NAME}"/a@s3 + +# This uses a@s2 as base snap again +../../../syncoid --debug --no-sync-snap --no-rollback --create-bookmark "${POOL_NAME}"/a "${POOL_NAME}"/b + +# verify +output=$(zfs list -t snapshot -r -H -o name "${POOL_NAME}") +checksum=$(echo "${output}" | shasum -a 256) + +if [ "${checksum}" != "${TARGET_CHECKSUM}" ]; then + exit 1 +fi + +exit 0 From 0d6b36815803c3b118bb9c8cdce676fe94324dd8 Mon Sep 17 00:00:00 2001 From: Felix Matouschek Date: Sat, 10 Jan 2026 11:03:31 +0000 Subject: [PATCH 2/2] Add --no-bookmark flag This prevents the use bookmarks when trying to find the latest common snapshot. This forces a rollback when the latest snapshot on the source was deleted but a common and older snapshot was found. Signed-off-by: Felix Matouschek --- syncoid | 25 ++++---- .../run.sh | 62 +++++++++++++++++++ 2 files changed, 76 insertions(+), 11 deletions(-) create mode 100755 tests/syncoid/016_require_rollback_when_not_using_bookmarks/run.sh diff --git a/syncoid b/syncoid index 02ec2e1..950886b 100755 --- a/syncoid +++ b/syncoid @@ -24,7 +24,7 @@ my %args = ('sshconfig' => '', 'sshkey' => '', 'sshport' => '', 'sshcipher' => ' GetOptions(\%args, "no-command-checks", "monitor-version", "compress=s", "dumpsnaps", "recursive|r", "sendoptions=s", "recvoptions=s", "source-bwlimit=s", "target-bwlimit=s", "sshconfig=s", "sshkey=s", "sshport=i", "sshcipher|c=s", "sshoption|o=s@", "debug", "quiet", "no-stream", "no-sync-snap", "no-resume", "exclude=s@", "skip-parent", "identifier=s", - "no-clone-handling", "no-privilege-elevation", "force-delete", "no-rollback", "create-bookmark", "use-hold", + "no-clone-handling", "no-privilege-elevation", "force-delete", "no-rollback", "create-bookmark", "no-bookmark", "use-hold", "pv-options=s" => \$pvoptions, "keep-sync-snap", "preserve-recordsize", "mbuffer-size=s" => \$mbuffer_size, "delete-target-snapshots", "insecure-direct-connection=s", "preserve-properties", "include-snaps=s@", "exclude-snaps=s@", "exclude-datasets=s@") @@ -609,18 +609,20 @@ sub syncdataset { my %bookmark = (); my $bookmarksnap = 0; - # find most recent matching bookmark - my %bookmarks = getbookmarks($sourcehost,$sourcefs,$sourceisroot); + if (! defined $args{'no-bookmark'}) { + # find most recent matching bookmark + my %bookmarks = getbookmarks($sourcehost,$sourcefs,$sourceisroot); - # check for matching guid of source bookmark and target snapshot (newest first) - foreach my $snap ( sort { sortsnapshots($snaps{'target'}, $b, $a) } keys %{ $snaps{'target'} }) { - my $guid = $snaps{'target'}{$snap}{'guid'}; + # check for matching guid of source bookmark and target snapshot (newest first) + foreach my $snap ( sort { sortsnapshots($snaps{'target'}, $b, $a) } keys %{ $snaps{'target'} }) { + my $guid = $snaps{'target'}{$snap}{'guid'}; - if (defined $bookmarks{$guid}) { - # found a match - %bookmark = %{ $bookmarks{$guid} }; - $bookmarksnap = $snap; - last; + if (defined $bookmarks{$guid}) { + # found a match + %bookmark = %{ $bookmarks{$guid} }; + $bookmarksnap = $snap; + last; + } } } @@ -2439,6 +2441,7 @@ Options: --no-sync-snap Does not create new snapshot, only transfers existing --keep-sync-snap Don't destroy created sync snapshots --create-bookmark Creates a zfs bookmark for the newest snapshot on the source after replication succeeds (only works with --no-sync-snap) + --no-bookmark Do not use bookmarks when trying to find the latest common snapshot. This forces a rollback when the latest snapshot on the source was deleted but a common and older snapshot was found. --use-hold Adds a hold to the newest snapshot on the source and target after replication succeeds and removes the hold after the next successful replication. The hold name includes the identifier if set. This allows for separate holds in case of multiple targets --preserve-recordsize Preserves the recordsize on initial sends to the target --preserve-properties Preserves locally set dataset properties similar to the zfs send -p flag but this one will also work for encrypted datasets in non raw sends diff --git a/tests/syncoid/016_require_rollback_when_not_using_bookmarks/run.sh b/tests/syncoid/016_require_rollback_when_not_using_bookmarks/run.sh new file mode 100755 index 0000000..f074231 --- /dev/null +++ b/tests/syncoid/016_require_rollback_when_not_using_bookmarks/run.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# test that a rollback is required when not using bookmarks + +set -x +set -e + +. ../../common/lib.sh + +POOL_IMAGE="/tmp/syncoid-test-016.zpool" +MOUNT_TARGET="/tmp/syncoid-test-016.mount" +POOL_SIZE="1000M" +POOL_NAME="syncoid-test-016" +TARGET_CHECKSUM="0ed2eed1488bbba5c35c2b24beee9e6e8a76f8de10aa1e3710dc737cf626635a -" + +truncate -s "${POOL_SIZE}" "${POOL_IMAGE}" + +zpool create -m "${MOUNT_TARGET}" -f "${POOL_NAME}" "${POOL_IMAGE}" + +function cleanUp { + zpool export "${POOL_NAME}" +} + +# export pool in any case +trap cleanUp EXIT + +zfs create "${POOL_NAME}"/a +zfs snapshot "${POOL_NAME}"/a@s0 + +# This fully replicates a to b +../../../syncoid --debug --no-sync-snap --no-rollback --create-bookmark "${POOL_NAME}"/a "${POOL_NAME}"/b + +echo "Test 1" > "${MOUNT_TARGET}"/a/file1 +zfs snapshot "${POOL_NAME}"/a@s1 + +# This incrementally replicates from a@s0 to a@s1 +../../../syncoid --debug --no-sync-snap --no-rollback --create-bookmark "${POOL_NAME}"/a "${POOL_NAME}"/b + +echo "Test 2" > "${MOUNT_TARGET}"/a/file2 +zfs snapshot "${POOL_NAME}"/a@s2 + +# Destroy latest common snap between a and b +zfs destroy "${POOL_NAME}"/a@s1 + +# This uses a@s0 and rolls b back to it although common and newer bookmark a#s1 exists +../../../syncoid --debug --no-sync-snap --no-bookmark --create-bookmark "${POOL_NAME}"/a "${POOL_NAME}"/b + +echo "Test 3" > "${MOUNT_TARGET}"/a/file3 +zfs snapshot "${POOL_NAME}"/a@s3 + +# This uses a@s2 as base snap again +../../../syncoid --debug --no-sync-snap --no-rollback --create-bookmark "${POOL_NAME}"/a "${POOL_NAME}"/b + +# verify +output=$(zfs list -t snapshot -r -H -o name "${POOL_NAME}") +checksum=$(echo "${output}" | shasum -a 256) + +if [ "${checksum}" != "${TARGET_CHECKSUM}" ]; then + exit 1 +fi + +exit 0