diff --git a/SAS/TMSS/backend/services/scheduling/lib/constraints.py b/SAS/TMSS/backend/services/scheduling/lib/constraints.py index 16425e050a4978669598e860297e2d0ffaad55ae..32e21905e54b7c239a14006147f57fa26294c4fb 100644 --- a/SAS/TMSS/backend/services/scheduling/lib/constraints.py +++ b/SAS/TMSS/backend/services/scheduling/lib/constraints.py @@ -28,7 +28,7 @@ These main methods are used in the dynamic_scheduler to pick the next best sched from datetime import datetime, timedelta from dateutil import parser -from typing import Callable, Union +from typing import Callable, Union, Tuple from astropy.coordinates import Angle from astropy.coordinates.earth import EarthLocation import astropy.units @@ -1699,6 +1699,16 @@ def can_run_without_used_stations(scheduling_unit: models.SchedulingUnitBlueprin return True + +def get_blocking_scheduled_units(scheduling_unit: models.SchedulingUnitBlueprint) -> Tuple[models.SchedulingUnitBlueprint]: + '''Get a list (tuple) of scheduled scheduling_units overlapping with the scheduled_start/stop_time of the given scheduling_unit''' + from .dynamic_scheduling import DEFAULT_INTER_OBSERVATION_GAP + scheduled_units = models.SchedulingUnitBlueprint.objects.filter(status__value=models.SchedulingUnitStatus.Choices.SCHEDULED.value) + scheduled_units = scheduled_units.filter(scheduled_stop_time__gt=scheduling_unit.scheduled_start_time - DEFAULT_INTER_OBSERVATION_GAP) + scheduled_units = scheduled_units.filter(scheduled_start_time__lte=scheduling_unit.scheduled_start_time + scheduling_unit.specified_main_observation_duration + DEFAULT_INTER_OBSERVATION_GAP) + return tuple(scheduled_units.all()) + + def determine_unschedulable_reason_and_mark_unschedulable_if_needed(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime, gridder: Gridder=None, raise_if_interruped: Callable=noop) -> models.SchedulingUnitBlueprint: try: logger.debug("determine_unschedulable_reason_and_mark_unschedulable_if_needed: scheduling_unit id=%s", scheduling_unit.id) @@ -1707,6 +1717,15 @@ def determine_unschedulable_reason_and_mark_unschedulable_if_needed(scheduling_u msg = "Stations %s are reserved" % (','.join([str(s) for s in missing_stations]), ) return mark_independent_subtasks_in_scheduling_unit_blueprint_as_unschedulable(scheduling_unit, msg) + blocking_units = get_blocking_scheduled_units(scheduling_unit) + if blocking_units: + if len(blocking_units) == 1: + msg = "Scheduling unit id=%s is blocking this unit from being scheduled" % (blocking_units[0].id, ) + return mark_independent_subtasks_in_scheduling_unit_blueprint_as_unschedulable(scheduling_unit, msg) + else: + msg = "Scheduling units id=%s is blocking this unit from being scheduled" % (','.join(s.id for s in blocking_units), ) + return mark_independent_subtasks_in_scheduling_unit_blueprint_as_unschedulable(scheduling_unit, msg) + if not can_run_without_used_stations(scheduling_unit): missing_stations = get_missing_stations_for_scheduling_unit(scheduling_unit) msg = "Stations %s are already used" % (','.join([str(s) for s in missing_stations]), ) diff --git a/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py b/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py index a2459ec817cb95069fde2655f35b498d4a83dd78..87654f8079acd74962556192c8f812023d199dd7 100755 --- a/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py +++ b/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py @@ -407,6 +407,58 @@ class TestFixedTimeScheduling(BaseDynamicSchedulingTestCase): # even though it's unschedulable, it should still be positioned at the requested 'at' time (in the past) self.assertEqual(at, scheduling_unit_blueprint.scheduled_start_time) + def test_bug_fix_TMSS_2296_a_fixed_time_unit_blocked_by_another_should_become_unschedulable(self): + """ + Test to reproduce the reported bug that a fixed_time unit is not made unschedulable when blocked by another. + See: https://support.astron.nl/jira/browse/TMSS-2296 + """ + at = round_to_second_precision(datetime.utcnow() + timedelta(days=1)) + scheduling_unit_draft = self.create_simple_observation_scheduling_unit_fixed_time(at=at) + scheduling_unit_blueprint = create_scheduling_unit_blueprint_and_tasks_and_subtasks_from_scheduling_unit_draft(scheduling_unit_draft) + scheduling_unit_blueprint.rank = models.SchedulingUnitRank.HIGHEST.value + scheduling_unit_blueprint.save() + + # assert blueprint has correct constraints, and is schedulable + self.assertEqual('fixed_time', scheduling_unit_blueprint.scheduling_constraints_doc['scheduler']) + self.assertEqual(at.isoformat(), scheduling_unit_blueprint.scheduling_constraints_doc['time']['at']) + self.assertEqual(scheduling_unit_blueprint.status.value, models.SchedulingUnitStatus.Choices.SCHEDULABLE.value) + + self.scheduler.schedule_fixed_time_scheduling_units() + + # Assert the scheduling_unit has been scheduled + scheduling_unit_blueprint.refresh_from_db() + self.assertEqual(models.SchedulingUnitStatus.Choices.SCHEDULED.value, scheduling_unit_blueprint.status.value) + self.assertEqual(at, scheduling_unit_blueprint.scheduled_start_time) + + # create a second unit, at the same time and try to schedule it. + # that should not be possible, and the unit should become unschedulable. + scheduling_unit_draft2 = self.create_simple_observation_scheduling_unit_fixed_time(at=at) + scheduling_unit_blueprint2 = create_scheduling_unit_blueprint_and_tasks_and_subtasks_from_scheduling_unit_draft(scheduling_unit_draft2) + scheduling_unit_blueprint2.rank = models.SchedulingUnitRank.LOWEST.value + scheduling_unit_blueprint2.save() + + # assert blueprint has correct constraints, and is schedulable initially... + self.assertEqual('fixed_time', scheduling_unit_blueprint2.scheduling_constraints_doc['scheduler']) + self.assertEqual(at.isoformat(), scheduling_unit_blueprint2.scheduling_constraints_doc['time']['at']) + self.assertEqual(scheduling_unit_blueprint2.status.value, models.SchedulingUnitStatus.Choices.SCHEDULABLE.value) + + # try to schedule the second unit... + self.scheduler.schedule_fixed_time_scheduling_units() + + # Assert the scheduling_unit has been NOT scheduled (but is unschedulable) + scheduling_unit_blueprint2.refresh_from_db() + self.assertEqual(models.SchedulingUnitStatus.Choices.UNSCHEDULABLE.value, scheduling_unit_blueprint2.status.value) + expected_msg = "Scheduling unit id=%s is blocking this unit from being scheduled" % (scheduling_unit_blueprint.id,) + self.assertEqual(expected_msg, scheduling_unit_blueprint2.unschedulable_reason) + self.assertEqual(at, scheduling_unit_blueprint2.scheduled_start_time) + + # Assert the original scheduling_unit is still scheduled + scheduling_unit_blueprint.refresh_from_db() + self.assertEqual(models.SchedulingUnitStatus.Choices.SCHEDULED.value, scheduling_unit_blueprint.status.value) + self.assertEqual(at, scheduling_unit_blueprint.scheduled_start_time) + + + def test_excetion_during_scheduling_sets_state_to_unschedulable(self): """