btrbk: search complete target tree for correlated subvolumes

- move matching for correlated subvolumes from get_receive_targets
  into new function _receive_target_nodes
- add lookup tables in btr_tree (RECEIVED_UUID_HASH, UUID_HASH),
  allowing for faster matching in _receive_target_nodes
- add vinfo_resolved() for mapping nodes to vinfo
- rename get_latest_common to get_best_parent (while moving some
  functionality to new function get_related)
- cleanup
pull/235/head
Axel Burri 2018-02-15 17:42:41 +01:00
parent 490f680f41
commit 6c502cbdcc
1 changed files with 141 additions and 132 deletions

251
btrbk
View File

@ -2053,9 +2053,13 @@ sub btr_tree($$)
SUBTREE => [] SUBTREE => []
); );
my %id = ( 5 => \%tree ); my %id = ( 5 => \%tree );
my %uuid_hash;
my %received_uuid_hash;
$tree{TREE_ROOT} = \%tree; $tree{TREE_ROOT} = \%tree;
$tree{ID_HASH} = \%id; $tree{ID_HASH} = \%id;
$tree{UUID_HASH} = \%uuid_hash;
$tree{RECEIVED_UUID_HASH} = \%received_uuid_hash;
my $node_list = btrfs_subvolume_list($vol); my $node_list = btrfs_subvolume_list($vol);
return undef unless(ref($node_list) eq "ARRAY"); return undef unless(ref($node_list) eq "ARRAY");
@ -2078,7 +2082,7 @@ sub btr_tree($$)
return $vol_root; return $vol_root;
} }
# fill ID_HASH and uuid_cache # fill our hashes and uuid_cache
my $gen_max = 0; my $gen_max = 0;
foreach my $node (@$node_list) foreach my $node (@$node_list)
{ {
@ -2086,6 +2090,8 @@ sub btr_tree($$)
die if exists($id{$node->{id}}); die if exists($id{$node->{id}});
$node->{SUBTREE} //= []; $node->{SUBTREE} //= [];
$id{$node->{id}} = $node; $id{$node->{id}} = $node;
$uuid_hash{$node->{uuid}} = $node;
push(@{$received_uuid_hash{$node->{received_uuid}}}, $node) if($node->{received_uuid} ne '-');
$uuid_cache{$node->{uuid}} = $node; $uuid_cache{$node->{uuid}} = $node;
$gen_max = $node->{gen} if($node->{gen} > $gen_max); $gen_max = $node->{gen} if($node->{gen} > $gen_max);
} }
@ -2139,13 +2145,15 @@ sub btr_tree_inject_node
my $subtree = $top_node->{SUBTREE} // die; my $subtree = $top_node->{SUBTREE} // die;
my $tree_root = $top_node->{TREE_ROOT}; my $tree_root = $top_node->{TREE_ROOT};
die unless($detail->{parent_uuid} && $detail->{received_uuid} && exists($detail->{readonly}));
$tree_inject_id -= 1; $tree_inject_id -= 1;
$tree_root->{GEN_MAX} += 1; $tree_root->{GEN_MAX} += 1;
my $uuid = sprintf("${fake_uuid_prefix}%012u", -($tree_inject_id)); my $uuid = sprintf("${fake_uuid_prefix}%012u", -($tree_inject_id));
my $node = { my $node = {
%$detail, # make a copy %$detail, # make a copy
TREE_ROOT => $top_node->{TREE_ROOT}, TREE_ROOT => $tree_root,
SUBTREE => [], SUBTREE => [],
TOP_LEVEL => $top_node, TOP_LEVEL => $top_node,
REL_PATH => $rel_path, REL_PATH => $rel_path,
@ -2158,6 +2166,8 @@ sub btr_tree_inject_node
push(@$subtree, $node); push(@$subtree, $node);
$uuid_cache{$uuid} = $node; $uuid_cache{$uuid} = $node;
$tree_root->{ID_HASH}->{$tree_inject_id} = $node; $tree_root->{ID_HASH}->{$tree_inject_id} = $node;
$tree_root->{UUID_HASH}->{$uuid} = $node;
push( @{$tree_root->{RECEIVED_UUID_HASH}->{$node->{received_uuid}}}, $node ) if($node->{received_uuid} ne '-');
return $node; return $node;
} }
@ -2569,6 +2579,27 @@ sub vinfo_subvol_list($;@)
} }
# returns vinfo_child if $node is in tree below $vol, or undef
sub vinfo_resolved($$)
{
my $node = shift || die;
my $vol = shift || die; # root vinfo node
my $top_id = $vol->{node}{id};
my @path;
my $nn = $node;
while(($nn->{id} != $top_id) && (!$nn->{is_root})) {
unshift(@path, $nn->{REL_PATH});
$nn = $nn->{TOP_LEVEL};
}
return undef if($nn->{is_root} && (!$vol->{node}{is_root}));
my $jpath = join('/', @path);
if($vol->{NODE_SUBDIR}) {
return undef unless($jpath =~ s/^\Q$vol->{NODE_SUBDIR}\E\///);
}
return vinfo_child($vol, $jpath);
}
sub vinfo_subvol($$) sub vinfo_subvol($$)
{ {
my $vol = shift || die; my $vol = shift || die;
@ -2719,14 +2750,11 @@ sub get_snapshot_children($$;$$)
} }
sub get_receive_targets($$;@) sub _receive_target_nodes($$)
{ {
my $droot = shift || die; my $droot = shift || die;
my $src_vol = shift || die; my $src_vol = shift || die;
my %opts = @_;
my $droot_subvols = $opts{droot_subvol_list} // vinfo_subvol_list($droot);
my @ret; my @ret;
my $unexpected_count = 0;
if($src_vol->{node}{is_root}) { if($src_vol->{node}{is_root}) {
DEBUG "Skip search for targets: source subvolume is btrfs root: $src_vol->{PRINT}"; DEBUG "Skip search for targets: source subvolume is btrfs root: $src_vol->{PRINT}";
@ -2741,85 +2769,58 @@ sub get_receive_targets($$;@)
my $uuid = $src_vol->{node}{uuid}; my $uuid = $src_vol->{node}{uuid};
my $received_uuid = $src_vol->{node}{received_uuid}; my $received_uuid = $src_vol->{node}{received_uuid};
$received_uuid = undef if($received_uuid eq '-'); $received_uuid = undef if($received_uuid eq '-');
TRACE "get_receive_targets: src_vol=\"$src_vol->{PRINT}\", droot=\"$droot->{PRINT}\""; TRACE "receive_target_nodes: src_vol=\"$src_vol->{PRINT}\", droot=\"$droot->{PRINT}\"";
foreach (@$droot_subvols) { my $tree_root = $droot->{node}{TREE_ROOT};
next unless($_->{node}{readonly}); my $received_uuid_hash = $droot->{node}{TREE_ROOT}{RECEIVED_UUID_HASH};
my $uuid_hash = $droot->{node}{TREE_ROOT}{UUID_HASH};
# match uuid/received_uuid combinations (silently ignore uuid==uuid matches) # match uuid/received_uuid combinations
my $matched = undef; my @match;
if($_->{node}{received_uuid} eq $uuid) { push(@match, @{ $received_uuid_hash->{$uuid} // [] }); # match src.uuid == target.received_uuid
$matched = 'src.uuid == target.received_uuid'; if($received_uuid) {
push(@match, $uuid_hash->{$received_uuid} ); # match src.received_uuid == target.uuid
push(@match, @{ $received_uuid_hash->{$received_uuid} }); # match src.received_uuid == target.received_uuid
} }
elsif(defined($received_uuid) && ($_->{node}{received_uuid} eq $received_uuid)) {
$matched = 'src.received_uuid == target.received_uuid';
}
elsif(defined($received_uuid) && ($_->{node}{uuid} eq $received_uuid)) {
$matched = 'src.received_uuid == target.uuid';
}
next unless($matched);
TRACE "get_receive_targets: Found receive target ($matched): $_->{SUBVOL_PATH}"; @ret = grep($_->{readonly}, @match);
push(@{$opts{seen}}, $_) if($opts{seen}); TRACE "receive_target_nodes: " . scalar(@ret) . " receive targets in \"$droot->{PRINT}/\" for: $src_vol->{PRINT}";
if($opts{exact_match} && !exists($_->{node}{BTRBK_RAW})) {
if($_->{direct_leaf} && ($_->{NAME} eq $src_vol->{NAME})) {
TRACE "get_receive_targets: exact_match: $_->{SUBVOL_PATH}";
}
else {
TRACE "get_receive_targets: skip non-exact match ($matched): $_->{PRINT}";
WARN "Receive target of \"$src_vol->{PRINT}\" exists at unexpected location: $_->{PRINT}" if($opts{warn});
next;
}
}
push(@ret, $_);
}
TRACE "get_receive_targets: " . scalar(@ret) . " receive targets in \"$droot->{PRINT}/\" for: $src_vol->{PRINT}";
return @ret; return @ret;
} }
sub get_receive_targets_fsroot($$@) # returns array of vinfo of receive targets matching btrbk name
sub get_receive_targets($$;@)
{ {
my $droot = shift // die; my $droot = shift || die;
my $src_vol = shift // die; my $src_vol = shift || die;
my %opts = @_; my %opts = @_;
my $id = $src_vol->{node}{id}; my @ret;
my $uuid = $src_vol->{node}{uuid};
my $received_uuid = $src_vol->{node}{received_uuid};
$received_uuid = undef if(defined($received_uuid) && ($received_uuid eq '-'));
my @unexpected; my @match = _receive_target_nodes($droot, $src_vol);
my @exclude; foreach (@match) {
@exclude = map { $_->{node}{id} } @{$opts{exclude}} if($opts{exclude}); my $vinfo = vinfo_resolved($_, $droot); # returns undef if not below $droot
if(exists($_->{BTRBK_RAW})) {
TRACE "get_receive_target_fsroot: uuid=$uuid, received_uuid=" . ($received_uuid // '-') . " exclude id={ " . join(', ', @exclude) . " }"; TRACE "get_receive_targets: found raw receive target: " . _fs_path($_);
}
# search in filesystem for matching received_uuid elsif($vinfo && ($vinfo->{SUBVOL_PATH} eq $src_vol->{NAME})) { # direct leaf, (SUBVOL_PATH = "", matching NAME)
foreach my $node ( TRACE "get_receive_targets: found receive target (exact_match): $vinfo->{PRINT}";
grep({ (not $_->{is_root}) && }
(($_->{received_uuid} eq $uuid) || # match src.uuid == target.received_uuid else {
(defined($received_uuid) && ($_->{received_uuid} eq $received_uuid)) || # match src.received_uuid == target.received_uuid TRACE "get_receive_targets: skip non-exact match: " . _fs_path($_);
(defined($received_uuid) && ($_->{uuid} eq $received_uuid))) # match src.received_uuid == target.uuid ${$opts{ret_unexpected}} = 1 if($opts{ret_unexpected});
} values(%{$droot->{node}{TREE_ROOT}{ID_HASH}}) ) )
{
next if(scalar grep($_ == $node->{id}, @exclude));
push @unexpected, $node;
if($opts{warn}) { if($opts{warn}) {
my $text; WARN "Receive target of \"$src_vol->{PRINT}\" exists at unexpected location: " . ($vinfo ? $vinfo->{PRINT} : _fs_path($_));
my @url = get_cached_url_by_uuid($node->{uuid});
if(scalar(@url)) {
$text = vinfo($url[0])->{PRINT};
} else {
$text = '"' . _fs_path($node) . "\" (in filesystem at \"$droot->{PRINT}\")";
} }
WARN "Receive target of \"$src_vol->{PRINT}\" exists at unexpected location: $text"; next;
} }
push(@ret, $vinfo);
} }
return @unexpected; return @ret;
} }
sub get_latest_common($$$;$) sub get_related_subvolumes($$$;$)
{ {
my $sroot = shift || die; my $sroot = shift || die;
my $svol = shift // die; my $svol = shift // die;
@ -2827,14 +2828,14 @@ sub get_latest_common($$$;$)
my $snapshot_dir = shift; # if not set, skip search for btrbk basename (set to empty string to enable at current dir) my $snapshot_dir = shift; # if not set, skip search for btrbk basename (set to empty string to enable at current dir)
my $sroot_subvol_list = vinfo_subvol_list($sroot); my $sroot_subvol_list = vinfo_subvol_list($sroot);
TRACE "get_latest_common: resolving latest common for subvolume: $svol->{PATH} (sroot=$sroot->{PRINT}, droot=$droot->{PRINT}, snapdir=\"" . ($snapshot_dir // '<undef>') . "\")"; TRACE "get_related: resolving latest common for subvolume: $svol->{PATH} (sroot=$sroot->{PRINT}, droot=$droot->{PRINT}, snapdir=\"" . ($snapshot_dir // '<undef>') . "\")";
my @candidate; my @candidate;
if($svol->{node}{readonly}) { if($svol->{node}{readonly}) {
if($svol->{node}{parent_uuid} ne '-') { if($svol->{node}{parent_uuid} ne '-') {
# add readonly parent # add readonly parent
@candidate = grep { $_->{node}{readonly} && ($_->{node}{uuid} eq $svol->{node}{parent_uuid}) } @$sroot_subvol_list; @candidate = grep { $_->{node}{readonly} && ($_->{node}{uuid} eq $svol->{node}{parent_uuid}) } @$sroot_subvol_list;
die "multiple parents for $svol->{node}{parent_uuid}" if(scalar(@candidate) > 1); die "multiple parents for $svol->{node}{parent_uuid}" if(scalar(@candidate) > 1);
TRACE "get_latest_common: subvolume has a read-only parent, add parent candidate" if(scalar(@candidate) > 0); TRACE "get_related: subvolume has a read-only parent, add parent candidate" if(scalar(@candidate) > 0);
# add snapshots with same parent_uuid (siblings) # add snapshots with same parent_uuid (siblings)
my @siblings = grep { $_->{node}{readonly} && ($_->{node}{parent_uuid} eq $svol->{node}{parent_uuid}) } @$sroot_subvol_list; my @siblings = grep { $_->{node}{readonly} && ($_->{node}{parent_uuid} eq $svol->{node}{parent_uuid}) } @$sroot_subvol_list;
@ -2842,7 +2843,7 @@ sub get_latest_common($$$;$)
my @siblings_newer = grep { $_->{node}{cgen} > $svol->{node}{cgen} } @siblings; my @siblings_newer = grep { $_->{node}{cgen} > $svol->{node}{cgen} } @siblings;
push @candidate, sort { $b->{node}{cgen} <=> $a->{node}{cgen} } @siblings_older; # older first, descending by cgen push @candidate, sort { $b->{node}{cgen} <=> $a->{node}{cgen} } @siblings_older; # older first, descending by cgen
push @candidate, sort { $a->{node}{cgen} <=> $b->{node}{cgen} } @siblings_newer; # then newer, ascending by cgen push @candidate, sort { $a->{node}{cgen} <=> $b->{node}{cgen} } @siblings_newer; # then newer, ascending by cgen
TRACE "get_latest_common: subvolume has siblings (same parent_uuid), add " . scalar(@siblings_older) . " older and " . scalar(@siblings_newer) . " newer (by cgen) candidates"; TRACE "get_related: subvolume has siblings (same parent_uuid), add " . scalar(@siblings_older) . " older and " . scalar(@siblings_newer) . " newer (by cgen) candidates";
} }
if(defined($snapshot_dir) && exists($svol->{node}{BTRBK_BASENAME})) { if(defined($snapshot_dir) && exists($svol->{node}{BTRBK_BASENAME})) {
@ -2852,19 +2853,19 @@ sub get_latest_common($$$;$)
my @naming_match_newer = grep { cmp_date($_->{node}{BTRBK_DATE}, $svol->{node}{BTRBK_DATE}) > 0 } @naming_match; my @naming_match_newer = grep { cmp_date($_->{node}{BTRBK_DATE}, $svol->{node}{BTRBK_DATE}) > 0 } @naming_match;
push @candidate, sort { cmp_date($b->{node}{BTRBK_DATE}, $a->{node}{BTRBK_DATE}) } @naming_match_older; push @candidate, sort { cmp_date($b->{node}{BTRBK_DATE}, $a->{node}{BTRBK_DATE}) } @naming_match_older;
push @candidate, sort { cmp_date($a->{node}{BTRBK_DATE}, $b->{node}{BTRBK_DATE}) } @naming_match_newer; push @candidate, sort { cmp_date($a->{node}{BTRBK_DATE}, $b->{node}{BTRBK_DATE}) } @naming_match_newer;
TRACE "get_latest_common: subvolume has btrbk naming scheme, add " . scalar(@naming_match_older) . " older and " . scalar(@naming_match_newer) . " newer (by file suffix) candidates with scheme: $sroot->{PRINT}/$snapshot_dir/$svol->{node}{BTRBK_BASENAME}.*"; TRACE "get_related: subvolume has btrbk naming scheme, add " . scalar(@naming_match_older) . " older and " . scalar(@naming_match_newer) . " newer (by file suffix) candidates with scheme: $sroot->{PRINT}/$snapshot_dir/$svol->{node}{BTRBK_BASENAME}.*";
} }
} }
else else
{ {
@candidate = sort { $b->{node}{cgen} <=> $a->{node}{cgen} } get_snapshot_children($sroot, $svol); @candidate = sort { $b->{node}{cgen} <=> $a->{node}{cgen} } get_snapshot_children($sroot, $svol);
TRACE "get_latest_common: subvolume is read-write, add " . scalar(@candidate) . " snapshot children, sorted by cgen: $svol->{PATH}"; TRACE "get_related: subvolume is read-write, add " . scalar(@candidate) . " snapshot children, sorted by cgen: $svol->{PATH}";
if(defined($snapshot_dir)) { if(defined($snapshot_dir)) {
# add subvolumes in same directory matching btrbk file name scheme (using $svol->{NAME} as basename) # add subvolumes in same directory matching btrbk file name scheme (using $svol->{NAME} as basename)
my @naming_match = grep { $_->{node}{readonly} && exists($_->{node}{BTRBK_BASENAME}) && ($_->{SUBVOL_DIR} eq $snapshot_dir) && ($_->{node}{BTRBK_BASENAME} eq $svol->{NAME}) } @$sroot_subvol_list; my @naming_match = grep { $_->{node}{readonly} && exists($_->{node}{BTRBK_BASENAME}) && ($_->{SUBVOL_DIR} eq $snapshot_dir) && ($_->{node}{BTRBK_BASENAME} eq $svol->{NAME}) } @$sroot_subvol_list;
push @candidate, sort { cmp_date($b->{node}{BTRBK_DATE}, $a->{node}{BTRBK_DATE}) } @naming_match; push @candidate, sort { cmp_date($b->{node}{BTRBK_DATE}, $a->{node}{BTRBK_DATE}) } @naming_match;
TRACE "get_latest_common: snapshot_dir is set, add " . scalar(@naming_match) . " candidates with scheme: $sroot->{PRINT}/$snapshot_dir/$svol->{NAME}.*"; TRACE "get_related: snapshot_dir is set, add " . scalar(@naming_match) . " candidates with scheme: $sroot->{PRINT}/$snapshot_dir/$svol->{NAME}.*";
} }
} }
@ -2873,15 +2874,15 @@ sub get_latest_common($$$;$)
my $search_depth = 0; my $search_depth = 0;
while($rnode && ($search_depth < 256)) { while($rnode && ($search_depth < 256)) {
last if($rnode->{parent_uuid} eq '-'); last if($rnode->{parent_uuid} eq '-');
TRACE "get_latest_common: searching parent chain (depth=$search_depth): $rnode->{uuid}"; TRACE "get_related: searching parent chain (depth=$search_depth): $rnode->{uuid}";
my @parents = grep { $_->{node}{uuid} eq $rnode->{parent_uuid} } @$sroot_subvol_list; my @parents = grep { $_->{node}{uuid} eq $rnode->{parent_uuid} } @$sroot_subvol_list;
if(scalar(@parents) == 1) { if(scalar(@parents) == 1) {
my $parent = $parents[0]; my $parent = $parents[0];
if($parent->{node}{readonly}) { if($parent->{node}{readonly}) {
TRACE "get_latest_common: found read-only parent (depth=$search_depth), add as candidate: $parent->{PRINT}"; TRACE "get_related: found read-only parent (depth=$search_depth), add as candidate: $parent->{PRINT}";
push @candidate, $parent; push @candidate, $parent;
} else { } else {
TRACE "get_latest_common: found read-write parent (depth=$search_depth), ignoring: $parent->{PRINT}"; TRACE "get_related: found read-write parent (depth=$search_depth), ignoring: $parent->{PRINT}";
} }
$rnode = $parent->{node}; $rnode = $parent->{node};
} }
@ -2894,21 +2895,31 @@ sub get_latest_common($$$;$)
$search_depth++; $search_depth++;
} }
return \@candidate;
}
# returns ( parent, first_matching_target_node )
sub get_best_parent($$$;$)
{
my $sroot = shift || die;
my $svol = shift // die;
my $droot = shift || die;
my $snapshot_dir = shift; # if not set, skip search for btrbk basename (set to empty string to enable at current dir)
my $related = get_related_subvolumes($sroot, $svol, $droot, $snapshot_dir);
# match receive targets of candidates # match receive targets of candidates
my $droot_subvol_list = vinfo_subvol_list($droot); # cache subvol list foreach my $child (@$related) {
foreach my $child (@candidate) { next if($child->{node}{id} == $svol->{node}{id}); # skip self
if($child->{node}{id} == $svol->{node}{id}) { my @receive_target_nodes = _receive_target_nodes($droot, $child);
TRACE "get_latest_common: skip self: $child->{PRINT}"; if(scalar @receive_target_nodes) {
next; DEBUG "Resolved best common parent for \"$svol->{PRINT}\": \"$child->{PRINT}\", " . join(",", map('"' . _fs_path($_) . '"',@receive_target_nodes));
} return ($child, $receive_target_nodes[0]);
my @receive_targets = get_receive_targets($droot, $child, droot_subvol_list => $droot_subvol_list);
if(scalar @receive_targets) {
DEBUG("Latest common subvolumes for: $svol->{PRINT}: src=$child->{PRINT} target=$receive_targets[0]->{PRINT}");
return ($child, $receive_targets[0]);
} }
} }
DEBUG("No common subvolumes of \"$svol->{PRINT}\" found in src=\"$sroot->{PRINT}/\", target=\"$droot->{PRINT}/\""); DEBUG("No common parents of \"$svol->{PRINT}\" found in src=\"$sroot->{PRINT}/\", target=\"$droot->{PRINT}/\"");
return (undef, undef); return undef;
} }
@ -3543,11 +3554,11 @@ sub macro_send_receive(@)
# NOTE: this is not necessarily the correct parent_uuid (on # NOTE: this is not necessarily the correct parent_uuid (on
# receive, btrfs-progs picks the uuid of the first (lowest id) # receive, btrfs-progs picks the uuid of the first (lowest id)
# matching possible parent), whereas the target_parent is the # matching possible parent), whereas the target_parent is the
# first from get_receive_targets(). # first from _receive_target_nodes().
# #
# NOTE: the parent_uuid of an injected receive target is not used # NOTE: the parent_uuid of an injected receive target is not used
# anywhere in btrbk at the time of writing # anywhere in btrbk at the time of writing
parent_uuid => $parent ? $info{latest_common_target}->{node}{uuid} : '-', parent_uuid => $parent ? $info{target_parent_node}->{uuid} : '-',
received_uuid => $source->{node}{received_uuid} eq '-' ? $source->{node}{uuid} : $source->{node}{received_uuid}, received_uuid => $source->{node}{received_uuid} eq '-' ? $source->{node}{uuid} : $source->{node}{received_uuid},
readonly => 1, readonly => 1,
TARGET_TYPE => $target_type, TARGET_TYPE => $target_type,
@ -3630,18 +3641,12 @@ sub macro_archive_target($$$;$)
my @schedule; my @schedule;
# NOTE: this is pretty much the same as "resume missing" # NOTE: this is pretty much the same as "resume missing"
my @unexpected_location; my $has_unexpected_location = 0;
my $droot_subvol_list = vinfo_subvol_list($droot); # cache subvol list for get_receive_targets()
foreach my $svol (@{vinfo_subvol_list($sroot, sort => 'path')}) foreach my $svol (@{vinfo_subvol_list($sroot, sort => 'path')})
{ {
next unless($svol->{node}{readonly}); next unless($svol->{node}{readonly});
next unless($svol->{btrbk_direct_leaf} && ($svol->{node}{BTRBK_BASENAME} eq $snapshot_name)); next unless($svol->{btrbk_direct_leaf} && ($svol->{node}{BTRBK_BASENAME} eq $snapshot_name));
next if(get_receive_targets($droot, $svol, warn => 1, ret_unexpected => \$has_unexpected_location));
my $warning_seen = [];
my @receive_targets = get_receive_targets($droot, $svol, exact_match => 1, warn => 1, seen => $warning_seen, droot_subvol_list => $droot_subvol_list );
push @unexpected_location, get_receive_targets_fsroot($droot, $svol, exclude => $warning_seen, warn => 1); # warn if unexpected on fs
next if(scalar(@receive_targets));
DEBUG "Adding archive candidate: $svol->{PRINT}"; DEBUG "Adding archive candidate: $svol->{PRINT}";
push @schedule, { value => $svol, push @schedule, { value => $svol,
@ -3650,14 +3655,14 @@ sub macro_archive_target($$$;$)
}; };
} }
if(scalar(@unexpected_location)) { if($has_unexpected_location) {
ABORTED($droot, "Receive targets of archive candidates exist at unexpected location"); ABORTED($droot, "Receive targets of archive candidates exist at unexpected location");
WARN "Skipping archiving of \"$sroot->{PRINT}/${snapshot_name}.*\": $abrt"; WARN "Skipping archiving of \"$sroot->{PRINT}/${snapshot_name}.*\": $abrt";
return undef; return undef;
} }
# add all present archives as informative_only: these are needed for correct results of schedule() # add all present archives as informative_only: these are needed for correct results of schedule()
foreach my $dvol (@$droot_subvol_list) foreach my $dvol (@{vinfo_subvol_list($droot)})
{ {
next unless($dvol->{btrbk_direct_leaf} && ($dvol->{node}{BTRBK_BASENAME} eq $snapshot_name)); next unless($dvol->{btrbk_direct_leaf} && ($dvol->{node}{BTRBK_BASENAME} eq $snapshot_name));
next unless($dvol->{node}{readonly}); next unless($dvol->{node}{readonly});
@ -3679,11 +3684,11 @@ sub macro_archive_target($$$;$)
my $archive_success = 0; my $archive_success = 0;
foreach my $svol (@archive) foreach my $svol (@archive)
{ {
my ($latest_common_src, $latest_common_target) = get_latest_common($sroot, $svol, $droot, ""); my ($parent, $target_parent_node) = get_best_parent($sroot, $svol, $droot, "");
if(macro_send_receive(source => $svol, if(macro_send_receive(source => $svol,
target => $droot, target => $droot,
parent => $latest_common_src, parent => $parent, # this is <undef> if no suitable parent found
latest_common_target => $latest_common_target, target_parent_node => $target_parent_node,
)) ))
{ {
$archive_success++; $archive_success++;
@ -5514,20 +5519,29 @@ MAIN:
foreach my $sroot (vinfo_subsection($config, 'volume')) { foreach my $sroot (vinfo_subsection($config, 'volume')) {
foreach my $svol (vinfo_subsection($sroot, 'subvolume')) { foreach my $svol (vinfo_subsection($sroot, 'subvolume')) {
my $found = 0; my $found = 0;
my $snapdir = config_key($svol, "snapshot_dir") // "";
my $snapshot_basename = config_key($svol, "snapshot_name") // die;
my @snapshot_children = sort({ cmp_date($b->{node}{BTRBK_DATE}, $a->{node}{BTRBK_DATE}) } # sort descending
get_snapshot_children($sroot, $svol, $snapdir, $snapshot_basename));
foreach my $droot (vinfo_subsection($svol, 'target')) { foreach my $droot (vinfo_subsection($svol, 'target')) {
my ($latest_common_src, $latest_common_target) = get_latest_common($sroot, $svol, $droot); foreach my $child (@snapshot_children) {
if ($latest_common_src && $latest_common_target) { my @receive_targets = get_receive_targets($droot, $child);
if(scalar(@receive_targets)) {
foreach(@receive_targets) {
push @data, { type => "latest_common", push @data, { type => "latest_common",
status => ($latest_common_src->{node}{cgen} == $svol->{node}{gen}) ? "up-to-date" : undef, status => ($child->{node}{cgen} == $svol->{node}{gen}) ? "up-to-date" : undef,
vinfo_prefixed_keys("source", $svol), vinfo_prefixed_keys("source", $svol),
vinfo_prefixed_keys("snapshot", $latest_common_src), vinfo_prefixed_keys("snapshot", $child),
vinfo_prefixed_keys("target", $latest_common_target), vinfo_prefixed_keys("target", $_),
}; };
}
$found = 1; $found = 1;
last;
} }
} }
unless($found) { }
my $latest_snapshot = get_latest_snapshot_child($sroot, $svol); if(!$found) {
my $latest_snapshot = $snapshot_children[0];
push @data, { type => "latest_snapshot", push @data, { type => "latest_snapshot",
status => ($latest_snapshot && ($latest_snapshot->{node}{cgen} == $svol->{node}{gen})) ? "up-to-date" : undef, status => ($latest_snapshot && ($latest_snapshot->{node}{cgen} == $svol->{node}{gen})) ? "up-to-date" : undef,
vinfo_prefixed_keys("source", $svol), vinfo_prefixed_keys("source", $svol),
@ -5777,13 +5791,9 @@ MAIN:
my $resume_total = 0; my $resume_total = 0;
my $resume_success = 0; my $resume_success = 0;
my $droot_subvol_list = vinfo_subvol_list($droot); # cache subvol list for get_receive_targets()
foreach my $child (@snapshot_children) foreach my $child (@snapshot_children)
{ {
my $warning_seen = []; if(get_receive_targets($droot, $child, warn => 1)){
my @receive_targets = get_receive_targets($droot, $child, exact_match => 1, warn => 1, seen => $warning_seen, droot_subvol_list => $droot_subvol_list );
get_receive_targets_fsroot($droot, $child, exclude => $warning_seen, warn => 1); # warn on unexpected on fs
if(scalar(@receive_targets)){
DEBUG "Found receive target of: $child->{PRINT}"; DEBUG "Found receive target of: $child->{PRINT}";
next; next;
} }
@ -5800,7 +5810,7 @@ MAIN:
{ {
DEBUG "Checking schedule for backup candidates"; DEBUG "Checking schedule for backup candidates";
# add all present backups as informative_only: these are needed for correct results of schedule() # add all present backups as informative_only: these are needed for correct results of schedule()
foreach my $vol (@$droot_subvol_list) { foreach my $vol (@{vinfo_subvol_list($droot)}) {
unless($vol->{btrbk_direct_leaf} && ($vol->{node}{BTRBK_BASENAME} eq $snapshot_basename)) { unless($vol->{btrbk_direct_leaf} && ($vol->{node}{BTRBK_BASENAME} eq $snapshot_basename)) {
TRACE "Receive target does not match btrbk filename scheme, skipping: $vol->{PRINT}"; TRACE "Receive target does not match btrbk filename scheme, skipping: $vol->{PRINT}";
next; next;
@ -5833,11 +5843,11 @@ MAIN:
} }
INFO "Creating subvolume backup (send-receive) for: $child->{PRINT}"; INFO "Creating subvolume backup (send-receive) for: $child->{PRINT}";
my ($latest_common_src, $latest_common_target) = get_latest_common($sroot, $child, $droot, $snapdir); my ($parent, $target_parent_node) = get_best_parent($sroot, $child, $droot, $snapdir);
if(macro_send_receive(source => $child, if(macro_send_receive(source => $child,
target => $droot, target => $droot,
parent => $latest_common_src, # this is <undef> if no common found parent => $parent, # this is <undef> if no suitable parent found
latest_common_target => $latest_common_target, target_parent_node => $target_parent_node,
)) ))
{ {
$resume_success++; $resume_success++;
@ -5891,9 +5901,8 @@ MAIN:
} }
# always preserve latest common snapshot/backup pair # always preserve latest common snapshot/backup pair
my $droot_subvol_list = vinfo_subvol_list($droot); # cache subvol list for get_receive_targets()
foreach my $child (@snapshot_children) { foreach my $child (@snapshot_children) {
my @receive_targets = get_receive_targets($droot, $child, droot_subvol_list => $droot_subvol_list); my @receive_targets = get_receive_targets($droot, $child);
if(scalar(@receive_targets)) { if(scalar(@receive_targets)) {
DEBUG "Force preserve for latest common snapshot: $child->{PRINT}"; DEBUG "Force preserve for latest common snapshot: $child->{PRINT}";
$child->{node}{FORCE_PRESERVE} = 'preserve forced: latest common snapshot'; $child->{node}{FORCE_PRESERVE} = 'preserve forced: latest common snapshot';