cmd/snap-update-ns: compute next action to transition mount profile #2938

Closed
wants to merge 8 commits into
from

Conversation

Projects
None yet
4 participants
Contributor

zyga commented Feb 25, 2017

This patch adds a function that given two sorted mount profiles (current
and desired) computes the next action that needs to be taken to
transition the current profile to the desired profile.

The function operates on a simple principle. Identical entires are
skipped. Entries that don't show up in the desired profile but exist in
the current profile are umounted. Entries that are in the desired
profile but are not in the current profile are mounted.

The logic ensures that we always unmount everything that should be
unmounted before proceeding with the first thing to be mounted.

Signed-off-by: Zygmunt Krynicki zygmunt.krynicki@canonical.com

@zyga zyga changed the title from cmd/snap-update-ns: compute next action to transiton mount profile to cmd/snap-update-ns: compute next action to transition mount profile Feb 25, 2017

@zyga zyga requested a review from jdstrand Mar 1, 2017

cmd/snap-update-ns: compute next action to transition mount profile
This patch adds a function that given two sorted mount profiles (current
and desired) computes the next action that needs to be taken to
transition the current profile to the desired profile.

The function operates on a simple principle. Identical entires are
skipped. Entries that don't show up in the desired profile but exist in
the current profile are umounted. Entries that are in the desired
profile but are not in the current profile are mounted.

The logic ensures that we always unmount everything that should be
unmounted before proceeding with the first thing to be mounted.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns/mount-entry-change.c
+ // Identical entries are just skipped and the algorithm continues.
+ c = c->next;
+ d = d->next;
+ goto again;
@jdstrand

jdstrand Mar 1, 2017

Contributor

gotos can be quite useful and the correct choice for jumping forward to error/cleanup/exit code, but I'd prefer a proper loop here instead of jumping backward via the goto.

@zyga

zyga Mar 1, 2017

Contributor

I'll look at re-doing this, thanks!

cmd/snap-update-ns/mount-entry-change.h
+ *
+ * Both lists *must* be sorted by the caller prior to using this function.
+ *
+ * The result is written back to all the there pointers passed to the
@jdstrand

jdstrand Mar 1, 2017

Contributor

'to all the there'

Contributor

zyga commented Mar 1, 2017

@jdstrand apart from mechanical changes is the algorithm correct?

zyga added some commits Mar 2, 2017

cmd/snap-update-ns: fix comment wording
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: use loop instead of goto
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>

@zyga zyga requested a review from jdstrand Mar 2, 2017

Contributor

zyga commented Mar 2, 2017

Please have a 2nd look on this.

cmd/snap-update-ns: fix typo
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>

I'm not sure if I got it right - it seems to me that sc_compute_required_mount_changes main loop processes a (desired, current) pair on each iteration, so if we have e.g.

desired:
A
B

current:
B
C

it will unmount B on 1st iteration, but then mount it back on 2nd iteration even if B mount entry is identical in both? In other words we would skip identical entires only if they had same positions in boths lists, or did I misunderstand it?

@zyga zyga added the Blocked label Mar 2, 2017

Contributor

zyga commented Mar 2, 2017

I need to re-think some things. The comment from Paweł made me realise there are some deeper problems here.

Contributor

zyga commented Mar 4, 2017

@stolowski I've corrected this issue and added a test case for it. Please have a 2nd look.

cmd/snap-update-ns: handle reused mount entries
This patch corrects a bug found by Paweł Stołowski where the algorithm
would unmount and re-mount and entry needlessly. This can be observed in
on the following input:

 desired = A B
 current = B C

The previous naive approach needlessly unmounted B and mounted it again
soon thereafter. The correct way to do it is to skip the unmount (and
subsequent mount) if the current entry can be found further down the
desired list.

This is achieved by tracking extra state in each mount entry (a flag)
and using this flag to track mount entries present in the desired
profile that were not unmounted and should not be mounted.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>

@zyga zyga removed the Blocked label Mar 6, 2017

@zyga zyga requested a review from stolowski Mar 8, 2017

