From 5fb5ac9718e37ca26a0cc66c547a2135f99d0001 Mon Sep 17 00:00:00 2001
From: Jorrit Schaap <schaap@astron.nl>
Date: Tue, 10 Nov 2020 16:51:08 +0100
Subject: [PATCH] TMSS-190: first step towards stategy pattern

---
 .../scheduling/lib/dynamic_scheduling.py      | 89 ++++++++++++-------
 SAS/TMSS/src/tmss/exceptions.py               |  4 +
 2 files changed, 62 insertions(+), 31 deletions(-)

diff --git a/SAS/TMSS/services/scheduling/lib/dynamic_scheduling.py b/SAS/TMSS/services/scheduling/lib/dynamic_scheduling.py
index f24e5b37ae5..926ba825f02 100644
--- a/SAS/TMSS/services/scheduling/lib/dynamic_scheduling.py
+++ b/SAS/TMSS/services/scheduling/lib/dynamic_scheduling.py
@@ -43,7 +43,9 @@ from lofar.sas.tmss.tmss.tmssapp.conversions import LOFAR_CENTER_OBSERVER, Time,
 def get_schedulable_scheduling_units() -> [models.SchedulingUnitBlueprint]:
     '''get a list of all schedulable scheduling_units'''
     defined_independend_subtasks = models.Subtask.independent_subtasks().filter(state__value='defined')
-    scheduling_units = models.SchedulingUnitBlueprint.objects.filter(id__in=defined_independend_subtasks.values('task_blueprint__scheduling_unit_blueprint_id').distinct()).all()
+    defined_independend_subtask_ids = defined_independend_subtasks.values('task_blueprint__scheduling_unit_blueprint_id').distinct().all()
+    # TODO: prefetch related models, like draft, spec, templates, etc
+    scheduling_units = models.SchedulingUnitBlueprint.objects.filter(id__in=defined_independend_subtask_ids).all()
     return [su for su in scheduling_units if su.status == 'schedulable']
 
 
@@ -56,37 +58,62 @@ def get_scheduled_scheduling_units(lower:datetime=None, upper:datetime=None) ->
         scheduled_subtasks = scheduled_subtasks.filter(start_time__lte=upper)
     return list(models.SchedulingUnitBlueprint.objects.filter(id__in=scheduled_subtasks.values('task_blueprint__scheduling_unit_blueprint_id').distinct()).all())
 
+def can_run_within_timewindow(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime) -> bool:
+    constraints_template = scheduling_unit.draft.scheduling_constraints_template
+    if constraints_template.name == 'constraints':
+        if constraints_template.version == 1:
+            return can_run_within_timewindow_template_constraints_1(scheduling_unit, lower_bound, upper_bound)
+
+    raise UnknownTemplateException("Cannot check if scheduling_unit id=%s can run between '%s' and '%s', because we have no constraint solver for scheduling constraints template '%s' version=%s" % (
+                                    scheduling_unit.id, lower_bound, upper_bound, constraints_template.name, constraints_template.version))
+
+def can_run_within_timewindow_template_constraints_1(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime) -> bool:
+    constraints = scheduling_unit.draft.scheduling_constraints_doc
+    return can_run_within_timewindow_template_constraints_1_daily(scheduling_unit, lower_bound, upper_bound)
+
+def can_run_within_timewindow_template_constraints_1_daily(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime) -> bool:
+    constraints = scheduling_unit.draft.scheduling_constraints_doc
+    if not (constraints['daily']['require_day'] and constraints['daily']['require_night']):
+        # no day/night restrictions, can run any time
+        return True
+
+    if constraints['daily']['require_day'] or constraints['daily']['require_night']:
+        # TODO: TMSS-254 and TMSS-255
+        # TODO: take avoid_twilight into account
+        # Please note that this first crude proof of concept treats sunset/sunrise as 'events',
+        # whereas in our definition they are transition periods. See: TMSS-435
+
+        # Ugly code. Should be improved. Works for demo.
+        # create a series of timestamps in the window of opportunity, and evaluate of there are all during day or night
+        possible_start_time = get_first_possible_start_time(scheduling_unit, lower_bound)
+        possible_stop_time = possible_start_time + scheduling_unit.duration
+        timestamps = [possible_start_time]
+        while timestamps[-1] < possible_stop_time - timedelta(hours=8):
+            timestamps.append(timestamps[-1] + timedelta(hours=8))
+        timestamps.append(possible_stop_time)
+
+        if constraints['daily']['require_night'] and all(LOFAR_CENTER_OBSERVER.is_night(timestamp) for timestamp in timestamps):
+            return True
+
+        if constraints['daily']['require_day'] and all(not LOFAR_CENTER_OBSERVER.is_night(timestamp) for timestamp in timestamps):
+            return True
+
+    return False
+
+
 def filter_scheduling_units_using_constraints(scheduling_units:[models.SchedulingUnitBlueprint], lower_bound_start_time: datetime, upper_bound_stop_time: datetime) -> [models.SchedulingUnitBlueprint]:
