5652: Merge branch 'master' into 5652-arvputget-silent-mode
authorLucas Di Pentima <lucas@curoverse.com>
Mon, 17 Jul 2017 17:05:48 +0000 (14:05 -0300)
committerLucas Di Pentima <lucas@curoverse.com>
Mon, 17 Jul 2017 17:05:48 +0000 (14:05 -0300)
Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas@curoverse.com>

29 files changed:
AUTHORS
build/build.list
build/package-build-dockerfiles/centos7/Dockerfile
build/package-build-dockerfiles/debian8/Dockerfile
build/package-build-dockerfiles/ubuntu1204/Dockerfile
build/package-build-dockerfiles/ubuntu1404/Dockerfile
build/package-build-dockerfiles/ubuntu1604/Dockerfile
build/package-test-dockerfiles/centos7/Dockerfile
build/package-test-dockerfiles/debian8/Dockerfile
build/package-test-dockerfiles/ubuntu1204/Dockerfile
build/package-test-dockerfiles/ubuntu1404/Dockerfile
build/package-test-dockerfiles/ubuntu1604/Dockerfile
build/run-build-packages.sh
sdk/cwl/arvados_cwl/__init__.py
sdk/cwl/arvados_cwl/arvtool.py
sdk/cwl/arvados_cwl/fsaccess.py
sdk/cwl/arvados_cwl/runner.py
sdk/cwl/setup.py
sdk/cwl/tests/test_job.py
sdk/cwl/tests/test_submit.py
sdk/cwl/tests/wf/expect_packed.cwl
services/api/app/models/container_request.rb
services/api/test/fixtures/container_requests.yml
services/api/test/fixtures/containers.yml
services/api/test/unit/container_request_test.rb
services/ws/event_source.go
services/ws/permission.go
services/ws/session_v0.go
services/ws/session_v0_test.go

diff --git a/AUTHORS b/AUTHORS
index ad72aea0aa98486b498465b51e4f1cfd9211658a..ea9fa4c7092e8c2069a2105d8eafb25e6107d3ab 100644 (file)
--- a/AUTHORS
+++ b/AUTHORS
@@ -16,3 +16,4 @@ Guillermo Carrasco <guille.ch.88@gmail.com>
 Joshua Randall <joshua.randall@sanger.ac.uk>
 President and Fellows of Harvard College <*@harvard.edu>
 Thomas Mooney <tmooney@genome.wustl.edu>
+Chen Chen <aflyhorse@gmail.com>
index 894eefafe8907c193b7570cb6e3bc6519d2f2022..596b4061bb5f1440eb95f1986f38171fb44b941f 100644 (file)
@@ -24,7 +24,7 @@ debian8,ubuntu1204,ubuntu1404,ubuntu1604,centos7|rdflib|4.2.2|2|python|all
 debian8,ubuntu1204,ubuntu1404,centos7|shellescape|3.4.1|2|python|all
 debian8,ubuntu1204,ubuntu1404,ubuntu1604,centos7|mistune|0.7.3|2|python|all
 debian8,ubuntu1204,ubuntu1404,ubuntu1604,centos7|typing|3.5.3.0|2|python|all
-debian8,ubuntu1204,ubuntu1404,centos7|avro|1.8.1|2|python|all
+debian8,ubuntu1204,ubuntu1404,ubuntu1604,centos7|avro|1.8.1|2|python|all
 debian8,ubuntu1204,ubuntu1404,centos7|ruamel.ordereddict|0.4.9|2|python|amd64
 debian8,ubuntu1204,ubuntu1404,ubuntu1604,centos7|cachecontrol|0.11.7|2|python|all
 debian8,ubuntu1204,ubuntu1404,ubuntu1604,centos7|pathlib2|2.1.0|2|python|all
index 0c03de6bbbf63baa5db7e11e2ea89e3ad13180e3..0f084b300b0c20a9114c06a50d2096e34d4fced3 100644 (file)
@@ -3,15 +3,11 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 FROM centos:7
-MAINTAINER Brett Smith <brett@curoverse.com>
+MAINTAINER Ward Vandewege <ward@curoverse.com>
 
-# Install build dependencies provided in base distribution
+# Install dependencies.
 RUN yum -q -y install make automake gcc gcc-c++ libyaml-devel patch readline-devel zlib-devel libffi-devel openssl-devel bzip2 libtool bison sqlite-devel rpm-build git perl-ExtUtils-MakeMaker libattr-devel nss-devel libcurl-devel which tar unzip scl-utils centos-release-scl postgresql-devel python-devel python-setuptools fuse-devel xz-libs git
 
-# Install golang binary
-ADD generated/go1.8.3.linux-amd64.tar.gz /usr/local/
-RUN ln -s /usr/local/go/bin/go /usr/local/bin/
-
 # Install RVM
 RUN gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
     curl -L https://get.rvm.io | bash -s stable && \
@@ -20,10 +16,17 @@ RUN gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
     /usr/local/rvm/bin/rvm-exec default gem install bundler && \
     /usr/local/rvm/bin/rvm-exec default gem install cure-fpm --version 1.6.0b
 