I desk checked this and assuming I know how sc_compute_required_mount_changes() is intended to be called, it appears correct. However, this is extremely complicated with the loop in sc_compute_required_mount_changes() and the expected loop that calls sc_compute_required_mount_changes(). I think it would be much cleaner if instead you called sc_compute_required_mount_changes() once and it build up a list of sc_mount_change then iterate on that list to perform the mounts and unmounts.

Eg, something like this psuedo-code:

// changes is a linked list of sc_mount_change where sc_compute_required_mount_changes
// builds up this list with a single invocation
sc_compute_required_mount_changes(desired, current, changes);

// now that changes has stuff in it, perform the operations
struct sc_mount_change *change = changes;
if change != NULL:
   while change != NULL:
      if change.action == SC_ACTION_UNMOUNT:
        umount(change.entry)
      elif change.action == SC_ACTION_UNMOUNT:
        mount(change.entry)
      change = change.next;

  deallocate what was allocated in changes

then write sc_compute_required_mount_changes() such that it does two passes-- one for mounts and one for unmounts. Eg in pseudo-code:

bool find(haystack, needle) { ... }
void sc_compute_required_mount_changes(desired, current, changes) {
    // first see what to unmount
    c = current;
    while c != NULL:
      if find(desired, c) == False:
        change = allocate for sc_mount_change
        change.entry = copy(c)
        change.action = SC_ACTION_UNMOUNT
        change.next = NULL;
        c = c->next;
        append change to changes

    // then see what to mount
    d = desired;
    while d != NULL:
      if find(current, d) == False:
        change = allocate for sc_mount_change
        change.entry = copy(d)
        change.action = SC_ACTION_MOUNT
        change.next = NULL;
        d = d->next;
        append change to changes
}

These loops are much easier to read IME and easier to verify. Please consider slightly longer tests like the following too (non-exhaustive):

desired        current        expected
BD             ACE            BD
ACE            BD             ACE
ABDEG          BCDE           ABDEG
BCDE           ABDEG          BCDE
cmd/snap-update-ns/mount-entry-change.c
+ d = *desiredp;
+ c = *currentp;
+
+ bool again;
@jdstrand

jdstrand Mar 8, 2017

Contributor

It would be nice to have a comment here on when again is expected to by used and how it relates to flag

@zyga

zyga Mar 9, 2017

Contributor

Again was removed so this is no longer an issue.

cmd/snap-update-ns/mount-entry-change.c
+ change->entry = c;
+ *currentp = c->next;
+ *desiredp = NULL;
+ } else if (c != NULL && d != NULL) {
@jdstrand

jdstrand Mar 8, 2017

Contributor

This could simply be: } else { // c != NULL && d != NULL

@zyga

zyga Mar 9, 2017

Contributor

Yes but I'd rather be explicit about it.

cmd/snap-update-ns/mount-entry-change.c
+ // Do another pass over the algorithm.
+ again = true;
+ continue;
+ } else {
@jdstrand

jdstrand Mar 8, 2017

Contributor

Since the continue is above, you could avoid the indentation here by simply doing:

    ...
    continue;
}
// entries are not equal which means that we need to unmount the current
// entry and mount the desired entry.
...
@zyga

zyga Mar 9, 2017

Contributor

I've simplified the code now.

