diff --git a/SAS/TMSS/services/scheduling/lib/constraints/__init__.py b/SAS/TMSS/services/scheduling/lib/constraints/__init__.py index b0e932e74ffeca511db786ebccf8def6e2eef61e..b27c578638e0824b2e320813ec9e30599994fcd7 100644 --- a/SAS/TMSS/services/scheduling/lib/constraints/__init__.py +++ b/SAS/TMSS/services/scheduling/lib/constraints/__init__.py @@ -22,7 +22,7 @@ """ This __init__ module for this constraints python package defines the 'API' to: - filter a list of schedulable scheduling_units by checking their constraints: see method filter_scheduling_units_using_constraints - - sort a (possibly filtered) list of schedulable scheduling_units evaluating their constraints and computing a 'finess' score: see method get_sorted_scheduling_units_scored_by_constraints + - sort a (possibly filtered) list of schedulable scheduling_units evaluating their constraints and computing a 'fitness' score: see method get_sorted_scheduling_units_scored_by_constraints These main methods are used in the dynamic_scheduler to pick the next best scheduling unit, and compute the midterm schedule. Currently we have only one SchedulingConstraintsTemplate in TMSS, named 'constraints', version 1. @@ -53,8 +53,14 @@ class ScoredSchedulingUnit(NamedTuple): weighted_score: float -def filter_scheduling_units_using_constraints(scheduling_units:[models.SchedulingUnitBlueprint], lower_bound: datetime, upper_bound: datetime) -> [models.SchedulingUnitBlueprint]: - '''return the schedulable scheduling_units which for which the constraints are 'go' within the given timewindow''' +def filter_scheduling_units_using_constraints(scheduling_units: [models.SchedulingUnitBlueprint], lower_bound: datetime, upper_bound: datetime) -> [models.SchedulingUnitBlueprint]: + """ + Filter the given scheduling_units by whether their constraints are met within the given timewindow. + :param lower_bound: evaluate and score the constrains at and after lower_bound_start_time. The returned unit has a start_time guaranteed at or after lower_bound_start_time. + :param upper_bound: evaluate and score the constrains before upper_bound_stop_time. The returned unit has a stop_time guaranteed before upper_bound_stop_time. + :param scheduling_units: evaluate/filter these scheduling_units. + Returns a list scheduling_units for which their constraints are met within the given timewindow. + """ runnable_scheduling_units = [] for scheduling_unit in scheduling_units: try: @@ -62,7 +68,10 @@ def filter_scheduling_units_using_constraints(scheduling_units:[models.Schedulin runnable_scheduling_units.append(scheduling_unit) except UnknownTemplateException as e: # TODO: how do we notify the user that we cannot dynamically schedule this sub due to an unknown template? - # current pragmatic solution: log warning, and set sub state to error via its schedulable subtasks + # current pragmatic solution: log warning, and set sub state to error via its schedulable subtasks. + # This ensures that the unit is not schedulable anymore, and forces the user to take action. + # For example, the user can choose a different template, + # or submit a feature request to implement constraint solvers for this new template. logger.warning(e) for subtask in models.Subtask.independent_subtasks().filter(task_blueprint__scheduling_unit_blueprint_id=scheduling_unit.id).all(): subtask.status = models.SubtaskState.objects.get(value=models.SubtaskState.Choices.ERROR.value) @@ -70,11 +79,65 @@ def filter_scheduling_units_using_constraints(scheduling_units:[models.Schedulin return runnable_scheduling_units -def get_sorted_scheduling_units_scored_by_constraints(scheduling_units:[models.SchedulingUnitBlueprint], lower_bound:datetime, upper_bound:datetime) -> [ScoredSchedulingUnit]: - scored_scheduling_units = [compute_scores(scheduling_unit, lower_bound, upper_bound) - for scheduling_unit in scheduling_units] - return sorted(scored_scheduling_units, key=lambda x: x.weighted_score, reverse=True) +def get_best_scored_scheduling_unit_scored_by_constraints(scheduling_units: [models.SchedulingUnitBlueprint], lower_bound_start_time:datetime, upper_bound_stop_time:datetime) -> ScoredSchedulingUnit: + """ + get the best scored schedulable scheduling_unit which can run withing the given time window from the given scheduling_units. + :param lower_bound_start_time: evaluate and score the constrains at and after lower_bound_start_time. The returned unit has a start_time guaranteed at or after lower_bound_start_time. + :param upper_bound_stop_time: evaluate and score the constrains before upper_bound_stop_time. The returned unit has a stop_time guaranteed before upper_bound_stop_time. + :param scheduling_units: evaluate these scheduling_units. + Returns a ScoredSchedulingUnit struct with the best next schedulable scheduling unit and its proposed start_time where it best fits its contraints. + """ + sorted_scored_scheduling_units = sort_scheduling_units_scored_by_constraints(scheduling_units, lower_bound_start_time, upper_bound_stop_time) + + if sorted_scored_scheduling_units: + # they are sorted best to worst, so return/use first. + best_scored_scheduling_unit = sorted_scored_scheduling_units[0] + return best_scored_scheduling_unit + + return None + + +def sort_scheduling_units_scored_by_constraints(scheduling_units: [models.SchedulingUnitBlueprint], lower_bound_start_time: datetime, upper_bound_stop_time: datetime) -> [ScoredSchedulingUnit]: + """ + Compute the score and proposed start_time for all given scheduling_units. Return them sorted by their weighted_score. + :param lower_bound_start_time: evaluate and score the constrains at and after lower_bound_start_time. The returned unit has a start_time guaranteed at or after lower_bound_start_time. + :param upper_bound_stop_time: evaluate and score the constrains before upper_bound_stop_time. The returned unit has a stop_time guaranteed before upper_bound_stop_time. + :param scheduling_units: evaluate these scheduling_units. + Returns a list of ScoredSchedulingUnit structs with the score details, a weighted_score and a proposed start_time where it best fits its contraints. + """ + + scored_scheduling_units = [] + for scheduling_unit in scheduling_units: + try: + scored_scheduling_unit = compute_scores(scheduling_unit, lower_bound_start_time, upper_bound_stop_time) + + # check and ensure that the proposed start_time is within the required [lower_bound_start_time, upper_bound_stop_time] window. + schedulable_unit = scored_scheduling_unit.scheduling_unit + proposed_start_time = scored_scheduling_unit.start_time + proposed_stop_time = proposed_start_time + schedulable_unit.duration + + if proposed_start_time < lower_bound_start_time: + raise DynamicSchedulingException("The best next schedulable scheduling_unit id=%s has a proposed start_time '%s' before the given lower bound '%s'" % ( + schedulable_unit.id, proposed_start_time, lower_bound_start_time)) + + if proposed_stop_time > upper_bound_stop_time: + raise DynamicSchedulingException("The best next schedulable scheduling_unit id=%s has a proposed stop_time '%s' after the given upper bound '%s'" % ( + schedulable_unit.id, proposed_stop_time, upper_bound_stop_time)) + + scored_scheduling_units.append(scored_scheduling_unit) + except (UnknownTemplateException, DynamicSchedulingException) as e: + # TODO: how do we notify the user that we cannot dynamically schedule this sub due to an unknown template? + # current pragmatic solution: log warning, and set sub state to error via its schedulable subtasks. + # This ensures that the unit is not schedulable anymore, and forces the user to take action. + # For example, the user can choose a different template, + # or submit a feature request to implement constraint solvers for this new template. + logger.warning(e) + for subtask in models.Subtask.independent_subtasks().filter(task_blueprint__scheduling_unit_blueprint_id=scheduling_unit.id).all(): + subtask.status = models.SubtaskState.objects.get(value=models.SubtaskState.Choices.ERROR.value) + subtask.save() + + return sorted(scored_scheduling_units, key=lambda x: x.weighted_score, reverse=True) ################## helper methods ################################################################# @@ -118,9 +181,8 @@ def compute_scores(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: scheduling_unit.id, constraints_template.name, constraints_template.version)) - -def get_earliest_possible_start_time(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime=None) -> datetime: - '''determine the earliest possible starttime for the given scheduling unit, taking into account all its constraints''' +def get_earliest_possible_start_time(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime) -> datetime: + '''determine the earliest possible start_time for the given scheduling unit, taking into account all its constraints''' constraints_template = scheduling_unit.draft.scheduling_constraints_template # choose appropriate method based on template (strategy pattern), or raise @@ -131,14 +193,12 @@ def get_earliest_possible_start_time(scheduling_unit: models.SchedulingUnitBluep # TODO: if we get more constraint templates or versions, then add a check here and import and use the new module with the constraint methods for that specific template. (strategy pattern) - raise UnknownTemplateException("Cannot compute first possible starttime for scheduling_unit id=%s, because we have no constraint checker for scheduling constraints template '%s' version=%s" % ( + raise UnknownTemplateException("Cannot compute earliest possible start_time for scheduling_unit id=%s, because we have no constraint checker for scheduling constraints template '%s' version=%s" % ( scheduling_unit.id, constraints_template.name, constraints_template.version)) -def get_min_earliest_possible_start_time(scheduling_units: [models.SchedulingUnitBlueprint], lower_bound: datetime=None) -> datetime: +def get_min_earliest_possible_start_time(scheduling_units: [models.SchedulingUnitBlueprint], lower_bound: datetime) -> datetime: '''deterimine the earliest possible starttime over all given scheduling units, taking into account all their constraints''' - if lower_bound is None: - lower_bound = datetime.utcnow() try: return min(get_earliest_possible_start_time(scheduling_unit, lower_bound) for scheduling_unit in scheduling_units) except ValueError: diff --git a/SAS/TMSS/services/scheduling/lib/constraints/template_constraints_v1.py b/SAS/TMSS/services/scheduling/lib/constraints/template_constraints_v1.py index 28e6ccdd4ba47cf49f6f6bd4e3646108bcdcd6fc..151c9efda2760b9233f7ab01e7c255d28d8d59dd 100644 --- a/SAS/TMSS/services/scheduling/lib/constraints/template_constraints_v1.py +++ b/SAS/TMSS/services/scheduling/lib/constraints/template_constraints_v1.py @@ -92,12 +92,7 @@ def can_run_within_timewindow_with_sky_constraints(scheduling_unit: models.Sched return True # for now, ignore sky contraints. -def get_earliest_possible_start_time(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime=None) -> datetime: - now = datetime.utcnow() - - if lower_bound is None: - lower_bound = now - +def get_earliest_possible_start_time(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime) -> datetime: constraints = scheduling_unit.draft.scheduling_constraints_doc try: @@ -128,7 +123,7 @@ def get_earliest_possible_start_time(scheduling_unit: models.SchedulingUnitBluep logger.exception(str(e)) # no constraints dictating starttime? make a guesstimate. - return max(lower_bound, now) + return lower_bound def compute_scores(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound:datetime, upper_bound:datetime) -> ScoredSchedulingUnit: @@ -149,7 +144,7 @@ def compute_scores(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: # add "common" scores which do not depend on constraints, such as project rank and creation date # TODO: should be normalized! scores['project_rank'] = scheduling_unit.draft.scheduling_set.project.priority_rank - scores['age'] = (datetime.utcnow() - scheduling_unit.created_at).total_seconds() + #scores['age'] = (datetime.utcnow() - scheduling_unit.created_at).total_seconds() try: # TODO: apply weights. Needs some new weight model in django, probably linked to constraints_template. diff --git a/SAS/TMSS/services/scheduling/lib/dynamic_scheduling.py b/SAS/TMSS/services/scheduling/lib/dynamic_scheduling.py index 0ec513939f903755473c366b108d4eabafa4aea6..41a0242a7779b221af828e08f9dc7bef36ce9599 100644 --- a/SAS/TMSS/services/scheduling/lib/dynamic_scheduling.py +++ b/SAS/TMSS/services/scheduling/lib/dynamic_scheduling.py @@ -60,43 +60,25 @@ def get_scheduled_scheduling_units(lower:datetime=None, upper:datetime=None) -> return list(models.SchedulingUnitBlueprint.objects.filter(id__in=scheduled_subtasks.values('task_blueprint__scheduling_unit_blueprint_id').distinct()).all()) -def find_best_next_schedulable_unit(lower_bound_start_time: datetime=None, upper_bound_stop_time: datetime=None, scheduling_units:[models.SchedulingUnitBlueprint]=None) -> ScoredSchedulingUnit: +def find_best_next_schedulable_unit(scheduling_units:[models.SchedulingUnitBlueprint], lower_bound_start_time: datetime, upper_bound_stop_time: datetime) -> ScoredSchedulingUnit: """ - find the best schedulable scheduling_unit which can run withing the given time window, for the given scheduling_units. - Returns a ScoredSchedulingUnit struct with a.o. the best next schedulable scheduling unit and its proposed starttime where it best fits its contraints. + find the best schedulable scheduling_unit which can run withing the given time window from the given scheduling_units. + :param lower_bound_start_time: evaluate the constrains at and after lower_bound_start_time. The returned unit has a start_time guaranteed at or after lower_bound_start_time. + :param upper_bound_stop_time: evaluate the constrains before upper_bound_stop_time. The returned unit has a stop_time guaranteed before upper_bound_stop_time. + :param scheduling_units: evaluate these scheduling_units. + Returns a ScoredSchedulingUnit struct with the best next schedulable scheduling unit and its proposed start_time where it best fits its contraints. """ - if lower_bound_start_time is None: - lower_bound_start_time = datetime.utcnow() - - if upper_bound_stop_time is None: - upper_bound_stop_time = datetime.utcnow() + timedelta(days=365) - # ensure upper is greater than or equal to lower upper_bound_stop_time = max(lower_bound_start_time, upper_bound_stop_time) - while lower_bound_start_time < upper_bound_stop_time: - if not scheduling_units: - scheduling_units = get_schedulable_scheduling_units() - - filtered_scheduling_units = filter_scheduling_units_using_constraints(scheduling_units, lower_bound_start_time, upper_bound_stop_time) - - if filtered_scheduling_units: - sorted_scored_scheduling_units = get_sorted_scheduling_units_scored_by_constraints(filtered_scheduling_units, lower_bound_start_time, upper_bound_stop_time) - best_scored_scheduling_unit = sorted_scored_scheduling_units[0] + filtered_scheduling_units = filter_scheduling_units_using_constraints(scheduling_units, lower_bound_start_time, upper_bound_stop_time) - best_next_schedulable_unit = best_scored_scheduling_unit.scheduling_unit - best_start_time = best_scored_scheduling_unit.start_time - - if best_start_time >= lower_bound_start_time and best_start_time+best_next_schedulable_unit.duration < upper_bound_stop_time: - return best_scored_scheduling_unit - else: - # try again a bit further into the future. There might be a spot there... - # ToDo: apply smarter guessed step size - lower_bound_start_time += timedelta(minutes=10) - else: - # no scheduling units... - return None + if filtered_scheduling_units: + best_scored_scheduling_unit = get_best_scored_scheduling_unit_scored_by_constraints(filtered_scheduling_units, lower_bound_start_time, upper_bound_stop_time) + return best_scored_scheduling_unit + # no filtered scheduling units found... + logger.info("No schedulable scheduling units found which meet the requirements between '%s' and '%s'", lower_bound_start_time, upper_bound_stop_time) return None @@ -104,26 +86,29 @@ def schedule_next_scheduling_unit() -> models.SchedulingUnitBlueprint: '''find the best next schedulable scheduling unit and try to schedule it. Overlapping existing scheduled units are unscheduled if their score is lower. :return: the scheduled scheduling unit.''' + + # --- setup of needed variables --- schedulable_units = get_schedulable_scheduling_units() # estimate the lower_bound_start_time - lower_bound_start_time = get_min_earliest_possible_start_time(schedulable_units, datetime.utcnow() + DEFAULT_INTER_OBSERVATION_GAP) + lower_bound_start_time = get_min_earliest_possible_start_time(schedulable_units, datetime.utcnow()) # estimate the upper_bound_stop_time, which may give us a small timewindow before any next scheduled unit, or a default window of a day try: - upper_bound_stop_time = max(su.start_time for su in get_scheduled_scheduling_units(lower=lower_bound_start_time) if su.start_time is not None) - except: + upper_bound_stop_time = max(su.start_time for su in get_scheduled_scheduling_units(lower=lower_bound_start_time, upper=lower_bound_start_time + timedelta(days=1))) + except ValueError: upper_bound_stop_time = lower_bound_start_time + timedelta(days=1) # no need to irritate user in log files with subsecond scheduling precision lower_bound_start_time = round_to_second_precision(lower_bound_start_time) upper_bound_stop_time = max(round_to_second_precision(upper_bound_stop_time), lower_bound_start_time) + # --- core routine --- while lower_bound_start_time < upper_bound_stop_time: try: # try to find the best next scheduling_unit - logger.info("schedule_next_scheduling_unit: searching for best scheduling unit to schedule starting between %s and %s", lower_bound_start_time, upper_bound_stop_time) - best_scored_scheduling_unit = find_best_next_schedulable_unit(lower_bound_start_time, upper_bound_stop_time, schedulable_units) + logger.info("schedule_next_scheduling_unit: searching for best scheduling unit to schedule starting between '%s' and '%s'", lower_bound_start_time, upper_bound_stop_time) + best_scored_scheduling_unit = find_best_next_schedulable_unit(schedulable_units, lower_bound_start_time, upper_bound_stop_time) if best_scored_scheduling_unit: best_scheduling_unit = best_scored_scheduling_unit.scheduling_unit best_scheduling_unit_score = best_scored_scheduling_unit.weighted_score @@ -171,7 +156,6 @@ def schedule_next_scheduling_unit() -> models.SchedulingUnitBlueprint: def assign_start_stop_times_to_schedulable_scheduling_units(lower_bound_start_time: datetime=None): '''''' - logger.info("Estimating mid-term schedule...") if lower_bound_start_time is None: @@ -184,7 +168,9 @@ def assign_start_stop_times_to_schedulable_scheduling_units(lower_bound_start_ti # update the start_times of the remaining ones (so they form queue, and can be visualized in a timeline) while scheduling_units: - best_scored_scheduling_unit = find_best_next_schedulable_unit(lower_bound_start_time=lower_bound_start_time, scheduling_units=scheduling_units) + best_scored_scheduling_unit = find_best_next_schedulable_unit(scheduling_units, + lower_bound_start_time=lower_bound_start_time, + upper_bound_stop_time=lower_bound_start_time + timedelta(days=365)) if best_scored_scheduling_unit: scheduling_unit = best_scored_scheduling_unit.scheduling_unit diff --git a/SAS/TMSS/services/scheduling/test/t_dynamic_scheduling.py b/SAS/TMSS/services/scheduling/test/t_dynamic_scheduling.py index bb37a0c37bbf8611fdd17afdf6a56d9e8a67474f..8375ef0cce1137a629f29de67af03e272455438a 100755 --- a/SAS/TMSS/services/scheduling/test/t_dynamic_scheduling.py +++ b/SAS/TMSS/services/scheduling/test/t_dynamic_scheduling.py @@ -27,44 +27,70 @@ from lofar.common.test_utils import skip_integration_tests if skip_integration_tests(): exit(3) -from lofar.messaging.messagebus import TemporaryExchange, BusListenerJanitor +TEST_UUID = uuid.uuid1() -from time import sleep from datetime import datetime, timedelta from lofar.common.json_utils import get_default_json_object_for_schema, add_defaults_to_json_object_for_schema +from lofar.messaging.messagebus import TemporaryExchange, BusListenerJanitor +tmp_exchange = TemporaryExchange("t_dynamic_scheduling_%s" % (TEST_UUID,)) +tmp_exchange.open() -class TestDynamicScheduling(unittest.TestCase): - ''' - Tests for the Dynamic Scheduling - ''' - @classmethod - def setUpClass(cls) -> None: - cls.TEST_UUID = uuid.uuid1() +# override DEFAULT_BUSNAME +import lofar +lofar.messaging.config.DEFAULT_BUSNAME = tmp_exchange.address - cls.tmp_exchange = TemporaryExchange("%s_%s" % (cls.__name__, cls.TEST_UUID)) - cls.tmp_exchange.open() +from lofar.sas.tmss.test.test_utils import TMSSTestEnvironment +tmss_test_env = TMSSTestEnvironment(exchange=tmp_exchange.address, + populate_schemas=True, populate_test_data=False, + start_postgres_listener=True, start_subtask_scheduler=False, + start_ra_test_environment=True, enable_viewflow=False, + start_dynamic_scheduler=False) # do not start the dynamic scheduler in the testenv, because it is the object-under-test. +tmss_test_env.start() - # override DEFAULT_BUSNAME - import lofar - lofar.messaging.config.DEFAULT_BUSNAME = cls.tmp_exchange.address +def tearDownModule(): + tmss_test_env.stop() + tmp_exchange.close() - # import here, and not at top of module, because DEFAULT_BUSNAME needs to be set before importing - from lofar.sas.tmss.test.test_utils import TMSSTestEnvironment +from lofar.sas.tmss.test.tmss_test_data_django_models import * +from lofar.sas.tmss.tmss.tmssapp import models +from lofar.sas.tmss.tmss.tmssapp.tasks import create_task_blueprints_and_subtasks_from_scheduling_unit_draft - cls.tmss_test_env = TMSSTestEnvironment(exchange=cls.tmp_exchange.address, - populate_schemas=True, populate_test_data=False, - start_postgres_listener=True, start_subtask_scheduler=False, - start_ra_test_environment=True, enable_viewflow=False, - start_dynamic_scheduler=False) # do not start the dynamic scheduler in the testenv, because it is the object-under-test. - cls.tmss_test_env.start() +# the module under test +from lofar.sas.tmss.services.scheduling.dynamic_scheduling import * +class TestDynamicScheduling(unittest.TestCase): + ''' + Tests for the Dynamic Scheduling + ''' @classmethod - def tearDownClass(cls) -> None: - cls.tmss_test_env.stop() - cls.tmp_exchange.close() - + def setUpClass(cls) -> None: + # make some re-usable projects with high/low priority + cls.project_low = models.Project.objects.create(**Project_test_data("dynamic scheduling test project %s"% (uuid.uuid4(),), priority_rank=1)) + cls.project_high = models.Project.objects.create(**Project_test_data("dynamic scheduling test project %s"% (uuid.uuid4(),), priority_rank=2)) + cls.scheduling_set_low = models.SchedulingSet.objects.create(**SchedulingSet_test_data(project=cls.project_low)) + cls.scheduling_set_high = models.SchedulingSet.objects.create(**SchedulingSet_test_data(project=cls.project_high)) + + def setUp(self) -> None: + for scheduling_set in [self.scheduling_set_low, self.scheduling_set_high]: + for scheduling_unit_draft in scheduling_set.scheduling_unit_drafts.all(): + for scheduling_unit_blueprint in scheduling_unit_draft.scheduling_unit_blueprints.all(): + for task_blueprint in scheduling_unit_blueprint.task_blueprints.all(): + for subtask in task_blueprint.subtasks.all(): + for output in subtask.outputs.all(): + for dataproduct in output.dataproducts.all(): + dataproduct.delete() + for consumer in output.consumers.all(): + consumer.delete() + output.delete() + for input in subtask.inputs.all(): + input.delete() + subtask.delete() + task_blueprint.draft.delete() + task_blueprint.delete() + scheduling_unit_blueprint.delete() + scheduling_unit_draft.delete() @staticmethod def create_simple_observation_scheduling_unit(name:str=None, scheduling_set=None, @@ -90,47 +116,35 @@ class TestDynamicScheduling(unittest.TestCase): def test_two_simple_observations_no_constraints_different_project_priority(self): - from lofar.sas.tmss.tmss.tmssapp.tasks import create_task_blueprints_and_subtasks_from_scheduling_unit_draft - from lofar.sas.tmss.tmss.tmssapp import models - from lofar.sas.tmss.test.tmss_test_data_django_models import SchedulingSet_test_data, Project_test_data - from lofar.sas.tmss.services.scheduling.dynamic_scheduling import schedule_next_scheduling_unit, assign_start_stop_times_to_schedulable_scheduling_units - - project1 = models.Project.objects.create(**Project_test_data("dynamic scheduling test project %s"% (uuid.uuid4(),), priority_rank=1)) - project2 = models.Project.objects.create(**Project_test_data("dynamic scheduling test project %s"% (uuid.uuid4(),), priority_rank=2)) - - scheduling_set1 = models.SchedulingSet.objects.create(**SchedulingSet_test_data(project=project1)) - scheduling_set2 = models.SchedulingSet.objects.create(**SchedulingSet_test_data(project=project2)) - - scheduling_unit_draft1 = self.create_simple_observation_scheduling_unit("scheduling unit 1", scheduling_set=scheduling_set1) - scheduling_unit_blueprint1 = create_task_blueprints_and_subtasks_from_scheduling_unit_draft(scheduling_unit_draft1) + scheduling_unit_draft_low = self.create_simple_observation_scheduling_unit("scheduling unit 1", scheduling_set=self.scheduling_set_low) + scheduling_unit_blueprint_low = create_task_blueprints_and_subtasks_from_scheduling_unit_draft(scheduling_unit_draft_low) - scheduling_unit_draft2 = self.create_simple_observation_scheduling_unit("scheduling unit 2", scheduling_set=scheduling_set2) - scheduling_unit_blueprint2 = create_task_blueprints_and_subtasks_from_scheduling_unit_draft(scheduling_unit_draft2) + scheduling_unit_draft_high = self.create_simple_observation_scheduling_unit("scheduling unit 2", scheduling_set=self.scheduling_set_high) + scheduling_unit_blueprint_high = create_task_blueprints_and_subtasks_from_scheduling_unit_draft(scheduling_unit_draft_high) # call the method-under-test. scheduled_scheduling_unit = schedule_next_scheduling_unit() # we expect the scheduling_unit with the highest project rank to be scheduled first self.assertIsNotNone(scheduled_scheduling_unit) - self.assertEqual(scheduling_unit_blueprint2.id, scheduled_scheduling_unit.id) + self.assertEqual(scheduling_unit_blueprint_high.id, scheduled_scheduling_unit.id) # check the results # we expect the sub2 to be scheduled, and sub1 to have a starttime after sub2 - scheduling_unit_blueprint1.refresh_from_db() - scheduling_unit_blueprint2.refresh_from_db() - self.assertEqual(scheduling_unit_blueprint1.status, 'schedulable') - self.assertEqual(scheduling_unit_blueprint2.status, 'scheduled') + scheduling_unit_blueprint_low.refresh_from_db() + scheduling_unit_blueprint_high.refresh_from_db() + self.assertEqual(scheduling_unit_blueprint_low.status, 'schedulable') + self.assertEqual(scheduling_unit_blueprint_high.status, 'scheduled') # check the scheduled subtask upcoming_scheduled_subtasks = models.Subtask.objects.filter(state__value='scheduled', - start_time__gt=datetime.utcnow(), - task_blueprint__scheduling_unit_blueprint__in=(scheduling_unit_blueprint1, scheduling_unit_blueprint2)).all() + task_blueprint__scheduling_unit_blueprint__in=(scheduling_unit_blueprint_low, scheduling_unit_blueprint_high)).all() self.assertEqual(1, upcoming_scheduled_subtasks.count()) - self.assertEqual(scheduling_unit_blueprint2.id, upcoming_scheduled_subtasks[0].task_blueprint.scheduling_unit_blueprint.id) + self.assertEqual(scheduling_unit_blueprint_high.id, upcoming_scheduled_subtasks[0].task_blueprint.scheduling_unit_blueprint.id) # create the "mid-term schedule" (consisting the the one remaining sub1) assign_start_stop_times_to_schedulable_scheduling_units() - self.assertGreater(scheduling_unit_blueprint1.start_time, scheduling_unit_blueprint2.start_time) + self.assertGreater(scheduling_unit_blueprint_low.start_time, scheduling_unit_blueprint_high.start_time) logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', level=logging.INFO) diff --git a/SAS/TMSS/src/tmss/exceptions.py b/SAS/TMSS/src/tmss/exceptions.py index 358a5ede249c6d5cef191b4c342f415f72e58c79..e45ba40745dbfac84a842d9334b3fd687ad2cc23 100644 --- a/SAS/TMSS/src/tmss/exceptions.py +++ b/SAS/TMSS/src/tmss/exceptions.py @@ -23,6 +23,10 @@ class SubtaskSchedulingException(SchedulingException): class TaskSchedulingException(SchedulingException): pass +class DynamicSchedulingException(SchedulingException): + pass + class UnknownTemplateException(TMSSException): '''raised when TMSS trying to base its processing routines on the chosen template, but this specific template is unknown.''' pass +