+# Install golang binary
+ADD generated/go1.8.3.linux-amd64.tar.gz /usr/local/
+RUN ln -s /usr/local/go/bin/go /usr/local/bin/
+
 # Need to "touch" RPM database to workaround bug in interaction between
 # overlayfs and yum (https://bugzilla.redhat.com/show_bug.cgi?id=1213602)
 RUN touch /var/lib/rpm/* && yum -q -y install python33
 RUN scl enable python33 "easy_install-3.3 pip" && easy_install-2.7 pip
 
+# Old versions of setuptools cannot build a schema-salad package.
+RUN pip install --upgrade setuptools
+
 ENV WORKSPACE /arvados
 CMD ["scl", "enable", "python33", "/usr/local/rvm/bin/rvm-exec default bash /jenkins/run-build-packages.sh --target centos7"]
index 99ae17f5c8d63444e4542fa1bd7b7217e9847b16..f5aced70d6d6d96131be2489e2cfc8da75aa321c 100644 (file)
@@ -5,7 +5,9 @@
 FROM debian:jessie
 MAINTAINER Ward Vandewege <ward@curoverse.com>
 
-# Install dependencies and set up system.
+ENV DEBIAN_FRONTEND noninteractive
+
+# Install dependencies.
 RUN /usr/bin/apt-get update && /usr/bin/apt-get install -q -y python2.7-dev python3 python-setuptools python3-setuptools libcurl4-gnutls-dev curl git procps libattr1-dev libfuse-dev libgnutls28-dev libpq-dev python-pip unzip
 
 # Install RVM
@@ -20,5 +22,8 @@ RUN gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
 ADD generated/go1.8.3.linux-amd64.tar.gz /usr/local/
 RUN ln -s /usr/local/go/bin/go /usr/local/bin/
 
+# Old versions of setuptools cannot build a schema-salad package.
+RUN pip install --upgrade setuptools
+
 ENV WORKSPACE /arvados
 CMD ["/usr/local/rvm/bin/rvm-exec", "default", "bash", "/jenkins/run-build-packages.sh", "--target", "debian8"]
index 3006b6d7daa3937b6dc64b4209eee7affe3cbebe..bf0908969103569ccf4638b4239f49a980e55310 100644 (file)
@@ -5,7 +5,9 @@
 FROM ubuntu:precise
 MAINTAINER Ward Vandewege <ward@curoverse.com>
 
-# Install dependencies and set up system.
+ENV DEBIAN_FRONTEND noninteractive
+
+# Install dependencies.
 RUN /usr/bin/apt-get update && /usr/bin/apt-get install -q -y python2.7-dev python3 python-setuptools python3-setuptools libcurl4-gnutls-dev curl git libattr1-dev libfuse-dev libpq-dev python-pip build-essential unzip
 
 # Install RVM
@@ -20,5 +22,8 @@ RUN gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
 ADD generated/go1.8.3.linux-amd64.tar.gz /usr/local/
 RUN ln -s /usr/local/go/bin/go /usr/local/bin/
 
+# Old versions of setuptools cannot build a schema-salad package.
+RUN pip install --upgrade setuptools
+
 ENV WORKSPACE /arvados
 CMD ["/usr/local/rvm/bin/rvm-exec", "default", "bash", "/jenkins/run-build-packages.sh", "--target", "ubuntu1204"]
index 3e2c86935052c160453731737faeb05493648cad..ecfcefc593b894eb55f8c9b8287211a9d5dcc7f0 100644 (file)
@@ -3,9 +3,11 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 FROM ubuntu:trusty
-MAINTAINER Brett Smith <brett@curoverse.com>
+MAINTAINER Ward Vandewege <ward@curoverse.com>
 
-# Install dependencies and set up system.
+ENV DEBIAN_FRONTEND noninteractive
+
+# Install dependencies.
 RUN /usr/bin/apt-get update && /usr/bin/apt-get install -q -y python2.7-dev python3 python-setuptools python3-setuptools libcurl4-gnutls-dev curl git libattr1-dev libfuse-dev libpq-dev python-pip unzip
 
 # Install RVM
@@ -20,5 +22,8 @@ RUN gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
 ADD generated/go1.8.3.linux-amd64.tar.gz /usr/local/
 RUN ln -s /usr/local/go/bin/go /usr/local/bin/
 
+# Old versions of setuptools cannot build a schema-salad package.
+RUN pip install --upgrade setuptools
+
 ENV WORKSPACE /arvados
 CMD ["/usr/local/rvm/bin/rvm-exec", "default", "bash", "/jenkins/run-build-packages.sh", "--target", "ubuntu1404"]
index 85c36b5243e2ed9c2a1bdcae0c04e7399a27b218..b7c02d79de421b4a587eab86ad18713cc24071bc 100644 (file)
@@ -5,7 +5,9 @@
 FROM ubuntu:xenial
 MAINTAINER Ward Vandewege <ward@curoverse.com>
 
-# Install dependencies and set up system.
+ENV DEBIAN_FRONTEND noninteractive
+
+# Install dependencies.
 RUN /usr/bin/apt-get update && /usr/bin/apt-get install -q -y python2.7-dev python3 python-setuptools python3-setuptools libcurl4-gnutls-dev libgnutls-dev curl git libattr1-dev libfuse-dev libpq-dev python-pip unzip tzdata
 
 # Install RVM
@@ -20,5 +22,8 @@ RUN gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
 ADD generated/go1.8.3.linux-amd64.tar.gz /usr/local/
 RUN ln -s /usr/local/go/bin/go /usr/local/bin/
 
+# Old versions of setuptools cannot build a schema-salad package.
+RUN pip install --upgrade setuptools
+
 ENV WORKSPACE /arvados
 CMD ["/usr/local/rvm/bin/rvm-exec", "default", "bash", "/jenkins/run-build-packages.sh", "--target", "ubuntu1604"]
index 4dc9aa141d683506879d384cda5014fb4800e7e4..fd2f9e3d8cdbafced2990875f56ce72a2719672f 100644 (file)
@@ -3,7 +3,7 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 FROM centos:7
-MAINTAINER Brett Smith <brett@curoverse.com>
+MAINTAINER Ward Vandewege <ward@curoverse.com>
 
 RUN yum -q -y install scl-utils centos-release-scl which tar
 
index 3a0b0e918ba6f5ed2b573fd0e0d461dc2ce0beb4..dcf581a1e036876cb85676e305b78eb399381448 100644 (file)
@@ -3,11 +3,13 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 FROM debian:8
-MAINTAINER Peter Amstutz <peter.amstutz@curoverse.com>
+MAINTAINER Ward Vandewege <ward@curoverse.com>
+
+ENV DEBIAN_FRONTEND noninteractive
 
 # Install RVM
 RUN apt-get update && \
-    DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends curl ca-certificates && \
+    apt-get -y install --no-install-recommends curl ca-certificates && \
     gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
     curl -L https://get.rvm.io | bash -s stable && \
     /usr/local/rvm/bin/rvm install 2.3 && \
index b1c14998fcb4da589195ba76efe3f5dedb597bed..75c0cea1ded4c54dbaa2cfc63a07ccf2cb48e621 100644 (file)
@@ -3,11 +3,13 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 FROM ubuntu:precise
-MAINTAINER Peter Amstutz <peter.amstutz@curoverse.com>
+MAINTAINER Ward Vandewege <ward@curoverse.com>
+
+ENV DEBIAN_FRONTEND noninteractive
 
 # Install RVM
 RUN apt-get update && \
-    DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends curl ca-certificates g++ && \
+    apt-get -y install --no-install-recommends curl ca-certificates g++ && \
     gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
     curl -L https://get.rvm.io | bash -s stable && \
     /usr/local/rvm/bin/rvm install 2.3 && \
index 42d446feba28282243c99192bc0cf74d5721e391..8416847433e046e3f0c2f36f5c3734f06cd4d60a 100644 (file)
@@ -3,11 +3,13 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 FROM ubuntu:trusty
-MAINTAINER Peter Amstutz <peter.amstutz@curoverse.com>
+MAINTAINER Ward Vandewege <ward@curoverse.com>
+
+ENV DEBIAN_FRONTEND noninteractive
 
 # Install RVM
 RUN apt-get update && \
-    DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends curl ca-certificates && \
+    apt-get -y install --no-install-recommends curl ca-certificates && \
     gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
     curl -L https://get.rvm.io | bash -s stable && \
     /usr/local/rvm/bin/rvm install 2.3 && \
index 0580e3afbfd075dd5eb619225464b738d0c2dc3f..25d49dc5019112cc7d9cc6b251904780471faa79 100644 (file)
@@ -5,9 +5,11 @@
 FROM ubuntu:xenial
 MAINTAINER Ward Vandewege <ward@curoverse.com>
 
+ENV DEBIAN_FRONTEND noninteractive
+
 # Install RVM
 RUN apt-get update && \
-    DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends curl ca-certificates && \
+    apt-get -y install --no-install-recommends curl ca-certificates && \
     gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
     curl -L https://get.rvm.io | bash -s stable && \
     /usr/local/rvm/bin/rvm install 2.3 && \
index a8e730c27f562f14210a28c2ce2567d8a06933aa..81aef7f34030728a783e35aa6a44e7f38b257461 100755 (executable)
@@ -409,7 +409,7 @@ fi
 saladversion=$(cat "$WORKSPACE/sdk/cwl/setup.py" | grep schema-salad== | sed "s/.*==\(.*\)'.*/\1/")
 test_package_presence python-schema-salad "$saladversion" python
 if [[ "$?" == "0" ]]; then
-  fpm_build schema_salad "" "" python $saladversion --depends "${PYTHON2_PKG_PREFIX}-lockfile >= 1:0.12.2-2"
+  fpm_build schema_salad "" "" python $saladversion --depends "${PYTHON2_PKG_PREFIX}-lockfile >= 1:0.12.2-2" --depends "${PYTHON2_PKG_PREFIX}-avro = 1.8.1-2"
 fi
 
 # And for cwltool we have the same problem as for schema_salad. Ward, 2016-03-17
index d642cbd243a2727c7eb65f87562714428c575601..695597f839c1ceee5ccbb67f3391218d3d1a8e34 100644 (file)
@@ -89,7 +89,7 @@ class ArvCwlRunner(object):
         self.collection_cache = CollectionCache(self.api, self.keep_client, self.num_retries)
 
         self.work_api = None
-        expected_api = ["containers", "jobs"]
+        expected_api = ["jobs", "containers"]
         for api in expected_api:
             try:
                 methods = self.api._rootDesc.get('resources')[api]['methods']
@@ -426,12 +426,6 @@ class ArvCwlRunner(object):
             if self.work_api == "containers":
                 if tool.tool["class"] == "CommandLineTool" and kwargs.get("wait"):
                     kwargs["runnerjob"] = tool.tool["id"]
-                    upload_dependencies(self,
-                                        kwargs["name"],
-                                        tool.doc_loader,
-                                        tool.tool,
-                                        tool.tool["id"],
-                                        False)
                     runnerjob = tool.job(job_order,
                                          self.output_callback,
                                          **kwargs).next()