cmd/snap-update-ns/mount-entry-change.c
+ } else {
+ // If the current entry is not desired then just unmount it.
+ change->action = SC_ACTION_UNMOUNT;
+ change->entry = c;
@jdstrand

jdstrand Mar 8, 2017

Contributor

You aren't managing change->entry->next here. I guess how this is meant to be used is:

loop on:
  sc_compute_required_mount_changes(...)
  if change:
    sc_do_some_umount_thing(change)
@zyga

zyga Mar 9, 2017

Contributor

entry->next is only used to combine entries into a list. It has no purpose for this part of the code.

Thanks for adding the handling of the case I questioned earlier. Looks good overall (modulo Jamie's comments). Just one nitpick re flag plus a general question: apparently sc_mount_entry_find introduces quadratic complexity - what kind of inputs do we expect here (how many entries? Do we expect the lists to be "misaligned" -so to speak- a lot so that sc_mount_entry_find kicks in very often?)?. Is this kind of complexity something we should care about here. If so, then perhaps a lookup map would be better than scanning the list every time, but I'm not going to push for premature optimization if this is not really justified.

@@ -30,6 +30,7 @@
struct sc_mount_entry {
struct mntent entry;
struct sc_mount_entry *next;
+ unsigned flag; // general-purpose flag, not compared
@stolowski

stolowski Mar 9, 2017

Contributor

If this is a general purpose-purpose flag (with a prospect of holding more states in the future), then I'd add a const (#define) for the possible states (just one state for now) and make sure if (d->flag) checks for the exact state. Alternatively, if we don't expect multiple states, then I'd rename "flag" to clearly indicate what is it about.

@mvo5

mvo5 Mar 9, 2017

Collaborator

If this is used for more in the future, maybe we can change this code here once this will actually be changed in the future? YAGNI and all :) ? Or do we know already that it will change and in what way?

zyga added some commits Mar 9, 2017

cmd/snap-update-ns: change API of sc_compute_required_mount_changes
This changes nothing semantically but makes the code a bit easier to
use. Instead of requiring the caller to make repeated calls and check
input arguments the function now allocates the chain of changes and
returns them in one swoop.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: vastly simplifiy the algorithm.
This patch changes how we compute the list of changes. Instead of doing
an iteration over two lists and having smart choices we now simply do
several trivial passes.

1. reset flags
2. flag identical entries for reuse.
3. unmount unflagged entries from the current list.
4. mount unflagged entries from the desired list.

Thanks to Jamie Strandboge for the suggestion.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-update-ns: simplifiy test code
This patch simplifies testing of struct sc_mount_change chains by adding
a dedicated function for comparing chains against test data.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>

Some comments, would be great if those could be addressed. Overall I think its fine.

+ struct sc_mount_entry *desired;
+ struct sc_mount_change *change;
+
+ sc_test_write_lines("desired.fstab", "A A A A 0 0", "B B B B 0 0",
@mvo5

mvo5 Mar 9, 2017

Collaborator

Wonder if we can have a test_entry_3 here? I found it slightly confusing why this is different than the other tests.

+ }
+}
+
+struct sc_mount_change *sc_compute_required_mount_changes(struct sc_mount_entry
@mvo5

mvo5 Mar 9, 2017

Collaborator

Maybe a small comment what this function is doing (in a high level sense). Like:

def sc_compute_required_mount_changes(desired_set, current_set):
    res = []
    for entry in current-desired:
        res.append((entry, "unmount"))
    for entry in desired-current:
        res.append((entry, "mount"))
    return res
+
+ // Reset flags in both lists as we use them to track reused entries.
+ for (entry = current; entry != NULL; entry = entry->next) {
+ entry->flag = 0;
@mvo5

mvo5 Mar 9, 2017

Collaborator

The flag feels overly generic. Maybe something like seen,processed,considered,present_in_both,equal.

@@ -30,6 +30,7 @@
struct sc_mount_entry {
struct mntent entry;
struct sc_mount_entry *next;
+ unsigned flag; // general-purpose flag, not compared
@stolowski

stolowski Mar 9, 2017

Contributor

If this is a general purpose-purpose flag (with a prospect of holding more states in the future), then I'd add a const (#define) for the possible states (just one state for now) and make sure if (d->flag) checks for the exact state. Alternatively, if we don't expect multiple states, then I'd rename "flag" to clearly indicate what is it about.

@mvo5

mvo5 Mar 9, 2017

Collaborator

If this is used for more in the future, maybe we can change this code here once this will actually be changed in the future? YAGNI and all :) ? Or do we know already that it will change and in what way?

Contributor

zyga commented Mar 10, 2017

I found another issue in this. I'll close the PR for a moment and push a fix and a new test.

@zyga zyga closed this Mar 10, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment