[libnbd PATCH 0/2] Smarter nbd_opt_info behavior
While trying to add a new API nbd_opt_set_meta_context(), I noticed some existing oddities with nbd_opt_list_meta_context() and nbd_opt_info(). Eric Blake (2): api: Better use of nbd_internal_reset_size_and_flags squash test generator/states-newstyle-opt-meta-context.c | 7 +++---- lib/handle.c | 4 ++++ tests/opt-info.c | 20 +++++++++++++------- 3 files changed, 20 insertions(+), 11 deletions(-) -- 2.37.2
The documentation for nbd_internal_reset_size_and_flags() claims we should wipe all knowledge of previously-negotiated meta contexts when swapping export names. However, we weren't actually doing that, which could lead to a user calling nbd_opt_info() for one export, then switching to another export name, then having nbd_get_size() still report the size of the old export. At present, this is only mildly confusing, but once the next patch exposes nbd_opt_set_meta_context() for user control, it would become actively dangerous: negotiating meta contexts for one export, then switching to a different export name before nbd_opt_go, can lead us into calling nbd_cmd_block_status() when the server is not expecting it. While fixing this, code things to check whether we are actually swapping export names; an idempotent nbd_set_export_name() is not observable by the server, and therefore does not need to invalidate what has already been negotiated. Conversely, calling nbd_opt_list_meta_context() does not need to wipe the list - it does not touch h->meta_contexts, but relies on the user's callback instead. --- generator/states-newstyle-opt-meta-context.c | 7 +++---- lib/handle.c | 4 ++++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/generator/states-newstyle-opt-meta-context.c b/generator/states-newstyle-opt-meta-context.c index 30b9617..76f1032 100644 --- a/generator/states-newstyle-opt-meta-context.c +++ b/generator/states-newstyle-opt-meta-context.c @@ -1,5 +1,5 @@ /* nbd client library in userspace: state machine - * Copyright (C) 2013-2020 Red Hat Inc. + * Copyright (C) 2013-2022 Red Hat Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -28,7 +28,6 @@ STATE_MACHINE { * contexts, when doing SET (but an empty LIST is okay). */ assert (h->gflags & LIBNBD_HANDSHAKE_FLAG_FIXED_NEWSTYLE); - nbd_internal_reset_size_and_flags (h); if (h->opt_current == NBD_OPT_LIST_META_CONTEXT) { assert (h->opt_mode); assert (h->structured_replies); @@ -37,6 +36,8 @@ STATE_MACHINE { } else { assert (CALLBACK_IS_NULL (h->opt_cb.fn.context)); + nbd_internal_reset_size_and_flags (h); + assert (h->meta_contexts == NULL); opt = NBD_OPT_SET_META_CONTEXT; if (!h->structured_replies || h->request_meta_contexts.len == 0) { SET_NEXT_STATE (%^OPT_GO.START); @@ -44,8 +45,6 @@ STATE_MACHINE { } } - assert (h->meta_contexts == NULL); - /* Calculate the length of the option request data. */ len = 4 /* exportname len */ + strlen (h->export_name) + 4 /* nr queries */; for (i = 0; i < h->request_meta_contexts.len; ++i) diff --git a/lib/handle.c b/lib/handle.c index 8713e18..2aa89b9 100644 --- a/lib/handle.c +++ b/lib/handle.c @@ -219,6 +219,10 @@ nbd_unlocked_set_export_name (struct nbd_handle *h, const char *export_name) return -1; } + if (strcmp (export_name, h->export_name) == 0) + return 0; + + nbd_internal_reset_size_and_flags (h); new_name = strdup (export_name); if (!new_name) { set_error (errno, "strdup"); -- 2.37.2
On 08/24/22 04:53, Eric Blake wrote:
The documentation for nbd_internal_reset_size_and_flags() claims we should wipe all knowledge of previously-negotiated meta contexts when swapping export names. However, we weren't actually doing that, which could lead to a user calling nbd_opt_info() for one export, then switching to another export name, then having nbd_get_size() still report the size of the old export. At present, this is only mildly confusing, but once the next patch exposes nbd_opt_set_meta_context() for user control,
(1) The "next patch" does not expose nbd_opt_set_meta_context(). In fact, I can't find nbd_opt_set_meta_context() -- the function -- in the codebase anywhere (@ 9e5bf05a2196). (I prefer writing "a subsequent patch in this series" rather than "next patch", because that allows me to reshuffle patches without having to rewrite commit messages, as long as inter-patch dependencies are still satisfied.) (2) I see the NBD_OPT_SET_META_CONTEXT opcode, but I'm not sure what it is used for. There seems to be documentation for <https://libguestfs.org/nbd_opt_list_meta_context.3.html>, but not for nbd_opt_set_meta_context(). So perhaps "introduces" is a better verb than "exposes"? Is NBD_OPT_SET_META_CONTEXT internally used by nbd_add_meta_context() and nbd_clear_meta_contexts()?
it would become actively dangerous: negotiating meta contexts for one export, then switching to a different export name before nbd_opt_go, can lead us into calling nbd_cmd_block_status() when the server is not expecting it.
(3) What is nbd_cmd_block_status()? I can only find nbd_block_status(). Do you mean the NBD extent callback that is called once by metacontext? (4) The word "server" seems like a typo; libnbd is used by NBD clients.
While fixing this, code things to check whether we are actually swapping export names; an idempotent nbd_set_export_name() is not observable by the server, and therefore does not need to invalidate what has already been negotiated.
OK.
Conversely, calling nbd_opt_list_meta_context() does not need to wipe the list - it does not touch h->meta_contexts, but relies on the user's callback instead.
(5) I kind of wish this were a separate patch, but I see the point of keeping it unified, too. (6) I don't understand the "but". I see no conflict (or even: relevance) with the user's callback. Listing the meta contexts does not touch "h->meta_contexts", so we should not clear that list, period. I don't understand what specifically we rely on the user's callback *for*, for this particular discussion. If it's important, please elaborate; otherwise I'm just getting confused! :) The code looks OK to my untrained eye, so I'm ready to R-b that. Thanks! Laszlo
--- generator/states-newstyle-opt-meta-context.c | 7 +++---- lib/handle.c | 4 ++++ 2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/generator/states-newstyle-opt-meta-context.c b/generator/states-newstyle-opt-meta-context.c index 30b9617..76f1032 100644 --- a/generator/states-newstyle-opt-meta-context.c +++ b/generator/states-newstyle-opt-meta-context.c @@ -1,5 +1,5 @@ /* nbd client library in userspace: state machine - * Copyright (C) 2013-2020 Red Hat Inc. + * Copyright (C) 2013-2022 Red Hat Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -28,7 +28,6 @@ STATE_MACHINE { * contexts, when doing SET (but an empty LIST is okay). */ assert (h->gflags & LIBNBD_HANDSHAKE_FLAG_FIXED_NEWSTYLE); - nbd_internal_reset_size_and_flags (h); if (h->opt_current == NBD_OPT_LIST_META_CONTEXT) { assert (h->opt_mode); assert (h->structured_replies); @@ -37,6 +36,8 @@ STATE_MACHINE { } else { assert (CALLBACK_IS_NULL (h->opt_cb.fn.context)); + nbd_internal_reset_size_and_flags (h); + assert (h->meta_contexts == NULL); opt = NBD_OPT_SET_META_CONTEXT; if (!h->structured_replies || h->request_meta_contexts.len == 0) { SET_NEXT_STATE (%^OPT_GO.START); @@ -44,8 +45,6 @@ STATE_MACHINE { } }
- assert (h->meta_contexts == NULL); - /* Calculate the length of the option request data. */ len = 4 /* exportname len */ + strlen (h->export_name) + 4 /* nr queries */; for (i = 0; i < h->request_meta_contexts.len; ++i) diff --git a/lib/handle.c b/lib/handle.c index 8713e18..2aa89b9 100644 --- a/lib/handle.c +++ b/lib/handle.c @@ -219,6 +219,10 @@ nbd_unlocked_set_export_name (struct nbd_handle *h, const char *export_name) return -1; }
+ if (strcmp (export_name, h->export_name) == 0) + return 0; + + nbd_internal_reset_size_and_flags (h); new_name = strdup (export_name); if (!new_name) { set_error (errno, "strdup");
On Wed, Aug 24, 2022 at 01:56:03PM +0200, Laszlo Ersek wrote:
On 08/24/22 04:53, Eric Blake wrote:
The documentation for nbd_internal_reset_size_and_flags() claims we should wipe all knowledge of previously-negotiated meta contexts when swapping export names. However, we weren't actually doing that, which could lead to a user calling nbd_opt_info() for one export, then switching to another export name, then having nbd_get_size() still report the size of the old export. At present, this is only mildly confusing, but once the next patch exposes nbd_opt_set_meta_context() for user control,
(1) The "next patch" does not expose nbd_opt_set_meta_context(). In fact, I can't find nbd_opt_set_meta_context() -- the function -- in the codebase anywhere (@ 9e5bf05a2196).
(I prefer writing "a subsequent patch in this series" rather than "next patch", because that allows me to reshuffle patches without having to rewrite commit messages, as long as inter-patch dependencies are still satisfied.)
Yep, exactly what happened; where the subsequent patch is still not quite polished enough to push to the list (but hopefully later today). Will fix before committing.
(2) I see the NBD_OPT_SET_META_CONTEXT opcode, but I'm not sure what it is used for. There seems to be documentation for <https://libguestfs.org/nbd_opt_list_meta_context.3.html>, but not for nbd_opt_set_meta_context(). So perhaps "introduces" is a better verb than "exposes"?
Is NBD_OPT_SET_META_CONTEXT internally used by nbd_add_meta_context() and nbd_clear_meta_contexts()?
Right now, the code base does: nbd_set_export_name() => update h->export_name; no server interaction nbd_add_meta_context() => append to h->request_meta_contexts; no server interaction nbd_clear_meta_contexts() => wipe all of h->request_meta_contexts; no server interaction nbd_opt_list_meta_context() => Issue NBD_OPT_LIST_META_CONTEXT to the server using h->request_meta_contexts as its query list and h->export_name, but collect the results only in the user's callback - not stateful on the server, and only stateful in the client if the callback does something to preserve the server's answers nbd_opt_info()/nbd_opt_go() => Issue NBD_OPT_SET_META_CONTEXT using h->request_meta_contexts as its query list and h->export_name, collect the results into h->meta_contexts, then issue NBD_OPT_INFO/GO to the server. Stateful, because the server's replies map the users context names (strings) to the servers ids (uint32_t); and later NBD_CMD_BLOCK_STATUS refers only to the server's 32-bit id, even though the libnbd API does not directly expose those ids. nbd_can_meta_context() => Query whether h->meta_contexts contains a server reply for an earlier call that triggered NBD_OPT_SET_META_CONTEXT and which has not been wiped by changing state via nbd_internal_reset_size_and_flags(); no server interaction. with the additional caveat that all the nbd_opt_* functions are unreachable unless you do nbd_set_opt_mode(true) to enable manual option negotiation (the default is that nbd_connect_*() behaves as if it does nbd_opt_go() on your behalf). The promised upcoming patch will add: nbd_set_request_meta_context(false) => Skip the NBD_OPT_SET_META_CONTEXT portion in nbd_opt_info()/nbd_opt_go() nbd_opt_set_meta_context() => Issue NBD_OPT_SET_META_CONTEXT to the server using h->request_meta_contexts as its query list and h->export_name, and collect the results into both the user's callback and h->meta_contexts at the same time. Stateful - wipes out any ids returned by an earlier SET_META_CONTEXT, but the ids returned by the server can survive a number of other NBD_OPT_ commands prior to NBD_OPT_GO, provided that NBD_OPT_GO uses the same export name as NBD_OPT_SET_META_CONTEXT. In that manner, it will become possible for manual option haggling to set the meta contexts at a different point in the negotiation phase, useful when trying to trigger various server corner cases. The server wipes meta context state any time it gets NBD_OPT_SET_META_CONTEXT or NBD_OPT_GO with a different export name than before. But since we don't pass in export name as a direct parameter to either nbd_opt_go() or the upcoming nbd_opt_set_meta_context(), we instead need to wipe the state when we change h->export_name (that is, at nbd_set_export_name()). This does mean that if a client calls nbd_set_export_name(nbd, "a"), nbd_set_meta_context(...), nbd_set_export_name(nbd, "b"), nbd_set_export_name(nbd, "a"), nbd_opt_go(nbd), that the server will not observe the name change, and will actually support the previously-negotiated contexts, but libnbd will have wiped access to those, and trigger client-side failure on attempts to call nbd_block_status() on the grounds that it doesn't remember any negotiated contexts. But a client actually doing this is unlikely.
it would become actively dangerous: negotiating meta contexts for one export, then switching to a different export name before nbd_opt_go, can lead us into calling nbd_cmd_block_status() when the server is not expecting it.
(3) What is nbd_cmd_block_status()? I can only find nbd_block_status(). Do you mean the NBD extent callback that is called once by metacontext?
Typo on my part. The API nbd_block_status() issues NBD_CMD_BLOCK_STATUS to the server.
(4) The word "server" seems like a typo; libnbd is used by NBD clients.
Here, it is intended. The NBD spec says a client should not issue NBD_OPT_BLOCK_STATUS to the server unless it successfully negotiated NBD_OPT_SET_META_CONTEXT with the same export name later passed to NBD_OPT_GO, without intervening commands that might wipe that state. The client code in nbd_block_status() tries to enforce that it won't confuse the server by checking whether h->meta_contexts is non-NULL before it will issue NBD_CMD_BLOCK_STATUS. But once we allow manual control over setting meta contexts, rather than tightly coupling it with NBD_OPT_GO, we need to make sure we wipe h->meta_contexts in at least all places the server wipes state (even if we also happen to wipe it in more places, as in my "a"/set/"b"/"a"/go example).
While fixing this, code things to check whether we are actually swapping export names; an idempotent nbd_set_export_name() is not observable by the server, and therefore does not need to invalidate what has already been negotiated.
OK.
Conversely, calling nbd_opt_list_meta_context() does not need to wipe the list - it does not touch h->meta_contexts, but relies on the user's callback instead.
(5) I kind of wish this were a separate patch, but I see the point of keeping it unified, too.
Maybe I can separate it; the test would be calling nbd_opt_info() to set h->meta_contexts, nbd_can_meta_context() to verify that "base:allocation" is supported by the server, nbd_opt_list_meta_context() to do a query, then nbd_can_meta_context() again to see whether "base:allocation" is still remembered or was accidentally wiped.
(6) I don't understand the "but". I see no conflict (or even: relevance) with the user's callback. Listing the meta contexts does not touch "h->meta_contexts", so we should not clear that list, period. I don't understand what specifically we rely on the user's callback *for*, for this particular discussion. If it's important, please elaborate; otherwise I'm just getting confused! :)
Fair enough. Basically, nbd_opt_list_meta_context() hands the server's answers to the user's callback instead of by modifications to h->meta_contexts, so it does not impact future nbd_can_meta_context(). I'll see if I can reword that a bit.
The code looks OK to my untrained eye, so I'm ready to R-b that.
Thanks! Laszlo
--- generator/states-newstyle-opt-meta-context.c | 7 +++---- lib/handle.c | 4 ++++ 2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/generator/states-newstyle-opt-meta-context.c b/generator/states-newstyle-opt-meta-context.c index 30b9617..76f1032 100644 --- a/generator/states-newstyle-opt-meta-context.c +++ b/generator/states-newstyle-opt-meta-context.c @@ -1,5 +1,5 @@ /* nbd client library in userspace: state machine - * Copyright (C) 2013-2020 Red Hat Inc. + * Copyright (C) 2013-2022 Red Hat Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -28,7 +28,6 @@ STATE_MACHINE { * contexts, when doing SET (but an empty LIST is okay). */ assert (h->gflags & LIBNBD_HANDSHAKE_FLAG_FIXED_NEWSTYLE); - nbd_internal_reset_size_and_flags (h); if (h->opt_current == NBD_OPT_LIST_META_CONTEXT) { assert (h->opt_mode); assert (h->structured_replies); @@ -37,6 +36,8 @@ STATE_MACHINE { } else { assert (CALLBACK_IS_NULL (h->opt_cb.fn.context)); + nbd_internal_reset_size_and_flags (h); + assert (h->meta_contexts == NULL); opt = NBD_OPT_SET_META_CONTEXT; if (!h->structured_replies || h->request_meta_contexts.len == 0) { SET_NEXT_STATE (%^OPT_GO.START); @@ -44,8 +45,6 @@ STATE_MACHINE { } }
- assert (h->meta_contexts == NULL); -
This hunk fixes the nbd_opt_list_meta_context() accidentally wiping context when it doesn't have to,
/* Calculate the length of the option request data. */ len = 4 /* exportname len */ + strlen (h->export_name) + 4 /* nr queries */; for (i = 0; i < h->request_meta_contexts.len; ++i) diff --git a/lib/handle.c b/lib/handle.c index 8713e18..2aa89b9 100644 --- a/lib/handle.c +++ b/lib/handle.c @@ -219,6 +219,10 @@ nbd_unlocked_set_export_name (struct nbd_handle *h, const char *export_name) return -1; }
+ if (strcmp (export_name, h->export_name) == 0) + return 0; + + nbd_internal_reset_size_and_flags (h);
and this hunk fixes the missing wipe (the testsuite coverage so far only covered this case).
new_name = strdup (export_name); if (!new_name) { set_error (errno, "strdup");
-- Eric Blake, Principal Software Engineer Red Hat, Inc. +1-919-301-3266 Virtualization: qemu.org | libvirt.org
On 08/24/22 23:20, Eric Blake wrote:
On Wed, Aug 24, 2022 at 01:56:03PM +0200, Laszlo Ersek wrote:
On 08/24/22 04:53, Eric Blake wrote:
The documentation for nbd_internal_reset_size_and_flags() claims we should wipe all knowledge of previously-negotiated meta contexts when swapping export names. However, we weren't actually doing that, which could lead to a user calling nbd_opt_info() for one export, then switching to another export name, then having nbd_get_size() still report the size of the old export. At present, this is only mildly confusing, but once the next patch exposes nbd_opt_set_meta_context() for user control,
(1) The "next patch" does not expose nbd_opt_set_meta_context(). In fact, I can't find nbd_opt_set_meta_context() -- the function -- in the codebase anywhere (@ 9e5bf05a2196).
(I prefer writing "a subsequent patch in this series" rather than "next patch", because that allows me to reshuffle patches without having to rewrite commit messages, as long as inter-patch dependencies are still satisfied.)
Yep, exactly what happened; where the subsequent patch is still not quite polished enough to push to the list (but hopefully later today). Will fix before committing.
(2) I see the NBD_OPT_SET_META_CONTEXT opcode, but I'm not sure what it is used for. There seems to be documentation for <https://libguestfs.org/nbd_opt_list_meta_context.3.html>, but not for nbd_opt_set_meta_context(). So perhaps "introduces" is a better verb than "exposes"?
Is NBD_OPT_SET_META_CONTEXT internally used by nbd_add_meta_context() and nbd_clear_meta_contexts()?
Right now, the code base does:
nbd_set_export_name() => update h->export_name; no server interaction
nbd_add_meta_context() => append to h->request_meta_contexts; no server interaction
nbd_clear_meta_contexts() => wipe all of h->request_meta_contexts; no server interaction
nbd_opt_list_meta_context() => Issue NBD_OPT_LIST_META_CONTEXT to the server using h->request_meta_contexts as its query list and h->export_name, but collect the results only in the user's callback - not stateful on the server, and only stateful in the client if the callback does something to preserve the server's answers
nbd_opt_info()/nbd_opt_go() => Issue NBD_OPT_SET_META_CONTEXT using h->request_meta_contexts as its query list and h->export_name, collect the results into h->meta_contexts, then issue NBD_OPT_INFO/GO to the server. Stateful, because the server's replies map the users context names (strings) to the servers ids (uint32_t); and later NBD_CMD_BLOCK_STATUS refers only to the server's 32-bit id, even though the libnbd API does not directly expose those ids.
nbd_can_meta_context() => Query whether h->meta_contexts contains a server reply for an earlier call that triggered NBD_OPT_SET_META_CONTEXT and which has not been wiped by changing state via nbd_internal_reset_size_and_flags(); no server interaction.
with the additional caveat that all the nbd_opt_* functions are unreachable unless you do nbd_set_opt_mode(true) to enable manual option negotiation (the default is that nbd_connect_*() behaves as if it does nbd_opt_go() on your behalf).
Thanks for writing this up; it's a lot to digest. I think I understand it now. There's a whole lot of state here, which is something I don't really like. (I don't know the design background however.) FWIW I've found this stuff easier to understand by thinking about it in terms of requests/responses, and by considering the client-side state only a "lightweight state", solely existing for implementing the request/response patterns. In other words, I kind of consider the client-side state "ad hoc" (whereas the server-side state is heavy-weight state). The overlap between nbd_opt_list_meta_context() and nbd_opt_info() is especially confusing. For the following reasons: - both functions perform the same listing at the base level (build an intersection between the client's desired meta context list and the server's supported meta context list) - the intersection is consumed differently on the client (the first function reports results through a callback, the second one requires subsequent, repeated calls to nbd_can_meta_context()). - the first function is stateless (considering only the server-side state "state" here), the second is stateful. While this is well reflected by the opcode names LIST vs. SET, those opcodes are hidden from the function names. In particular, the second function says "info" in the name, which I would not expect to change state on the server. - NBD_OPT_INFO in particular looks like a useless opcode. NBD_OPT_SET_META_CONTEXT handles the filtering (intersection) and mapping to numeric IDs. NBD_OPT_GO transitions the server to the "ready" state (completes negotiation). So those two look useful. I don't understand what the precise responsibility is that *only* NBD_OPT_INFO can cover. The manual at <https://libguestfs.org/nbd_opt_info.3.html> says that nbd_opt_info() enables export size querying (for example), but that kind of querying is just as possible without nbd_opt_info(); only nbd_set_opt_mode() is needed -- see the example code at <https://libguestfs.org/nbd_set_opt_mode.3.html>.
The promised upcoming patch will add:
nbd_set_request_meta_context(false) => Skip the NBD_OPT_SET_META_CONTEXT portion in nbd_opt_info()/nbd_opt_go()
This makes sense for nbd_opt_go(), as it will then retain a single responsibility: transitioning the server to the ready state. However, with SET_META_CONTEXT skipped, nbd_opt_go() appears to be reduced to sending the NBD_OPT_INFO opcode -- and that one looks entirely useless to me (see above).
nbd_opt_set_meta_context() => Issue NBD_OPT_SET_META_CONTEXT to the server using h->request_meta_contexts as its query list and h->export_name, and collect the results into both the user's callback and h->meta_contexts at the same time. Stateful - wipes out any ids returned by an earlier SET_META_CONTEXT, but the ids returned by the server can survive a number of other NBD_OPT_ commands prior to NBD_OPT_GO, provided that NBD_OPT_GO uses the same export name as NBD_OPT_SET_META_CONTEXT.
Right, so nbd_opt_set_meta_context() is the "first half" extracted from the current behavior of nbd_opt_info()/nbd_opt_go(). (Again with my personal confusion that the "second half" of nbd_opt_info(), i.e., the sending of the NB_OPT_INFO opcode, looks useless.)
In that manner, it will become possible for manual option haggling to set the meta contexts at a different point in the negotiation phase, useful when trying to trigger various server corner cases.
The server wipes meta context state any time it gets NBD_OPT_SET_META_CONTEXT or NBD_OPT_GO with a different export name than before. But since we don't pass in export name as a direct parameter to either nbd_opt_go() or the upcoming nbd_opt_set_meta_context(), we instead need to wipe the state when we change h->export_name (that is, at nbd_set_export_name()). This does mean that if a client calls nbd_set_export_name(nbd, "a"), nbd_set_meta_context(...), nbd_set_export_name(nbd, "b"), nbd_set_export_name(nbd, "a"), nbd_opt_go(nbd), that the server will not observe the name change, and will actually support the previously-negotiated contexts, but libnbd will have wiped access to those, and trigger client-side failure on attempts to call nbd_block_status() on the grounds that it doesn't remember any negotiated contexts. But a client actually doing this is unlikely.
So this is the problem with stateful protocols; we need to remember and recognize the points at which the state goes stale and must be cleared. I wonder why the NBD protocol was not designed to take as many parameters as possible per-request (minimizing the state in both client and server), and only turn the kind of information into actual "state" that is required by the *bulk* operations (basically the block-level operations on the device, such as read, write, zero/trim, etc) where the parameter repetitions would have significant bandwidth and computation (formatting/parsing) cost. The cost of the negotiation phase looks trivial in comparison to the cost of the "ready state" work (IIRC the "ready state"); all the cached information does not seem to buy us much.
it would become actively dangerous: negotiating meta contexts for one export, then switching to a different export name before nbd_opt_go, can lead us into calling nbd_cmd_block_status() when the server is not expecting it.
(3) What is nbd_cmd_block_status()? I can only find nbd_block_status(). Do you mean the NBD extent callback that is called once by metacontext?
Typo on my part. The API nbd_block_status() issues NBD_CMD_BLOCK_STATUS to the server.
(4) The word "server" seems like a typo; libnbd is used by NBD clients.
Here, it is intended. The NBD spec says a client should not issue NBD_OPT_BLOCK_STATUS to the server unless it successfully negotiated NBD_OPT_SET_META_CONTEXT with the same export name later passed to NBD_OPT_GO, without intervening commands that might wipe that state. The client code in nbd_block_status() tries to enforce that it won't confuse the server by checking whether h->meta_contexts is non-NULL before it will issue NBD_CMD_BLOCK_STATUS. But once we allow manual control over setting meta contexts, rather than tightly coupling it with NBD_OPT_GO, we need to make sure we wipe h->meta_contexts in at least all places the server wipes state (even if we also happen to wipe it in more places, as in my "a"/set/"b"/"a"/go example).
OK. The wording of your commit message ("actively dangerous") confused me, as I didn't expect the client to be able to break the server (for any definition of "break"). However, if the spec uses the "SHOULD NOT" term, then preventing that sequence of operations in the library makes sense.
While fixing this, code things to check whether we are actually swapping export names; an idempotent nbd_set_export_name() is not observable by the server, and therefore does not need to invalidate what has already been negotiated.
OK.
Conversely, calling nbd_opt_list_meta_context() does not need to wipe the list - it does not touch h->meta_contexts, but relies on the user's callback instead.
(5) I kind of wish this were a separate patch, but I see the point of keeping it unified, too.
Maybe I can separate it; the test would be calling nbd_opt_info() to set h->meta_contexts, nbd_can_meta_context() to verify that "base:allocation" is supported by the server, nbd_opt_list_meta_context() to do a query, then nbd_can_meta_context() again to see whether "base:allocation" is still remembered or was accidentally wiped.
.... Again, anything around nbd_opt_info() keeps confusing me. What we actually need here is the NBD_OPT_SET_META_CONTEXT. Currently, we can do that in two ways, with nbd_opt_info() and nbd_opt_go(). The latter is no good, as it kicks us out of the negotiation phase. The former I *dislike*, because it does something *in addition* to NBD_OPT_SET_META_CONTEXT that I don't understand -- namely the NBD_OPT_INFO opcode. Therefore, the new function nbd_opt_set_meta_context() would be *just right* for this step, as it exposes NBD_OPT_SET_META_CONTEXT (and the population of h->meta_contexts) in isilation. Perhaps introduce the new function before updating the test case?
(6) I don't understand the "but". I see no conflict (or even: relevance) with the user's callback. Listing the meta contexts does not touch "h->meta_contexts", so we should not clear that list, period. I don't understand what specifically we rely on the user's callback *for*, for this particular discussion. If it's important, please elaborate; otherwise I'm just getting confused! :)
Fair enough. Basically, nbd_opt_list_meta_context() hands the server's answers to the user's callback instead of by modifications to h->meta_contexts, so it does not impact future nbd_can_meta_context(). I'll see if I can reword that a bit.
Upon re-reading the commit message, after your explanation, I think the "but" is fine, but benefits from a bit of expanding: Conversely, calling nbd_opt_list_meta_context() does not need to wipe the list - it does not touch h->meta_contexts, but relies on the user's callback instead, *for collecting the list of those metacontexts that the server acknowledges*. How does it sound? Laszlo
The code looks OK to my untrained eye, so I'm ready to R-b that.
Thanks! Laszlo
--- generator/states-newstyle-opt-meta-context.c | 7 +++---- lib/handle.c | 4 ++++ 2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/generator/states-newstyle-opt-meta-context.c b/generator/states-newstyle-opt-meta-context.c index 30b9617..76f1032 100644 --- a/generator/states-newstyle-opt-meta-context.c +++ b/generator/states-newstyle-opt-meta-context.c @@ -1,5 +1,5 @@ /* nbd client library in userspace: state machine - * Copyright (C) 2013-2020 Red Hat Inc. + * Copyright (C) 2013-2022 Red Hat Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -28,7 +28,6 @@ STATE_MACHINE { * contexts, when doing SET (but an empty LIST is okay). */ assert (h->gflags & LIBNBD_HANDSHAKE_FLAG_FIXED_NEWSTYLE); - nbd_internal_reset_size_and_flags (h); if (h->opt_current == NBD_OPT_LIST_META_CONTEXT) { assert (h->opt_mode); assert (h->structured_replies); @@ -37,6 +36,8 @@ STATE_MACHINE { } else { assert (CALLBACK_IS_NULL (h->opt_cb.fn.context)); + nbd_internal_reset_size_and_flags (h); + assert (h->meta_contexts == NULL); opt = NBD_OPT_SET_META_CONTEXT; if (!h->structured_replies || h->request_meta_contexts.len == 0) { SET_NEXT_STATE (%^OPT_GO.START); @@ -44,8 +45,6 @@ STATE_MACHINE { } }
- assert (h->meta_contexts == NULL); -
This hunk fixes the nbd_opt_list_meta_context() accidentally wiping context when it doesn't have to,
/* Calculate the length of the option request data. */ len = 4 /* exportname len */ + strlen (h->export_name) + 4 /* nr queries */; for (i = 0; i < h->request_meta_contexts.len; ++i) diff --git a/lib/handle.c b/lib/handle.c index 8713e18..2aa89b9 100644 --- a/lib/handle.c +++ b/lib/handle.c @@ -219,6 +219,10 @@ nbd_unlocked_set_export_name (struct nbd_handle *h, const char *export_name) return -1; }
+ if (strcmp (export_name, h->export_name) == 0) + return 0; + + nbd_internal_reset_size_and_flags (h);
and this hunk fixes the missing wipe (the testsuite coverage so far only covered this case).
new_name = strdup (export_name); if (!new_name) { set_error (errno, "strdup");
On Thu, Aug 25, 2022 at 12:13:42PM +0200, Laszlo Ersek wrote: [I wrote my reply out-of-order in various spurts, apologies if I repeat myself, or if I make an argument earlier in the mail that doesn't make sense until you read more background later in the mail] ...
Thanks for writing this up; it's a lot to digest. I think I understand it now.
There's a whole lot of state here, which is something I don't really like. (I don't know the design background however.) FWIW I've found this stuff easier to understand by thinking about it in terms of requests/responses, and by considering the client-side state only a "lightweight state", solely existing for implementing the request/response patterns. In other words, I kind of consider the client-side state "ad hoc" (whereas the server-side state is heavy-weight state).
The design background does play a big role. When we first started libnbd, we did not have an easy way to write an API that would take a string vector as a single argument that the generator could then translate nicely into C, OCaml, and Python APIs (this was before we later added golang translations). So instead of having a single API: nbd_connect_with_contexts(uri, vector_of_contexts) it was thus easier given the limitation of our generator at the time to have: nbd_add_meta_context(a) [appends to h->request_meta_contexts] nbd_add_meta_context(b) [appends to h->request_meta_contexts] nbd_connect(uri) [implicitly uses h->request_meta_contexts from earlier adds] And we definitely don't have a clean way for a single API to produce a direct output of a string vector (easy in Python, but hard in C); our approach there was initially using client-side state (build the list internally to the nbd_handle, let the user query it after the fact: nbd_connect_* populates h->meta_contexts followed by nbd_can_meta_context() querying it), although it has also proven viable to utilize a callback function (let the user build their own vector as we pass in one string at a time: nbd_opt_list() nbd_opt_list_meta_context() both use the same callback signature). Also, when we first started, libnbd did everything in nbd_connect_* from opening the socket to NBD_OPT_GO; you could only fine-tune the handshake by what was set prior to nbd_connect_. In libnbd 1.4, we added nbd_set_opt_mode and nbd_opt_* to try and make it more fine-tuned, giving more control to a power user over aspects of handshaking (most obvious is nbdinfo that wants to run NBD_OPT_LIST to learn the list of available exports, then NBD_OPT_INFO and NBD_OPT_SET/LIST_META_CONTEXT on each of those exports in turn), while still keeping the common case simple (if you already know your connection name, nbd_add_meta_context("base:allocation") + nbd_connect_uri(uri) going all the way to NBD_OPT_GO really is nicer than forcing you to spell out all the individual steps). So backwards-compatibility is an issue - the older an API is, the more steps it tends to do by default; we have been introducing new API to break it into smaller independent chunks where that is useful, but still have to keep the old complexity in place to not break existing code bases. So this is client-side state in the reverse direction - now we have lots of APIs for each knob we have added over the years to break large APIs into smaller tasks (see nbd_set_request_structured_replies, nbd_get_request_structured_replies, as well as my proposal to add nbd_set_request_meta_context, ...), which have nothing to do with server state but do impact how other nbd_* APIs will behave. Over time, our generator has gotten better (we've added support for more parameter types), so it may be worth adding API(s) that take the context name list as explicit parameters rather than reusing the implicit list registered one-at-a-time. Where we currently have: nbd_add_meta_context(a) nbd_add_meta_context(b) nbd_opt_list_meta_context(export, callback) it may indeed be worth adding: nbd_opt_list_meta_context_from_list(export, vector, callback) that uses an explicit vector of queries rather than the implied vector previously set by nbd_add_meta_context(). At the same time, our set of nbd_connect_* calls has grown, so adding yet another variant to each of those that takes a context vector may be easy in the generator, but scale exponentially in the number of APIs added and needing unit tests. You are correct that h->request_meta_contexts is light-weight client state (our hack to work around the lack of a single vector argument, when passing a query to NBD_OPT_LIST_META_CONTEXT or NBD_OPT_SET_META_CONTEXT), while h->meta_contexts is more heavy-weight (both the client and the server must agree on the same name-to-id mapping of which contexts were set by the last successful NBD_OPT_SET_META_CONTEXT and not wiped out by other state-clearing commands like NBD_OPT_STARTTLS or NBD_OPT_GO with a different export name; because that name-to-id mapping is essential to later NBD_CMD_BLOCK_STATUS). Thus, we have several conflated issues. - Can we reduce ad hoc client state? Yes, if we extend the generator and add new APIs that take the query vector as a direct argument instead of the indirect h->request_meta_contexts, and/or provide a callback argument to let the caller build up their own reply vector instead of having to query an internal result vector after the fact (so far, not something I have tried, but now you have me curious on whether it is worth it). - Can we get more fine-grained control over negotiation? Yes, nbd_opt_info() and nbd_opt_go() currently default to a sequence of 2 server commands (NBD_OPT_SET_META_CONTEXT, NBD_OPT_INFO/GO), but v2 of this series will add nbd_set_request_meta_context() which cuts out the first half, as well as nbd_opt_set_meta_context() which runs the first half in isolation. - Are we properly tracking when server state changes? No - in some cases we weren't clearing state even when the server does (teach nbd_set_export_name() to clear state); in other cases we clear state even though we don't have to (teach nbd_opt_list_meta_context() to not clear state). v2 is taking me more time to polish than I thought, because I've been trying to break it down into even more individual fixes with accompanying tests, as well as finish the API additions, but I'm liking the direction your review has set me on. In particular, I've noticed that there are really two different groups of client states to track after server interaction: export information (nbd_get_size, nbd_is_read_only, nbd_can_trim, ...) - gated by h->eflags being non-zero, but tracked in multiple variables such as h->eflags, h->exportsize, h->block_minimum, ... - should be made valid by successful NBD_OPT_INFO or NBD_OPT_GO - should be wiped by: - failed NBD_OPT_INFO or NBD_OPT_GO - any NBD_OPT_STARTTLS (but I have to add nbd_opt_starttls to allow that much fine-grained control; on my list of things to add) - nbd_set_export_name() changing names (not directly caused by server state, but because of how many of our interfaces implicitly reuse h->export_name, and where the server would return different state for NBD_OPT_INFO on a different name; currently broken, v1 tried to fix it) - should persist over: - NBD_OPT_LIST_META_CONTEXT (this is stateless in the server, so nbd_opt_list_meta_context() should be stateless in the client; currently broken, v1 tried to fix it) - NBD_OPT_SET_META_CONTEXT (setting contexts does not change the export size, so my new nbd_opt_set_meta_context() should not wipe it) context mappings (nbd_can_meta_context) - currently gated by h->eflags being non-zero, but tracked by h->meta_contexts (v2 will change the gating) - should be made valid (returns 0/1 for all names, according to h->meta_contexts lookup) by: - successful NBD_OPT_SET_META_CONTEXT - if nbd_set_request_meta_context() is true (default once the API is added), a successful nbd_opt_info() or nbd_opt_go() that was unable to set contexts (server was oldstyle, or structured replies are lacking, or client didn't register any names with nbd_add_meta_context; so h->meta_contexts is empty) - should be wiped (returns -1 for all names) by: - failed NBD_OPT_SET_META_CONTEXT (regardless if triggered by nbd_opt_go() or by my new nbd_opt_set_meta_context(), currently broken and unaddressed in v1, see also [1] below) - any NBD_OPT_STARTTLS - nbd_set_export_name() changing names (same story as export name change wiping export information: currently broken, v1 tried to fix it) - should persist over: - bare NBD_OPT_GO, NBD_OPT_LIST (untestable until adding nbd_set_request_meta_context(false) to teach nbd_opt_info() and nbd_opt_go() to skip their first half) - NBD_OPT_LIST_META_CONTEXT (currently broken, v1 tried to fix it)
The overlap between nbd_opt_list_meta_context() and nbd_opt_info() is especially confusing. For the following reasons:
- both functions perform the same listing at the base level (build an intersection between the client's desired meta context list and the server's supported meta context list)
- the intersection is consumed differently on the client (the first function reports results through a callback, the second one requires subsequent, repeated calls to nbd_can_meta_context()).
nbd_can_meta_context() is probing the name-to-id table returned by NBD_OPT_SET_META_CONTEXT; but NBD_OPT_LIST_META_CONTEXT does not provide a name-to-id table (the NBD spec says that LIST should use an id of 0 for everything, partly because only the LIST form supports wildcards in both client and server directions; note that a wildcard has no valid id, but 0 can be a valid id under SET).
- the first function is stateless (considering only the server-side state "state" here), the second is stateful. While this is well reflected by the opcode names LIST vs. SET, those opcodes are hidden from the function names. In particular, the second function says "info" in the name, which I would not expect to change state on the server.
Maybe this will help. NBD_OPT_LIST_META_CONTEXT says "what context names does the server support, where the server can disregard the export name if that makes sense, and where both directions can use wildcards to allow the server to advertise a family of export names and the client to ask for more details within that family". With qemu-nbd as server, LIST_META_CONTEXT([], "") (the empty vector) might currently cause the server to reply with ["base:allocation", "qemu:dirty-bitmap:a", "qemu:dirty-bitmap:b"] (all the contexts it specifically supports on the export ""), while LIST_META_CONTEXT(["qemu:"], "") would only reply with ["qemu:dirty-bitmap:a", "qemu:dirty-bitmap:b"]. But it would also have been possible for qemu-nbd to reply to the empty vector query with ["base:allocation", "qemu:"] ("base:allocation" is supported on all exports, regardless of name, but instead of spelling out individual "qemu:..." contexts available for the specific context, returning the wildcard "qemu:" means that the client must do a further LIST_META_CONTEXT to learn about specific contexts for a given export). Conversely, NBD_OPT_SET_META_CONTEXT does not use wildcards, in either the client or the server direction. It is documented as strictly "the client wants these contexts for this export; the server must supply a context-to-id mapping for the ones it supports, and that mapping is then valid until the next NBD_OPT_SET_META_CONTEXT, NBD_OPT_STARTTLS, or use of NBD_OPT_GO with a different export name". The nbd_opt_list() function is trying to learn as much information about a specific export: what it its size and readonly status (from NBD_OPT_LIST), and what contexts can it support (here, NBD_OPT_SET_META_CONTEXT guarantees correct answers, but is rather heavy-handed in setting server state; NBD_OPT_LIST_META_CONTEXT might return wildcards instead of exact answers, but for nbd_opt_info(), that's probably good enough). Then again, since I wrote it without a callback function, the only way to query which contexts is via nbd_can_meta_context(), which requires the name-to-id mapping, which we get from NBD_OPT_SET_META_CONTEXT. Of course, once I add nbd_set_request_meta_context(), we can skip the NBD_OPT_SET_META_CONTEXT done by nbd_opt_info(), and let the user manually use nbd_opt_list_meta_context() for its own stateless NBD_OPT_LIST_META_CONTEXT query of the supported contexts. So it may also be worth a new API: nbd_opt_info_with_contexts(export_name, vector_of_queries, callback) to be used in place of nbd_add_meta_context(context) nbd_opt_info(export_name) nbd_can_meta_context(context) for less stateful pollution.
- NBD_OPT_INFO in particular looks like a useless opcode.
Useless when you already know what export you want to connect to. But useful during the 'nbdinfo' app when you want to learn as much as possible about every export exposed by a single nbd server, while still remaining in negotiation after each export thus queried.
NBD_OPT_SET_META_CONTEXT handles the filtering (intersection) and mapping to numeric IDs. NBD_OPT_GO transitions the server to the "ready" state (completes negotiation). So those two look useful. I don't understand what the precise responsibility is that *only* NBD_OPT_INFO can cover. The manual at <https://libguestfs.org/nbd_opt_info.3.html> says that nbd_opt_info() enables export size querying (for example), but that kind of querying is just as possible without nbd_opt_info(); only nbd_set_opt_mode() is needed -- see the example code at <https://libguestfs.org/nbd_set_opt_mode.3.html>.
That example is only able to print what NBD_OPT_LIST returns (the export name, and any description the server provides) - some servers might provide the export size in the human-readable description, but the only way to get a machine-readable readout of each size is to further call NBD_OPT_INFO on each name returned by NBD_OPT_LIST. nbd_set_opt_mode() is what lets it be possible to call nbd_opt_list(), but it still requires nbd_opt_list() before you can use nbd_get_size() while still remaining in negotiation mode.
The promised upcoming patch will add:
nbd_set_request_meta_context(false) => Skip the NBD_OPT_SET_META_CONTEXT portion in nbd_opt_info()/nbd_opt_go()
This makes sense for nbd_opt_go(), as it will then retain a single responsibility: transitioning the server to the ready state.
However, with SET_META_CONTEXT skipped, nbd_opt_go() appears to be reduced to sending the NBD_OPT_INFO opcode -- and that one looks entirely useless to me (see above).
The state machine for OPT_GO handles both NBD_OPT_INFO and NBD_OPT_GO under the hood (the two have identical query and response layouts, the only real difference is that the choice of opcode determines whether the state machine transitions on to transmission phase or remains back in negotiation phase). Similarly, the state machine for OPT_META_CONTEXT handles both NBD_OPT_LIST_META_CONTEXT and NBD_OPT_SET_META_CONTEXT (same query and response layouts).
nbd_opt_set_meta_context() => Issue NBD_OPT_SET_META_CONTEXT to the server using h->request_meta_contexts as its query list and h->export_name, and collect the results into both the user's callback and h->meta_contexts at the same time. Stateful - wipes out any ids returned by an earlier SET_META_CONTEXT, but the ids returned by the server can survive a number of other NBD_OPT_ commands prior to NBD_OPT_GO, provided that NBD_OPT_GO uses the same export name as NBD_OPT_SET_META_CONTEXT.
Right, so nbd_opt_set_meta_context() is the "first half" extracted from the current behavior of nbd_opt_info()/nbd_opt_go(). (Again with my personal confusion that the "second half" of nbd_opt_info(), i.e., the sending of the NB_OPT_INFO opcode, looks useless.)
For nbd_opt_info(), the second half is NBD_OPT_INFO; for nbd_opt_go(), the second half is NBD_OPT_GO, but both use the same state machine for both halves. NBD_OPT_INFO is useless if you want to do reads or writes from the export, but useful if you want to merely learn information about what multiple exports that the server is letting you choose from.
In that manner, it will become possible for manual option haggling to set the meta contexts at a different point in the negotiation phase, useful when trying to trigger various server corner cases.
The server wipes meta context state any time it gets NBD_OPT_SET_META_CONTEXT or NBD_OPT_GO with a different export name than before. But since we don't pass in export name as a direct parameter to either nbd_opt_go() or the upcoming nbd_opt_set_meta_context(), we instead need to wipe the state when we change h->export_name (that is, at nbd_set_export_name()). This does mean that if a client calls nbd_set_export_name(nbd, "a"), nbd_set_meta_context(...), nbd_set_export_name(nbd, "b"), nbd_set_export_name(nbd, "a"), nbd_opt_go(nbd), that the server will not observe the name change, and will actually support the previously-negotiated contexts, but libnbd will have wiped access to those, and trigger client-side failure on attempts to call nbd_block_status() on the grounds that it doesn't remember any negotiated contexts. But a client actually doing this is unlikely.
So this is the problem with stateful protocols; we need to remember and recognize the points at which the state goes stale and must be cleared. I wonder why the NBD protocol was not designed to take as many parameters as possible per-request (minimizing the state in both client and server), and only turn the kind of information into actual "state" that is required by the *bulk* operations (basically the block-level operations on the device, such as read, write, zero/trim, etc) where the parameter repetitions would have significant bandwidth and computation (formatting/parsing) cost. The cost of the negotiation phase looks trivial in comparison to the cost of the "ready state" work (IIRC the "ready state"); all the cached information does not seem to buy us much.
We have been trying to keep the NBD protocol to track as little state as possible. However, at the time we added NBD_CMD_BLOCK_STATUS, the protocol did not have a way to indicate payload size on transmission requests, so the solution in the protocol was to have a single context-to-id stateful mapping set during negotiation by NBD_OPT_SET_META_CONTEXT that is then used for all subsequent NBD_CMD_BLOCK_STATUS; if you negotiate multiple contexts, the server is then obligated to give you an answer for each of those contexts on every block status request, even if your reason for calling block status was to only find out information on just one of those contexts (then again, many clients only negotiate a single context of "base:allocation", so for those clients, it's not as onerous as it sounds). Part of my (ongoing) work at adding 64-bit extensions to the NBD protocol is extending the transmission phase to actually advertise payload lengths from the client request, at which point it DOES become possible to have a variant of NBD_CMD_BLOCK_STATUS where the client can ask for just one context on a given block status request, even though both the client and server are aware of more contexts being available. But whether that filtering should be in the form of passing in a string name (that the server must parse on every NBD_CMD_BLOCK_STATUS which includes a filter payload), or whether the server should rely on its name-to-id mapping set up once during NBD_OPT_SET_META_CONTEXT, is still something we could debate when I post my 64-bit extension patches.
it would become actively dangerous: negotiating meta contexts for one export, then switching to a different export name before nbd_opt_go, can lead us into calling nbd_cmd_block_status() when the server is not expecting it.
(3) What is nbd_cmd_block_status()? I can only find nbd_block_status(). Do you mean the NBD extent callback that is called once by metacontext?
Typo on my part. The API nbd_block_status() issues NBD_CMD_BLOCK_STATUS to the server.
(4) The word "server" seems like a typo; libnbd is used by NBD clients.
Here, it is intended. The NBD spec says a client should not issue NBD_OPT_BLOCK_STATUS to the server unless it successfully negotiated NBD_OPT_SET_META_CONTEXT with the same export name later passed to NBD_OPT_GO, without intervening commands that might wipe that state. The client code in nbd_block_status() tries to enforce that it won't confuse the server by checking whether h->meta_contexts is non-NULL before it will issue NBD_CMD_BLOCK_STATUS. But once we allow manual control over setting meta contexts, rather than tightly coupling it with NBD_OPT_GO, we need to make sure we wipe h->meta_contexts in at least all places the server wipes state (even if we also happen to wipe it in more places, as in my "a"/set/"b"/"a"/go example).
OK. The wording of your commit message ("actively dangerous") confused me, as I didn't expect the client to be able to break the server (for any definition of "break"). However, if the spec uses the "SHOULD NOT" term, then preventing that sequence of operations in the library makes sense.
[1] As part of my v2 patch, I have written a 2-line nbdkit hack patch that makes nbdkit behave as a server that exposes yet another pre-existing bug in libnbd: if NBD_OPT_SET_META_CONTEXT starts replying with a valid name-to-id mapping (NBD_REP_META_CONTEXT), then fails the overall command for an unrelated reason (NBD_REP_ERR...) rather than concluding with NBD_REP_ACK; the NBD spec says that in this case, the client should assume no name-to-id mapping is valid and NBD_CMD_BLOCK_STATUS should not be attempted; but current libnbd forgets to clear h->meta_contexts state. As mentioned in my musings above about export state being different from context state, it is nice that nbd_can_meta_context() normally returns 0 after nbd_opt_go() for all unknown names, even for a server that lacked NBD_OPT_SET_META_CONTEXT; but I think it makes sense to have nbd_can_meta_context() actively return -1 for all names even after nbd_opt_go() if NBD_OPT_SET_META_CONTEXT failed for any reason other than NBD_REP_ERR_UNSUP (this is a rare server corner case that does not crop up in normal practice - but is worth diagnosing if it does, as with my 2-line hacked nbdkit). It also means I'm also leaning towards: nbd_set_opt_mode(true) nbd_set_request_meta_context(false) nbd_connect_...() nbd_opt_go() nbd_can_meta_context(anything) => -1 where we explicitly indicate that the user asked to take control over context negotiation, but then failed to call nbd_opt_set_meta_context(), and therefore nbd_can_meta_context() should be an error rather than successfully claiming false.
While fixing this, code things to check whether we are actually swapping export names; an idempotent nbd_set_export_name() is not observable by the server, and therefore does not need to invalidate what has already been negotiated.
OK.
Conversely, calling nbd_opt_list_meta_context() does not need to wipe the list - it does not touch h->meta_contexts, but relies on the user's callback instead.
(5) I kind of wish this were a separate patch, but I see the point of keeping it unified, too.
Maybe I can separate it; the test would be calling nbd_opt_info() to set h->meta_contexts, nbd_can_meta_context() to verify that "base:allocation" is supported by the server, nbd_opt_list_meta_context() to do a query, then nbd_can_meta_context() again to see whether "base:allocation" is still remembered or was accidentally wiped.
.... Again, anything around nbd_opt_info() keeps confusing me.
What we actually need here is the NBD_OPT_SET_META_CONTEXT. Currently, we can do that in two ways, with nbd_opt_info() and nbd_opt_go(). The latter is no good, as it kicks us out of the negotiation phase. The former I *dislike*, because it does something *in addition* to NBD_OPT_SET_META_CONTEXT that I don't understand -- namely the NBD_OPT_INFO opcode.
Therefore, the new function nbd_opt_set_meta_context() would be *just right* for this step, as it exposes NBD_OPT_SET_META_CONTEXT (and the population of h->meta_contexts) in isilation. Perhaps introduce the new function before updating the test case?
I had originally planned nbd_set_request_meta_context() and nbd_opt_set_meta_context() to be added in the same patch, but in what I've written above, I now have sufficient test-case proof that I can separate those API additions into separate patches for easier review of incremental improvements.
(6) I don't understand the "but". I see no conflict (or even: relevance) with the user's callback. Listing the meta contexts does not touch "h->meta_contexts", so we should not clear that list, period. I don't understand what specifically we rely on the user's callback *for*, for this particular discussion. If it's important, please elaborate; otherwise I'm just getting confused! :)
Fair enough. Basically, nbd_opt_list_meta_context() hands the server's answers to the user's callback instead of by modifications to h->meta_contexts, so it does not impact future nbd_can_meta_context(). I'll see if I can reword that a bit.
Upon re-reading the commit message, after your explanation, I think the "but" is fine, but benefits from a bit of expanding:
Conversely, calling nbd_opt_list_meta_context() does not need to wipe the list - it does not touch h->meta_contexts, but relies on the user's callback instead, *for collecting the list of those metacontexts that the server acknowledges*.
How does it sound?
If that wording survives anywhere into my (larger) v2 series, I like your improved wording and will utilize it. -- Eric Blake, Principal Software Engineer Red Hat, Inc. +1-919-301-3266 Virtualization: qemu.org | libvirt.org
On 08/25/22 17:05, Eric Blake wrote:
On Thu, Aug 25, 2022 at 12:13:42PM +0200, Laszlo Ersek wrote:
[...]
The design background does play a big role. When we first started libnbd, we did not have an easy way to write an API that would take a string vector as a single argument that the generator could then translate nicely into C, OCaml, and Python APIs (this was before we later added golang translations).
Thanks for the explanation. I didn't expect these two principles to have driven the design (generability, and long-term convenience calls). I think the more usual (albeit likely less programmer-friendly) approach is to (a) expose the minute details and let the end-programmer deal with them, (b) let people write / maintain other-than-C language wrappers manually.
noticed that there are really two different groups of client states to track after server interaction:
export information (nbd_get_size, nbd_is_read_only, nbd_can_trim, ...) [...] context mappings (nbd_can_meta_context) [...]
This is a huge amount of nuances -- I remember being in a position where, as a long-term maintainer, I could collect, or at least understand, this many bits intuitively, but that doesn't work for me as a newcomer to a project. So please try to write surgical, super-focused patches, if I'm to have a chance at reviewing them. :)
Maybe this will help. NBD_OPT_LIST_META_CONTEXT says "what context names does the server support, where the server can disregard the export name if that makes sense, and where both directions can use wildcards to allow the server to advertise a family of export names and the client to ask for more details within that family". With qemu-nbd as server, LIST_META_CONTEXT([], "") (the empty vector) might currently cause the server to reply with ["base:allocation", "qemu:dirty-bitmap:a", "qemu:dirty-bitmap:b"] (all the contexts it specifically supports on the export ""), while LIST_META_CONTEXT(["qemu:"], "") would only reply with ["qemu:dirty-bitmap:a", "qemu:dirty-bitmap:b"]. But it would also have been possible for qemu-nbd to reply to the empty vector query with ["base:allocation", "qemu:"] ("base:allocation" is supported on all exports, regardless of name, but instead of spelling out individual "qemu:..." contexts available for the specific context, returning the wildcard "qemu:" means that the client must do a further LIST_META_CONTEXT to learn about specific contexts for a given export).
I don't understand why this is useful, as a part of the protocol. It seems difficult to program a robust client if the server is permitted to respond in so different styles! Maybe the output of this operation was not (primarily) meant to be consumed by a program (but humans that are happy to iterate and to construct new queries on the spot).
Conversely, NBD_OPT_SET_META_CONTEXT does not use wildcards, in either the client or the server direction. It is documented as strictly "the client wants these contexts for this export; the server must supply a context-to-id mapping for the ones it supports, and that mapping is then valid until the next NBD_OPT_SET_META_CONTEXT, NBD_OPT_STARTTLS, or use of NBD_OPT_GO with a different export name".
Hmm, OK.
The nbd_opt_list() function is trying to learn as much information about a specific export: what it its size and readonly status (from NBD_OPT_LIST), and what contexts can it support (here, NBD_OPT_SET_META_CONTEXT guarantees correct answers, but is rather heavy-handed in setting server state; NBD_OPT_LIST_META_CONTEXT might return wildcards instead of exact answers, but for nbd_opt_info(), that's probably good enough). Then again, since I wrote it without a callback function, the only way to query which contexts is via nbd_can_meta_context(), which requires the name-to-id mapping, which we get from NBD_OPT_SET_META_CONTEXT.
Of course, once I add nbd_set_request_meta_context(), we can skip the NBD_OPT_SET_META_CONTEXT done by nbd_opt_info(), and let the user manually use nbd_opt_list_meta_context() for its own stateless NBD_OPT_LIST_META_CONTEXT query of the supported contexts.
So it may also be worth a new API:
nbd_opt_info_with_contexts(export_name, vector_of_queries, callback)
to be used in place of
nbd_add_meta_context(context) nbd_opt_info(export_name) nbd_can_meta_context(context)
for less stateful pollution.
Given that we need to keep the old APIs for compatibility's sake, I think new APIs should not be introduced unless they add a new capability. There is already a multitude of APIs.
- NBD_OPT_INFO in particular looks like a useless opcode.
Useless when you already know what export you want to connect to. But useful during the 'nbdinfo' app when you want to learn as much as possible about every export exposed by a single nbd server, while still remaining in negotiation after each export thus queried.
Such "discovery" looks useful for humans, but is it useful to client programs other than nbdinfo?
NBD_OPT_SET_META_CONTEXT handles the filtering (intersection) and mapping to numeric IDs. NBD_OPT_GO transitions the server to the "ready" state (completes negotiation). So those two look useful. I don't understand what the precise responsibility is that *only* NBD_OPT_INFO can cover. The manual at <https://libguestfs.org/nbd_opt_info.3.html> says that nbd_opt_info() enables export size querying (for example), but that kind of querying is just as possible without nbd_opt_info(); only nbd_set_opt_mode() is needed -- see the example code at <https://libguestfs.org/nbd_set_opt_mode.3.html>.
That example is only able to print what NBD_OPT_LIST returns (the export name, and any description the server provides) - some servers might provide the export size in the human-readable description, but the only way to get a machine-readable readout of each size is to further call NBD_OPT_INFO on each name returned by NBD_OPT_LIST. nbd_set_opt_mode() is what lets it be possible to call nbd_opt_list(), but it still requires nbd_opt_list() before you can use nbd_get_size()
(s/nbd_opt_list/nbd_opt_info/ ?)
while still remaining in negotiation mode.
Aha! So the trick is that the example code uses nbd_get_size() *after* nbd_opt_go(), but we may want to call nbd_get_size() before entering the "ready" state. [...] I've read the rest too -- it's a lot to digest. I've nothing more to add at this moment. Thank you! Laszlo
On Tue, Aug 30, 2022 at 01:16:02PM +0200, Laszlo Ersek wrote:
Thanks for the explanation. I didn't expect these two principles to have driven the design (generability, and long-term convenience calls). I think the more usual (albeit likely less programmer-friendly) approach is to (a) expose the minute details and let the end-programmer deal with them, (b) let people write / maintain other-than-C language wrappers manually.
The trouble with this is that maintaining the non-C language wrappers becomes a huge amount of work that is constantly behind (see also: libvirt). Generating bindings means less work and more utility for everyone. The down-side is that the bindings may not be completely natural in all cases in all languages and there's a bit of a lowest common denominator effect. Despite this, the non-C bindings are still pretty good for libnbd.
- NBD_OPT_INFO in particular looks like a useless opcode.
Useless when you already know what export you want to connect to. But useful during the 'nbdinfo' app when you want to learn as much as possible about every export exposed by a single nbd server, while still remaining in negotiation after each export thus queried.
Such "discovery" looks useful for humans, but is it useful to client programs other than nbdinfo?
Maybe in practice everyone is going to use nbdinfo :-) but it's still useful to have a way to query what exports are available. (Same for metadata contexts.) Rich. -- Richard Jones, Virtualization Group, Red Hat http://people.redhat.com/~rjones Read my programming and virtualization blog: http://rwmj.wordpress.com virt-p2v converts physical machines to virtual machines. Boot with a live CD or over the network (PXE) and turn machines into KVM guests. http://libguestfs.org/virt-v2v
Add testsuite coverage that exposes the flaw fixed in the previous patch. --- tests/opt-info.c | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/opt-info.c b/tests/opt-info.c index b9739a5..2402a31 100644 --- a/tests/opt-info.c +++ b/tests/opt-info.c @@ -1,5 +1,5 @@ /* NBD client library in userspace - * Copyright (C) 2013-2020 Red Hat Inc. + * Copyright (C) 2013-2022 Red Hat Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -80,15 +80,11 @@ main (int argc, char *argv[]) exit (EXIT_FAILURE); } - /* info on something not present fails, wipes out prior info */ + /* changing export wipes out prior info */ if (nbd_set_export_name (nbd, "a") == -1) { fprintf (stderr, "%s\n", nbd_get_error ()); exit (EXIT_FAILURE); } - if (nbd_opt_info (nbd) != -1) { - fprintf (stderr, "expecting error for opt_info\n"); - exit (EXIT_FAILURE); - } if (nbd_get_size (nbd) != -1) { fprintf (stderr, "expecting error for get_size\n"); exit (EXIT_FAILURE); @@ -102,7 +98,13 @@ main (int argc, char *argv[]) exit (EXIT_FAILURE); } - /* info for a different export */ + /* info on something not present fails */ + if (nbd_opt_info (nbd) != -1) { + fprintf (stderr, "expecting error for opt_info\n"); + exit (EXIT_FAILURE); + } + + /* info for a different export; idempotent name change is no-op */ if (nbd_set_export_name (nbd, "b") == -1) { fprintf (stderr, "%s\n", nbd_get_error ()); exit (EXIT_FAILURE); @@ -111,6 +113,10 @@ main (int argc, char *argv[]) fprintf (stderr, "%s\n", nbd_get_error ()); exit (EXIT_FAILURE); } + if (nbd_set_export_name (nbd, "b") == -1) { + fprintf (stderr, "%s\n", nbd_get_error ()); + exit (EXIT_FAILURE); + } if ((r = nbd_get_size (nbd)) != 1) { fprintf (stderr, "expecting size of 1, got %" PRId64 "\n", r); exit (EXIT_FAILURE); -- 2.37.2
On 08/24/22 04:53, Eric Blake wrote:
Add testsuite coverage that exposes the flaw fixed in the previous patch.
--- tests/opt-info.c | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-)
diff --git a/tests/opt-info.c b/tests/opt-info.c index b9739a5..2402a31 100644 --- a/tests/opt-info.c +++ b/tests/opt-info.c @@ -1,5 +1,5 @@ /* NBD client library in userspace - * Copyright (C) 2013-2020 Red Hat Inc. + * Copyright (C) 2013-2022 Red Hat Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -80,15 +80,11 @@ main (int argc, char *argv[]) exit (EXIT_FAILURE); }
- /* info on something not present fails, wipes out prior info */ + /* changing export wipes out prior info */ if (nbd_set_export_name (nbd, "a") == -1) { fprintf (stderr, "%s\n", nbd_get_error ()); exit (EXIT_FAILURE); } - if (nbd_opt_info (nbd) != -1) { - fprintf (stderr, "expecting error for opt_info\n"); - exit (EXIT_FAILURE); - } if (nbd_get_size (nbd) != -1) { fprintf (stderr, "expecting error for get_size\n"); exit (EXIT_FAILURE);
Is this well targeted though? Here we can't say whether nbd_get_size() fails because export "a" is different from the previous "" export, or because "a" does not exist. If "a" existed and nbd_get_size() still failed, that would be more to the point.
@@ -102,7 +98,13 @@ main (int argc, char *argv[]) exit (EXIT_FAILURE); }
- /* info for a different export */ + /* info on something not present fails */ + if (nbd_opt_info (nbd) != -1) { + fprintf (stderr, "expecting error for opt_info\n"); + exit (EXIT_FAILURE); + } +
Yes, this makes sense, assuming the previous point is "good enough".
+ /* info for a different export; idempotent name change is no-op */ if (nbd_set_export_name (nbd, "b") == -1) { fprintf (stderr, "%s\n", nbd_get_error ()); exit (EXIT_FAILURE); @@ -111,6 +113,10 @@ main (int argc, char *argv[]) fprintf (stderr, "%s\n", nbd_get_error ()); exit (EXIT_FAILURE); } + if (nbd_set_export_name (nbd, "b") == -1) { + fprintf (stderr, "%s\n", nbd_get_error ()); + exit (EXIT_FAILURE); + } if ((r = nbd_get_size (nbd)) != 1) { fprintf (stderr, "expecting size of 1, got %" PRId64 "\n", r); exit (EXIT_FAILURE);
This looks OK to me. Laszlo
On Wed, Aug 24, 2022 at 02:49:27PM +0200, Laszlo Ersek wrote:
On 08/24/22 04:53, Eric Blake wrote:
Add testsuite coverage that exposes the flaw fixed in the previous patch.
--- tests/opt-info.c | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-)
diff --git a/tests/opt-info.c b/tests/opt-info.c index b9739a5..2402a31 100644 --- a/tests/opt-info.c +++ b/tests/opt-info.c @@ -1,5 +1,5 @@ /* NBD client library in userspace - * Copyright (C) 2013-2020 Red Hat Inc. + * Copyright (C) 2013-2022 Red Hat Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -80,15 +80,11 @@ main (int argc, char *argv[]) exit (EXIT_FAILURE); }
- /* info on something not present fails, wipes out prior info */ + /* changing export wipes out prior info */ if (nbd_set_export_name (nbd, "a") == -1) { fprintf (stderr, "%s\n", nbd_get_error ()); exit (EXIT_FAILURE); } - if (nbd_opt_info (nbd) != -1) { - fprintf (stderr, "expecting error for opt_info\n"); - exit (EXIT_FAILURE); - } if (nbd_get_size (nbd) != -1) { fprintf (stderr, "expecting error for get_size\n"); exit (EXIT_FAILURE);
Is this well targeted though?
Here we can't say whether nbd_get_size() fails because export "a" is different from the previous "" export, or because "a" does not exist. If "a" existed and nbd_get_size() still failed, that would be more to the point.
Good observation. I'll set name to "b" (which does exist, and whereas export "" has size 0, export "b" has size 1, so it should be very obvious that returning 0 is wrong, returning 1 is only possible if we queried the server about the new export which hasn't happened without an nbd_opt_ call, and therefore -1 is the only sane answer)...
@@ -102,7 +98,13 @@ main (int argc, char *argv[]) exit (EXIT_FAILURE); }
- /* info for a different export */ + /* info on something not present fails */ + if (nbd_opt_info (nbd) != -1) { + fprintf (stderr, "expecting error for opt_info\n"); + exit (EXIT_FAILURE); + } +
Yes, this makes sense, assuming the previous point is "good enough".
and then add one more nbd_set_export_name("a") before the expected-failing nbd_opt_info().
+ /* info for a different export; idempotent name change is no-op */ if (nbd_set_export_name (nbd, "b") == -1) { fprintf (stderr, "%s\n", nbd_get_error ()); exit (EXIT_FAILURE); @@ -111,6 +113,10 @@ main (int argc, char *argv[]) fprintf (stderr, "%s\n", nbd_get_error ()); exit (EXIT_FAILURE); } + if (nbd_set_export_name (nbd, "b") == -1) { + fprintf (stderr, "%s\n", nbd_get_error ()); + exit (EXIT_FAILURE); + } if ((r = nbd_get_size (nbd)) != 1) { fprintf (stderr, "expecting size of 1, got %" PRId64 "\n", r); exit (EXIT_FAILURE);
This looks OK to me.
I need to repost it anyway; the Python, Go, and OCaml bindings all have the same test (under the name *230*), and can similarly be changed for multi-lingual coverage of the idiom (if nothing else, to prove we aren't maintaining any separate language-specific state). v2 coming up later today, with more patches (the promised nbd_opt_set_meta_context() among them). -- Eric Blake, Principal Software Engineer Red Hat, Inc. +1-919-301-3266 Virtualization: qemu.org | libvirt.org
participants (3)
-
Eric Blake -
Laszlo Ersek -
Richard W.M. Jones