10
10
import six
11
11
from six import string_types
12
12
13
+ import psutil
14
+
13
15
from .builder import Builder # pylint: disable=unused-import
14
16
from .errors import WorkflowException
15
17
from .job import JobBase # pylint: disable=unused-import
@@ -129,35 +131,82 @@ class MultithreadedJobExecutor(JobExecutor):
129
131
"""
130
132
Experimental multi-threaded CWL executor.
131
133
132
- Can easily overload a system as it does not do resource accounting.
134
+ Does simple resource accounting, will not start a job unless it
135
+ has cores / ram available, but does not make any attempt to
136
+ optimize usage.
133
137
"""
138
+
134
139
def __init__ (self ): # type: () -> None
135
140
super (MultithreadedJobExecutor , self ).__init__ ()
136
141
self .threads = set () # type: Set[threading.Thread]
137
142
self .exceptions = [] # type: List[WorkflowException]
143
+ self .pending_jobs = []
144
+
145
+ self .max_ram = psutil .virtual_memory ().total / 2 ** 20
146
+ self .max_cores = psutil .cpu_count ()
147
+ self .allocated_ram = 0
148
+ self .allocated_cores = 0
149
+
150
+ def select_resources (self , request , builder ):
151
+ result = {}
152
+ maxrsc = {
153
+ "cores" : self .max_cores ,
154
+ "ram" : self .max_ram ,
155
+ "tmpdir" : psutil .disk_usage (builder .tmpdir ).free / 2 ** 20 ,
156
+ "outdir" : psutil .disk_usage (builder .outdir ).free / 2 ** 20
157
+ }
158
+ for rsc in ("cores" , "ram" , "tmpdir" , "outdir" ):
159
+ key = rsc + "Size" if rsc .endswith ("dir" ) else rsc
160
+ if request [rsc + "Min" ] > maxrsc [rsc ]:
161
+ raise WorkflowException ("Requested at least %d %s but only %d available" , request [rsc + "Min" ], rsc , maxrsc [rsc ])
162
+ if request [rsc + "Max" ] < maxrsc [rsc ]:
163
+ result [key ] = request [rsc + "Max" ]
164
+ else :
165
+ result [key ] = maxrsc [rsc ]
166
+
167
+ return result
138
168
139
169
def run_job (self ,
140
170
job , # type: JobBase
141
171
runtimeContext # type: RuntimeContext
142
172
): # type: (...) -> None
143
173
""" Execute a single Job in a seperate thread. """
144
- def runner ():
145
- """ Job running thread. """
146
- try :
147
- job .run (runtimeContext )
148
- except WorkflowException as err :
149
- self .exceptions .append (err )
150
- except Exception as err :
151
- self .exceptions .append (WorkflowException (Text (err )))
152
- finally :
153
- with runtimeContext .workflow_eval_lock :
154
- self .threads .remove (thread )
155
- runtimeContext .notifyAll ()
156
-
157
- thread = threading .Thread (target = runner )
158
- thread .daemon = True
159
- self .threads .add (thread )
160
- thread .start ()
174
+
175
+ if job is not None :
176
+ self .pending_jobs .append (job )
177
+
178
+ while self .pending_jobs :
179
+ job = self .pending_jobs [0 ]
180
+ if isinstance (job , JobBase ):
181
+ if ((self .allocated_ram + job .builder .resources ["ram" ]) > self .max_ram or
182
+ (self .allocated_cores + job .builder .resources ["cores" ]) > self .max_cores ):
183
+ return
184
+
185
+ self .pending_jobs .pop (0 )
186
+
187
+ def runner ():
188
+ """ Job running thread. """
189
+ try :
190
+ job .run (runtimeContext )
191
+ except WorkflowException as err :
192
+ self .exceptions .append (err )
193
+ except Exception as err :
194
+ self .exceptions .append (WorkflowException (Text (err )))
195
+ finally :
196
+ with runtimeContext .workflow_eval_lock :
197
+ self .threads .remove (thread )
198
+ if isinstance (job , JobBase ):
199
+ self .allocated_ram -= job .builder .resources ["ram" ]
200
+ self .allocated_cores -= job .builder .resources ["cores" ]
201
+ runtimeContext .workflow_eval_lock .notifyAll ()
202
+
203
+ thread = threading .Thread (target = runner )
204
+ thread .daemon = True
205
+ self .threads .add (thread )
206
+ if isinstance (job , JobBase ):
207
+ self .allocated_ram += job .builder .resources ["ram" ]
208
+ self .allocated_cores += job .builder .resources ["cores" ]
209
+ thread .start ()
161
210
162
211
def wait_for_next_completion (self , runtimeContext ): # type: () -> None
163
212
""" Wait for jobs to finish. """
@@ -181,8 +230,10 @@ def run_jobs(self,
181
230
job .builder = runtimeContext .builder
182
231
if job .outdir :
183
232
self .output_dirs .add (job .outdir )
184
- self .run_job (job , runtimeContext )
185
- else :
233
+
234
+ self .run_job (job , runtimeContext )
235
+
236
+ if job is None :
186
237
if self .threads :
187
238
self .wait_for_next_completion (runtimeContext )
188
239
else :
0 commit comments