diff mbox

btrfs-progs: receive explicit parent support

Message ID 1430043165-20641-1-git-send-email-lauri.vosandi@gmail.com (mailing list archive)
State New, archived
Headers show

Commit Message

lauri April 26, 2015, 10:12 a.m. UTC
This patch adds command-line flag -p to btrfs receive
which makes it possible to disable automatic parent
search for incremental snapshots and use explicitly
specified path instead.

Signed-off-by: Lauri Võsandi <lauri.vosandi@gmail.com>
---
 cmds-receive.c | 34 ++++++++++++++++++++++++++--------
 1 file changed, 26 insertions(+), 8 deletions(-)

Comments

Hugo Mills April 26, 2015, 11:19 a.m. UTC | #1
On Sun, Apr 26, 2015 at 12:12:45PM +0200, Lauri Võsandi wrote:
> This patch adds command-line flag -p to btrfs receive
> which makes it possible to disable automatic parent
> search for incremental snapshots and use explicitly
> specified path instead.

   As I said when we discussed this on IRC, I think this is absolutely
the wrong way to go about solving this problem. This -p option has two
failure modes: the first is that a referenced file simply doesn't
exist in the supplied parent, and the receive fails hard partway
through. The second failure mode is more subtle and not checked for in
any way, which is that the referenced file exists, but doesn't contain
the same data as the original parent subvolume on the send side. In
that case, you will end up with silent corruption of files.

   This is a NAK from me.

   I believe that the correct way of dealing with the issue of
non-transitive sends is twofold:

1) When sending a subvolume which already has a received-uuid field,
   preserve that field in the receiving side rather than overwriting
   it.

2) When specifying the parent to the receiving side, send both the
   uuid and received-uuid fields, and search against both of these in
   the subvolumes on the receiving side to find the parent. This will
   (I think) probably involve a bump of the stream format, and adding
   the received-uuid to the UUID tree.

   Part 1) means that, writing (U, R) for UUID and Received-UUID, we
currently have the following situation:

Subvol A on source side: (A, -)
Send this to A' on target side: (A', A)
Send this back to A'' on source side: (A'', A')

and would change this behaviour to:

Subvol A on source side: (A, -)
Send this to A' on target side: (A', A)
Send this back to A'' on source side: (A'', A) <-- Note the A here, not A'

   More later, when I've had a little time to play with things and
think through the semantics properly.

   Hugo.

