Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce bind mounting (and thus --bind args) in containers. #1387

Closed
wants to merge 13 commits into from

Conversation

DailyDreaming
Copy link
Contributor

@DailyDreaming DailyDreaming commented Dec 10, 2020

Eases an issue where too many --bind args can break the (2mb on my system) limit on the command line length. This is a much bigger Toil problem as I think cwltool handles directories of files a bit more elegantly.

Discussed in: DataBiosphere/toil#3358
And in: https://cwl.discourse.group/t/too-many-arguments-on-the-command-line/248

This is currently a draft and won't run/work because I don't run the hard-linking script currently (see below; open to suggestions).

This PR takes any individual files that are being added as --bind volumes to singularity and instead creates a hardlink of the file inside of one universal folder which is bind-mounted with only one --bind arg. I don't do anything about folders, since they're a two-way street and need to be able to have outputs dropped in them, so I'm not sure I can do much about those and they can still, in theory, break the limit if you add enough of them.

A script is then created that, when run inside of the container, hard links all of the files to where they should be.

I haven't actually figured out a good way to prepend the hard linking script command before the real command runs.

Multiple commands , such as singularity exec docker://ubuntu bash -c "echo hello; echo goodbye" work, so I'd like to sandwich the command into a place like this if possible: https://github.com/common-workflow-language/cwltool/blob/main/cwltool/command_line_tool.py#L1022 .

I'm having trouble finding a way of doing that in a way that isn't jarring to me though. I was hoping for something like the docker run --entrypoint, but I don't think that something like that exists here. I'll think about it a bit more and post here when things actually run and pass. Very open to suggestions in the meantime (especially if I'm doing something wrong, against the design, or there's a more obvious way to do this, please let me know).

@lgtm-com
Copy link

lgtm-com bot commented Dec 10, 2020

This pull request introduces 2 alerts when merging 13d604c into ac60dc1 - view on LGTM.com

new alerts:

  • 1 for First argument to super() is not enclosing class
  • 1 for Wrong number of arguments in a call

@tetron
Copy link
Member

tetron commented Dec 10, 2020

@DailyDreaming

