[BUG] DataladDatagrabber will remove a dataset which was cloned outside Junifer. #53
Labels
No labels
CRITICAL
Stale
WIP
bug
concept
coordinate
dataset
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
maintenance
maps
marker
mask
on hold
parcellation
preprocess
question
ready
storage
template-space
triage
wontfix
No milestone
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
juaml/junifer#53
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Looking into the implementation, I see that on
__exit__removeis called. This is running datalad-remove without any other parameter thanrecursive=True.removehas a lot of options, though, mostly to disable safeguards. For that, there'srecklesswhich allows for different modes possibly- ignoring that there are unsaved modifications
- ignoring that there's content in the dataset that appears to not have been pushed elsewhere
- ignoring that this location is potential "zombie" in git-annex' availability records
- ...
Since this appears to be a clean up, one would probably want to wipe out the dataset no matter what, suggesting to add
reckless='kill'. However, this should be a conscious decision. Note, that not only would this prevent (needless) failure during clean up, but also speed things up.I kept thinking about this. Mainly the issues is that the datalad dataset might exist already (if the user points to an existing clone as the
datadir).If this is the case, then we will call
removeon a dataset that we did not clone. If we even addreckless='kill'we might even make a mess with this dataset.What about these options:
1- If dataset does not exist: clone and remove with
reckless='kill'.2- If dataset exists: no clone, no remove, just drop files that we called "get" on.
@bpoldrack any suggestion?
Is there a way of checking if a dataset is already cloned?
@fraimondo: That's a good point.
Agree. Would require to keep track of what you used
getfor obviously. Keep in mind, that callinggeton it is insufficient to determine whether you should drop afterwards, b/c it may have been there already (whichgetwould be fine with). However, all datalad commands return a result dictionary. So you can distinguish whethergetactually did anything by checking thestatuskey in that dict (should be'ok'as opposed to'notneeded'for a "no-op get").A
Datasetcan be instantiated with a path that does not yet exist or does not contain a dataset (yet). So, you can checkDataset(datadir).is_installed()to figure whether there already is a dataset at this location.Furthermore, in the case of an existing dataset, you could check
Dataset.idto match the expectation. This is the committed by datalad ID, that is therefore persistent across versions and locations.@bpoldrack: what do you think about this from #123 ?
Short summary:
1- On install, we check if the dataset needs to be cloned or not. If it has to be cloned, no changes. But if it is already present, we check that it was cloned from the same place (siblings URI). If more than one remote sibling is present, the we mark the dataset as "dirty". This is a flag that will be stored in the metadata of the feature, so we know that it might not have been the original datalad dataset as in the commit id.
github.com/juaml/junifer@29a290a0b0/junifer/datagrabber/datalad_base.py (L147-L183)2- On get, we check if the file was actually downloaded or not. If it was, save it in a list to drop it on cleanup. Otherwise, set the dirty flag as true.
github.com/juaml/junifer@29a290a0b0/junifer/datagrabber/datalad_base.py (L93-L145)3- On cleanup, just undo exactly what we did:
github.com/juaml/junifer@29a290a0b0/junifer/datagrabber/datalad_base.py (L185-L194)@fraimondo:
Overall, looks good to me. I would avoid to call the flag "dirty", because in git context this term is used to indicate a worktree that is not reported "clean" by git. That may lead to confusion sooner or later.
Pragmatically a sane detection of something that wasn't straight up cloned from the same place. However, I wonder where the original URI comes from. Why is not more specific to begin with? If you know the dataset ID and/or the desired commit the check could be more specific. Related: I'd record the dataset ID as well as the commit in the metadata. If one wants to inspect those records later, the commit alone only gives you something to detect a difference. Ultimately that depends a bit on what are the things to search for a previously used dataset, but it could be easier to find the dataset if you had the dataset ID as well.
When getting the files, you have a separate
getcall per file. This comes with an overhead. If you assemble a list of paths to get and pass the list to get instead, things could be faster (and possibly be done in parallel withinget, if you passjobsparameter).Additional hint: all datalad commands take a bunch of general options. You may want to consider giving
result_renderer='disabled'to thecloneandgetcalls to not have datalad generate output on stdout but only yield the result dicts.Noticed one more:
This is relatively expensive, because
repois a property that is evaluated whenever it's accessed and that involves file system operation. If you have testedis_installedbefore, this is likely unnecessary, asrepocan only becomeNonewhen the dataset was deleted in the meantime. It may be fine to crash in this case, since this is either a bug leading to this situation or a user interfered from outside while this is running. Either way - you probably don't want to proceed anyway.So, if you have seen mypy failing with this, I'd catch that to fail more informative (like "dataset vanished") rather than checking upfront.
I'm not sure how often this would be called, especially with a dataset with a lot of subdatasets, so the cost may be irrelevant or substantial. That's for you to judge, I guess.
@bpoldrack Thanks for this. Is quite useful!
It's kind of the idea here. And I think that getting the dirty flag from git is even better. Is there any way in which we can check if the dataset is dirty? Worst-case scenario the user does "commit" but not "push". But in this case, the commit id will be recorded and nowhere to be found.
Because this is a generic class for any datalad dataset. We can ask the user to provide the URI + ID, but it will be more of the same (as the user might have to go to the repo and check for the ID). The whole idea though is not to prevent the user from running, but rather store that information for later. So someone using the features computed will have access to this information stored in the "meta":
Thanks for this one, just changed the code to refelct this.
For the moment I don't mind all the output. It might be useful to debug datalad-related issues from junifer. Although I think that at some point I will link the verbosity of the junifer logger to the datalad logger. So if the user select's info or debug, all output is there. Currently datalad is info even if junifer is not. I just did it and realized that there's also stdout output. I just added this
result_renderer='disabled'.Nevertheless, I must tell you that I'm upset (:P) that the output of
datalad.getdoes not respect the order of the input:@fraimondo
Generally:
datalad status(see help for tons of options what to report). Just likegit statusit would messageclean. A convenient way of having a look at datalad commands' result dicts is calling it with ajson_ppresult renderer:datalad -f json_pp status. (Note, that the CLI's-f/--output-formatis the result renderer. So,'json_pp'or'json'would be an alternative todisabledif you'd want structured output instead of none). You can programatically assess whether everything is clean by checking whether all results havestatusokandstateclean.This would normally be assessed by datalad as a safeguard when calling
drop. Seedrop's help regarding the--recklessswitch to turn off particular or all kinds these checks. The checks obviously do come at a runtime cost, though.I don't think we have the check available as a stand-alone thing at the moment, though.
Yes, logging is different. This is about what we call result rendering. Commands are generators yielding those result records. You can obviously get them directly via python and independently on that, they can be rendered. The standard renderers would write to stdout. One can choose different ones. See
datalad --help-->Global options-->-f. Those values can be passed toresult_rendererin the python interface.Hehe. Well, all datalad commands are generators and supposed to yield those results as they are accomplished, which matters a lot at scale. That means, technically we are not even in control of the order actually executed (and therefore yielded), but git-annex is (where we pass the relevant paths to). There may be something to rectify in this regard within datalad, but: We can't keep the promise of maintaining the order anyway, because of possible internal parallelization. We'd need to reorder afterwards, which would prevent us from yielding as soon as things are completed. So, unlikely to change towards that promise.
@bpoldrack
I'm now testing
Dataset.statusto get the "state" of each file too. Now this will result in a different "meta" for dirty and non-dirty elements. So this means that dirty subjects will be grouped into a different "feature". As the "dataset" is dirty as a whole, then everything is dirty. So now my take is to do it oninstallwhich is the method that either clones the dataset, or just verifies its integrity.The main problem resides on a dataset that it was not cloned because it was already installed. In this case, my take is to run
Dataset.status()on the whole dataset. However, this gives me one entry per file. Which could be inconvenient for very big datasets. So far it works, and can't be worse than cloning the dataset, as it only needs to check the files locally.Here's what I do now.
If there is more than one sibling with a different URI, we consider that dataset as "dirty". The ideal option would be to inspect the dataset ID from the remote URI and see if it matches the local one. Do you now if this is possible without cloning the dataset? I know it's in the
.datalad/configremote file.Check the status. If at least one file is not clean, we consider that dataset as dirty.
github.com/juaml/junifer@97e0a9ef9f/junifer/datagrabber/datalad_base.py (L161-L176)For 1):
maybe use:
This gets only the file, but will it work with any URI?
@fraimondo
It should work with any URI. But this approach requires a place to checkout this remote and to hold the clone and both the checkout (even if limited to a singe file) and the clone itself are unnecessary file system operations. I would suggest something different:
git cat-file blob <branch-name>:<relative path within repo>. This saves you writing it to disc first (yourgit checkout HEAD .datalad/config).The trouble is what remote branch to look at, really in order to generalize properly. That depends on why exactly you are concerned with different siblings. If it's about what the local dataset was cloned from then there should be a
remote/<name>/HEADto look at. Usuallynamewill be origin, but in any case - the cloned from remote should be the only one with a HEAD as far as I'm aware. That may be a good enough validation for you.If the concern is broader I'd check the remote's branch with the same name as the branch the local dataset is on. Technically one may even want to check configured tracking branches.
But ultimately it's really about the local state and you are looking for proxy indications of deviation from the state of the "real" URI, right?
So, why not go the other way around? Find the sibling that actually is the "right" one and figure its HEAD, get the commit and compare to local HEAD?
git fetch <remote> HEADshould give you the ref locally inFETCH_HEAD. You can then find the commit withgit show FETCH_HEAD(see man page to format the output to something easily parseable).General hint:
Dataset.repo.call_git()can be used to run git commands. This is making sure those calls are using the same git as datalad and git-annex in case of several being installed. There's also some convenience for getting the output, having the call be a generator and so on.Check those:
One could go even further: If you take the above suggested approach of fetching current HEAD of the correct sibling and this is not the current state of the local dataset, but the local dataset is clean - then you could also store the current branch/commit and checkout the remote HEAD, run on this and checkout the previous one on clean up. That is likely cheaper than a fresh clone.
However, that kinda depends on what you want and what the datasets look like. Recursive checkout of subdatasets is somewhat cumbersome in the general case. But in a "simple" one, that may be a relatively easy performance gain.
Re 2.)
That looks alright to me.
Well, I thought it through and checking the sibling is not necessary. In the end, everything I care is to figure out from which dataset it came. So when we read two sets of different features, we can easily check if they came from the same dataset or not.
So only one check is required: datalad dataset ID. If the ID is different, then we just don't allow it to continue. This is most probably an error from the user.
Now the rest can't really be checked in runtime. Even the commit ID does not tell the whole history. One could just cherry pick one commit ID on to a different branch. What we can do though, is to set a flag that indicates if the dataset its a fresh clone or not. Ideally, users should not need to run junifer on "previously" cloned datasets. But if they need to do so, they still have the option. However, any other user "re-using" features from a dirty or previously cloned dataset should know that there's a big orange flag that you can trust as much as the "CSV with features that some master's student computed one year ago".
@fraimondo
Edit: Actually that's not true. (Forget what I wrote before in case you saw it - I confused myself)
You cannot have the same commit SHA but not the same history. You can have the same data, but not the same history. But this is irrelevant here. If you detect the "correct" commit, everything is alright. No matter where it comes from.
A commit consists of the committed diff, obv. but also the parent commit(s), committer, author, timestamp. Any of that changes -> new commit with new commit SHA.
@bpoldrack I was replying to you while reading about commits SHA and stuff. And I started to doubt myself!
As it stands right now, you can do any modification on the dataset. You can avoid the flag providing that you let junifer clone the dataset from the remote.
But in any case, you can always beat the system if you want, and we can't prevent the user from doing it.
You can create a repo anywhere, modify data, cherry pick the last commit from head, push to the repo and then tell junifer to use a "fresh clone from here". This will not be flagged, however, the URI will be saved in the meta. And this is where users that want the features need to also check where the data comes from. It's not the same if it comes from the "official datalad" datasets in jugit/gin that from
/p/project/new_project/test_fmriprep_2Ok, then that's alright for me.
@fraimondo
That is the part that still confuses me, though. The URI is useful, but it bears no meaning with respect to the data. It merely is a location. But git is a distributed system. Same commit is the same thing - location doesn't matter. It's useful for provenance and to actually backtrack/reproduce/find things, but it doesn't determine what exact data the extraction ran on. So, the only reason I can see to make a canonical location special is a promise of persistence - you/we/whoever promises that later on that same commit can be found there again, whereas a student's home folder is more likely to vanish. Other than that - the location seems irrelevant even to the provenance, since again - same commit == same history. That may be important, since making sure it comes from there implies we have that commit in longterm storage, but the "validity" of the data processing is not affected at all.
Just want to get that conceptually clear.
Yes! Got it!
So we keep the URI for reproducibility. The user should be able to get the exact configuration that was used to create the features from the meta.
If the URI changes, but you have the same commit id since it's a clone on some other place, you will still end up with two different features (as they have different meta). However, this does not prevent any user from "merging" features. The user just need to compare the commit ID instead of the URI, as the URI does not indicate anything but location (and that's why I don't check the URI anymore, but dataset ID) and store the commit in the meta.
the flag I spoke before will not be added.