> Signed-off-by: Lauri Võsandi <lauri.vosandi@gmail.com>
> ---
>  cmds-receive.c | 34 ++++++++++++++++++++++++++--------
>  1 file changed, 26 insertions(+), 8 deletions(-)
> 
> diff --git a/cmds-receive.c b/cmds-receive.c
> index b7cf3f9..391d281 100644
> --- a/cmds-receive.c
> +++ b/cmds-receive.c
> @@ -61,6 +61,7 @@ struct btrfs_receive
>  	char *root_path;
>  	char *dest_dir_path; /* relative to root_path */
>  	char *full_subvol_path;
> +	char *explicit_parent_path;
>  	int dest_dir_chroot;
>  
>  	struct subvol_info *cur_subvol;
> @@ -220,20 +221,32 @@ static int process_snapshot(const char *path, const u8 *uuid, u64 ctransid,
>  		fprintf(stderr, "receiving snapshot %s uuid=%s, "
>  				"ctransid=%llu ", path, uuid_str,
>  				r->cur_subvol->stransid);
> -		uuid_unparse(parent_uuid, uuid_str);
> -		fprintf(stderr, "parent_uuid=%s, parent_ctransid=%llu\n",
> -				uuid_str, parent_ctransid);
>  	}
>  
>  	memset(&args_v2, 0, sizeof(args_v2));
>  	strncpy_null(args_v2.name, path);
>  
> -	parent_subvol = subvol_uuid_search(&r->sus, 0, parent_uuid,
> -			parent_ctransid, NULL, subvol_search_by_received_uuid);
> -	if (!parent_subvol) {
> +	if (r->explicit_parent_path) {
> +		if (g_verbose) {
> +			fprintf(stderr, "using explicit parent %s\n",
> +					r->explicit_parent_path);
> +		}
> +		parent_subvol = subvol_uuid_search(&r->sus, 0, NULL,
> +			0, r->explicit_parent_path, subvol_search_by_path);
> +	} else {
> +		if (g_verbose) {
> +			uuid_unparse(parent_uuid, uuid_str);
> +			fprintf(stderr, "parent_uuid=%s, parent_ctransid=%llu\n",
> +					uuid_str, parent_ctransid);
> +		}
>  		parent_subvol = subvol_uuid_search(&r->sus, 0, parent_uuid,
> -				parent_ctransid, NULL, subvol_search_by_uuid);
> +			parent_ctransid, NULL, subvol_search_by_received_uuid);
> +		if (!parent_subvol) {
> +			parent_subvol = subvol_uuid_search(&r->sus, 0, parent_uuid,
> +					parent_ctransid, NULL, subvol_search_by_uuid);
> +		}
>  	}
> +
>  	if (!parent_subvol) {
>  		ret = -ENOENT;
>  		fprintf(stderr, "ERROR: could not find parent subvolume\n");
> @@ -962,11 +975,14 @@ int cmd_receive(int argc, char **argv)
>  			{ NULL, 0, NULL, 0 }
>  		};
>  
> -		c = getopt_long(argc, argv, "Cevf:", long_opts, NULL);
> +		c = getopt_long(argc, argv, "Cevf:p:", long_opts, NULL);
>  		if (c < 0)
>  			break;
>  
>  		switch (c) {
> +		case 'p':
> +			r.explicit_parent_path = optarg;
> +			break;
>  		case 'v':
>  			g_verbose++;
>  			break;
> @@ -1028,6 +1044,8 @@ const char * const cmd_receive_usage[] = {
>  	"                 in the data stream. Without this option,",
>  	"                 the receiver terminates only if an error",
>  	"                 is recognized or on EOF.",
> +	"-p <subvol>      Disables the automatic searching for parents",
> +	"                 if incremental streams are received.",
>  	"-C|--chroot      confine the process to <mount> using chroot",
>  	"--max-errors <N> Terminate as soon as N errors happened while",
>  	"                 processing commands from the send stream.",
lauri April 26, 2015, 11:37 a.m. UTC | #2
Hi,

> Subvol A on source side: (A, -)
> Send this to A' on target side: (A', A)
> Send this back to A'' on source side: (A'', A) <-- Note the A here, not A'

I also think your approach is the real solution to the problem, but as
some pointed out on IRC this changes the behaviour of btrfs receive
and will break things for someone. When I asked whose obscure workflow
does it break nobody could come up with any reasonable example whereas
sending snapshots back and forth seems to be the major usecase for
btrfs receive and it's impossible to for example restore snapshots
with current implementation.

I know that -p may fail horribly but the idea was to have *option* to
replace parent lookup logic and instead of relying on UUID-s simply
have it specified by userspace application.

>    More later, when I've had a little time to play with things and
> think through the semantics properly.

You want to give it a try?
Stefan Behrens April 27, 2015, 7:28 a.m. UTC | #3
On Sun, 26 Apr 2015 12:12:45 +0200, Lauri Võsandi wrote:
> This patch adds command-line flag -p to btrfs receive
> which makes it possible to disable automatic parent
> search for incremental snapshots and use explicitly
> specified path instead.
> 
> Signed-off-by: Lauri Võsandi <lauri.vosandi@gmail.com>
> ---
>  cmds-receive.c | 34 ++++++++++++++++++++++++++--------
>  1 file changed, 26 insertions(+), 8 deletions(-)
> 
> diff --git a/cmds-receive.c b/cmds-receive.c
> index b7cf3f9..391d281 100644
> --- a/cmds-receive.c
> +++ b/cmds-receive.c
> @@ -61,6 +61,7 @@ struct btrfs_receive
>  	char *root_path;
>  	char *dest_dir_path; /* relative to root_path */
>  	char *full_subvol_path;
> +	char *explicit_parent_path;
>  	int dest_dir_chroot;
>  
>  	struct subvol_info *cur_subvol;
> @@ -220,20 +221,32 @@ static int process_snapshot(const char *path, const u8 *uuid, u64 ctransid,
>  		fprintf(stderr, "receiving snapshot %s uuid=%s, "
>  				"ctransid=%llu ", path, uuid_str,
>  				r->cur_subvol->stransid);
> -		uuid_unparse(parent_uuid, uuid_str);
> -		fprintf(stderr, "parent_uuid=%s, parent_ctransid=%llu\n",
> -				uuid_str, parent_ctransid);
>  	}
>  
>  	memset(&args_v2, 0, sizeof(args_v2));
>  	strncpy_null(args_v2.name, path);
>  
> -	parent_subvol = subvol_uuid_search(&r->sus, 0, parent_uuid,
> -			parent_ctransid, NULL, subvol_search_by_received_uuid);
> -	if (!parent_subvol) {
> +	if (r->explicit_parent_path) {
> +		if (g_verbose) {
> +			fprintf(stderr, "using explicit parent %s\n",
> +					r->explicit_parent_path);
> +		}
> +		parent_subvol = subvol_uuid_search(&r->sus, 0, NULL,
> +			0, r->explicit_parent_path, subvol_search_by_path);

This won't work if you receive more than one snapshot, each one derived
from the previous one ("btrfs send -e snap1 snap2 snap3 snap4"). You
would always use the first one as the parent, not the predecessor.
That's implemented differently for the -p option in
git://git.kernel.org/pub/scm/linux/kernel/git/arne/far-progs.git


> +	} else {
> +		if (g_verbose) {
> +			uuid_unparse(parent_uuid, uuid_str);
> +			fprintf(stderr, "parent_uuid=%s, parent_ctransid=%llu\n",
> +					uuid_str, parent_ctransid);
> +		}
>  		parent_subvol = subvol_uuid_search(&r->sus, 0, parent_uuid,
> -				parent_ctransid, NULL, subvol_search_by_uuid);
> +			parent_ctransid, NULL, subvol_search_by_received_uuid);
> +		if (!parent_subvol) {
> +			parent_subvol = subvol_uuid_search(&r->sus, 0, parent_uuid,
> +					parent_ctransid, NULL, subvol_search_by_uuid);
> +		}

This used to be a search for the received_uuid only. Why is this code
changed like this in the branch that is executed when -p is not
specified, in a patch that has the goal to add -p with new functionality
if -p is specified?


>  	}
> +
>  	if (!parent_subvol) {
>  		ret = -ENOENT;
>  		fprintf(stderr, "ERROR: could not find parent subvolume\n");
> @@ -962,11 +975,14 @@ int cmd_receive(int argc, char **argv)
>  			{ NULL, 0, NULL, 0 }
>  		};
>  
> -		c = getopt_long(argc, argv, "Cevf:", long_opts, NULL);
> +		c = getopt_long(argc, argv, "Cevf:p:", long_opts, NULL);
>  		if (c < 0)
>  			break;
>  
>  		switch (c) {
> +		case 'p':
> +			r.explicit_parent_path = optarg;
> +			break;
>  		case 'v':
>  			g_verbose++;
>  			break;
> @@ -1028,6 +1044,8 @@ const char * const cmd_receive_usage[] = {
>  	"                 in the data stream. Without this option,",
>  	"                 the receiver terminates only if an error",
>  	"                 is recognized or on EOF.",
> +	"-p <subvol>      Disables the automatic searching for parents",
> +	"                 if incremental streams are received.",
>  	"-C|--chroot      confine the process to <mount> using chroot",
>  	"--max-errors <N> Terminate as soon as N errors happened while",
>  	"                 processing commands from the send stream.",
> 

--
To unsubscribe from this list: send the line "unsubscribe linux-btrfs" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html
lauri April 27, 2015, 9:23 a.m. UTC | #4
Hi,

> This won't work if you receive more than one snapshot, each one derived
> from the previous one ("btrfs send -e snap1 snap2 snap3 snap4"). You
> would always use the first one as the parent, not the predecessor.
> That's implemented differently for the -p option in
> git://git.kernel.org/pub/scm/linux/kernel/git/arne/far-progs.git

I'll take another look on the multi-subvolume stream mode.

> This used to be a search for the received_uuid only. Why is this code
> changed like this in the branch that is executed when -p is not
> specified, in a patch that has the goal to add -p with new functionality
> if -p is specified?

Many things have changed since far-progs, take a look in the original
source I used as basis for my patch:

https://github.com/kdave/btrfs-progs/blob/1b7dd327f43777bcdd217a0500e6bda78128a290/cmds-receive.c#L233

I am pretty sure lookup logic isn't affected if -p is not used.

2015-04-27 9:28 GMT+02:00 Stefan Behrens <sbehrens@giantdisaster.de>:
> On Sun, 26 Apr 2015 12:12:45 +0200, Lauri Võsandi wrote:
>> This patch adds command-line flag -p to btrfs receive
>> which makes it possible to disable automatic parent
>> search for incremental snapshots and use explicitly
>> specified path instead.
>>
>> Signed-off-by: Lauri Võsandi <lauri.vosandi@gmail.com>
>> ---
>>  cmds-receive.c | 34 ++++++++++++++++++++++++++--------
>>  1 file changed, 26 insertions(+), 8 deletions(-)
>>
>> diff --git a/cmds-receive.c b/cmds-receive.c
>> index b7cf3f9..391d281 100644
>> --- a/cmds-receive.c
>> +++ b/cmds-receive.c
>> @@ -61,6 +61,7 @@ struct btrfs_receive
>>       char *root_path;
>>       char *dest_dir_path; /* relative to root_path */
>>       char *full_subvol_path;
>> +     char *explicit_parent_path;
>>       int dest_dir_chroot;
>>
>>       struct subvol_info *cur_subvol;
>> @@ -220,20 +221,32 @@ static int process_snapshot(const char *path, const u8 *uuid, u64 ctransid,
>>               fprintf(stderr, "receiving snapshot %s uuid=%s, "
>>                               "ctransid=%llu ", path, uuid_str,
>>                               r->cur_subvol->stransid);
>> -             uuid_unparse(parent_uuid, uuid_str);
>> -             fprintf(stderr, "parent_uuid=%s, parent_ctransid=%llu\n",
>> -                             uuid_str, parent_ctransid);
>>       }
>>
>>       memset(&args_v2, 0, sizeof(args_v2));
>>       strncpy_null(args_v2.name, path);
>>
>> -     parent_subvol = subvol_uuid_search(&r->sus, 0, parent_uuid,
>> -                     parent_ctransid, NULL, subvol_search_by_received_uuid);
>> -     if (!parent_subvol) {
>> +     if (r->explicit_parent_path) {
>> +             if (g_verbose) {
>> +                     fprintf(stderr, "using explicit parent %s\n",
>> +                                     r->explicit_parent_path);
>> +             }
>> +             parent_subvol = subvol_uuid_search(&r->sus, 0, NULL,
>> +                     0, r->explicit_parent_path, subvol_search_by_path);
>
> This won't work if you receive more than one snapshot, each one derived
> from the previous one ("btrfs send -e snap1 snap2 snap3 snap4"). You
> would always use the first one as the parent, not the predecessor.
> That's implemented differently for the -p option in
> git://git.kernel.org/pub/scm/linux/kernel/git/arne/far-progs.git
>
>
>> +     } else {
>> +             if (g_verbose) {
>> +                     uuid_unparse(parent_uuid, uuid_str);
>> +                     fprintf(stderr, "parent_uuid=%s, parent_ctransid=%llu\n",
>> +                                     uuid_str, parent_ctransid);
>> +             }
>>               parent_subvol = subvol_uuid_search(&r->sus, 0, parent_uuid,
>> -                             parent_ctransid, NULL, subvol_search_by_uuid);
>> +                     parent_ctransid, NULL, subvol_search_by_received_uuid);
>> +             if (!parent_subvol) {
>> +                     parent_subvol = subvol_uuid_search(&r->sus, 0, parent_uuid,
>> +                                     parent_ctransid, NULL, subvol_search_by_uuid);
>> +             }
>
> This used to be a search for the received_uuid only. Why is this code
> changed like this in the branch that is executed when -p is not
> specified, in a patch that has the goal to add -p with new functionality
> if -p is specified?
>
>
>>       }
>> +
>>       if (!parent_subvol) {
>>               ret = -ENOENT;
>>               fprintf(stderr, "ERROR: could not find parent subvolume\n");
>> @@ -962,11 +975,14 @@ int cmd_receive(int argc, char **argv)
>>                       { NULL, 0, NULL, 0 }
>>               };
>>
>> -             c = getopt_long(argc, argv, "Cevf:", long_opts, NULL);
>> +             c = getopt_long(argc, argv, "Cevf:p:", long_opts, NULL);
>>               if (c < 0)
>>                       break;
>>
>>               switch (c) {
>> +             case 'p':
>> +                     r.explicit_parent_path = optarg;
>> +                     break;
>>               case 'v':
>>                       g_verbose++;
>>                       break;
>> @@ -1028,6 +1044,8 @@ const char * const cmd_receive_usage[] = {
>>       "                 in the data stream. Without this option,",
>>       "                 the receiver terminates only if an error",
>>       "                 is recognized or on EOF.",
>> +     "-p <subvol>      Disables the automatic searching for parents",
>> +     "                 if incremental streams are received.",
>>       "-C|--chroot      confine the process to <mount> using chroot",
>>       "--max-errors <N> Terminate as soon as N errors happened while",
>>       "                 processing commands from the send stream.",
>>
>
Stefan Behrens April 27, 2015, 7:29 p.m. UTC | #5
On 4/27/2015 11:23 AM, lauri wrote:
> Hi,
>
>> This won't work if you receive more than one snapshot, each one derived
>> from the previous one ("btrfs send -e snap1 snap2 snap3 snap4"). You
>> would always use the first one as the parent, not the predecessor.
>> That's implemented differently for the -p option in
>> git://git.kernel.org/pub/scm/linux/kernel/git/arne/far-progs.git
>
> I'll take another look on the multi-subvolume stream mode.
>
>> This used to be a search for the received_uuid only. Why is this code
>> changed like this in the branch that is executed when -p is not
>> specified, in a patch that has the goal to add -p with new functionality
>> if -p is specified?
>
> Many things have changed since far-progs, take a look in the original
> source I used as basis for my patch:
>
> https://github.com/kdave/btrfs-progs/blob/1b7dd327f43777bcdd217a0500e6bda78128a290/cmds-receive.c#L233
>
> I am pretty sure lookup logic isn't affected if -p is not used.

You are right! My bad, not looking precisely enough. Before the patch, 
the code did the two lookups as well.

But the first issue is real. One possible fix is to update 
explicit_parent_path after each received snapshot.


>
> 2015-04-27 9:28 GMT+02:00 Stefan Behrens <sbehrens@giantdisaster.de>:
>> On Sun, 26 Apr 2015 12:12:45 +0200, Lauri Võsandi wrote:
>>> This patch adds command-line flag -p to btrfs receive
>>> which makes it possible to disable automatic parent
>>> search for incremental snapshots and use explicitly
>>> specified path instead.
>>>
>>> Signed-off-by: Lauri Võsandi <lauri.vosandi@gmail.com>
>>> ---
>>>   cmds-receive.c | 34 ++++++++++++++++++++++++++--------
>>>   1 file changed, 26 insertions(+), 8 deletions(-)
>>>
>>> diff --git a/cmds-receive.c b/cmds-receive.c
>>> index b7cf3f9..391d281 100644
>>> --- a/cmds-receive.c
>>> +++ b/cmds-receive.c
>>> @@ -61,6 +61,7 @@ struct btrfs_receive
>>>        char *root_path;
>>>        char *dest_dir_path; /* relative to root_path */
>>>        char *full_subvol_path;
>>> +     char *explicit_parent_path;
>>>        int dest_dir_chroot;
>>>
>>>        struct subvol_info *cur_subvol;
>>> @@ -220,20 +221,32 @@ static int process_snapshot(const char *path, const u8 *uuid, u64 ctransid,
>>>                fprintf(stderr, "receiving snapshot %s uuid=%s, "
>>>                                "ctransid=%llu ", path, uuid_str,
>>>                                r->cur_subvol->stransid);
>>> -             uuid_unparse(parent_uuid, uuid_str);
>>> -             fprintf(stderr, "parent_uuid=%s, parent_ctransid=%llu\n",
>>> -                             uuid_str, parent_ctransid);
>>>        }
>>>
>>>        memset(&args_v2, 0, sizeof(args_v2));
>>>        strncpy_null(args_v2.name, path);
>>>
>>> -     parent_subvol = subvol_uuid_search(&r->sus, 0, parent_uuid,
>>> -                     parent_ctransid, NULL, subvol_search_by_received_uuid);
>>> -     if (!parent_subvol) {
>>> +     if (r->explicit_parent_path) {
>>> +             if (g_verbose) {
>>> +                     fprintf(stderr, "using explicit parent %s\n",
>>> +                                     r->explicit_parent_path);
>>> +             }
>>> +             parent_subvol = subvol_uuid_search(&r->sus, 0, NULL,
>>> +                     0, r->explicit_parent_path, subvol_search_by_path);
>>
>> This won't work if you receive more than one snapshot, each one derived
>> from the previous one ("btrfs send -e snap1 snap2 snap3 snap4"). You
>> would always use the first one as the parent, not the predecessor.
>> That's implemented differently for the -p option in
>> git://git.kernel.org/pub/scm/linux/kernel/git/arne/far-progs.git
>>
>>
>>> +     } else {
>>> +             if (g_verbose) {
>>> +                     uuid_unparse(parent_uuid, uuid_str);
>>> +                     fprintf(stderr, "parent_uuid=%s, parent_ctransid=%llu\n",
>>> +                                     uuid_str, parent_ctransid);
>>> +             }
>>>                parent_subvol = subvol_uuid_search(&r->sus, 0, parent_uuid,
>>> -                             parent_ctransid, NULL, subvol_search_by_uuid);
>>> +                     parent_ctransid, NULL, subvol_search_by_received_uuid);
>>> +             if (!parent_subvol) {
>>> +                     parent_subvol = subvol_uuid_search(&r->sus, 0, parent_uuid,
>>> +                                     parent_ctransid, NULL, subvol_search_by_uuid);
>>> +             }
>>
>> This used to be a search for the received_uuid only. Why is this code
>> changed like this in the branch that is executed when -p is not
>> specified, in a patch that has the goal to add -p with new functionality
>> if -p is specified?
>>
>>
>>>        }
>>> +
>>>        if (!parent_subvol) {
>>>                ret = -ENOENT;
>>>                fprintf(stderr, "ERROR: could not find parent subvolume\n");
>>> @@ -962,11 +975,14 @@ int cmd_receive(int argc, char **argv)
>>>                        { NULL, 0, NULL, 0 }
>>>                };
>>>
>>> -             c = getopt_long(argc, argv, "Cevf:", long_opts, NULL);
>>> +             c = getopt_long(argc, argv, "Cevf:p:", long_opts, NULL);
>>>                if (c < 0)
>>>                        break;
>>>
>>>                switch (c) {
>>> +             case 'p':
>>> +                     r.explicit_parent_path = optarg;
>>> +                     break;
>>>                case 'v':
>>>                        g_verbose++;
>>>                        break;
>>> @@ -1028,6 +1044,8 @@ const char * const cmd_receive_usage[] = {
>>>        "                 in the data stream. Without this option,",
>>>        "                 the receiver terminates only if an error",
>>>        "                 is recognized or on EOF.",
>>> +     "-p <subvol>      Disables the automatic searching for parents",
>>> +     "                 if incremental streams are received.",
>>>        "-C|--chroot      confine the process to <mount> using chroot",
>>>        "--max-errors <N> Terminate as soon as N errors happened while",
>>>        "                 processing commands from the send stream.",
>>>
--
To unsubscribe from this list: send the line "unsubscribe linux-btrfs" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html
lauri May 7, 2015, 7:06 p.m. UTC | #6
Even if the UUID stuff gets fixed there will be plenty of machines
which are unable to adapt the new approach due to conflicting
received_uuid values and having the option to specify -p would help
them to work around the issue to adapt to new received_uuid scheme
--
To unsubscribe from this list: send the line "unsubscribe linux-btrfs" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html
Hugo Mills May 14, 2015, 5:35 p.m. UTC | #7
On Sun, Apr 26, 2015 at 02:37:58PM +0300, lauri wrote:
> > Subvol A on source side: (A, -)
> > Send this to A' on target side: (A', A)
> > Send this back to A'' on source side: (A'', A) <-- Note the A here, not A'
> 
> I also think your approach is the real solution to the problem, but as
> some pointed out on IRC this changes the behaviour of btrfs receive
> and will break things for someone. When I asked whose obscure workflow
> does it break nobody could come up with any reasonable example whereas
> sending snapshots back and forth seems to be the major usecase for
> btrfs receive and it's impossible to for example restore snapshots
> with current implementation.
> 
> I know that -p may fail horribly but the idea was to have *option* to
> replace parent lookup logic and instead of relying on UUID-s simply
> have it specified by userspace application.

   My argument is that having this option present will mean that
people will use it. Getting send/receive right is hard enough as it is
without giving someone an option which can completely screw things up
in *most* cases. If you want an analogy, it's a bit like giving
someone a drum of nerve toxin to get rid of the fly in their kitchen,
rather than a fly swatter. (Yes, a bad analogy is like a leaky
screwdriver, and this one's engaging in a bit of hyperbole, but I hope
it gets my point across.)

> >    More later, when I've had a little time to play with things and
> > think through the semantics properly.
> 
> You want to give it a try?

   Finally, I managed to spend some time with it. What follows is long
and detailed, but I hope it's clear, and rigorous. I'd like someone
with a mathematical turn of mind to give this a good kicking, because
there may be some errors in it, but I think it's right.

   A subvolume may be described by the tuple (u, r, p), where u is the
UUID of the subvolume itself, r is the "received UUID" set to indicate
the original send source of the subvolume, and p is the UUID of the
parent subvolume, set when a snapshot is taken. We indicate a
read-only snapshot with a * suffix. The location of a subvolume is
shown with the filesystem prefixed to it: m(u, r, p).

   We use capital letters (A, B, C, ...) to indicate subvolumes, and
S, T to indicate the "source" and "target" filesystems. Note that send
operations will sometimes go from T to S in the examples that follow.

   There are four functions we need to consider: two for snapshots and
two for send operations. The current definitions of the functions are:

Ordinary snapshot:
   Snap(m(u1, #, #))  -> m(u2, -, u1)

Read-only snapshot:
   SnapR(m(u1, #, #))  -> m(u2, -, u1)*

Full send:
   Send(m1(u1, #, #)*, m2) -> m2(u2, u1, -)*

Incremental send:
   SendP(m1(u2, #, p1)*, m1(u1, r1, p1)*, m2) -> m2(u3, u2, p2)*
            precondition: m2(p2, u1, #)* exists for some p2.

where # means "don't care" in all cases.

   This latter function sends m1(u2, #, p1)* to m2, using m1(u1, r1,
p1)* as the parameter to the -p command option (e.g. the reference
point for the incremental stream). The precondition is necessary to
ensure that the reference subvolumes on each side are identical. The
m1(u1, r1, p1)* is the same subvolume data as m2(p2, u1, #)* because
the only way that the second field of a subvolume can be set is via
one of the Send functions, which don't modify the subvolume data (or,
through the read-only property, allow it to be modified).

   Now, there are two use cases we want to support: 1) restore from
backups and continue with incrementals; 2) bidirectional
synchronisation, where incremental sends are used to shift the state
of a subvolume from one machine to another and back again. These are
very similar cases -- the former case simply uses a full send from T
to S, rather than the incremental send used in the latter case.

First use case: restore from backups
------------------------------------

The first use case goes something like this:

1) Create a subvolume, S(A, -, -) as the working subvol.
2) Make a backup to T:
 a  SnapR(S(A, -, -))                   ->  S(B, -, A)*
 b  Send(S(B, -, A)*, T)                ->               T(C, B, -)*
3) Subvol A is modified
4) Make an incremental backup to T
 a  SnapR(S(A, -, -))                   ->  S(D, -, A)*
 b  SendP(S(D, -, A)*, S(B, -, A)*, T)  ->               T(E, D, C)*
 (repeat 3, 4 ad libitum)
5) A meteorite strike destroys S, and it is replaced with a blank machine
6) Restore the backup, making a new working subvol G
 a  Send(T(E, D, C)*, S)                ->  S(F, E, -)*
 b  Snap(S(F, E, -)*)                   ->  S(G, -, F)
7) The new working subvol S(G, -, F) is modifed
8) Make an incremental backup to T
 a  SnapR(S(G, -, F))                   ->  S(H, -, G)*
 b  SendP(S(H, -, G)*, S(F, E, -)*, T)  ->               T(I, H, E)*

Now, with the current implementation, 8b will fail for two reasons:

 - We can't immediately tell that S(H, -, G)* and S(F, E, -)* are
   related, and
 - T(#, F, #)* doesn't exist.

However, it should be allowed to succeed, because there's a chain
of snapshots connecting S(F, E, -)* to S(H, -, G)*, so one is a direct
ancestor of the other. Further, a subvolume equivalent to E exists on
both S and T after step 7 -- E and F respectively. The subvolume S(F,
E, -)* has enough metadata to be able to correctly identify that it's
equivalent to T(E, D, C)*, but the precondition on the SendP function
doesn't support it.

Second use case: bidirectional synchronisation
----------------------------------------------

   Let's leave that there for the moment and look at the second
use-case:

1) Create a subvolume, S(A, -, -) as the working subvol.
2) Make a backup to T:
 a  SnapR(S(A, -, -))                   ->  S(B, -, A)*
 b  Send(S(B, -, A)*, T)                ->               T(C, B, -)*
3) Subvol A is modified
4) Make an incremental backup to T
 a  SnapR(S(A, -, -))                   ->  S(D, -, A)*
 b  SendP(S(D, -, A)*, S(B, -, A)*, T)  ->               T(E, D, C)*
 (repeat 3, 4 ad libitum)
5) Make a working copy on T
    Snap(T(E, D, C))                    ->               T(F, -, E)
6) Subvol F is modified
7) Send the changes back to S
 a  SnapR(T(F, -, E))                   ->               T(G, F, -)*
 b  SendP(T(G, F, -)*, T(E, D, C)*, S)  ->  S(H, G, D)*

Again, with the current implementation, this will fail -- at step 7b
this time. As before, there's two reasons why:

 - We can't immediately tell that T(G, F, -)* and T(E, D, C)* are
   related, and
 - T(#, E, #)* doesn't exist.

As before, it should be allowed, because there's a chain of snapshots
connecting T(E, D, C)* to T(G, F, -)*, so the former is a direct
ancestor of the latter, and because S(D, -, A)* is a duplicate of the
reference subvolume T(E, D, C)*.

   Now, in the first case we've got S(F, E, -)* on the source side,
and T(E, D, C)* on the target side. In the second case, we have T(E,
D, C)* on the source and S(D, -, A)* on the target. In both cases,
we're looking for a subvolume on the target whose UUID matches the
Received UUID of the source side's reference subvolume. This contrasts
with the original algorithm, where we only look for a subvolume on the
target whose Received UUID matches the UUID of the source side's
reference.

What to do about it
-------------------

   So, to support these two use-cases, our SendP function needs to
look like this:

   SendP(m1(u2, #, p1)*, m1(u1, r1, p1)*, m2) -> m2(u3, u2, p2)*
            precondition: m2(p2, u1, #)* exists for some p2,
   or
   SendP(m1(u2, #, p1)*, m1(u1, r1, #)*, m2) -> m2(u3, u2, r1)*
            precondition: m2(r1, #, #)* exists,
	              and m1(p1, -, u1)# exists.

or, in other words, the first form is where the reference subvolume
has been sent to the target machine; the second form is where the
reference subvolume has come *from* the target machine. The other term
in the precondition simply verifies that there has been a chain of
snapshots on the local machine that connects the subject of the send
back to the reference subvolume. So, finally, we end up with the final
form of the function as:

   SendP(m1(u2, #, p1)*, m1(u1, r1, p2)*, m2) -> m2(u3, u2, p3)*
          preconditions:
              (chain rule)     m1(p1, -, u1)# exists or p1 == p2
          and (reference rule) m2(p2, u1, #)* exists or m2(r1, #, #)* exists

The chain rule can (must) be done on the send side. The reference rule
means that we need both r1 and u1 to be available on the receiving
side. I haven't checked, but this will probably involve a change to
the stream format to send r1.

Other things
------------

   As mentioned above, there's a use-case where we do something like
this:

1) S(A, -, -)
2) Send a backup
 a  SnapR(S(A, -, -))               ->  S(B, -, A)*
 b  Send(S(B, -, A)*, T)            ->               T(C, B, -)*
3) Roll back to the state of B
    Snap(S(B, -, A))                ->  S(D, -, B)
4) S(D, -, B) is used for a while
5) Send a backup
 a  SnapR(S(D, -, B))               ->  S(E, -, D)*
 b  SendP(S(E, -, D)*, S(B, -, A)*  ->               T(F, E, C)*

This is supported with the above infrastructure, but it's not going to
work if there's more snapshots in between stages 3 and 4 (because the
"parent" UUID is rotated out each time). We would need a
generalisation of the "chain rule", so that we look for a chain of
snapshots of _any_ length from the reference to the subject. However,
this would either require us to keep a full history of all ancestors
with each snapshot, or to find such a path in the tree of snapshot
history when we run the send (and to require that all snapshots in
that sequence still exist). Neither of these is a particularly
attractive proposition, and I would suggest not trying to support that
use case right now.

   The other thing that we don't support at all is merging changes
from divergent snapshots. i.e. snapshot A to B and C, modify both B
and C, and then later try to merge B and C into D. (Possibly with
additional sends between all of those stages). This gets into issues
of semantic merging. I feel entirely justified in telling anyone who
wants to implement that kind of workflow that it's out of scope for
send/receive, and referring them to a distributed revision control
system like git.

   Finally, there's an interesting use case of trying to rotate the
bidirectional sync through several machines in turn (S, T, Q, R and
back to S). I haven't had time to look at that one in detail, and I
probably won't do so, but at least the analysis of it should be
tractable with the above notation, if anyone does feel moved to look
into it.

   Hugo.
lauri May 21, 2015, 6:02 p.m. UTC | #8
Hi,

> My argument is that having this option present will mean that
> people will use it. Getting send/receive right is hard enough as it is
> without giving someone an option which can completely screw things up
> in *most* cases.

I agree that providing workarounds like this is not nice, however considering
the users who are already stuck with the UUID-s of current architecture
should be provided a way to get back on the track with better implementation :)

> The chain rule can (must) be done on the send side. The reference rule
> means that we need both r1 and u1 to be available on the receiving
> side. I haven't checked, but this will probably involve a change to
> the stream format to send r1.

I've also concluded the real solution involves bundling both subvolume
UUID and received UUID with the btrfs stream. This implies btrfs send
stream format change and if I've understood correctly involves kernel
changes as btrfs stream is generated in kernelspace.

>    The other thing that we don't support at all is merging changes
> from divergent snapshots. i.e. snapshot A to B and C, modify both B
> and C, and then later try to merge B and C into D. (Possibly with
> additional sends between all of those stages). This gets into issues
> of semantic merging. I feel entirely justified in telling anyone who
> wants to implement that kind of workflow that it's out of scope for
> send/receive, and referring them to a distributed revision control
> system like git.

I've never suggested merges, when I mentioned Git-like workflow
I was referring to simply push/pull. Obviously implementing
merge is out of the scope of any filesystem ;)
diff mbox

Patch

diff --git a/cmds-receive.c b/cmds-receive.c
index b7cf3f9..391d281 100644
--- a/cmds-receive.c
+++ b/cmds-receive.c
@@ -61,6 +61,7 @@  struct btrfs_receive
 	char *root_path;
 	char *dest_dir_path; /* relative to root_path */
 	char *full_subvol_path;
+	char *explicit_parent_path;
 	int dest_dir_chroot;
 
 	struct subvol_info *cur_subvol;
@@ -220,20 +221,32 @@  static int process_snapshot(const char *path, const u8 *uuid, u64 ctransid,
 		fprintf(stderr, "receiving snapshot %s uuid=%s, "
 				"ctransid=%llu ", path, uuid_str,
 				r->cur_subvol->stransid);
-		uuid_unparse(parent_uuid, uuid_str);
-		fprintf(stderr, "parent_uuid=%s, parent_ctransid=%llu\n",
-				uuid_str, parent_ctransid);
 	}
 
 	memset(&args_v2, 0, sizeof(args_v2));
 	strncpy_null(args_v2.name, path);
 
-	parent_subvol = subvol_uuid_search(&r->sus, 0, parent_uuid,
-			parent_ctransid, NULL, subvol_search_by_received_uuid);
-	if (!parent_subvol) {
+	if (r->explicit_parent_path) {
+		if (g_verbose) {
+			fprintf(stderr, "using explicit parent %s\n",
+					r->explicit_parent_path);
+		}
+		parent_subvol = subvol_uuid_search(&r->sus, 0, NULL,
+			0, r->explicit_parent_path, subvol_search_by_path);
+	} else {
+		if (g_verbose) {
+			uuid_unparse(parent_uuid, uuid_str);
+			fprintf(stderr, "parent_uuid=%s, parent_ctransid=%llu\n",
+					uuid_str, parent_ctransid);
+		}
 		parent_subvol = subvol_uuid_search(&r->sus, 0, parent_uuid,
-				parent_ctransid, NULL, subvol_search_by_uuid);
+			parent_ctransid, NULL, subvol_search_by_received_uuid);
+		if (!parent_subvol) {
+			parent_subvol = subvol_uuid_search(&r->sus, 0, parent_uuid,
+					parent_ctransid, NULL, subvol_search_by_uuid);
+		}
 	}
+
 	if (!parent_subvol) {
 		ret = -ENOENT;
 		fprintf(stderr, "ERROR: could not find parent subvolume\n");
@@ -962,11 +975,14 @@  int cmd_receive(int argc, char **argv)
 			{ NULL, 0, NULL, 0 }
 		};
 
-		c = getopt_long(argc, argv, "Cevf:", long_opts, NULL);
+		c = getopt_long(argc, argv, "Cevf:p:", long_opts, NULL);
 		if (c < 0)
 			break;
 
 		switch (c) {
+		case 'p':
+			r.explicit_parent_path = optarg;
+			break;
 		case 'v':
 			g_verbose++;
 			break;
@@ -1028,6 +1044,8 @@  const char * const cmd_receive_usage[] = {
 	"                 in the data stream. Without this option,",
 	"                 the receiver terminates only if an error",
 	"                 is recognized or on EOF.",
+	"-p <subvol>      Disables the automatic searching for parents",
+	"                 if incremental streams are received.",
 	"-C|--chroot      confine the process to <mount> using chroot",
 	"--max-errors <N> Terminate as soon as N errors happened while",
 	"                 processing commands from the send stream.",