Repository: DataBiosphere/toil

Issue:
Fix Job.Service.addChild
Making Job.Service.addChild public is dangerous, as a user may call it after the service system is setup. It is better to modify Job.addService to allow service graphs to be defined.



Discussion:

Resolving patch:
diff --git a/src/toil/job.py b/src/toil/job.py
--- a/src/toil/job.py
+++ b/src/toil/job.py
@@ -139,7 +139,7 @@ def addFollowOn(self, followOnJob):
         followOnJob._addPredecessor(self)
         return followOnJob
 
-    def addService(self, service):
+    def addService(self, service, parentService=None):
         """
         Add a service. 
         
@@ -153,16 +153,29 @@ def addService(self, service):
         
         :raises toil.job.JobException: If service has already been made the child of a job or another service. 
         :param toil.job.Job.Service service: Service to add.
+        :param toil.job.Job.Service parentService: Service that will be started before 'service' is started.\
+        Allows trees of services to be established. parentService must be a service of this job.
         :return: a promise that will be replaced with the return value from \
         :func:`toil.job.Job.Service.start` of service in any successor of the job.
-        :rtype: toil.job.PromisedJobReturnValue 
-        """
-        if service._hasParent:
-            raise JobException("The service already has a parent service")
-        service._hasParent = True
-        jobService = ServiceJob(service)
-        self._services.append((service, jobService))
-        return jobService.rv()
+        :rtype:toil.job.PromisedJobReturnValue
+        """
+        if parentService != None:
+            # Do check to ensure that parentService is a service of this job
+            def check(services):
+                for s, jS in services:
+                    if s == parentService or check(s._childServices):
+                        return True
+                return False
+            if not check(self._services):
+                raise JobException("Parent service is not a service of the given job")
+            return parentService._addChild(service)
+        else:
+            if service._hasParent:
+                raise JobException("The service already has a parent service")
+            service._hasParent = True
+            jobService = ServiceJob(service)
+            self._services.append((service, jobService))
+            return jobService.rv()
 
     ##Convenience functions for creating jobs
 
@@ -919,9 +932,11 @@ def check(self):
             """
             pass
         
-        def addChild(self, service):
+        def _addChild(self, service):
             """
-            Add a child service to start up after this service has started. 
+            Add a child service to start up after this service has started.
+            This should not be called by the user, instead use :func:`toil.job.Job.Service.addService`
+            with the "parentService" option.
             
             :raises toil.job.JobException: If service has already been made the child of a job or another service. 
             :param toil.job.Job.Service service: Service to add as a "child" of this service


Test patch:
diff --git a/src/toil/test/src/jobServiceTest.py b/src/toil/test/src/jobServiceTest.py
--- a/src/toil/test/src/jobServiceTest.py
+++ b/src/toil/test/src/jobServiceTest.py
@@ -150,7 +150,7 @@ def serviceTestRecursive(job, outFile, messages):
         for i in xrange(1, len(messages)):
             randInt = random.randint(1, sys.maxint)
             service2 = TestService(messages[i] + randInt, cores=0.1)
-            child = child.addChildJobFn(serviceAccessor, service.addChild(service2), outFile, randInt, cores=0.1)
+            child = child.addChildJobFn(serviceAccessor, job.addService(service2, parentService=service), outFile, randInt, cores=0.1)
             service = service2
 
 def serviceTestParallelRecursive(job, outFiles, messageBundles):
@@ -168,7 +168,7 @@ def serviceTestParallelRecursive(job, outFiles, messageBundles):
             for i in xrange(1, len(messages)):
                 randInt = random.randint(1, sys.maxint)
                 service2 = TestService(messages[i] + randInt, cores=0.1)
-                child = child.addChildJobFn(serviceAccessor, service.addChild(service2), outFile, randInt, cores=0.1)
+                child = child.addChildJobFn(serviceAccessor, job.addService(service2, parentService=service), outFile, randInt, cores=0.1)
                 service = service2
 
 class TestService(Job.Service):