[ARVADOS] updated: c9ff74363edc2f3271c117184be2d1ad7fad633a

git at public.curoverse.com git at public.curoverse.com
Wed Feb 11 15:45:55 EST 2015


Summary of changes:
 doc/user/topics/arv-web.html.textile.liquid | 20 +++++---
 services/arv-web/arv-web.py                 | 77 +++++++++++++----------------
 services/fuse/arvados_fuse/__init__.py      |  8 ++-
 3 files changed, 55 insertions(+), 50 deletions(-)

       via  c9ff74363edc2f3271c117184be2d1ad7fad633a (commit)
      from  da2492bfc43032c3374b6509a7208127ec48093a (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit c9ff74363edc2f3271c117184be2d1ad7fad633a
Author: Peter Amstutz <peter.amstutz at curoverse.com>
Date:   Wed Feb 11 15:48:00 2015 -0500

    4904: Detach from Docker container to get cid from stdout instead of fiddling
    with cid file.  Poll "docker ps" to check if the container is still running.  Fixed bugs.
    Updated documentation.

diff --git a/doc/user/topics/arv-web.html.textile.liquid b/doc/user/topics/arv-web.html.textile.liquid
index d902216..cf3a3cc 100644
--- a/doc/user/topics/arv-web.html.textile.liquid
+++ b/doc/user/topics/arv-web.html.textile.liquid
@@ -11,13 +11,15 @@ h2. Usage
 @arv-web@ enables you to set up a web service based on the most recent collection in a project.  An arv-web application is a reproducible, immutable application bundle where the web app is packaged with both the code to run and the data to serve.  Because Arvados Collections can be updated with minimum duplication, it is efficient to produce a new application bundle when the code or data needs to be updated; retaining old application bundles makes it easy to go back and run older versions of your web app.
 
 <pre>
-usage: arv-web.py [-h] --project PROJECT [--port PORT] [--image IMAGE]
+usage: arv-web.py [-h] --project-uuid PROJECT_UUID [--port PORT]
+                  [--image IMAGE]
 
 optional arguments:
-  -h, --help         show this help message and exit
-  --project PROJECT  Project to watch
-  --port PORT        Local bind port
-  --image IMAGE      Docker image to run
+  -h, --help            show this help message and exit
+  --project-uuid PROJECT_UUID
+                        Project uuid to watch
+  --port PORT           Host port to listen on (default 8080)
+  --image IMAGE         Docker image to run
 </pre>
 
 At startup, @arv-web@ queries an Arvados project and mounts the most recently modified collection into a temporary directory.  It then runs a Docker image with the collection bound to @/mnt@ inside the container.  When a new collection is added to the project, or an existing project is updated, it will stop the running Docker container, unmount the old collection, mount the new most recently modified collection, and restart the Docker container with the new mount.
@@ -36,7 +38,7 @@ To build the Docker image:
 
 h2. Running sample applications
 
-First, in Arvados Workbench, create a new project.  Copy the project UUID from the URL bar (this is the part of the URL after @projects/...@)
+First, in Arvados Workbench, create a new project.  Copy the project UUID from the URL bar (this is the part of the URL after @projects/...@).
 
 Now upload a collection containing a "Python WSGI web app:":http://wsgi.readthedocs.org/en/latest/
 
@@ -89,4 +91,8 @@ You can also serve static content from the @public@ directory of the collection.
 
 h3. Custom images
 
-You can provide your own Docker image.  The Docker image that will be used create the web application container is specified in the "docker_image" file in the root of the collection.  Specify @--image@ on the command @arv-web@ line to override the contents of "docker_image".
+You can provide your own Docker image.  The Docker image that will be used create the web application container is specified in the @docker_image@ file in the root of the collection.  You can also specify @--image@ on the command @arv-web@ line to choose the docker image (this will override the contents of @docker_image@).
+
+h3. Reloading the web service
+
+Stopping the Docker container and starting it again can result in a small amount of downtime.  When the collection containing a new or updated web application uses the same Docker image as the currently running web application, it is possible to avoid this downtime by keeping the existing container and only reloading the web server.  This is accomplished by providing a file called @reload@ in the root of the collection, which should contain the commands necessary to reload the web server inside the container.
diff --git a/services/arv-web/arv-web.py b/services/arv-web/arv-web.py
index 1fd61fd..7dbd304 100755
--- a/services/arv-web/arv-web.py
+++ b/services/arv-web/arv-web.py
@@ -26,7 +26,6 @@ class ArvWeb(object):
         self.project = project
         self.loop = True
         self.cid = None
-        self.docker_proc = None
         self.prev_docker_image = None
         self.mountdir = None
         self.collection = None
@@ -35,15 +34,25 @@ class ArvWeb(object):
         self.evqueue = Queue.Queue()
         self.api = SafeApi(arvados.config)
 
-        if arvados.util.group_uuid_patternmatch(project) is None:
+        if arvados.util.group_uuid_pattern.match(project) is None:
             raise arvados.errors.ArgumentError("Project uuid is not valid")
 
-        collections = api.collections().list(filters=[["owner_uuid", "=", project]],
+        collections = self.api.collections().list(filters=[["owner_uuid", "=", project]],
                         limit=1,
                         order='modified_at desc').execute()['items']
-        self.newcollection = collections[0]['uuid'] if len(collections) > 0 else None
+        self.newcollection = collections[0]['uuid'] if collections else None
 
-        self.ws = arvados.events.subscribe(api, [["object_uuid", "is_a", "arvados#collection"]], self.on_message)
+        self.ws = arvados.events.subscribe(self.api, [["object_uuid", "is_a", "arvados#collection"]], self.on_message)
+
+    def check_docker_running(self):
+        # It would be less hacky to use "docker events" than poll "docker ps"
+        # but that would require writing a bigger pile of code.
+        if self.cid:
+            ps = subprocess.check_output(["docker", "ps", "--no-trunc=true", "--filter=status=running"])
+            for l in ps.splitlines():
+                if l.startswith(self.cid):
+                    return True
+        return False
 
     # Handle messages from Arvados event bus.
     def on_message(self, ev):
@@ -74,11 +83,11 @@ class ArvWeb(object):
         self.mountdir = tempfile.mkdtemp()
 
         self.operations = Operations(os.getuid(), os.getgid(), "utf-8")
-        self.cdir = CollectionDirectory(llfuse.ROOT_INODE, self.operations.inodes, api, 2, self.collection)
-        self.operations.inodes.add_entry(cdir)
+        self.cdir = CollectionDirectory(llfuse.ROOT_INODE, self.operations.inodes, self.api, 2, self.collection)
+        self.operations.inodes.add_entry(self.cdir)
 
         # Initialize the fuse connection
-        llfuse.init(operations, mountdir, ['allow_other'])
+        llfuse.init(self.operations, self.mountdir, ['allow_other'])
 
         t = threading.Thread(None, llfuse.main)
         t.start()
@@ -95,18 +104,20 @@ class ArvWeb(object):
             if self.mountdir:
                 with llfuse.lock:
                     self.cdir.clear()
+                    # Switch the FUSE directory object so that it stores
+                    # the newly selected collection
                     if self.collection:
-                        # Switch the FUSE directory object so that it stores
-                        # the newly selected collection
                         logger.info("Mounting %s", self.collection)
-                        cdir.change_collection(self.collection)
+                    else:
+                        logger.info("Mount is empty")
+                    self.cdir.change_collection(self.collection)
+
 
     def stop_docker(self):
         if self.cid:
             logger.info("Stopping Docker container")
-            subprocess.check_call(["docker", "stop", cid])
+            subprocess.call(["docker", "stop", self.cid])
             self.cid = None
-            self.docker_proc = None
 
     def run_docker(self):
         try:
@@ -139,7 +150,7 @@ class ArvWeb(object):
 
             if docker_image == self.prev_docker_image and self.cid is not None and has_reload:
                 logger.info("Running container reload command")
-                subprocess.check_call(["docker", "exec", cid, "/mnt/reload"])
+                subprocess.check_call(["docker", "exec", self.cid, "/mnt/reload"])
                 return
 
             self.stop_docker()
@@ -147,26 +158,11 @@ class ArvWeb(object):
             logger.info("Starting Docker container %s", docker_image)
             ciddir = tempfile.mkdtemp()
             cidfilepath = os.path.join(ciddir, "cidfile")
-            self.docker_proc = subprocess.Popen(["docker", "run",
-                                            "--cidfile=%s" % (cidfilepath),
-                                            "--publish=%i:80" % (self.port),
-                                            "--volume=%s:/mnt:ro" % self.mountdir,
-                                            docker_image])
-            self.cid = None
-            while self.cid is None and self.docker_proc.poll() is None:
-                try:
-                    with open(cidfilepath) as cidfile:
-                        self.cid = cidfile.read().strip()
-                except IOError as e:
-                    # XXX check for ENOENT
-                    pass
-
-            try:
-                if os.path.exists(cidfilepath):
-                    os.unlink(cidfilepath)
-                os.rmdir(ciddir)
-            except OSError:
-                pass
+            self.cid = subprocess.check_output(["docker", "run",
+                                                "--detach=true",
+                                                "--publish=%i:80" % (self.port),
+                                                "--volume=%s:/mnt:ro" % self.mountdir,
+                                                docker_image]).strip()
 
             self.prev_docker_image = docker_image
             logger.info("Container id %s", self.cid)
@@ -196,18 +192,17 @@ class ArvWeb(object):
                 if eq[1] in ('add', 'update', 'create'):
                     self.newcollection = eq[2]
                 elif eq[1] == 'remove':
-                    collections = api.collections().list(filters=[["owner_uuid", "=", project]],
+                    collections = self.api.collections().list(filters=[["owner_uuid", "=", self.project]],
                                                         limit=1,
                                                         order='modified_at desc').execute()['items']
-                    self.newcollection = collections[0]['uuid'] if len(collections) > 0 else None
+                    self.newcollection = collections[0]['uuid'] if collections else None
                 running = False
             except Queue.Empty:
                 pass
 
-            if self.docker_proc and self.docker_proc.poll() is not None:
+            if self.cid and not self.check_docker_running():
                 logger.warning("Service has terminated.  Will try to restart.")
                 self.cid = None
-                self.docker_proc = None
                 running = False
 
 
@@ -228,9 +223,7 @@ class ArvWeb(object):
                     self.ws.close()
                     self.loop = False
         finally:
-            if self.cid:
-                logger.info("Stopping docker container")
-                subprocess.call(["docker", "stop", self.cid])
+            self.stop_docker()
 
             if self.mountdir:
                 logger.info("Unmounting")
@@ -249,7 +242,7 @@ def main(argv):
     signal.signal(signal.SIGTERM, lambda signal, frame: sys.exit(0))
 
     try:
-        arvweb = ArvWeb(args.project_uuid, args.image, args.ports)
+        arvweb = ArvWeb(args.project_uuid, args.image, args.port)
         arvweb.run()
     except arvados.errors.ArgumentError as e:
         logger.error(e)
diff --git a/services/fuse/arvados_fuse/__init__.py b/services/fuse/arvados_fuse/__init__.py
index 870b9a0..52c70ba 100644
--- a/services/fuse/arvados_fuse/__init__.py
+++ b/services/fuse/arvados_fuse/__init__.py
@@ -305,7 +305,9 @@ class CollectionDirectory(Directory):
     def same(self, i):
         return i['uuid'] == self.collection_locator or i['portable_data_hash'] == self.collection_locator
 
+    # Used by arv-web.py to switch the contents of the CollectionDirectory
     def change_collection(self, new_locator):
+        """Switch the contents of the CollectionDirectory.  Must be called with llfuse.lock held."""
         self.collection_locator = new_locator
         self.collection_object = None
         self.update()
@@ -333,6 +335,10 @@ class CollectionDirectory(Directory):
             if self.collection_object is not None and portable_data_hash_pattern.match(self.collection_locator):
                 return True
 
+            if self.collection_locator is None:
+                self.fresh()
+                return True
+
             with llfuse.lock_released:
                 coll_reader = arvados.CollectionReader(
                     self.collection_locator, self.api, self.api.localkeep(),
@@ -354,7 +360,7 @@ class CollectionDirectory(Directory):
 
             self.fresh()
             return True
-        except apiclient.errors.NotFoundError:
+        except arvados.errors.NotFoundError:
             _logger.exception("arv-mount %s: error", self.collection_locator)
         except arvados.errors.ArgumentError as detail:
             _logger.warning("arv-mount %s: error %s", self.collection_locator, detail)

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list