1
1
import logging
2
- import random
3
2
import sys
4
3
import traceback
5
4
@@ -70,8 +69,8 @@ class Process(models.Model, metaclass=BaseProcess):
70
69
:attr:`.edges`.
71
70
"""
72
71
id = models .BigAutoField (primary_key = True , editable = False )
73
- started = models .DateTimeField (auto_now_add = True , db_index = True )
74
- completed = models .DateTimeField (blank = True , null = True , editable = False , db_index = True )
72
+ created = models .DateTimeField (auto_now_add = True , db_index = True )
73
+ modified = models .DateTimeField (auto_now = True , db_index = True )
75
74
76
75
task_set = GenericRelation ('galahad.Task' , object_id_field = '_process_id' )
77
76
@@ -267,60 +266,112 @@ def get_instance_graph_svg(self, output_format='svg'):
267
266
graph .format = output_format
268
267
return SafeString (graph .pipe ().decode ('utf-8' ))
269
268
269
+ def save (self , ** kwargs ):
270
+ if self .pk :
271
+ try :
272
+ update_fields = kwargs ['update_fields' ]
273
+ except KeyError :
274
+ pass
275
+ else :
276
+ update_fields .append ('modified' )
277
+ super ().save (** kwargs )
278
+
270
279
271
280
def process_subclasses ():
272
281
from django .apps import apps
273
282
274
283
apps .check_models_ready ()
275
284
query = models .Q ()
276
- for app_models in apps .all_models .values ():
277
- for model in app_models .values ():
278
- if issubclass (model , Process ) and model is not Process :
279
- opts = model ._meta
280
- query |= models .Q (app_label = opts .app_label , model = opts .model_name )
285
+ for model in apps .get_models ():
286
+ if issubclass (model , Process ) and model is not Process :
287
+ opts = model ._meta
288
+ query |= models .Q (app_label = opts .app_label , model = opts .model_name )
281
289
return query
282
290
283
291
284
292
class TasksQuerySet (models .query .QuerySet ):
285
- is_completed = models .Q (completed__isnull = False )
286
- is_failed = models .Q (failed__isnull = False )
287
293
288
294
def scheduled (self ):
289
- return self .filter (~ ( self .is_failed | self . is_completed ) )
295
+ return self .filter (status = self .model . SCHEDULED )
290
296
291
297
def succeeded (self ):
292
- return self .filter (self .is_completed )
298
+ return self .filter (status = self .model .SUCCEEDED )
299
+
300
+ def not_succeeded (self ):
301
+ return self .exclude (status = self .model .SUCCEEDED )
293
302
294
303
def failed (self ):
295
- return self .filter (self .is_failed )
304
+ return self .filter (status = self .model . FAILED )
296
305
297
306
298
307
class Task (models .Model ):
299
308
id = models .BigAutoField (primary_key = True , editable = False )
300
- _process = models .ForeignKey ('galahad.Process' , on_delete = models .CASCADE , db_column = 'process_id' , editable = False )
309
+ _process = models .ForeignKey (
310
+ 'galahad.Process' ,
311
+ on_delete = models .CASCADE ,
312
+ db_column = 'process_id'
313
+ , editable = False ,
314
+ )
301
315
content_type = models .ForeignKey (
302
- 'contenttypes.ContentType' , on_delete = models .CASCADE ,
303
- editable = False , limit_choices_to = process_subclasses ,
316
+ 'contenttypes.ContentType' ,
317
+ on_delete = models .CASCADE ,
318
+ editable = False ,
319
+ limit_choices_to = process_subclasses ,
304
320
related_name = 'galahad_task_set' ,
305
321
)
306
322
process = GenericForeignKey ('content_type' , '_process_id' )
323
+
307
324
node_name = models .TextField (db_index = True , editable = False )
308
- parent_task_set = models .ManyToManyField ('self' , related_name = 'child_task_set' , editable = False , symmetrical = False )
309
325
310
- assignees = models .ManyToManyField (settings .AUTH_USER_MODEL , verbose_name = t ('assignees' ), related_name = 'galahad_task_set' )
326
+ HUMAN = 'human'
327
+ MACHINE = 'machine'
328
+ _node_type_choices = (
329
+ (HUMAN , t (HUMAN )),
330
+ (MACHINE , t (MACHINE )),
331
+ )
332
+ node_type = models .TextField (
333
+ choices = _node_type_choices ,
334
+ editable = False ,
335
+ db_index = True ,
336
+ )
337
+
338
+ parent_task_set = models .ManyToManyField (
339
+ 'self' ,
340
+ related_name = 'child_task_set' ,
341
+ editable = False ,
342
+ symmetrical = False ,
343
+ )
344
+
345
+ FAILED = 'failed'
346
+ SUCCEEDED = 'succeeded'
347
+ SCHEDULED = 'scheduled'
348
+ _status_choices = (
349
+ (FAILED , t (FAILED )),
350
+ (SUCCEEDED , t (SUCCEEDED )),
351
+ (SCHEDULED , t (SCHEDULED )),
352
+ )
353
+ status = models .TextField (
354
+ choices = _status_choices ,
355
+ default = SCHEDULED ,
356
+ editable = False ,
357
+ db_index = True ,
358
+ )
359
+
360
+ assignees = models .ManyToManyField (
361
+ settings .AUTH_USER_MODEL ,
362
+ verbose_name = t ('assignees' ),
363
+ related_name = 'galahad_task_set' ,
364
+ )
311
365
312
366
created = models .DateTimeField (auto_now_add = True , db_index = True )
367
+ modified = models .DateTimeField (auto_now = True , db_index = True )
313
368
completed = models .DateTimeField (blank = True , null = True , editable = False , db_index = True )
314
- failed = models .DateTimeField (blank = True , null = True , editable = False , db_index = True )
315
369
316
370
exception = models .TextField (blank = True )
317
371
stacktrace = models .TextField (blank = True )
318
372
319
373
objects = TasksQuerySet .as_manager ()
320
374
321
- def __str__ (self ):
322
- return '%s (%s)' % (self .node_name , self .pk )
323
-
324
375
class Meta :
325
376
ordering = ('-completed' , '-created' )
326
377
get_latest_by = ('created' ,)
@@ -330,6 +381,21 @@ class Meta:
330
381
)
331
382
default_manager_name = 'objects'
332
383
384
+ def __str__ (self ):
385
+ return '%s (%s)' % (self .node_name , self .pk )
386
+
387
+ def save (self , ** kwargs ):
388
+ if self .pk :
389
+ try :
390
+ update_fields = kwargs ['update_fields' ]
391
+ except KeyError as e :
392
+ raise ValueError (
393
+ "You need to provide explicit 'update_fields' to avoid race conditions."
394
+ ) from e
395
+ else :
396
+ update_fields .append ('modified' )
397
+ super ().save (** kwargs )
398
+
333
399
def get_absolute_url (self ):
334
400
if self .completed :
335
401
return
@@ -345,17 +411,19 @@ def node(self):
345
411
346
412
def finish (self ):
347
413
self .completed = timezone .now ()
414
+ self .status = self .SUCCEEDED
348
415
if self .pk :
349
- self .save (update_fields = ['completed' ])
416
+ self .save (update_fields = ['status' , ' completed' ])
350
417
else :
351
418
self .save ()
352
419
353
420
def fail (self ):
354
- self .failed = timezone .now ()
421
+ self .completed = timezone .now ()
422
+ self .status = self .FAILED
355
423
tb = traceback .format_exception (* sys .exc_info ())
356
424
self .exception = tb [- 1 ].strip ()
357
425
self .stacktrace = "" .join (tb )
358
- self .save (update_fields = ['failed ' , 'exception' , 'stacktrace' ])
426
+ self .save (update_fields = ['status ' , 'exception' , 'stacktrace' ])
359
427
360
428
def enqueue (self , countdown = None , eta = None ):
361
429
"""
@@ -372,12 +440,22 @@ def enqueue(self, countdown=None, eta=None):
372
440
celery.result.AsyncResult: Celery task result.
373
441
374
442
"""
375
- return celery .task_wrapper .apply_async (
443
+ self .status = self .SCHEDULED
444
+ self .completed = None
445
+ self .exception = ''
446
+ self .stacktrace = ''
447
+ self .save (update_fields = [
448
+ 'status' ,
449
+ 'completed' ,
450
+ 'exception' ,
451
+ 'stacktrace' ,
452
+ ])
453
+ transaction .on_commit (lambda : celery .task_wrapper .apply_async (
376
454
args = (self .pk , self ._process_id ),
377
455
countdown = countdown ,
378
456
eta = eta ,
379
457
queue = settings .GALAHAD_CELERY_QUEUE_NAME ,
380
- )
458
+ ))
381
459
382
460
@transaction .atomic ()
383
461
def start_next_tasks (self , next_nodes : list = None ):
@@ -400,11 +478,12 @@ def start_next_tasks(self, next_nodes: list = None):
400
478
# Some nodes – like Join – implement their own method to create new tasks.
401
479
task = node .create_task (self .process )
402
480
except AttributeError :
403
- task = self .process .task_set .create (node_name = node .node_name , completed = None , failed = None )
481
+ task = self .process .task_set .create (
482
+ node_name = node .node_name ,
483
+ node_type = node .node_type ,
484
+ )
404
485
task .parent_task_set .add (self )
405
486
if callable (node ):
406
487
transaction .on_commit (task .enqueue )
407
488
tasks .append (task )
408
- else :
409
- self .process .finish ()
410
489
return tasks
0 commit comments