@@ -632,7 +626,7 @@ def arg_parser():  # type: () -> argparse.ArgumentParser
     parser.add_argument("--api", type=str,
                         default=None, dest="work_api",
                         choices=("jobs", "containers"),
-                        help="Select work submission API.  Default is 'containers' if that API is available, otherwise 'jobs'.")
+                        help="Select work submission API.  Default is 'jobs' if that API is available, otherwise 'containers'.")
 
     parser.add_argument("--compute-checksum", action="store_true", default=False,
                         help="Compute checksum of contents while collecting outputs",
@@ -756,6 +750,7 @@ def main(args, stdout, stderr, api_client=None, keep_client=None):
     arvargs.use_container = True
     arvargs.relax_path_checks = True
     arvargs.validate = None
+    arvargs.print_supported_versions = False
 
     make_fs_access = partial(CollectionFsAccess,
                            collection_cache=runner.collection_cache)
index 8ab86c173f42ba16cc28699cba9495ec48e8281b..35a068f91be7b28b7eebd0f32538e9222367d8a1 100644 (file)
@@ -15,7 +15,7 @@ class ArvadosCommandTool(CommandLineTool):
         self.arvrunner = arvrunner
         self.work_api = kwargs["work_api"]
 
-    def makeJobRunner(self):
+    def makeJobRunner(self, use_container=True):
         if self.work_api == "containers":
             return ArvadosContainer(self.arvrunner)
         elif self.work_api == "jobs":
index 5582d63cfc6362acd0477f7b5508346772d1d6d3..93e2819084601d784a973c33ca30bbbdb2d6db49 100644 (file)
@@ -95,7 +95,7 @@ class CollectionFsAccess(cwltool.stdfsaccess.StdFsAccess):
 
     def exists(self, fn):
         collection, rest = self.get_collection(fn)
-        if collection:
+        if collection is not None:
             if rest:
                 return collection.exists(rest)
             else:
index 683f548c48cc835cd5ff7664cd0306cb0ea7f17e..087fed3e16e72cb26c95500b4ccb03a83bf71806 100644 (file)
@@ -18,7 +18,7 @@ from cwltool.draft2tool import CommandLineTool
 import cwltool.workflow
 from cwltool.process import get_feature, scandeps, UnsupportedRequirement, normalizeFilesDirs, shortname
 from cwltool.load_tool import fetch_document
-from cwltool.pathmapper import adjustFileObjs, adjustDirObjs
+from cwltool.pathmapper import adjustFileObjs, adjustDirObjs, visit_class
 from cwltool.utils import aslist
 from cwltool.builder import substitute
 from cwltool.pack import pack
@@ -46,6 +46,17 @@ def trim_anonymous_location(obj):
     if obj.get("location", "").startswith("_:"):
         del obj["location"]
 
+def find_defaults(d, op):
+    if isinstance(d, list):
+        for i in d:
+            find_defaults(i, op)
+    elif isinstance(d, dict):
+        if "default" in d:
+            op(d)
+        else:
+            for i in d.itervalues():
+                find_defaults(i, op)
+
 def upload_dependencies(arvrunner, name, document_loader,
                         workflowobj, uri, loadref_run, include_primary=True):
     """Upload the dependencies of the workflowobj document to Keep.
@@ -101,6 +112,23 @@ def upload_dependencies(arvrunner, name, document_loader,
         for s in workflowobj["$schemas"]:
             sc.append({"class": "File", "location": s})
 
+    def capture_default(obj):
+        remove = [False]
+        def add_default(f):
+            if "location" not in f and "path" in f:
+                f["location"] = f["path"]
+                del f["path"]
+            if "location" in f and not arvrunner.fs_access.exists(f["location"]):
+                # Remove from sc
+                sc[:] = [x for x in sc if x["location"] != f["location"]]
+                # Delete "default" from workflowobj
+                remove[0] = True
+        visit_class(obj["default"], ("File", "Directory"), add_default)
+        if remove[0]:
+            del obj["default"]
+
+    find_defaults(workflowobj, capture_default)
+
     mapper = ArvPathMapper(arvrunner, sc, "",
                            "keep:%s",
                            "keep:%s/%s",
index 55987ca1fc1796fb5f84215bdf3d60b5bdf00544..375485e62735effea3c935fde640b6db5dbc170e 100644 (file)
@@ -51,8 +51,8 @@ setup(name='arvados-cwl-runner',
       # Note that arvados/build/run-build-packages.sh looks at this
       # file to determine what version of cwltool and schema-salad to build.
       install_requires=[
-          'cwltool==1.0.20170525215327',
-          'schema-salad==2.5.20170428142041',
+          'cwltool==1.0.20170707200431',
+          'schema-salad==2.6.20170630075932',
           'typing==3.5.3.0',
           'ruamel.yaml==0.13.7',
           'arvados-python-client>=0.1.20170526013812',
index 89f4677e3b34f84ad4b5b0601f22fa2c9bc6d286..5d140ba6b9d56a052f3a75eb0f4697f29b8dad79 100644 (file)
@@ -315,7 +315,7 @@ class TestWorkflow(unittest.TestCase):
         api._rootDesc = get_rootDesc()
 
         runner = arvados_cwl.ArvCwlRunner(api)
-        runner.work_api = 'jobs'
+        self.assertEqual(runner.work_api, 'jobs')
 
         list_images_in_arv.return_value = [["zzzzz-4zz18-zzzzzzzzzzzzzzz"]]
         runner.api.collections().get().execute.return_vaulue = {"portable_data_hash": "99999999999999999999999999999993+99"}
index eec37ef62aeeaf3adbf6f06d52f6c3da1024ac1a..3d6b91536a2f2732ad2e97d5c172bf41e2ee60e7 100644 (file)
@@ -79,7 +79,12 @@ def stubs(func):
                 if uuid in (v["uuid"], v["portable_data_hash"]):
                     return CollectionExecute(v)
 
-        created_collections = {}
+        created_collections = {
+            "99999999999999999999999999999998+99": {
+                "uuid": "",
+                "portable_data_hash": "99999999999999999999999999999998+99",
+                "manifest_text": ". 99999999999999999999999999999998+99 0:0:file1.txt"
+            }}
         stubs.api.collections().create.side_effect = functools.partial(collection_createstub, created_collections)
         stubs.api.collections().get.side_effect = functools.partial(collection_getstub, created_collections)
 
@@ -142,10 +147,22 @@ def stubs(func):
                     'runtime_constraints': {'docker_image': 'arvados/jobs:'+arvados_cwl.__version__, 'min_ram_mb_per_node': 1024},
                     'script_parameters': {
                         'y': {"value": {'basename': '99999999999999999999999999999998+99', 'location': 'keep:99999999999999999999999999999998+99', 'class': 'Directory'}},
-                        'x': {"value": {'basename': 'blorp.txt', 'class': 'File', 'location': 'keep:169f39d466a5438ac4a90e779bf750c7+53/blorp.txt'}},
+                        'x': {"value": {
+                            'basename': 'blorp.txt',
+                            'class': 'File',
+                            'location': 'keep:169f39d466a5438ac4a90e779bf750c7+53/blorp.txt',
+                            "nameext": ".txt",
+                            "nameroot": "blorp"
+                        }},
                         'z': {"value": {'basename': 'anonymous', 'class': 'Directory',
                               'listing': [
-                                  {'basename': 'renamed.txt', 'class': 'File', 'location': 'keep:99999999999999999999999999999998+99/file1.txt'}
+                                  {
+                                      'basename': 'renamed.txt',
+                                      'class': 'File', 'location':
+                                      'keep:99999999999999999999999999999998+99/file1.txt',
+                                      "nameext": ".txt",
+                                      "nameroot": "renamed"
+                                  }
                               ]}},
                         'cwl:tool': '3fffdeaa75e018172e1b583425f4ebff+60/workflow.cwl#main',
                         'arv:enable_reuse': True,
@@ -191,10 +208,24 @@ def stubs(func):
                 '/var/lib/cwl/cwl.input.json': {
                     'kind': 'json',
                     'content': {
-                        'y': {'basename': '99999999999999999999999999999998+99', 'location': 'keep:99999999999999999999999999999998+99', 'class': 'Directory'},
-                        'x': {'basename': u'blorp.txt', 'class': 'File', 'location': u'keep:169f39d466a5438ac4a90e779bf750c7+53/blorp.txt'},
+                        'y': {
+                            'basename': '99999999999999999999999999999998+99',
+                            'location': 'keep:99999999999999999999999999999998+99',
+                            'class': 'Directory'},
+                        'x': {
+                            'basename': u'blorp.txt',
+                            'class': 'File',
+                            'location': u'keep:169f39d466a5438ac4a90e779bf750c7+53/blorp.txt',
+                            "nameext": ".txt",
+                            "nameroot": "blorp"
+                        },
                         'z': {'basename': 'anonymous', 'class': 'Directory', 'listing': [
-                            {'basename': 'renamed.txt', 'class': 'File', 'location': 'keep:99999999999999999999999999999998+99/file1.txt'}
+                            {'basename': 'renamed.txt',
+                             'class': 'File',
+                             'location': 'keep:99999999999999999999999999999998+99/file1.txt',
+                             "nameext": ".txt",
+                             "nameroot": "renamed"
+                            }
                         ]}
                     },
                     'kind': 'json'
@@ -319,7 +350,7 @@ class TestSubmit(unittest.TestCase):
     def test_submit_runner_ram(self, stubs, tm):
         capture_stdout = cStringIO.StringIO()
         exited = arvados_cwl.main(
-            ["--submit", "--no-wait", "--api=jobs", "--debug", "--submit-runner-ram=2048",
+            ["--submit", "--no-wait", "--debug", "--submit-runner-ram=2048",
              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
             capture_stdout, sys.stderr, api_client=stubs.api)
         self.assertEqual(exited, 0)
@@ -338,7 +369,7 @@ class TestSubmit(unittest.TestCase):
     def test_submit_invalid_runner_ram(self, stubs, tm):
         capture_stdout = cStringIO.StringIO()
         exited = arvados_cwl.main(
-            ["--submit", "--no-wait", "--api=jobs", "--debug", "--submit-runner-ram=-2048",
+            ["--submit", "--no-wait", "--debug", "--submit-runner-ram=-2048",
              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
             capture_stdout, sys.stderr, api_client=stubs.api)
         self.assertEqual(exited, 1)
@@ -350,7 +381,7 @@ class TestSubmit(unittest.TestCase):
 
         capture_stdout = cStringIO.StringIO()
         exited = arvados_cwl.main(
-            ["--submit", "--no-wait", "--api=jobs", "--debug", "--output-name", output_name,
+            ["--submit", "--no-wait", "--debug", "--output-name", output_name,
              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
             capture_stdout, sys.stderr, api_client=stubs.api)
         self.assertEqual(exited, 0)
@@ -369,7 +400,7 @@ class TestSubmit(unittest.TestCase):
     def test_submit_pipeline_name(self, stubs, tm):
         capture_stdout = cStringIO.StringIO()
         exited = arvados_cwl.main(
-            ["--submit", "--no-wait", "--api=jobs", "--debug", "--name=hello job 123",
+            ["--submit", "--no-wait", "--debug", "--name=hello job 123",
              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
             capture_stdout, sys.stderr, api_client=stubs.api)
         self.assertEqual(exited, 0)
@@ -389,7 +420,7 @@ class TestSubmit(unittest.TestCase):
 
         capture_stdout = cStringIO.StringIO()
         exited = arvados_cwl.main(
-            ["--submit", "--no-wait", "--api=jobs", "--debug", "--output-tags", output_tags,
+            ["--submit", "--no-wait", "--debug", "--output-tags", output_tags,
              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
             capture_stdout, sys.stderr, api_client=stubs.api)
         self.assertEqual(exited, 0)
@@ -408,7 +439,7 @@ class TestSubmit(unittest.TestCase):
         project_uuid = 'zzzzz-j7d0g-zzzzzzzzzzzzzzz'
 
         exited = arvados_cwl.main(
-            ["--submit", "--no-wait", "--api=jobs",
+            ["--submit", "--no-wait",
              "--project-uuid", project_uuid,
              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
             sys.stdout, sys.stderr, api_client=stubs.api)
@@ -424,7 +455,7 @@ class TestSubmit(unittest.TestCase):
         capture_stdout = cStringIO.StringIO()
         try:
             exited = arvados_cwl.main(
-                ["--submit", "--no-wait", "--debug",
+                ["--submit", "--no-wait", "--api=containers", "--debug",
                  "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
                 capture_stdout, sys.stderr, api_client=stubs.api, keep_client=stubs.keep_client)
             self.assertEqual(exited, 0)
@@ -895,7 +926,7 @@ class TestSubmit(unittest.TestCase):
                          arvados_cwl.runner.arvados_jobs_image(arvrunner, "arvados/jobs:"+arvados_cwl.__version__))
 
 class TestCreateTemplate(unittest.TestCase):
-    existing_template_uuid = "zzzzz-p5p6p-validworkfloyml"
+    existing_template_uuid = "zzzzz-d1hrv-validworkfloyml"
 
     def _adjust_script_params(self, expect_component):
         expect_component['script_parameters']['x'] = {
@@ -1237,7 +1268,7 @@ class TestTemplateInputs(unittest.TestCase):
     @stubs
     def test_inputs_empty(self, stubs):
         exited = arvados_cwl.main(
-            ["--create-template", "--api=jobs",
+            ["--create-template",
              "tests/wf/inputs_test.cwl", "tests/order/empty_order.json"],
             cStringIO.StringIO(), sys.stderr, api_client=stubs.api)
         self.assertEqual(exited, 0)
@@ -1248,7 +1279,7 @@ class TestTemplateInputs(unittest.TestCase):
     @stubs
     def test_inputs(self, stubs):
         exited = arvados_cwl.main(
-            ["--create-template", "--api=jobs",
+            ["--create-template",
              "tests/wf/inputs_test.cwl", "tests/order/inputs_test_order.json"],
             cStringIO.StringIO(), sys.stderr, api_client=stubs.api)
         self.assertEqual(exited, 0)
index 561daf40ff186841dd28645373877aac89ba7d63..32a9255e90d442a475394036ea54dd55a797e6fa 100644 (file)
@@ -24,7 +24,7 @@ $graph:
   - id: '#main/x'
     type: File
     default: {class: File, location: 'keep:169f39d466a5438ac4a90e779bf750c7+53/blorp.txt',
-      basename: blorp.txt}
+      basename: blorp.txt, nameroot: blorp, nameext: .txt}
   - id: '#main/y'
     type: Directory
     default: {class: Directory, location: 'keep:99999999999999999999999999999998+99',
@@ -32,7 +32,8 @@ $graph:
   - id: '#main/z'
     type: Directory
     default: {class: Directory, basename: anonymous, listing: [{basename: renamed.txt,
-          class: File, location: 'keep:99999999999999999999999999999998+99/file1.txt'}]}
+          class: File, location: 'keep:99999999999999999999999999999998+99/file1.txt',
+          nameroot: renamed, nameext: .txt}]}
   outputs: []
   steps:
   - id: '#main/step1'
index e8ab0cbf7117e0e25139cf403db566ca86d2e72f..94e4e1f9ddd4289bc8bbe23c0f61d54d06213a18 100644 (file)
@@ -28,6 +28,7 @@ class ContainerRequest < ArvadosModel
   after_save :update_priority
   after_save :finalize_if_needed
   before_create :set_requesting_container_uuid
+  before_destroy :set_priority_zero
 
   api_accessible :user, extend: :common do |t|
     t.add :command
@@ -262,6 +263,10 @@ class ContainerRequest < ArvadosModel
     end
   end
 
+  def set_priority_zero
+    self.update_attributes!(priority: 0) if self.state != Final
+  end
+
   def set_requesting_container_uuid
     return !new_record? if self.requesting_container_uuid   # already set
 
index da138c480e8d4da5206a646560975f441ccb5913..836f840aea3d6a1f3686efa12f6b9feecd330a11 100644 (file)
@@ -272,6 +272,25 @@ canceled_with_running_container:
     vcpus: 1
     ram: 123
 
+running_to_be_deleted:
+  uuid: zzzzz-xvhdp-cr5runningcntnr
+  owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  name: running to be deleted
+  state: Committed
+  priority: 1
+  created_at: <%= 2.days.ago.to_s(:db) %>
+  updated_at: <%= 1.days.ago.to_s(:db) %>
+  modified_at: <%= 1.days.ago.to_s(:db) %>
+  modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  container_image: test
+  cwd: test
+  output_path: test
+  command: ["echo", "hello"]
+  container_uuid: zzzzz-dz642-runnincntrtodel
+  runtime_constraints:
+    vcpus: 1
+    ram: 123
+
 completed_with_input_mounts:
   uuid: zzzzz-xvhdp-crwithinputmnts
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
index 929366976e654b4d335ae19042f1b959121b1cb4..a7ad6f0378d90679cb1ac183f69233c34fddc88a 100644 (file)
@@ -216,3 +216,20 @@ running_container_with_logs:
   runtime_constraints:
     ram: 12000000000
     vcpus: 4
+
+running_to_be_deleted:
+  uuid: zzzzz-dz642-runnincntrtodel
+  owner_uuid: zzzzz-tpzed-000000000000000
+  state: Running
+  priority: 1
+  created_at: <%= 1.minute.ago.to_s(:db) %>
+  updated_at: <%= 1.minute.ago.to_s(:db) %>
+  started_at: <%= 1.minute.ago.to_s(:db) %>
+  container_image: test
+  cwd: test
+  output_path: test
+  command: ["echo", "hello"]
+  runtime_constraints:
+    ram: 12000000000
+    vcpus: 4
+  auth_uuid: zzzzz-gj3su-077z32aux8dg2s2
index 280e93962c719c13f8ed65aa71c67111c79d523b..e751d6158cdc2eb1b26fd3d80fa277839d7ad590 100644 (file)
@@ -750,4 +750,28 @@ class ContainerRequestTest < ActiveSupport::TestCase
       end
     end
   end
+
+  test "delete container_request and check its container's priority" do
+    act_as_user users(:active) do
+      cr = ContainerRequest.find_by_uuid container_requests(:running_to_be_deleted).uuid
+
+      # initially the cr's container has priority > 0
+      c = Container.find_by_uuid(cr.container_uuid)
+      assert_equal 1, c.priority
+
+      # destroy the cr
+      assert_nothing_raised {cr.destroy}
+
+      # the cr's container now has priority of 0
+      c = Container.find_by_uuid(cr.container_uuid)
+      assert_equal 0, c.priority
+    end
+  end
+
+  test "delete container_request in final state and expect no error due to before_destroy callback" do
+    act_as_user users(:active) do
+      cr = ContainerRequest.find_by_uuid container_requests(:completed).uuid
+      assert_nothing_raised {cr.destroy}
+    end
+  end
 end
index a4e886872c49829a27a1223d6cdfd691a2e5efff..edeb647e4628e675be696cb68f4b61892b4cc606 100644 (file)
@@ -243,6 +243,7 @@ func (ps *pgEventSource) NewSink() eventSink {
 }
 
 func (ps *pgEventSource) DB() *sql.DB {
+       ps.WaitReady()
        return ps.db
 }
 
index b40c1fa1863219048a2ba70f8b2afb1ffc394391..a39a959312aed582957d8845dddfc70394b649a4 100644 (file)
@@ -74,7 +74,8 @@ func (pc *cachingPermChecker) Check(uuid string) (bool, error) {
 
        pc.nMisses++
        err = pc.RequestAndDecode(&buf, "GET", path, nil, url.Values{
-               "select": {`["uuid"]`},
+               "include_trash": {"true"},
+               "select":        {`["uuid"]`},
        })
 
        var allowed bool
index bc7e6ecb9e391e5afef37edabfeffd791c4cd710..58c64231cb53c1204ceed70b0ea030a7050ebb95 100644 (file)
@@ -20,7 +20,16 @@ var (
        errQueueFull   = errors.New("client queue full")
        errFrameTooBig = errors.New("frame too big")
 
-       sendObjectAttributes = []string{"state", "name", "owner_uuid", "portable_data_hash"}
+       // Send clients only these keys from the
+       // log.properties.old_attributes and
+       // log.properties.new_attributes hashes.
+       sendObjectAttributes = []string{
+               "is_trashed",
+               "name",
+               "owner_uuid",
+               "portable_data_hash",
+               "state",
+       }
 
        v0subscribeOK   = []byte(`{"status":200}`)
        v0subscribeFail = []byte(`{"status":400}`)
@@ -90,7 +99,17 @@ func (sess *v0session) EventMessage(e *event) ([]byte, error) {
                return nil, nil
        }
 
-       ok, err := sess.permChecker.Check(detail.ObjectUUID)
+       var permTarget string
+       if detail.EventType == "delete" {
+               // It's pointless to check permission by reading
+               // ObjectUUID if it has just been deleted, but if the
+               // client has permission on the parent project then
+               // it's OK to send the event.
+               permTarget = detail.ObjectOwnerUUID
+       } else {
+               permTarget = detail.ObjectUUID
+       }
+       ok, err := sess.permChecker.Check(permTarget)
        if err != nil || !ok {
                return nil, err
        }
@@ -143,7 +162,7 @@ func (sub *v0subscribe) sendOldEvents(sess *v0session) {
        if sub.LastLogID == 0 {
                return
        }
-       sess.log.WithField("LastLogID", sub.LastLogID).Debug("getOldEvents")
+       sess.log.WithField("LastLogID", sub.LastLogID).Debug("sendOldEvents")
        // Here we do a "select id" query and queue an event for every
        // log since the given ID, then use (*event)Detail() to
        // retrieve the whole row and decide whether to send it. This
@@ -158,17 +177,26 @@ func (sub *v0subscribe) sendOldEvents(sess *v0session) {
                sub.LastLogID,
                time.Now().UTC().Add(-10*time.Minute).Format(time.RFC3339Nano))
        if err != nil {
-               sess.log.WithError(err).Error("db.Query failed")
+               sess.log.WithError(err).Error("sendOldEvents db.Query failed")
                return
        }
-       defer rows.Close()
+
+       var ids []uint64
        for rows.Next() {
                var id uint64
                err := rows.Scan(&id)
                if err != nil {
-                       sess.log.WithError(err).Error("row Scan failed")
+                       sess.log.WithError(err).Error("sendOldEvents row Scan failed")
                        continue
                }
+               ids = append(ids, id)
+       }
+       if err := rows.Err(); err != nil {
+               sess.log.WithError(err).Error("sendOldEvents db.Query failed")
+       }
+       rows.Close()
+
+       for _, id := range ids {
                for len(sess.sendq)*2 > cap(sess.sendq) {
                        // Ugly... but if we fill up the whole client
                        // queue with a backlog of old events, a
@@ -193,9 +221,6 @@ func (sub *v0subscribe) sendOldEvents(sess *v0session) {
                        }
                }
        }
-       if err := rows.Err(); err != nil {
-               sess.log.WithError(err).Error("db.Query failed")
-       }
 }
 
 type v0subscribe struct {
index 1213be5d140555f0a533cfc4aa639ef9959e7db2..9f743e0b5e3d58312d2b3a2636b148bd493b51e0 100644 (file)
@@ -11,6 +11,7 @@ import (
        "io"
        "net/url"
        "os"
+       "sync"
        "time"
 
        "git.curoverse.com/arvados.git/sdk/go/arvados"
@@ -32,14 +33,29 @@ type v0Suite struct {
        serverSuite serverSuite
        token       string
        toDelete    []string
+       wg          sync.WaitGroup
+       ignoreLogID uint64
 }
 
 func (s *v0Suite) SetUpTest(c *check.C) {
        s.serverSuite.SetUpTest(c)
+       go s.serverSuite.srv.Run()
+       s.serverSuite.srv.WaitReady()
+
        s.token = arvadostest.ActiveToken
+       s.ignoreLogID = s.lastLogID(c)
+}
+
+func (s *v0Suite) TearDownTest(c *check.C) {
+       s.wg.Wait()
+       s.serverSuite.srv.Close()
 }
 
 func (s *v0Suite) TearDownSuite(c *check.C) {
+       s.deleteTestObjects(c)
+}
+
+func (s *v0Suite) deleteTestObjects(c *check.C) {
        ac := arvados.NewClientFromEnv()
        ac.AuthToken = arvadostest.AdminToken
        for _, path := range s.toDelete {
@@ -48,11 +64,11 @@ func (s *v0Suite) TearDownSuite(c *check.C) {
                        panic(err)
                }
        }
+       s.toDelete = nil
 }
 
 func (s *v0Suite) TestFilters(c *check.C) {
-       srv, conn, r, w := s.testClient()
-       defer srv.Close()
+       conn, r, w := s.testClient()
        defer conn.Close()
 
        c.Check(w.Encode(map[string]interface{}{
@@ -67,15 +83,40 @@ func (s *v0Suite) TestFilters(c *check.C) {
 }
 
 func (s *v0Suite) TestLastLogID(c *check.C) {
-       var lastID uint64
-       c.Assert(testDB().QueryRow(`SELECT MAX(id) FROM logs`).Scan(&lastID), check.IsNil)
+       lastID := s.lastLogID(c)
 
-       srv, conn, r, w := s.testClient()
-       defer srv.Close()
-       defer conn.Close()
+       checkLogs := func(r *json.Decoder, uuid string) {
+               for _, etype := range []string{"create", "blip", "update"} {
+                       lg := s.expectLog(c, r)
+                       for lg.ObjectUUID != uuid {
+                               lg = s.expectLog(c, r)
+                       }
+                       c.Check(lg.EventType, check.Equals, etype)
+               }
+       }
 
-       uuidChan := make(chan string, 2)
+       // Connecting connEarly (before sending the early events) lets
+       // us confirm all of the "early" events have already passed
+       // through the server.
+       connEarly, rEarly, wEarly := s.testClient()
+       defer connEarly.Close()
+       c.Check(wEarly.Encode(map[string]interface{}{
+               "method": "subscribe",
+       }), check.IsNil)
+       s.expectStatus(c, rEarly, 200)
+
+       // Send the early events.
+       uuidChan := make(chan string, 1)
        s.emitEvents(uuidChan)
+       uuidEarly := <-uuidChan
+
+       // Wait for the early events to pass through.
+       checkLogs(rEarly, uuidEarly)
+
+       // Connect the client that wants to get old events via
+       // last_log_id.
+       conn, r, w := s.testClient()
+       defer conn.Close()
 
        c.Check(w.Encode(map[string]interface{}{
                "method":      "subscribe",
@@ -83,36 +124,13 @@ func (s *v0Suite) TestLastLogID(c *check.C) {
        }), check.IsNil)
        s.expectStatus(c, r, 200)
 
-       avoidRace := make(chan struct{}, cap(uuidChan))
-       go func() {
-               // When last_log_id is given, although v0session sends
-               // old events in order, and sends new events in order,
-               // it doesn't necessarily finish sending all old
-               // events before sending any new events. To avoid
-               // hitting this bug in the test, we wait for the old
-               // events to arrive before emitting any new events.
-               <-avoidRace
-               s.emitEvents(uuidChan)
-               close(uuidChan)
-       }()
-
-       go func() {
-               for uuid := range uuidChan {
-                       for _, etype := range []string{"create", "blip", "update"} {
-                               lg := s.expectLog(c, r)
-                               for lg.ObjectUUID != uuid {
-                                       lg = s.expectLog(c, r)
-                               }
-                               c.Check(lg.EventType, check.Equals, etype)
-                       }
-                       avoidRace <- struct{}{}
-               }
-       }()
+       checkLogs(r, uuidEarly)
+       s.emitEvents(uuidChan)
+       checkLogs(r, <-uuidChan)
 }
 
 func (s *v0Suite) TestPermission(c *check.C) {
-       srv, conn, r, w := s.testClient()
-       defer srv.Close()
+       conn, r, w := s.testClient()
        defer conn.Close()
 
        c.Check(w.Encode(map[string]interface{}{
@@ -137,9 +155,71 @@ func (s *v0Suite) TestPermission(c *check.C) {
        }
 }
 
+// Two users create private objects; admin deletes both objects; each
+// user receives a "delete" event for their own object (not for the
+// other user's object).
+func (s *v0Suite) TestEventTypeDelete(c *check.C) {
+       clients := []struct {
+               token string
+               uuid  string
+               conn  *websocket.Conn
+               r     *json.Decoder
+               w     *json.Encoder
+       }{{token: arvadostest.ActiveToken}, {token: arvadostest.SpectatorToken}}
+       for i := range clients {
+               uuidChan := make(chan string, 1)
+               s.token = clients[i].token
+               s.emitEvents(uuidChan)
+               clients[i].uuid = <-uuidChan
+               clients[i].conn, clients[i].r, clients[i].w = s.testClient()
+
+               c.Check(clients[i].w.Encode(map[string]interface{}{
+                       "method": "subscribe",
+               }), check.IsNil)
+               s.expectStatus(c, clients[i].r, 200)
+       }
+
+       s.ignoreLogID = s.lastLogID(c)
+       s.deleteTestObjects(c)
+
+       for _, client := range clients {
+               lg := s.expectLog(c, client.r)
+               c.Check(lg.ObjectUUID, check.Equals, client.uuid)
+               c.Check(lg.EventType, check.Equals, "delete")
+       }
+}
+
+// Trashing/deleting a collection produces an "update" event with
+// properties["new_attributes"]["is_trashed"] == true.
+func (s *v0Suite) TestTrashedCollection(c *check.C) {
+       ac := arvados.NewClientFromEnv()
+       ac.AuthToken = s.token
+
+       coll := &arvados.Collection{ManifestText: ""}
+       err := ac.RequestAndDecode(coll, "POST", "arvados/v1/collections", s.jsonBody("collection", coll), map[string]interface{}{"ensure_unique_name": true})
+       c.Assert(err, check.IsNil)
+       s.ignoreLogID = s.lastLogID(c)
+
+       conn, r, w := s.testClient()
+       defer conn.Close()
+
+       c.Check(w.Encode(map[string]interface{}{
+               "method": "subscribe",
+       }), check.IsNil)
+       s.expectStatus(c, r, 200)
+
+       err = ac.RequestAndDecode(nil, "DELETE", "arvados/v1/collections/"+coll.UUID, nil, nil)
+       c.Assert(err, check.IsNil)
+
+       lg := s.expectLog(c, r)
+       c.Check(lg.ObjectUUID, check.Equals, coll.UUID)
+       c.Check(lg.EventType, check.Equals, "update")
+       c.Check(lg.Properties["old_attributes"].(map[string]interface{})["is_trashed"], check.Equals, false)
+       c.Check(lg.Properties["new_attributes"].(map[string]interface{})["is_trashed"], check.Equals, true)
+}
+
 func (s *v0Suite) TestSendBadJSON(c *check.C) {
-       srv, conn, r, w := s.testClient()
-       defer srv.Close()
+       conn, r, w := s.testClient()
        defer conn.Close()
 
        c.Check(w.Encode(map[string]interface{}{
@@ -158,8 +238,7 @@ func (s *v0Suite) TestSendBadJSON(c *check.C) {
 }
 
 func (s *v0Suite) TestSubscribe(c *check.C) {
-       srv, conn, r, w := s.testClient()
-       defer srv.Close()
+       conn, r, w := s.testClient()
        defer conn.Close()
 
        s.emitEvents(nil)
@@ -190,6 +269,9 @@ func (s *v0Suite) TestSubscribe(c *check.C) {
 // created workflow. If uuidChan is not nil, send the new workflow
 // UUID to uuidChan as soon as it's known.
 func (s *v0Suite) emitEvents(uuidChan chan<- string) {
+       s.wg.Add(1)
+       defer s.wg.Done()
+
        ac := arvados.NewClientFromEnv()
        ac.AuthToken = s.token
        wf := &arvados.Workflow{
@@ -240,7 +322,9 @@ func (s *v0Suite) expectLog(c *check.C, r *json.Decoder) *arvados.Log {
        lg := &arvados.Log{}
        ok := make(chan struct{})
        go func() {
-               c.Check(r.Decode(lg), check.IsNil)
+               for lg.ID <= s.ignoreLogID {
+                       c.Check(r.Decode(lg), check.IsNil)
+               }
                close(ok)
        }()
        select {
@@ -251,9 +335,7 @@ func (s *v0Suite) expectLog(c *check.C, r *json.Decoder) *arvados.Log {
        }
 }
 
-func (s *v0Suite) testClient() (*server, *websocket.Conn, *json.Decoder, *json.Encoder) {
-       go s.serverSuite.srv.Run()
-       s.serverSuite.srv.WaitReady()
+func (s *v0Suite) testClient() (*websocket.Conn, *json.Decoder, *json.Encoder) {
        srv := s.serverSuite.srv
        conn, err := websocket.Dial("ws://"+srv.listener.Addr().String()+"/websocket?api_token="+s.token, "", "http://"+srv.listener.Addr().String())
        if err != nil {
@@ -261,5 +343,11 @@ func (s *v0Suite) testClient() (*server, *websocket.Conn, *json.Decoder, *json.E
        }
        w := json.NewEncoder(conn)
        r := json.NewDecoder(conn)
-       return srv, conn, r, w
+       return conn, r, w
+}
+
+func (s *v0Suite) lastLogID(c *check.C) uint64 {
+       var lastID uint64
+       c.Assert(testDB().QueryRow(`SELECT MAX(id) FROM logs`).Scan(&lastID), check.IsNil)
+       return lastID
 }