I think a better approach would be to leverage cwltool.process.stage_files (https://github.com/common-workflow-language/cwltool/blob/main/cwltool/process.py#L257) to assemble directories before the container is invoked. This would be useful for all container runtimes, not just Singularity.

The one thing to be aware of is that currently stage_files can either copy or symlink files. As I said in another thread, the brute-force solution would be to copy all the files, the more clever solution is to use either symlinks (requires bind-mounting the job store into the container so that the symlinks have something to point to) or hard links (but these have to be on the same file system as the job store).

@DailyDreaming
Copy link
Contributor Author

@tetron Thanks for taking a look! Leveraging cwltool.process.stage_files seems like a great way to do it, thanks for the suggestion. I wouldn't mind adding hardlinks to stage_files if you think that that's reasonable, though I guess I'm assuming the filesystem should all be available and local by the time it's inside of the CommandLineJob's execute method? I'd rather not just force copying and symlinks seem like they may more trouble to get to work than the hardlinks, but if filesystem consistency is a problem, I'll try to tackle how to get the symlinks to work.

@tetron
Copy link
Member

tetron commented Dec 10, 2020

Yes, it should be trivial to add hardlink support to stage_files()

It's an inherit limitation of hard links on unix that they can't cross file systems, the code just has to be aware of that.

In other words, if the job store (where the files actually live) and the temp directory where you are going to create the directory tree are on different mounts, the hard link will fail. It is something you could check for ahead of time ("warning: job store and temp directory are on different file systems, files will be copied instead of linked")

@codecov
Copy link

codecov bot commented Feb 17, 2021

Codecov Report

Merging #1387 (abf614c) into main (3df258a) will decrease coverage by 10.01%.
The diff coverage is n/a.

❗ Current head abf614c differs from pull request most recent head 664df85. Consider uploading reports for the commit 664df85 to get more accurate results
Impacted file tree graph

@@             Coverage Diff             @@
##             main    #1387       +/-   ##
===========================================
- Coverage   65.86%   55.84%   -10.02%     
===========================================
  Files          91       45       -46     
  Lines       16061     7875     -8186     
  Branches     4138     1981     -2157     
===========================================
- Hits        10578     4398     -6180     
+ Misses       4351     2955     -1396     
+ Partials     1132      522      -610     
Impacted Files Coverage Δ
job.py 58.60% <0.00%> (-3.70%) ⬇️
executors.py 64.00% <0.00%> (-1.32%) ⬇️
process.py 69.20% <0.00%> (-1.00%) ⬇️
docker.py 49.12% <0.00%> (-0.66%) ⬇️
expression.py 81.77% <0.00%> (-0.30%) ⬇️
validate_js.py 82.35% <0.00%> (-0.30%) ⬇️
workflow_job.py 80.49% <0.00%> (-0.26%) ⬇️
context.py 93.10% <0.00%> (-0.14%) ⬇️
workflow.py 73.83% <0.00%> (-0.13%) ⬇️
argparser.py 76.01% <0.00%> (-0.11%) ⬇️
... and 59 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 59f1675...664df85. Read the comment docs.

@DailyDreaming
Copy link
Contributor Author

DailyDreaming commented Feb 17, 2021

Just an update on this. This is a fairly naive attempt, and changes the paths and bindmounts a new staging temp dir, but it still includes the targets from the original pathmapper dir (/var/lib/cwl), which don't exist because they weren't staged there now.

My current thinking is that I need to set the stagedir earlier, when the pathmapper is originally instantiated, so that the correct paths propogate to the docker/singularity command itself (and elsewhere). I'm trying to determine where to best do this.

(venv) quokka@qcore 12:17 PM ~/git/cwl-v1.2$ cwltool --outdir=/tmp/tmpd1zp1sdd tests/bwa-mem-tool.cwl tests/bwa-mem-job.json
INFO Resolved 'tests/bwa-mem-tool.cwl' to 'file:///home/quokka/git/cwl-v1.2/tests/bwa-mem-tool.cwl'
INFO [job bwa-mem-tool.cwl] /tmp/dl0gg46k$ docker \
    run \
    -i \
    --mount=type=bind,source=/tmp/dl0gg46k,target=/vbpxaZ \
    --mount=type=bind,source=/tmp/_cxurpaz,target=/tmp \
    --mount=type=bind,source=/tmp/tmp_ngia29n,target=/tmp/tmp_ngia29n,readonly \
    --mount=type=bind,source=/tmp/tmplrzep2o7,target=/tmp/tmplrzep2o7,readonly \
    --workdir=/vbpxaZ \
    --read-only=true \
    --net=none \
    --log-driver=none \
    --user=1000:1000 \
    --rm \
    --env=TMPDIR=/tmp \
    --env=HOME=/vbpxaZ \
    --cidfile=/tmp/oog3rwe9/20210217122340-899868.cid \
    python:2-slim \
    python \
    /var/lib/cwl/stgbabdbb9e-b9e4-4d4a-b8c0-735a05c661db/args.py \
    bwa \
    mem \
    -t \
    2 \
    -I \
    1,2,3,4 \
    -m \
    3 \
    /var/lib/cwl/stg27a16925-4658-4ba7-b9b4-cf075d561b14/chr20.fa \
    /var/lib/cwl/stg4a7c8064-5cbd-44b0-be65-ff80171af4ce/example_human_Illumina.pe_1.fastq \
    /var/lib/cwl/stg53754e58-09b8-49fb-bacb-89bf0e2d7c95/example_human_Illumina.pe_2.fastq > /tmp/dl0gg46k/output.sam
python: can't open file '/var/lib/cwl/stgbabdbb9e-b9e4-4d4a-b8c0-735a05c661db/args.py': [Errno 2] No such file or directory
INFO [job bwa-mem-tool.cwl] Max memory used: 0MiB
ERROR [job bwa-mem-tool.cwl] Job error:
Error validating output record. the `args` field is not valid because
  the value None is not a list, expected list of string
 in {
    "sam": {
        "location": "file:///tmp/dl0gg46k/output.sam",
        "basename": "output.sam",
        "nameroot": "output",
        "nameext": ".sam",
        "class": "File",
        "checksum": "sha1$da39a3ee5e6b4b0d3255bfef95601890afd80709",
        "size": 0
    },
    "args": null
}
WARNING [job bwa-mem-tool.cwl] completed permanentFail
{}
WARNING Final process status is permanentFail

@DailyDreaming
Copy link
Contributor Author

DailyDreaming commented Feb 23, 2021

I've changed this to be a more conservative change, and only bind mount the base directories of read-only files, which should fix the originally posted issue and I assume a large number of others in the near term.

Trying to do something like separateDirs = False, but account for path conflicts.

@tetron tetron self-requested a review February 23, 2021 14:46
@tetron
Copy link
Member

tetron commented Feb 23, 2021

I'll have to find time to look this over. @mr-c also reminded me that we need to migrate the pull request tests off of travis to something that isn't so severely quota constrained, so that's also a blocker to be able to merge this PR.

@DailyDreaming
Copy link
Contributor Author

DailyDreaming commented Feb 25, 2021

@tetron Three conformance tests are failing in v1.2. There are file path conflicts and needs to be handled more intelligently. Will update this to fix the issue.

@DailyDreaming
Copy link
Contributor Author

DailyDreaming commented Mar 9, 2021

So this fails with conformance tests: cwltest --verbose --tool=cwltool --test=conformance_tests.yaml -n=56,99,338

It looks like the first two are easier, and it's a matter of making sure the dir destinations don't collide rather than the entire bind mount.

Test 338 is weirder. It's a permissions issue, where a directory is kind of created as root via bind mounting as the subdir of another bind mounted dir, since root's the default user in this particular image. So this outputs a root owned directory and then attempts to reinitialize the workdir by symlinking a file into it and fails. The current passing behavior actually seems to generate a root owned file, doesn't error because it skips the symlink due to FileExistsError and then ultimately generates a normal user-owned output file: https://github.com/common-workflow-language/cwltool/blob/main/cwltool/job.py#L168

@tetron
Copy link
Member

tetron commented Mar 9, 2021

This is still on my to-do list to look at. In the mean time, we moved over to github actions for CI and a bunch of tests are not reporting. Maybe you need to merge master?

@mr-c
Copy link
Member

mr-c commented Mar 11, 2021

The following tests fail:

  FAILED tests/test_iwdr.py::test_directory_literal_with_real_inputs_inside - a...
  FAILED tests/test_iwdr.py::test_iwdr_permutations - AssertionError: assert 1 ...
  FAILED tests/test_iwdr.py::test_iwdr_permutations_inplace - AssertionError: a...
  FAILED tests/test_iwdr.py::test_iwdr_permutations_singularity - AssertionErro...
  FAILED tests/test_iwdr.py::test_iwdr_permutations_singularity_inplace - Asser...

https://github.com/common-workflow-language/cwltool/runs/2087522096?check_suite_focus=true#step:9:2071

     File "/home/runner/work/cwltool/cwltool/cwltool/job.py", line 408, in _execute
      inplace_update=self.inplace_update,
    File "/home/runner/work/cwltool/cwltool/cwltool/job.py", line 168, in relink_initialworkdir
      os.symlink(vol.resolved, host_outdir_tgt)
  PermissionError: [Errno 13] Permission denied: '/home/runner/work/cwltool/cwltool/tests/__init__.py' -> '/tmp/z40e8ecb/subdir/__init__.py'

https://github.com/common-workflow-language/cwltool/runs/2087522096?check_suite_focus=true#step:9:242

   docker: Error response from daemon: OCI runtime create failed: container_linux.go:367: starting container process caused: process_linux.go:495: container init caused: rootfs_linux.go:60: mounting "/tmp/ud6ktcf6/tenth_writable_directory_literal" to rootfs at "/var/lib/docker/overlay2/136be57b994e1cf36a3536b428a2004188ee067b50d9068a9b6168208fb8bdbe/merged/my_path/tenth_writable_directory_literal" caused: mkdir /var/lib/docker/overlay2/136be57b994e1cf36a3536b428a2004188ee067b50d9068a9b6168208fb8bdbe/merged/my_path/tenth_writable_directory_literal: read-only file system: unknown.

https://github.com/common-workflow-language/cwltool/runs/2087522096?check_suite_focus=true#step:9:386

@@ -171,7 +171,9 @@ def setup(self, referenced_files: List[CWLObjectType], basedir: str) -> None:
stagedir = self.stagedir
for fob in referenced_files:
if self.separateDirs:
stagedir = os.path.join(self.stagedir, "stg%s" % uuid.uuid4())
stagedir = os.path.join(self.stagedir,
"s5g%s" % uuid.uuid5(uuid.UUID('6a56ca02-b6f0-4c1a-a4b0-fb0068ce80ad'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

???

Copy link
Contributor Author

@DailyDreaming DailyDreaming Mar 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uuid5 is deterministic. This groups together files that were previously grouped together by providing the same uuid for each file with the same os.path.dirname, so it violates completely separating files. This should avoid conflicts.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok! Why s5g instead of stg?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't you be passing something to uuid.uuid5 related to the fob.location?

Copy link
Contributor Author

@DailyDreaming DailyDreaming Mar 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed s5g just in case this needs to distinguish behavior in the future based on versions. Though it's not necessary.

And fob['location'] is line 176, the comment blurb doesn't make it visible here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So "s5g%s" % uuid.uuid5(uuid.UUID('6a56ca02-b6f0-4c1a-a4b0-fb0068ce80ad' generates a static string that will never change, to which you make a subdir based upon the directory name of the location field. So why not skip to the directory name itself? Or use it to see the uuid5 generator?

What if there are location URIs that end in the same directory name but are otherwise unrelated? This could lead to files overwriting each other. Or files being adjacent to one another when they should not be.

Copy link
Contributor Author

@DailyDreaming DailyDreaming Mar 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll go through my thinking on this, and please point out any flaws.

Uuid5 generates different uuids for different seeds, the seed in this PR being both the file's directory and a static uuid. So uuid.uuid5(arg1, arg2) = uuid_a and uuid.uuid5(arg1, arg3) = uuid_b. The difference is that if uuid.uuid5(arg1, arg2) = uuid_a occurs again, it will generate the same uuid_a output.

>>> from uuid import uuid5, UUID
>>> uuid5(UUID('6a56ca02-b6f0-4c1a-a4b0-fb0068ce80ad'), '/tmp/1')
UUID('e3e49c59-338c-5369-a215-3d68bda76d69')
>>> uuid5(UUID('6a56ca02-b6f0-4c1a-a4b0-fb0068ce80ad'), '/tmp/2')
UUID('48220b67-6c36-500b-ae08-ecfa48951864')
>>> uuid5(UUID('6a56ca02-b6f0-4c1a-a4b0-fb0068ce80ad'), '/tmp')
UUID('98da1ac5-114d-539b-b727-948e7754ec95')
>>> uuid5(UUID('6a56ca02-b6f0-4c1a-a4b0-fb0068ce80ad'), '/tmp/1')
UUID('e3e49c59-338c-5369-a215-3d68bda76d69')

The idea being that all pre-existing paths cannot conflict since they co-existed together peacefully in the first place.

So files:

/tmp/a/b.txt
/tmp/b/a.txt
/tmp/b/b.txt
/tmp/c/b.txt

would map to these inside of the container:

uuid_a/b.txt
uuid_b/a.txt
uuid_b/b.txt
uuid_c/b.txt

So why not skip to the directory name itself?

If I understand your question correctly, we could use the same directory names as the files originally had, but they might conflict with folders that already exist within the container.

Or use it to see the uuid5 generator?

If I understand this to be to "seed" the uuid5 generator with the directory, I'm using os.path.dirname(fob['location'])) to seed the uuid5. Though I'm not sure if that's the question here.

What if there are location URIs that end in the same directory name but are otherwise unrelated? This could lead to files overwriting each other. Or files being adjacent to one another when they should not be.

I was wondering about this, and was trying to reason if this could happen if the input files were writable in a an input dir outside of the workdir. I haven't tested this.

Some of this too is that I tried to switch to a simpler strategy and it may just not work.

src = os.path.dirname(src)
dst = os.path.dirname(dst)
bind_arg = f"--bind={src}:{dst}:{writable}"
if bind_arg not in runtime:
Copy link
Contributor

@ionox0 ionox0 Jun 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tried running this PR after getting the argument list too long error when using large number of input directories, and it seems that the source directory here is actually different for each file, and therefore it did not reduce the number of --bind arguments:

...
--bind=/juno/work/access/testing/users/johnsoni/access_qc/5500_HF/jobStore/files/for-job/kind-CWLJob/instance-aop8iz_w/file-34cfcc9ed208473bb67710a5108f1a32:/var/lib/cwl/s5g5ff703f1-41b2-56d3-93b3-7e36c1a37224/all_qc_files/duplex_bam_sequence_qc_dir/C-R0CLHU-N001-d:ro \
--bind=/juno/work/access/testing/users/johnsoni/access_qc/5500_HF/jobStore/files/for-job/kind-CWLJob/instance-aop8iz_w/file-fee328e946c9448c92ee501c6daa4b5a:/var/lib/cwl/s5g5ff703f1-41b2-56d3-93b3-7e36c1a37224/all_qc_files/duplex_bam_sequence_qc_dir/C-R0CLHU-N001-d:ro \
...

It seems toil actually produces different parent directories for files that might have originally came from the same Directory object. So if files that came from the same directory can be stored as such in the jobstore, I would expect this change to fix the issue

@DailyDreaming DailyDreaming changed the title Reduce bind mounting (and thus --bind args) in Singularity. Reduce bind mounting (and thus --bind args) in containers. Aug 25, 2021
@DailyDreaming
Copy link
Contributor Author

@mr-c @tetron I've only implemented this for docker and not singularity. It's a hack that builds a new docker container with the files copied inside if the CLI character limit of 2mb is reached.

It uses a staging directory because docker needs all files to be within the runtime context and a staging directory guarantees that. It currently just copies files, but that could be modified to be linking.

This is not as good as the link tree Peter originally proposed, but I'm really not sure how to do that without injecting something into the command to link things after the staging directory is bind mounted.

If this makes sense to you for docker, I can do the same for singularity. Conformance tests pass on my system when I remove the len(runtime) conditional check and force new docker containers built for all tests with valid inputs.

@lgtm-com
Copy link

lgtm-com bot commented Aug 26, 2021

This pull request introduces 1 alert when merging abf614c into 2b17b14 - view on LGTM.com

new alerts:

  • 1 for Clear-text storage of sensitive information

@lgtm-com
Copy link

lgtm-com bot commented Oct 19, 2021

This pull request introduces 2 alerts when merging 664df85 into 59f1675 - view on LGTM.com

new alerts:

  • 1 for Unused import
  • 1 for Clear-text storage of sensitive information

@DailyDreaming
Copy link
Contributor Author

Closing in favor of #1568.

@DailyDreaming DailyDreaming deleted the mount-only-dirs branch November 17, 2021 01:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants