From ce9a24fb700707414f7730b67462130beb9a9ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20K=C3=BCnsem=C3=B6ller?= <jkuensem@physik.uni-bielefeld.de> Date: Mon, 3 May 2021 18:53:50 +0200 Subject: [PATCH] TMSS-703: Cancel running obs on triggered obs, add project property to subtask, modify path_to_project to deal with ManyToMany fields --- .../backend/src/tmss/tmssapp/models/common.py | 27 ++++++++- .../src/tmss/tmssapp/models/scheduling.py | 5 +- .../src/tmss/tmssapp/models/specification.py | 57 ++++++++----------- 3 files changed, 53 insertions(+), 36 deletions(-) diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/common.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/common.py index 4eeeb68e1a4..f9245411fe4 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/models/common.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/common.py @@ -14,6 +14,12 @@ from django.urls import reverse as reverse_url import json import jsonschema from datetime import timedelta +from django.utils.functional import cached_property +from lofar.sas.tmss.tmss.exceptions import TMSSException + +# +# Mixins +# class RefreshFromDbInvalidatesCachedPropertiesMixin(): """Helper Mixin class which invalidates all 'cached_property' attributes on a model upon refreshing from the db""" @@ -22,11 +28,30 @@ class RefreshFromDbInvalidatesCachedPropertiesMixin(): return super().refresh_from_db(*args, **kwargs) def invalidate_cached_properties(self): - from django.utils.functional import cached_property for key, value in self.__class__.__dict__.items(): if isinstance(value, cached_property): self.__dict__.pop(key, None) + +class ProjectPropertyMixin(RefreshFromDbInvalidatesCachedPropertiesMixin): + @cached_property + def project(self): # -> Project: + '''return the related project of this task + ''' + if not hasattr(self, 'path_to_project'): + return TMSSException("Please define a 'path_to_project' attribute on the %s object for the ProjectPropertyMixin to function." % type(self)) + obj = self + for attr in self.path_to_project.split('__'): + obj = getattr(obj, attr) + if attr == 'project': + return obj + if obj and not isinstance(obj, Model): # ManyToMany fields + obj = obj.first() + if obj is None: + logger.warning("The element '%s' in the path_to_project of the %s object returned None for pk=%s" % (attr, type(self), self.pk)) + return None + + # abstract models class BasicCommon(Model): diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py index 3fa4cc2134a..82179139a3f 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py @@ -12,7 +12,7 @@ from django.db.models import Model, ForeignKey, OneToOneField, CharField, DateTi ManyToManyField, CASCADE, SET_NULL, PROTECT, QuerySet, BigAutoField, UniqueConstraint from django.contrib.postgres.fields import ArrayField, JSONField from django.contrib.auth.models import User -from .common import AbstractChoice, BasicCommon, Template, NamedCommon, TemplateSchemaMixin +from .common import AbstractChoice, BasicCommon, Template, NamedCommon, TemplateSchemaMixin, ProjectPropertyMixin from enum import Enum from django.db.models.expressions import RawSQL from django.core.exceptions import ValidationError @@ -138,7 +138,7 @@ class SIPidentifier(Model): # # Instance Objects # -class Subtask(BasicCommon, TemplateSchemaMixin): +class Subtask(BasicCommon, ProjectPropertyMixin, TemplateSchemaMixin): """ Represents a low-level task, which is an atomic unit of execution, such as running an observation, running inspection plots on the observed data, etc. Each task has a specific configuration, will have resources allocated @@ -156,6 +156,7 @@ class Subtask(BasicCommon, TemplateSchemaMixin): created_or_updated_by_user = ForeignKey(User, null=True, editable=False, on_delete=PROTECT, help_text='The user who created / updated the subtask.') raw_feedback = CharField(null=True, max_length=1048576, help_text='The raw feedback for this Subtask') global_identifier = OneToOneField('SIPidentifier', null=False, editable=False, on_delete=PROTECT, help_text='The global unique identifier for LTA SIP.') + path_to_project = 'task_blueprints__scheduling_unit_blueprint__draft__scheduling_set__project' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py index 68091010b7e..195957a49bb 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py @@ -10,7 +10,7 @@ from django.contrib.postgres.fields import JSONField from enum import Enum from django.db.models.expressions import RawSQL from django.db.models.deletion import ProtectedError -from .common import AbstractChoice, BasicCommon, Template, NamedCommon, TemplateSchemaMixin, NamedCommonPK, RefreshFromDbInvalidatesCachedPropertiesMixin +from .common import AbstractChoice, BasicCommon, Template, NamedCommon, TemplateSchemaMixin, NamedCommonPK, RefreshFromDbInvalidatesCachedPropertiesMixin, ProjectPropertyMixin from lofar.common.json_utils import validate_json_against_schema, validate_json_against_its_schema, add_defaults_to_json_object_for_schema from lofar.sas.tmss.tmss.exceptions import * from django.core.exceptions import ValidationError @@ -19,24 +19,6 @@ from collections import Counter from django.utils.functional import cached_property -# -# Mixins -# - -class ProjectPropertyMixin(RefreshFromDbInvalidatesCachedPropertiesMixin): - @cached_property - def project(self): # -> Project: - '''return the related project of this task - ''' - if not hasattr(self, 'path_to_project'): - return TMSSException("Please define a 'path_to_project' attribute on the object for the ProjectPropertyMixin to function.") - obj = self - for attr in self.path_to_project.split('__'): - obj = getattr(obj, attr) - if attr == 'project': - return obj - - # # I/O # @@ -497,6 +479,29 @@ class SchedulingUnitBlueprint(RefreshFromDbInvalidatesCachedPropertiesMixin, Tem def save(self, force_insert=False, force_update=False, using=None, update_fields=None): self.annotate_validate_add_defaults_to_doc_using_template('requirements_doc', 'requirements_template') + if self._state.adding and self.is_triggered and self.project.can_trigger: + from lofar.sas.tmss.services.scheduling.constraints import can_run_after + start_time = datetime.datetime.utcnow() + datetime.timedelta(minutes=3) + logger.info('Checking if triggered obs name=%s can run after start_time=%s' % (self.name, start_time)) + if self.draft.scheduling_constraints_template is None or can_run_after(self, start_time): # todo: use self.scheduling_constraints_template after TMSS-697 was merged + from lofar.sas.tmss.tmss.tmssapp.subtasks import cancel_subtask + from lofar.sas.tmss.tmss.tmssapp.models import Subtask, SubtaskState, SubtaskType + current_obs_subtasks = Subtask.objects.filter(state__value__in=[SubtaskState.Choices.STARTING.value, SubtaskState.Choices.STARTED.value], + specifications_template__type__value=SubtaskType.Choices.OBSERVATION.value).all() + if len(current_obs_subtasks) == 0: + logger.info('No currently running observation detected, so nothing to cancel for triggered obs name=%s' % self.name) + for current_obs_subtask in current_obs_subtasks: + if self.project is None: + logger.warning('Triggered obs name=%s cannot cancel running subtask obs pk=%s because it does not belong to a project and hence has unknown priority' % (self.name, current_obs_subtask.pk)) + continue + if current_obs_subtask.project is None: + logger.warning('Triggered obs name=%s priority=% cannot cancel running subtask obs pk=%s because the running obs does not belong to a project and hence has unknown priority' % (self.name, self.project.trigger_priority, current_obs_subtask.pk)) + continue + logger.info('Checking if triggered obs name=%s priority=%s should cancel running subtask obs pk=%s priority=%s' % (self.name, self.project.trigger_priority, current_obs_subtask.pk, current_obs_subtask.project.trigger_priority)) + if self.project.trigger_priority > current_obs_subtask.project.trigger_priority: + logger.info('Cancelling running obs subtask pk=%s for triggered obs name=%s' % (current_obs_subtask.pk, self.name)) + cancel_subtask(current_obs_subtask) + # This code only happens if the objects is not in the database yet. self._state.adding is True creating if self._state.adding and hasattr(self, 'draft'): self.ingest_permission_required = self.draft.ingest_permission_required @@ -728,20 +733,6 @@ class SchedulingUnitBlueprint(RefreshFromDbInvalidatesCachedPropertiesMixin, Tem return fields_found -class ProjectPropertyMixin(): - @cached_property - def project(self) -> Project: - '''return the related project of this task - ''' - if not hasattr(self, 'path_to_project'): - return TMSSException("Please define a 'path_to_project' attribute on the object for the ProjectPropertyMixin to function.") - obj = self - for attr in self.path_to_project.split('__'): - obj = getattr(obj, attr) - if attr == 'project': - return obj - - class TaskDraft(NamedCommon, ProjectPropertyMixin, TemplateSchemaMixin): specifications_doc = JSONField(help_text='Specifications for this task.') copies = ForeignKey('TaskDraft', related_name="copied_from", on_delete=SET_NULL, null=True, help_text='Source reference, if we are a copy (NULLable).') -- GitLab