diff --git a/SAS/TMSS/backend/services/scheduling/lib/subtask_scheduling.py b/SAS/TMSS/backend/services/scheduling/lib/subtask_scheduling.py index 4ca2887f4bc7ce9c82fa6068964db11081cb4e85..9a1fd362797441554ac6f4567bda806922c54bc7 100644 --- a/SAS/TMSS/backend/services/scheduling/lib/subtask_scheduling.py +++ b/SAS/TMSS/backend/services/scheduling/lib/subtask_scheduling.py @@ -65,25 +65,29 @@ class TMSSSubTaskSchedulingEventMessageHandler(TMSSEventMessageHandler): try: suc_subtask_id = successor['id'] suc_subtask_state = successor['state_value'] - - if suc_subtask_state == "defined": - successor_predecessors = self.tmss_client.get_subtask_predecessors(suc_subtask_id) - - if any([suc_pred['state_value']!='finished' for suc_pred in successor_predecessors]): - logger.info("skipping scheduling of successor subtask %s for finished subtask %s because not all its other predecessor subtasks are finished", suc_subtask_id, id) + suc_subtask_obsolete_since = successor['obsolete_since'] + + if suc_subtask_obsolete_since is None: + if suc_subtask_state == "defined": + successor_predecessors = self.tmss_client.get_subtask_predecessors(suc_subtask_id) + + if any([suc_pred['state_value'] != 'finished' and suc_pred['obsolete_since'] is None for suc_pred in successor_predecessors]): + logger.info("skipping scheduling of successor subtask %s for finished subtask %s because not all its other (non-obsolete) predecessor subtasks are finished", suc_subtask_id, id) + else: + logger.info("trying to schedule successor subtask %s for finished subtask %s", suc_subtask_id, id) + # try scheduling the subtask. + # if it succeeds, then the state will be 'scheduled' afterwards + # if there is a specification error, then the state will be 'error' afterwards + # if there is another kind of error (like needing ingest-permission), then the state will be 'defined' afterwards, so you can retry. + # for the ingest-permission we will retry automatically when that permission is granted + scheduled_successor = self.tmss_client.schedule_subtask(suc_subtask_id) + suc_subtask_state = scheduled_successor['state_value'] + logger.log(logging.INFO if suc_subtask_state=='scheduled' else logging.WARNING, + "successor subtask %s for finished subtask %s now has state '%s', see %s", suc_subtask_id, id, suc_subtask_state, scheduled_successor['url']) else: - logger.info("trying to schedule successor subtask %s for finished subtask %s", suc_subtask_id, id) - # try scheduling the subtask. - # if it succeeds, then the state will be 'scheduled' afterwards - # if there is a specification error, then the state will be 'error' afterwards - # if there is another kind of error (like needing ingest-permission), then the state will be 'defined' afterwards, so you can retry. - # for the ingest-permission we will retry automatically when that permission is granted - scheduled_successor = self.tmss_client.schedule_subtask(suc_subtask_id) - suc_subtask_state = scheduled_successor['state_value'] - logger.log(logging.INFO if suc_subtask_state=='scheduled' else logging.WARNING, - "successor subtask %s for finished subtask %s now has state '%s', see %s", suc_subtask_id, id, suc_subtask_state, scheduled_successor['url']) + logger.warning("skipping scheduling of successor subtask %s for finished subtask %s because its state is '%s'", suc_subtask_id, id, suc_subtask_state) else: - logger.warning("skipping scheduling of successor subtask %s for finished subtask %s because its state is '%s'", suc_subtask_id, id, suc_subtask_state) + logger.warning("skipping scheduling of successor subtask %s for finished subtask %s because it is markes obsolete since %s", suc_subtask_id, id, suc_subtask_obsolete_since) except Exception as e: logger.error(e) @@ -96,7 +100,7 @@ class TMSSSubTaskSchedulingEventMessageHandler(TMSSEventMessageHandler): if subtask['state_value'] == 'defined': subtask_template = self.tmss_client.get_url_as_json_object(subtask['specifications_template']) if subtask_template['type_value'] == 'ingest': - if all(pred['state_value'] == 'finished' for pred in self.tmss_client.get_subtask_predecessors(subtask['id'])): + if all(pred['state_value'] == 'finished' or pred['obsolete_since'] is not None for pred in self.tmss_client.get_subtask_predecessors(subtask['id'])): logger.info("trying to schedule ingest subtask id=%s for scheduling_unit_blueprint id=%s...", subtask['id'], id) self.tmss_client.schedule_subtask(subtask['id']) diff --git a/SAS/TMSS/backend/src/CMakeLists.txt b/SAS/TMSS/backend/src/CMakeLists.txt index f80c85dc26bfdd8d5a769f107b6b4e0830c00016..167c3911f70d60354827352a249b0b3c8fc75394 100644 --- a/SAS/TMSS/backend/src/CMakeLists.txt +++ b/SAS/TMSS/backend/src/CMakeLists.txt @@ -32,7 +32,6 @@ find_python_module(swagger_spec_validator REQUIRED) # pip3 install swagger-spec- set(_py_files manage.py - remakemigrations.py ) python_install(${_py_files} diff --git a/SAS/TMSS/backend/src/remakemigrations.py b/SAS/TMSS/backend/src/remakemigrations.py deleted file mode 100755 index ceb04af2b7220d8f40cc0ebb7c75d17b8edd48dd..0000000000000000000000000000000000000000 --- a/SAS/TMSS/backend/src/remakemigrations.py +++ /dev/null @@ -1,283 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright (C) 2018 ASTRON (Netherlands Institute for Radio Astronomy) -# P.O. Box 2, 7990 AA Dwingeloo, The Netherlands -# -# This file is part of the LOFAR software suite. -# The LOFAR software suite is free software: you can redistribute it and/or -# modify it under the terms of the GNU General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# The LOFAR software suite is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with the LOFAR software suite. If not, see <http://www.gnu.org/licenses/>. - -# $Id: $ - -# This script automates the procedure to replace the existing migrations on the source tree with initital migrations -# based on the current datamodel. Django offers a call 'makemigrations' through manage.py, which creates new migrations -# after the datamodel implementation has changed. These additional migrations apply those changes to an existing -# database reflecting the previous datamodel. -# This is a very nice feature for production, but there are a few downsides that this script tackles: -# -# 1. During development, where the datamodel constantly changes, whe typically don't want a ton of iterative migrations, -# but just have a clean start with a fresh initial database state without the whole provenance is perfectly fine. (We -# start up a fresh database anyway for every test or test deployment.) This can be achieved by removing all existing -# migrations prior to creating new ones. -# A difficulty with this approach is that we do have a manual migration to populate the database with fixtures. -# This migration needs to be restored or re-created after Django created fresh migrations for the database itself. -# -# 2. Since in settings.py we refer to the tmss app in the lofar environment, Django uses the build or installed version. -# A consequence is that the created migrations are placed in there and need to be copied to the source tree. -# -# This script requires a running postgres database instance to work against. -# To use specific database credentials, run e.g. ./remakemigrations.py -C b5f881c4-d41a-4f24-b9f5-23cd6a7f37d0 - - -import os -from glob import glob -import subprocess as sp -import logging -import argparse -from shutil import copy -import lofar.sas.tmss - -logger = logging.getLogger(__file__) - -# set up paths -tmss_source_directory = os.path.dirname(__file__) -if tmss_source_directory == '': - tmss_source_directory = '.' -tmss_env_directory = os.path.dirname(lofar.sas.tmss.__file__) -relative_migrations_directory = '/tmss/tmssapp/migrations/' - -# template for manual changes and fixture (applied last): -template = """ -# -# auto-generated by remakemigrations.py -# -# ! Please make sure to apply any changes to the template in that script ! -# -from django.db import migrations - -from lofar.sas.tmss.tmss.tmssapp.populate import * - -class Migration(migrations.Migration): - - dependencies = [ - ('tmssapp', '{migration_dependency}'), - ] - - operations = [ migrations.RunSQL('ALTER SEQUENCE tmssapp_SubTask_id_seq RESTART WITH 2000000;'), # Start SubTask id with 2 000 000 to avoid overlap with 'old' (test/production) OTDB - # add an SQL trigger in the database enforcing correct state transitions. - # it is crucial that illegal subtask state transitions are block at the "lowest level" (i.e.: in the database) so we can guarantee that the subtask state machine never breaks. - # see: https://support.astron.nl/confluence/display/TMSS/Subtask+State+Machine - # Explanation of SQl below: A trigger function is called upon each create/update of the subtask. - # If the state changes, then it is checked if the state transition from old to new is present in the SubtaskAllowedStateTransitions table. - # If not an Exception is raised, thus enforcing a rollback, thus enforcing the state machine to follow the design. - # It is thereby enforced upon the user/caller to handle these blocked illegal state transitions, and act more wisely. - migrations.RunSQL('''CREATE OR REPLACE FUNCTION tmssapp_check_subtask_state_transition() - RETURNS trigger AS - $BODY$ - BEGIN - IF TG_OP = 'INSERT' THEN - IF NOT (SELECT EXISTS(SELECT id FROM tmssapp_subtaskallowedstatetransitions WHERE old_state_id IS NULL AND new_state_id=NEW.state_id)) THEN - RAISE EXCEPTION 'ILLEGAL SUBTASK STATE TRANSITION FROM % TO %', NULL, NEW.state_id; - END IF; - END IF; - IF TG_OP = 'UPDATE' THEN - IF OLD.state_id <> NEW.state_id AND NOT (SELECT EXISTS(SELECT id FROM tmssapp_subtaskallowedstatetransitions WHERE old_state_id=OLD.state_id AND new_state_id=NEW.state_id)) THEN - RAISE EXCEPTION 'ILLEGAL SUBTASK STATE TRANSITION FROM "%" TO "%"', OLD.state_id, NEW.state_id; - END IF; - END IF; - RETURN NEW; - END; - $BODY$ - LANGUAGE plpgsql VOLATILE; - DROP TRIGGER IF EXISTS tmssapp_trigger_on_check_subtask_state_transition ON tmssapp_SubTask ; - CREATE TRIGGER tmssapp_trigger_on_check_subtask_state_transition - BEFORE INSERT OR UPDATE ON tmssapp_SubTask - FOR EACH ROW EXECUTE PROCEDURE tmssapp_check_subtask_state_transition();'''), - - # use database triggers to block updates on blueprint tables for immutable fields - migrations.RunSQL('''CREATE OR REPLACE FUNCTION tmssapp_block_scheduling_unit_blueprint_immutable_fields_update() - RETURNS trigger AS - $BODY$ - BEGIN - IF OLD.specifications_template_id <> NEW.specifications_template_id OR - OLD.name <> NEW.name OR - OLD.description <> NEW.description THEN - RAISE EXCEPTION 'ILLEGAL UPDATE OF IMMUTABLE BLUEPRINT FIELD'; - END IF; - RETURN NEW; - END; - $BODY$ - LANGUAGE plpgsql VOLATILE; - DROP TRIGGER IF EXISTS tmssapp_trigger_block_scheduling_unit_blueprint_immutable_fields_update ON tmssapp_SchedulingUnitBlueprint ; - CREATE TRIGGER tmssapp_block_scheduling_unit_blueprint_immutable_fields_update - BEFORE UPDATE ON tmssapp_SchedulingUnitBlueprint - FOR EACH ROW EXECUTE PROCEDURE tmssapp_block_scheduling_unit_blueprint_immutable_fields_update(); - '''), - migrations.RunSQL('''CREATE OR REPLACE FUNCTION tmssapp_block_task_blueprint_immutable_fields_update() - RETURNS trigger AS - $BODY$ - BEGIN - IF OLD.specifications_doc <> NEW.specifications_doc OR - OLD.name <> NEW.name OR - OLD.description <> NEW.description OR - OLD.short_description <> NEW.short_description OR - OLD.specifications_template_id <> NEW.specifications_template_id OR - OLD.scheduling_unit_blueprint_id <> NEW.scheduling_unit_blueprint_id THEN - RAISE EXCEPTION 'ILLEGAL UPDATE OF IMMUTABLE BLUEPRINT FIELD'; - END IF; - RETURN NEW; - END; - $BODY$ - LANGUAGE plpgsql VOLATILE; - DROP TRIGGER IF EXISTS tmssapp_trigger_block_task_blueprint_immutable_fields_update ON tmssapp_TaskBlueprint ; - CREATE TRIGGER tmssapp_block_task_blueprint_immutable_fields_update - BEFORE UPDATE ON tmssapp_TaskBlueprint - FOR EACH ROW EXECUTE PROCEDURE tmssapp_block_task_blueprint_immutable_fields_update(); - '''), - migrations.RunSQL('''CREATE OR REPLACE FUNCTION tmssapp_block_task_relation_blueprint_immutable_fields_update() - RETURNS trigger AS - $BODY$ - BEGIN - IF OLD.selection_doc <> NEW.selection_doc OR - OLD.producer_id <> NEW.producer_id OR - OLD.consumer_id <> NEW.consumer_id OR - OLD.input_role_id <> NEW.input_role_id OR - OLD.output_role_id <> NEW.output_role_id OR - OLD.input_role_id <> NEW.input_role_id OR - OLD.selection_template_id <> NEW.selection_template_id THEN - RAISE EXCEPTION 'ILLEGAL UPDATE OF IMMUTABLE BLUEPRINT FIELD'; - END IF; - RETURN NEW; - END; - $BODY$ - LANGUAGE plpgsql VOLATILE; - DROP TRIGGER IF EXISTS tmssapp_trigger_block_task_relation_blueprint_immutable_fields_update ON tmssapp_TaskRelationBlueprint ; - CREATE TRIGGER tmssapp_block_task_relation_blueprint_immutable_fields_update - BEFORE UPDATE ON tmssapp_TaskRelationBlueprint - FOR EACH ROW EXECUTE PROCEDURE tmssapp_block_task_relation_blueprint_immutable_fields_update(); - '''), - migrations.RunSQL('''CREATE OR REPLACE FUNCTION tmssapp_block_subtask_immutable_fields_update() - RETURNS trigger AS - $BODY$ - BEGIN - IF OLD.specifications_doc <> NEW.specifications_doc OR - OLD.primary <> NEW.primary OR - OLD.task_blueprint_id <> NEW.task_blueprint_id OR - OLD.specifications_template_id <> NEW.specifications_template_id OR - OLD.cluster_id <> NEW.cluster_id THEN - RAISE EXCEPTION 'ILLEGAL UPDATE OF IMMUTABLE SUBTASK FIELD'; - END IF; - RETURN NEW; - END; - $BODY$ - LANGUAGE plpgsql VOLATILE; - DROP TRIGGER IF EXISTS tmssapp_trigger_block_subtask_immutable_fields_update ON tmssapp_Subtask ; - CREATE TRIGGER tmssapp_block_subtask_immutable_fields_update - BEFORE UPDATE ON tmssapp_Subtask - FOR EACH ROW EXECUTE PROCEDURE tmssapp_block_subtask_immutable_fields_update(); - '''), - migrations.RunPython(populate_choices), - migrations.RunPython(populate_subtask_allowed_state_transitions), - migrations.RunPython(populate_settings), - migrations.RunPython(populate_misc), - migrations.RunPython(populate_resources), - migrations.RunPython(populate_cycles), - migrations.RunPython(populate_projects) ] - -""" - - -def execute_and_log(cmd): - - logger.info('COMMAND: %s' % cmd) - p = sp.Popen(cmd, shell=True, stdout=sp.PIPE, stderr=sp.PIPE) - out, err = p.communicate() - if out is not None: - logger.info("STDOUT: %s" % out.decode('utf-8').strip()) - if err is not None: - logger.info("STDERR: %s" % err.decode('utf-8').strip()) - - -def delete_old_migrations(): - logger.info('Removing old migrations...') - - files = glob_migrations() - for f in [path for path in files if ("initial" in path or "auto" in path or "populate" in path)]: - logger.info('Deleting: %s' % f) - os.remove(f) - - -def make_django_migrations(dbcredentials=None): - - logger.info('Making Django migrations...') - if dbcredentials: - os.environ['TMSS_DBCREDENTIALS'] = dbcredentials - execute_and_log('/usr/bin/env python3 %s/manage.py makemigrations' % tmss_source_directory) - - -def make_populate_migration(): - - logger.info('Making migration for populating database...') - last_migration = determine_last_migration() - migration = template.format(migration_dependency=last_migration) - - path = tmss_env_directory + relative_migrations_directory + '%s_populate.py' % str(int(last_migration.split('_')[0])+1).zfill(4) - logger.info('Writing to: %s' % path) - with open(path,'w') as f: - f.write(migration) - - -def glob_migrations(directories=(tmss_source_directory, tmss_env_directory)): - paths = [] - for directory in directories: - paths += glob(directory + '/' + relative_migrations_directory + '0*_*') - return paths - - -def copy_migrations_to_source(): - logger.info('Copying over migrations to source directory...') - files = glob_migrations(directories=[tmss_env_directory]) - for file in files: - logger.info('Copying %s to %s' % (file, tmss_source_directory + '/' + relative_migrations_directory)) - copy(file, tmss_source_directory + '/' + relative_migrations_directory) - - -def determine_last_migration(): - logger.info('Determining last migration...') - files = glob_migrations() - files = [os.path.basename(path) for path in files] - f = max(files) - last_migration = f.split('.py')[0] - logger.info('Determined last migration: %s' % last_migration) - return last_migration - - -def remake_migrations(dbcredentials=None): - delete_old_migrations() - make_django_migrations(dbcredentials) - make_populate_migration() - copy_migrations_to_source() - - -if __name__ == "__main__": - - logger.setLevel(logging.DEBUG) - - handler = logging.StreamHandler() - handler.setLevel(logging.INFO) - logger.addHandler(handler) - - parser = argparse.ArgumentParser() - parser.add_argument("-C", action="store", dest="dbcredentials", help="use database specified in these dbcredentials") - args = parser.parse_args() - remake_migrations(args.dbcredentials) diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0004_subtask_error_reason.py b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0004_subtask_error_reason.py new file mode 100644 index 0000000000000000000000000000000000000000..a784361a44d64cb70e4f95effc9cc722dbeb8ef0 --- /dev/null +++ b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0004_subtask_error_reason.py @@ -0,0 +1,55 @@ +# Generated by Django 3.0.9 on 2021-10-15 07:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tmssapp', '0003_taskblueprint_shortdescription'), + ] + + operations = [ + migrations.AddField( + model_name='subtask', + name='error_reason', + field=models.CharField(help_text='Reason why the Subtask went to error.', max_length=200, null=True), + ), + migrations.RunSQL('''CREATE OR REPLACE FUNCTION tmssapp_block_subtask_immutable_fields_update() + RETURNS trigger AS + $BODY$ + BEGIN + IF TG_OP = 'UPDATE' THEN + IF OLD.specifications_doc <> NEW.specifications_doc OR + OLD.primary <> NEW.primary OR + OLD.task_blueprint_id <> NEW.task_blueprint_id OR + OLD.specifications_template_id <> NEW.specifications_template_id OR + OLD.cluster_id <> NEW.cluster_id THEN + RAISE EXCEPTION 'ILLEGAL UPDATE OF IMMUTABLE SUBTASK FIELD'; + END IF; + + IF OLD.error_reason <> NEW.error_reason OR (OLD.error_reason IS NULL AND NEW.error_reason IS NOT NULL) THEN + IF OLD.error_reason IS NOT NULL THEN + RAISE EXCEPTION 'subtask.error_reason may only be set once'; + END IF; + IF NEW.error_reason IS NOT NULL AND NEW.state_id <> 'error' THEN + RAISE EXCEPTION 'subtask.error_reason may only be set when state==error'; + END IF; + END IF; + END IF; + + IF TG_OP = 'INSERT' THEN + IF NEW.error_reason IS NOT NULL AND NEW.state_id <> 'error' THEN + RAISE EXCEPTION 'subtask.error_reason may only be set when state==error'; + END IF; + END IF; + RETURN NEW; + END; + $BODY$ + LANGUAGE plpgsql VOLATILE; + DROP TRIGGER IF EXISTS tmssapp_block_subtask_immutable_fields_update ON tmssapp_Subtask ; + CREATE TRIGGER tmssapp_block_subtask_immutable_fields_update + BEFORE UPDATE OR INSERT ON tmssapp_Subtask + FOR EACH ROW EXECUTE PROCEDURE tmssapp_block_subtask_immutable_fields_update(); + '''), + ] diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0005_subtask_obsolete_since.py b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0005_subtask_obsolete_since.py new file mode 100644 index 0000000000000000000000000000000000000000..998c4e047f5f6cae7f0a5a1ffb136f1e2ef2e65f --- /dev/null +++ b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0005_subtask_obsolete_since.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.9 on 2021-10-12 07:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tmssapp', '0004_subtask_error_reason'), + ] + + operations = [ + migrations.AddField( + model_name='subtask', + name='obsolete_since', + field=models.DateTimeField(help_text='When this subtask was marked obsolete, or NULL if not obsolete (NULLable).', null=True), + ), + ] diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py index 2b7f642cf5e9bfb8a9625c6831845a22810b765c..1d743ff33d751b4b14738e57d0b915d4dafbcfd8 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py @@ -50,7 +50,6 @@ class SubtaskState(AbstractChoice): CANCELLED = "cancelled" ERROR = "error" UNSCHEDULABLE = "unschedulable" - OBSOLETE = "obsolete" class SubtaskType(AbstractChoice): @@ -150,9 +149,11 @@ class Subtask(BasicCommon, ProjectPropertyMixin, TemplateSchemaMixin): cluster = ForeignKey('Cluster', null=True, on_delete=PROTECT, help_text='Where the Subtask is scheduled to run (NULLable).') # resource_claim = ForeignKey("ResourceClaim", null=False, on_delete=PROTECT) # todo <-- how is this external reference supposed to work? created_or_updated_by_user = ForeignKey(User, null=True, editable=False, on_delete=PROTECT, help_text='The user who created / updated the subtask.') + error_reason = CharField(null=True, max_length=200, help_text='Reason why the Subtask went to error.') 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_blueprint__scheduling_unit_blueprint__draft__scheduling_set__project' + obsolete_since = DateTimeField(null=True, help_text='When this subtask was marked obsolete, or NULL if not obsolete (NULLable).') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -160,6 +161,17 @@ class Subtask(BasicCommon, ProjectPropertyMixin, TemplateSchemaMixin): # keep original state for logging self.__original_state_id = self.state_id + # keep original obsolete_since to detect changes on save + # Note: we cannot use self.obsolete_since here since that causes an infinite loop of update_from_db + if 'obsolete_since' in kwargs.keys(): + self.__original_obsolete_since = kwargs['obsolete_since'] + else: + field_names = [f.name for f in self._meta.fields] + if len(args) == len(field_names): + self.__original_obsolete_since = args[field_names.index('obsolete_since')] + else: + self.__original_obsolete_since = None + @property def duration(self) -> timedelta: '''the duration of this subtask (stop-start), or 0 if start/stop are None''' @@ -315,6 +327,10 @@ class Subtask(BasicCommon, ProjectPropertyMixin, TemplateSchemaMixin): if self.scheduled_on_sky_start_time is None: raise SubtaskSchedulingException("Cannot schedule subtask id=%s when start time is 'None'." % (self.pk, )) + # make sure that obsolete_since can only be set when it is None, but prevents changes: + if self.obsolete_since != self.__original_obsolete_since and self.__original_obsolete_since is not None: + raise ValidationError("This Subtask has been marked obsolete on %s and that cannot be changed to %s" % (self.__original_obsolete_since, self.obsolete_since)) + # set actual_process_start_time when subtask goes to STARTED state if self.state.value == SubtaskState.Choices.STARTED.value and self.__original_state_id == SubtaskState.Choices.STARTING.value: self.actual_process_start_time = datetime.utcnow() diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py index 5e7ac1e262f1b2e3f208f503d04e4b94c34b0714..bdf01cc87506a545bfe81e82c17b865221fffd87 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py @@ -6,7 +6,7 @@ import logging logger = logging.getLogger(__name__) from django.db import transaction -from django.db.models import Model, CharField, DateTimeField, BooleanField, ForeignKey, CASCADE, IntegerField, FloatField, SET_NULL, PROTECT, ManyToManyField, UniqueConstraint, QuerySet, OneToOneField, Q +from django.db.models import Model, CharField, DateTimeField, BooleanField, ForeignKey, CASCADE, IntegerField, FloatField, SET_NULL, PROTECT, ManyToManyField, UniqueConstraint, QuerySet, OneToOneField, Q, Max from django.contrib.postgres.fields import JSONField from enum import Enum from django.db.models.expressions import RawSQL @@ -604,7 +604,6 @@ class SchedulingUnitBlueprint(ProjectPropertyMixin, TemplateSchemaMixin, NamedCo SCHEDULED = "scheduled" SCHEDULABLE = "schedulable" UNSCHEDULABLE = "unschedulable" - OBSOLETE = "obsolete" # todo: are many of these fields supposed to be immutable in the database? # Or are we fine to just not allow most users to change them? @@ -630,7 +629,11 @@ class SchedulingUnitBlueprint(ProjectPropertyMixin, TemplateSchemaMixin, NamedCo if 'scheduling_constraints_doc' in kwargs.keys(): self.__original_scheduling_constraints_doc = kwargs['scheduling_constraints_doc'] else: - self.__original_scheduling_constraints_doc = None + field_names = [f.name for f in self._meta.fields] + if len(args) == len(field_names): + self.__original_scheduling_constraints_doc = args[field_names.index('scheduling_constraints_doc')] + else: + self.__original_scheduling_constraints_doc = None self.__original_scheduling_constraints_template_id = self.scheduling_constraints_template_id def save(self, force_insert=False, force_update=False, using=None, update_fields=None): @@ -864,8 +867,9 @@ class SchedulingUnitBlueprint(ProjectPropertyMixin, TemplateSchemaMixin, NamedCo status_overview_counter = Counter() status_overview_counter_per_type = {type.value: Counter() for type in TaskType.Choices} for tb in self.task_blueprints.select_related('specifications_template').all(): - status_overview_counter[tb.status] += 1 - status_overview_counter_per_type[tb.specifications_template.type.value][tb.status] += 1 + if tb.obsolete_since is None: + status_overview_counter[tb.status] += 1 + status_overview_counter_per_type[tb.specifications_template.type.value][tb.status] += 1 # The actual determination of the SchedulingunitBlueprint status if not self._task_graph_instantiated(): @@ -876,9 +880,6 @@ class SchedulingUnitBlueprint(ProjectPropertyMixin, TemplateSchemaMixin, NamedCo return SchedulingUnitBlueprint.Status.CANCELLED.value elif self._any_task_error(status_overview_counter): return SchedulingUnitBlueprint.Status.ERROR.value - elif self._any_task_obsolete(status_overview_counter): - # TODO: in TMSS-850 implement the various conditional aggregations for the 'obsolete' vs 'finished' states - return SchedulingUnitBlueprint.Status.OBSOLETE.value elif self._any_task_unschedulable(status_overview_counter): return SchedulingUnitBlueprint.Status.UNSCHEDULABLE.value elif self._any_task_started_observed_finished(status_overview_counter): @@ -1146,13 +1147,12 @@ class TaskDraft(NamedCommon, TemplateSchemaMixin, ProjectPropertyMixin): # return None -class TaskBlueprint(ProjectPropertyMixin, TemplateSchemaMixin, NamedCommon): +class TaskBlueprint(ProjectPropertyMixin, TemplateSchemaMixin, RefreshFromDbInvalidatesCachedPropertiesMixin, NamedCommon): class Status(Enum): DEFINED = "defined" FINISHED = "finished" CANCELLED = "cancelled" ERROR = "error" - OBSOLETE = "obsolete" OBSERVED = "observed" STARTED = "started" SCHEDULED = "scheduled" @@ -1265,6 +1265,17 @@ class TaskBlueprint(ProjectPropertyMixin, TemplateSchemaMixin, NamedCommon): else: return None + @cached_property + def obsolete_since(self) -> datetime or None: + '''return the earliest obsolete_since time of all subtasks of this task + ''' + if self.subtasks.filter(obsolete_since__isnull=True).exists(): + # there is at least one subtask that is not obsolete, so this task is not obsolete as well + return None + + # return latest subtask obsolete_since timestamp, since this is when this task got obsolete + return self.subtasks.filter(obsolete_since__isnull=False).values('obsolete_since').aggregate(Max('obsolete_since'))['obsolete_since__max'] + @property def status(self): """ @@ -1274,7 +1285,8 @@ class TaskBlueprint(ProjectPropertyMixin, TemplateSchemaMixin, NamedCommon): """ # one call to the db fetching only the needed values - subtasks = tuple(self.subtasks.values('state', 'specifications_template__type_id').all()) + # we ignore non-primary subtasks that have been marked obsolete + subtasks = tuple(self.subtasks.exclude(primary=False, obsolete_since__isnull=False).values('state', 'specifications_template__type_id').all()) # simple python filtering counting by state and type. Saves many round-trip-db calls! nr_of_subtasks = len(subtasks) @@ -1293,10 +1305,6 @@ class TaskBlueprint(ProjectPropertyMixin, TemplateSchemaMixin, NamedCommon): if any(s for s in subtasks if s['state'] == 'error'): return TaskBlueprint.Status.ERROR.value - # TODO: in TMSS-850 implement the various conditional aggregations for the 'obsolete' vs 'finished' states - if any(s for s in subtasks if s['state'] == 'obsolete'): - return TaskBlueprint.Status.OBSOLETE.value - observations = [s for s in subtasks if s['specifications_template__type_id'] == 'observation'] if observations and all(obs and obs['state'] in ('finishing', 'finished') for obs in observations): return TaskBlueprint.Status.OBSERVED.value diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/populate.py b/SAS/TMSS/backend/src/tmss/tmssapp/populate.py index 671fba35fcf444def92a7250f789ab31b8774581..e7c757539af412c484ff799f7bb48ad9fe8b4056 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/populate.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/populate.py @@ -151,7 +151,6 @@ def populate_subtask_allowed_state_transitions(apps, schema_editor): CANCELLED = SubtaskState.objects.get(value=SubtaskState.Choices.CANCELLED.value) ERROR = SubtaskState.objects.get(value=SubtaskState.Choices.ERROR.value) UNSCHEDULABLE = SubtaskState.objects.get(value=SubtaskState.Choices.UNSCHEDULABLE.value) - OBSOLETE = SubtaskState.objects.get(value=SubtaskState.Choices.OBSOLETE.value) SubtaskAllowedStateTransitions.objects.bulk_create([ SubtaskAllowedStateTransitions(old_state=None, new_state=DEFINING), @@ -182,10 +181,6 @@ def populate_subtask_allowed_state_transitions(apps, schema_editor): SubtaskAllowedStateTransitions(old_state=FINISHING, new_state=ERROR), SubtaskAllowedStateTransitions(old_state=CANCELLING, new_state=ERROR), - # allow transition from the "end"-states cancelled/error to obsolete to indicate user-intent - SubtaskAllowedStateTransitions(old_state=CANCELLED, new_state=OBSOLETE), - SubtaskAllowedStateTransitions(old_state=ERROR, new_state=OBSOLETE), - SubtaskAllowedStateTransitions(old_state=DEFINED, new_state=CANCELLING), SubtaskAllowedStateTransitions(old_state=SCHEDULED, new_state=CANCELLING), SubtaskAllowedStateTransitions(old_state=QUEUED, new_state=CANCELLING), diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/scheduling.py b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/scheduling.py index 72c0cf92be4ea0be4c810804f9cb2443e757667c..92746b6c3c340e7b90ed9683c8d60c5e033c1f80 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/scheduling.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/scheduling.py @@ -6,6 +6,7 @@ import logging logger = logging.getLogger(__name__) from rest_framework import serializers +from rest_framework.exceptions import ValidationError from .. import models from .widgets import JSONEditorField from .common import FloatDurationField, RelationalHyperlinkedModelSerializer, AbstractTemplateSerializer, DynamicRelationalHyperlinkedModelSerializer @@ -74,7 +75,6 @@ class SubtaskSerializer(DynamicRelationalHyperlinkedModelSerializer): input_dataproducts = serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='dataproduct-detail') output_dataproducts = serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='dataproduct-detail') - class Meta: model = models.Subtask fields = '__all__' diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py index d9371f15ae3a707c18449d3968d69eb60dd1b36c..d17bf85c214e9cf38e3167fd278e9ddc415d966e 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py @@ -290,7 +290,8 @@ class TaskBlueprintSerializer(DynamicRelationalHyperlinkedModelSerializer): model = models.TaskBlueprint fields = '__all__' extra_fields = ['subtasks', 'produced_by', 'consumed_by', 'first_scheduling_relation', 'second_scheduling_relation', 'duration', - 'start_time', 'stop_time', 'relative_start_time', 'relative_stop_time', 'status', 'task_type'] + 'start_time', 'stop_time', 'relative_start_time', 'relative_stop_time', 'status', 'task_type', + 'obsolete_since'] expandable_fields = { 'draft': 'lofar.sas.tmss.tmss.tmssapp.serializers.TaskDraftSerializer', 'scheduling_unit_blueprint': 'lofar.sas.tmss.tmss.tmssapp.serializers.SchedulingUnitBlueprintSerializer', diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py b/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py index ba36e9feff0c0990626f04413ec4d18c5a94b706..7b1d1931bb25ca1dc9921429c3171b077d2afef9 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py @@ -983,8 +983,9 @@ def schedule_subtask(subtask: Subtask) -> Subtask: subtask.state = SubtaskState.objects.get(value=SubtaskState.Choices.UNSCHEDULABLE.value) subtask.save() else: - # set the subtask to state 'ERROR'. TODO: we should annotate in the db what error occurred. + # set the subtask to state 'ERROR'. subtask.state = SubtaskState.objects.get(value=SubtaskState.Choices.ERROR.value) + subtask.error_reason = f'{e}' subtask.save() except Exception as e2: logger.error(e2) @@ -1095,6 +1096,9 @@ def check_prerequities_for_scheduling(subtask: Subtask) -> bool: if subtask.state.value != SubtaskState.Choices.DEFINED.value: raise SubtaskSchedulingException("Cannot schedule subtask id=%d because it is not DEFINED. Current state=%s" % (subtask.pk, subtask.state.value)) + if subtask.obsolete_since is not None: + raise SubtaskSchedulingException("Cannot schedule subtask id=%d because it is marked as obsolete since %s" % (subtask.pk, subtask.obsolete_since)) + for predecessor in subtask.predecessors.all(): if predecessor.state.value != SubtaskState.Choices.FINISHED.value: raise SubtaskSchedulingException("Cannot schedule subtask id=%d because its predecessor id=%s in not FINISHED but state=%s" @@ -2290,6 +2294,7 @@ def cancel_subtask(subtask: Subtask) -> Subtask: except Exception as e: logger.error("Error while cancelling subtask id=%s type=%s state=%s '%s'", subtask.id, subtask.specifications_template.type.value, subtask.state.value, e) subtask.state = SubtaskState.objects.get(value=SubtaskState.Choices.ERROR.value) + subtask.error_reason = f'{e}' subtask.save() if isinstance(e, SubtaskCancellingException): # we intentionally raised the SubtaskCancellingException, so re-raise it and let the caller handle it diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/tasks.py b/SAS/TMSS/backend/src/tmss/tmssapp/tasks.py index aa9b0b3a0a68df9fda3df95771a725d07fa1a2fe..7c6de38050c99629252080fa0a147c647489e467 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/tasks.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/tasks.py @@ -504,10 +504,9 @@ def cancel_task_blueprint(task_blueprint: TaskBlueprint) -> TaskBlueprint: def mark_task_blueprint_as_obsolete(task_blueprint: TaskBlueprint) -> TaskBlueprint: '''Convenience method: mark all cancelled/error subtasks in the task_blueprint as obsolete''' - obsolete_state = models.SubtaskState.objects.get(value=models.SubtaskState.Choices.OBSOLETE.value) - transitionable_states = models.SubtaskAllowedStateTransitions.allowed_old_states(obsolete_state) - for subtask in task_blueprint.subtasks.filter(state__in=transitionable_states): - subtask.state = obsolete_state + now = datetime.utcnow() + for subtask in task_blueprint.subtasks.all(): + subtask.obsolete_since = now subtask.save() task_blueprint.refresh_from_db() return task_blueprint diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/scheduling.py b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/scheduling.py index 5ab6c1e51a1a535c129b8b895f456bf046e20eba..c5adc99347792fe207ec34d8ff8efa0a331201e4 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/scheduling.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/scheduling.py @@ -127,6 +127,7 @@ class SubTaskFilter(property_filters.PropertyFilterSet): id_max = filters.NumberFilter(field_name='id', lookup_expr='lte') state = filters.ModelMultipleChoiceFilter(field_name='state', queryset=models.SubtaskState.objects.all()) name = filters.CharFilter(field_name='task_blueprint__scheduling_unit_blueprint__name', lookup_expr='icontains') # todo: correct name? + error_reason = filters.CharFilter(field_name='error_reason', lookup_expr='icontains') on_sky_start_time__lt = property_filters.PropertyIsoDateTimeFilter(field_name='on_sky_start_time', lookup_expr='lt') on_sky_start_time__gt = property_filters.PropertyIsoDateTimeFilter(field_name='on_sky_start_time', lookup_expr='gt') on_sky_stop_time__lt = property_filters.PropertyIsoDateTimeFilter(field_name='on_sky_stop_time', lookup_expr='lt') diff --git a/SAS/TMSS/backend/test/CMakeLists.txt b/SAS/TMSS/backend/test/CMakeLists.txt index d8e4d47175ee8b1a3f33dfbdcb13ef91781d3c71..ef05b8393501c0ec3706c05efec772f66a0d99ae 100644 --- a/SAS/TMSS/backend/test/CMakeLists.txt +++ b/SAS/TMSS/backend/test/CMakeLists.txt @@ -42,6 +42,7 @@ if(BUILD_TESTING) lofar_add_test(t_reservations) lofar_add_test(t_swagger) + set_tests_properties(t_adapter PROPERTIES TIMEOUT 300) set_tests_properties(t_scheduling PROPERTIES TIMEOUT 300) set_tests_properties(t_tmssapp_scheduling_REST_API PROPERTIES TIMEOUT 300) set_tests_properties(t_tmssapp_specification_REST_API PROPERTIES TIMEOUT 600) diff --git a/SAS/TMSS/backend/test/t_scheduling.py b/SAS/TMSS/backend/test/t_scheduling.py index 2bc761f60cb8a2fd2cfb0b28657a159e747b00e8..6c33a9d5878a7d915a939b3b8a3abf35fbf13d23 100755 --- a/SAS/TMSS/backend/test/t_scheduling.py +++ b/SAS/TMSS/backend/test/t_scheduling.py @@ -78,6 +78,8 @@ from lofar.sas.tmss.tmss.tmssapp.tasks import * from lofar.sas.tmss.test.test_utils import set_subtask_state_following_allowed_transitions from lofar.messaging.rpc import RPCService, ServiceMessageHandler import threading +import dateutil.parser + def create_subtask_object_for_testing(subtask_type_value, subtask_state_value): """ @@ -230,16 +232,28 @@ class SchedulingTest(unittest.TestCase): self.assertEqual('cancelled', subtask['state_value']) # mark it as obsolete... (the user thereby states that the cancelled subtask will is not to be used again) + self.assertIsNone(subtask['obsolete_since']) + before = datetime.utcnow() subtask = client.mark_subtask_as_obsolete(subtask_id) - self.assertEqual('obsolete', subtask['state_value']) + after = datetime.utcnow() + obsolete_since = dateutil.parser.parse(subtask['obsolete_since'], ignoretz=True) + self.assertIsNotNone(obsolete_since) + self.assertLess(before, obsolete_since) + self.assertGreater(after, obsolete_since) # scheduling should fail with self.assertRaises(Exception): client.schedule_subtask(subtask_id) - # and status should still be obsolete + # marking an obsolete subtask as obsolete again should be prevented + with self.assertRaises(Exception) as context: + subtask = client.mark_subtask_as_obsolete(subtask_id) + self.assertIn("has been marked obsolete on %s" % obsolete_since, str(context.exception)) + + # and obsolete_since timestamp should still be the same as before subtask = client.get_subtask(subtask_id) - self.assertEqual('obsolete', subtask['state_value']) + obsolete_since_new = dateutil.parser.parse(subtask['obsolete_since'], ignoretz=True) + self.assertEqual(obsolete_since, obsolete_since_new) def test_cancel_scheduled_observation_subtask(self): with tmss_test_env.create_tmss_client() as client: diff --git a/SAS/TMSS/backend/test/t_subtasks.py b/SAS/TMSS/backend/test/t_subtasks.py index 9323947f44de942291823bbf2d8440085f532a4c..53d98a77b916deb19018831a5d3295c2fdf7a645 100755 --- a/SAS/TMSS/backend/test/t_subtasks.py +++ b/SAS/TMSS/backend/test/t_subtasks.py @@ -754,8 +754,17 @@ class SubtaskAllowedStateTransitionsTest(unittest.TestCase): # then go to the error state (should be allowed from any of these intermediate states) subtask.state = SubtaskState.objects.get(value=SubtaskState.Choices.ERROR.value) + ERROR_MESSAGE = 'test_helper_method_set_subtask_state_following_allowed_transitions_error_path' + subtask.error_reason = ERROR_MESSAGE subtask.save() self.assertEqual(SubtaskState.Choices.ERROR.value, subtask.state.value) + self.assertEqual(ERROR_MESSAGE, subtask.error_reason) + + # overwriting error reason should not be allowed + from django.db.utils import InternalError + with self.assertRaises(InternalError) as context: + subtask.error_reason = "overwriting error reason" + subtask.save() def test_helper_method_set_subtask_state_following_allowed_transitions_cancel_path(self): for desired_end_state_value in (SubtaskState.Choices.CANCELLING.value,SubtaskState.Choices.CANCELLED.value): @@ -794,17 +803,13 @@ class SubtaskAllowedStateTransitionsTest(unittest.TestCase): def test_end_states(self): '''Check if the end states that we cannot get out of are according to the design''' - # there should be only one state to go to from ERROR, that is OBSOLETE + # there should be no state to go to from ERROR allowed_states_from_ERROR = SubtaskAllowedStateTransitions.objects.filter(old_state__value=SubtaskState.Choices.ERROR.value) - self.assertEqual(1, allowed_states_from_ERROR.count()) - self.assertEqual(SubtaskState.Choices.OBSOLETE.value, allowed_states_from_ERROR[0].new_state.value) + self.assertEqual(0, allowed_states_from_ERROR.count()) # there should be no state to go to from FINISHED self.assertEqual(0, SubtaskAllowedStateTransitions.objects.filter(old_state__value=SubtaskState.Choices.FINISHED.value).count()) - # there should be no state to go to from OBSOLETE - self.assertEqual(0, SubtaskAllowedStateTransitions.objects.filter(old_state__value=SubtaskState.Choices.OBSOLETE.value).count()) - def test_illegal_state_transitions(self): for state_value in [choice.value for choice in SubtaskState.Choices]: # assume helper method set_subtask_state_following_allowed_transitions is working (see other tests above) diff --git a/SAS/TMSS/backend/test/t_tmssapp_specification_REST_API.py b/SAS/TMSS/backend/test/t_tmssapp_specification_REST_API.py index af16f953e9eccda2773674ecd9e9a321408658ee..ee4e0ceac36ff5acb8100c810e5d76dbfcb765f3 100755 --- a/SAS/TMSS/backend/test/t_tmssapp_specification_REST_API.py +++ b/SAS/TMSS/backend/test/t_tmssapp_specification_REST_API.py @@ -2384,7 +2384,7 @@ class TaskBlueprintTestCase(unittest.TestCase): # assert response_1 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?status=defined&status=finished', 200) - response_2 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?status=obsolete', 200) + response_2 = GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?status=observed', 200) GET_and_assert_equal_expected_code(self, BASE_URL + '/task_blueprint/?status=gibberish', 400) self.assertGreater(response_1['count'], 0) self.assertEqual(response_2['count'], 0) diff --git a/SAS/TMSS/backend/test/t_tmssapp_specification_django_API.py b/SAS/TMSS/backend/test/t_tmssapp_specification_django_API.py index 63dc0bc8747294f21aa90d0a2f69b0adf374c4cf..3a0ca74d9d48e9bcff090432026020fbe7cf8e0a 100755 --- a/SAS/TMSS/backend/test/t_tmssapp_specification_django_API.py +++ b/SAS/TMSS/backend/test/t_tmssapp_specification_django_API.py @@ -739,6 +739,7 @@ class SchedulingUnitBlueprintTest(unittest.TestCase): task_blueprint = models.TaskBlueprint.objects.create(**TaskBlueprint_test_data(scheduling_unit_blueprint=scheduling_unit_blueprint)) subtask = models.Subtask.objects.create(**Subtask_test_data(task_blueprint=task_blueprint)) subtask.state = models.SubtaskState.objects.get(value='error') # the derived SUB status is then also error + subtask.error_reason = 'test_SchedulingUnitBlueprint_prevents_updating_scheduling_constraints_template_if_not_in_correct_state' subtask.save() scheduling_unit_blueprint.refresh_from_db() @@ -779,6 +780,7 @@ class SchedulingUnitBlueprintTest(unittest.TestCase): task_blueprint = models.TaskBlueprint.objects.create(**TaskBlueprint_test_data(scheduling_unit_blueprint=scheduling_unit_blueprint)) subtask = models.Subtask.objects.create(**Subtask_test_data(task_blueprint=task_blueprint)) subtask.state = models.SubtaskState.objects.get(value='error') # the derived SUB status is then also error + subtask.error_reason = 'test_SchedulingUnitBlueprint_prevents_updating_scheduling_constraints_doc_if_not_in_correct_state' subtask.save() scheduling_unit_blueprint.refresh_from_db() diff --git a/SAS/TMSS/backend/test/test_utils.py b/SAS/TMSS/backend/test/test_utils.py index e22f2ecb6a37912694fa32e5b7626ac8e87d1c84..86ac97ee5d768f26f1ce624b8e9b9c7d18148e0f 100644 --- a/SAS/TMSS/backend/test/test_utils.py +++ b/SAS/TMSS/backend/test/test_utils.py @@ -92,7 +92,7 @@ def set_subtask_state_following_allowed_transitions(subtask: typing.Union[Subtas subtask = Subtask.objects.get(id=subtask) # end states that we cannot get out of according to the design - END_STATE_VALUES = (SubtaskState.Choices.FINISHED.value, SubtaskState.Choices.OBSOLETE.value) + END_STATE_VALUES = (SubtaskState.Choices.FINISHED.value) while subtask.state.value != desired_state_value and (subtask.state.value not in END_STATE_VALUES): # handle "unsuccessful path" to cancelled/canceling end state @@ -111,17 +111,7 @@ def set_subtask_state_following_allowed_transitions(subtask: typing.Union[Subtas SubtaskState.Choices.FINISHING.value, SubtaskState.Choices.CANCELLING.value): subtask.state = SubtaskState.objects.get(value=SubtaskState.Choices.ERROR.value) - - # handle "unsuccessful path" to OBSOLETE end state (via CANCELLED) - elif desired_state_value == SubtaskState.Choices.OBSOLETE.value: - if subtask.state.value == SubtaskState.Choices.DEFINING.value: - subtask.state = SubtaskState.objects.get(value=SubtaskState.Choices.DEFINED.value) - elif subtask.state.value in (SubtaskState.Choices.DEFINED.value, SubtaskState.Choices.SCHEDULED.value, SubtaskState.Choices.QUEUED.value, SubtaskState.Choices.ERROR.value): - subtask.state = SubtaskState.objects.get(value=SubtaskState.Choices.CANCELLING.value) - elif subtask.state.value == SubtaskState.Choices.CANCELLING.value: - subtask.state = SubtaskState.objects.get(value=SubtaskState.Choices.CANCELLED.value) - elif subtask.state.value == SubtaskState.Choices.CANCELLED.value: - subtask.state = SubtaskState.objects.get(value=SubtaskState.Choices.OBSOLETE.value) + subtask.error_reason = 'set_subtask_state_following_allowed_transitions' # handle "unsuccessful path" to unschedulable state elif desired_state_value == SubtaskState.Choices.UNSCHEDULABLE.value and (subtask.state.value == SubtaskState.Choices.DEFINED.value or subtask.state.value == SubtaskState.Choices.SCHEDULING.value): diff --git a/SAS/TMSS/client/lib/mains.py b/SAS/TMSS/client/lib/mains.py index 75334b8eac55cee4577e20ac6809131459c483bc..2f575a3c9f89da6a84c6510f562a6a3aee4d6594 100644 --- a/SAS/TMSS/client/lib/mains.py +++ b/SAS/TMSS/client/lib/mains.py @@ -138,7 +138,7 @@ def main_schedule_subtask(): try: with TMSSsession.create_from_dbcreds_for_ldap(dbcreds_name=args.rest_api_credentials) as session: pprint(session.schedule_subtask(args.subtask_id, - start_time=datetime.datetime.fromisoformat(args.start_time) if args.start_time else None, + scheduled_on_sky_start_time=datetime.datetime.fromisoformat(args.start_time) if args.start_time else None, retry_count=3)) except Exception as e: print(e) diff --git a/SAS/TMSS/client/lib/tmss_http_rest_client.py b/SAS/TMSS/client/lib/tmss_http_rest_client.py index f3d3ee856da693c0d2538d038e140055dfd20e64..39520ac73daf3382458e2087fb2c0ee0e04bc586 100644 --- a/SAS/TMSS/client/lib/tmss_http_rest_client.py +++ b/SAS/TMSS/client/lib/tmss_http_rest_client.py @@ -440,10 +440,16 @@ class TMSSsession(object): returns the cancelled scheduling_unit_blueprint upon success, or raises.""" return self.post_to_path_and_get_result_as_json_object('scheduling_unit_blueprint/%s/cancel' % (scheduling_unit_blueprint_id), retry_count=retry_count) - def mark_subtask_as_obsolete(self, subtask_id: int, retry_count: int=0) -> {}: + def mark_subtask_as_obsolete(self, subtask_id: int) -> {}: """mark the subtask for the given subtask_id as obsolete. returns the marked_as_obsolete subtask upon success, or raises.""" - return self.set_subtask_status(subtask_id=subtask_id, status='obsolete') + logger.info("marking subtask id=%s as obsolete", subtask_id) + response = self.session.patch(url='%s/subtask/%s/' % (self.api_url, subtask_id), json={'obsolete_since': "%s" % datetime.utcnow().isoformat()}) + + if response.status_code >= 200 and response.status_code < 300: + return json.loads(response.content.decode('utf-8')) + + raise Exception("Could not mark subtask as obsolete with url %s - %s %s - %s" % (response.request.url, response.status_code, responses.get(response.status_code), response.text)) def mark_task_blueprint_as_obsolete(self, task_blueprint_id: int, retry_count: int=0) -> {}: """mark the task_blueprint for the given task_blueprint_id as obsolete. @@ -655,3 +661,4 @@ class TMSSsession(object): Returns the created scheduling_unit_draft/blueprint upon success, or raises. Use the method get_trigger_specification_doc_for_scheduling_unit_observing_strategy_template to get a valid trigger json doc, edit it, and sumbit using this method.""" return self.post_to_path_and_get_result_as_json_object('/submit_trigger', json_data=trigger_doc, retry_count=retry_count, retry_interval=retry_interval) +