-    filtered_scheduling_units = []
-    for scheduling_unit in scheduling_units:
-        constraints = scheduling_unit.draft.scheduling_constraints_doc
-
-        if not constraints:
-            # accept any scheduling_unit with no constraints
-            filtered_scheduling_units.append(scheduling_unit)
-            continue
-
-        # TODO: use factory to get filter function based on scheduling_constraints_template and/or constraint name
-        # for now, assume there is only one template, allowing straightforward filtering.
-        if not (constraints['daily']['require_day'] and constraints['daily']['require_night']):
-            filtered_scheduling_units.append(scheduling_unit)
-        elif constraints['daily']['require_day'] or constraints['daily']['require_night']:
-            # TODO: take avoid_twilight into account
-            # Ugly code. Should be improved. Works for demo.
-            # create a series of timestamps in the window of opportunity, and evaluate of there are all during day or night
-            possible_start_time = get_first_possible_start_time(scheduling_unit, lower_bound_start_time)
-            possible_stop_time = possible_start_time + scheduling_unit.duration
-            timestamps = [possible_start_time]
-            while timestamps[-1] < possible_stop_time-timedelta(hours=8):
-                timestamps.append(timestamps[-1] + timedelta(hours=8))
-            timestamps.append(possible_stop_time)
-
-            if constraints['daily']['require_night'] and all(LOFAR_CENTER_OBSERVER.is_night(timestamp) for timestamp in timestamps):
-                filtered_scheduling_units.append(scheduling_unit)
-            elif constraints['daily']['require_day'] and all(not LOFAR_CENTER_OBSERVER.is_night(timestamp) for timestamp in timestamps):
-                filtered_scheduling_units.append(scheduling_unit)
-
-    return filtered_scheduling_units
+    '''return the schedulable scheduling_units which for which the constraints are 'go' within the given timewindow'''
+
+    def can_run_within_timewindow_no_exception(scheduling_unit, lower_bound, upper_bound):
+        try:
+            return can_run_within_timewindow(scheduling_unit, lower_bound, upper_bound)
+        except UnknownTemplateException as e:
+            logger.warning(e)
+            return False
+
+    return [sub for sub in scheduling_units
+            if can_run_within_timewindow_no_exception(sub, lower_bound_start_time, upper_bound_stop_time)]
+
 
 def find_best_next_schedulable_unit(lower_bound_start_time: datetime=None, upper_bound_stop_time: datetime=None, scheduling_units:[models.SchedulingUnitBlueprint]=None) -> (models.SchedulingUnitBlueprint, datetime):
     """
diff --git a/SAS/TMSS/src/tmss/exceptions.py b/SAS/TMSS/src/tmss/exceptions.py
index a320dbd527a..358a5ede249 100644
--- a/SAS/TMSS/src/tmss/exceptions.py
+++ b/SAS/TMSS/src/tmss/exceptions.py
@@ -22,3 +22,7 @@ class SubtaskSchedulingException(SchedulingException):
 
 class TaskSchedulingException(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
-- 
GitLab