From 0580478d4391eb7499535d68e6f2cf00fb661a52 Mon Sep 17 00:00:00 2001
From: David Trattnig <david.trattnig@o94.at>
Date: Fri, 20 Nov 2020 19:22:35 +0100
Subject: [PATCH] Extended scheduling window. #41

---
 config/sample-development.engine.ini | 23 +++++++----------------
 config/sample-docker.engine.ini      | 21 ++++++---------------
 config/sample-production.engine.ini  | 21 ++++++---------------
 docs/engine-features.md              | 14 ++++++++++----
 src/scheduling/scheduler.py          | 21 +++++++++++++++------
 5 files changed, 44 insertions(+), 56 deletions(-)

diff --git a/config/sample-development.engine.ini b/config/sample-development.engine.ini
index e854ad7a..ad093281 100644
--- a/config/sample-development.engine.ini
+++ b/config/sample-development.engine.ini
@@ -86,23 +86,14 @@ api_engine_store_health="http://localhost:8008/api/v1/source/health/${ENGINE_NUM
 
 # How often should the calendar be fetched in seconds. This determines the time of the last changes applied, before a specific show aired
 fetching_frequency=30
-# The scheduling window defines when the entries of each timeslot are queued for play-out in an ideal scenario. 
-
-# The actual window (scheduling_window_start - scheduling_window_end) should be higher then the `fetching_frequency` to allow proper queuing. 
-# Otherwise the fetch might never hit the scheduling window, because the scheduling logic is attached to the fetching logic. 
-# 
-# Following operations are related to the scheduling window:
-# 
-#   - Deletion of timeslots: Those are only accepted until the **start** of the scheduling window
-#   - Update/Delete/Assignment of playlists and entries: Those are accepted until the **end** of the the scheduling window; existing queued entries are updated
-#
-# After the end of the scheduling window the pre-loading phase starts.
-# Note, the values for windows is defined as a offset minus the actual start of the timeslot in seconds.
-scheduling_window_start=120
-scheduling_window_end=15
+# The scheduling window defines when the entries of each timeslot are queued for play-out. The windows start at (timeslot.start - window_start) seconds
+# and ends at (timeslot.end - window.end) seconds. Its also worth noting, that timeslots can only be deleted before the start of the window.                
+scheduling_window_start=60
+scheduling_window_end=60
 # How many seconds before the actual schedule time the entry should be pre-rolled. Note to provide enough timeout for
-# contents which take longer to load (big files, bad connectivity to streams etc.)
-preload_offset=10
+# contents which take longer to load (big files, bad connectivity to streams etc.). If the planned start time is in
+# the past the offset is ignored and the entry is played as soon as possible
+preload_offset=15
 
 # Sometimes it might take longer to get a stream connected. Here you can define a viable length.
 # But note, that this may affect the preloading time (see `preload_offset`), hence affecting the 
diff --git a/config/sample-docker.engine.ini b/config/sample-docker.engine.ini
index 72973b2c..cb599129 100644
--- a/config/sample-docker.engine.ini
+++ b/config/sample-docker.engine.ini
@@ -86,23 +86,14 @@ api_engine_store_health="http://127.0.0.1:8008/api/v1/source/health/${ENGINE_NUM
 
 # How often should the calendar be fetched in seconds. This determines the time of the last changes applied, before a specific show aired
 fetching_frequency=300
-# The scheduling window defines when the entries of each timeslot are queued for play-out in an ideal scenario. 
-
-# The actual window (scheduling_window_start - scheduling_window_end) should be higher then the `fetching_frequency` to allow proper queuing. 
-# Otherwise the fetch might never hit the scheduling window, because the scheduling logic is attached to the fetching logic. 
-# 
-# Following operations are related to the scheduling window:
-# 
-#   - Deletion of timeslots: Those are only accepted until the **start** of the scheduling window
-#   - Update/Delete/Assignment of playlists and entries: Those are accepted until the **end** of the the scheduling window; existing queued entries are updated
-#
-# After the end of the scheduling window the pre-loading phase starts.
-# Note, the values for windows is defined as a offset minus the actual start of the timeslot in seconds.
-scheduling_window_start=600
+# The scheduling window defines when the entries of each timeslot are queued for play-out. The windows start at (timeslot.start - window_start) seconds
+# and ends at (timeslot.end - window.end) seconds. Its also worth noting, that timeslots can only be deleted before the start of the window.                
+scheduling_window_start=60
 scheduling_window_end=60
 # How many seconds before the actual schedule time the entry should be pre-rolled. Note to provide enough timeout for
-# contents which take longer to load (big files, bad connectivity to streams etc.)
-preload_offset=30
+# contents which take longer to load (big files, bad connectivity to streams etc.). If the planned start time is in
+# the past the offset is ignored and the entry is played as soon as possible
+preload_offset=15
 
 # Sometimes it might take longer to get a stream connected. Here you can define a viable length.
 # But note, that this may affect the preloading time (see `preload_offset`), hence affecting the 
diff --git a/config/sample-production.engine.ini b/config/sample-production.engine.ini
index ddc70384..2a2f1bec 100644
--- a/config/sample-production.engine.ini
+++ b/config/sample-production.engine.ini
@@ -86,23 +86,14 @@ api_engine_store_health="http://localhost:8008/api/v1/source/health/${ENGINE_NUM
 
 # How often should the calendar be fetched in seconds. This determines the time of the last changes applied, before a specific show aired
 fetching_frequency=300
-# The scheduling window defines when the entries of each timeslot are queued for play-out in an ideal scenario. 
-
-# The actual window (scheduling_window_start - scheduling_window_end) should be higher then the `fetching_frequency` to allow proper queuing. 
-# Otherwise the fetch might never hit the scheduling window, because the scheduling logic is attached to the fetching logic. 
-# 
-# Following operations are related to the scheduling window:
-# 
-#   - Deletion of timeslots: Those are only accepted until the **start** of the scheduling window
-#   - Update/Delete/Assignment of playlists and entries: Those are accepted until the **end** of the the scheduling window; existing queued entries are updated
-#
-# After the end of the scheduling window the pre-loading phase starts.
-# Note, the values for windows is defined as a offset minus the actual start of the timeslot in seconds.
-scheduling_window_start=600
+# The scheduling window defines when the entries of each timeslot are queued for play-out. The windows start at (timeslot.start - window_start) seconds
+# and ends at (timeslot.end - window.end) seconds. Its also worth noting, that timeslots can only be deleted before the start of the window.                
+scheduling_window_start=60
 scheduling_window_end=60
 # How many seconds before the actual schedule time the entry should be pre-rolled. Note to provide enough timeout for
-# contents which take longer to load (big files, bad connectivity to streams etc.)
-preload_offset=30
+# contents which take longer to load (big files, bad connectivity to streams etc.). If the planned start time is in
+# the past the offset is ignored and the entry is played as soon as possible
+preload_offset=15
 
 # Sometimes it might take longer to get a stream connected. Here you can define a viable length.
 # But note, that this may affect the preloading time (see `preload_offset`), hence affecting the 
diff --git a/docs/engine-features.md b/docs/engine-features.md
index d6d29142..724cdc1e 100644
--- a/docs/engine-features.md
+++ b/docs/engine-features.md
@@ -66,7 +66,7 @@ point in time and the involved phase before:
 - **Scheduling Window**: Within the scheduling window any commands for controlling
     the mixer of the soundsystem are prepared and queued.
 
-    Until the start of the window, timeslot can be added or removed via external API Endpoints
+    Only until the start of the window, timeslots can be updated or removed via external API Endpoints
     (e.g. using Steering or Dashboard). Until here any changes on the timeslot itself will be reflected
     in the actual play-out. This only affects the start and end time of the "timeslot" itself.
     It does not involve related playlists and their entries. Those can still be modified after the
@@ -74,7 +74,8 @@ point in time and the involved phase before:
 
     The start and the end of the window is defined by the start of the timeslot minus
     a configured amount of seconds (see `scheduling_window_start` and `scheduling_window_end`
-    in `engine.ini`).
+    in `engine.ini`). The actual start of the window is calcuated by (timeslot start - window start)
+    and the end by (timeslot end - window end)
 
     During the scheduling window, the external API Endpoints are pulled continiously, to
     check for updated timeslots and related playlists. Also, any changes to playlists and
@@ -88,8 +89,13 @@ point in time and the involved phase before:
     the scheduled play-out time to avoid any delays in timing. Set the maximum time reserved for
     pre-loading in your configuration (compare `preload_offset`in `engine.ini`).
 
-    > Important: The offset should not exceed the time between the end of the scheduling-window and the
-    start of the actual timeslot playout.
+    If there is not enough time to reserve the given amount of time for preloading (i.e. some entry
+    should have started in the past already) the offset is ignored and the entry is played as soon as possible.
+
+    > Important: To ensure proper timings, the offset should not exceed the time between the start of
+    the scheduling-window and the start of the actual timeslot playout. Practically, of course there
+    are scenario where playout start later than planned e.g. during startup of the engine during a timeslot
+    or due to some severe connectivity issues to some external stream.
 
 - **Play-out**: Finally the actual play-out is happening. The faders of the virtual mixers are pushed
     all the way up, as soon it's "time to play" for one of the pre-loaded entries.
diff --git a/src/scheduling/scheduler.py b/src/scheduling/scheduler.py
index c50b888c..b32b3f30 100644
--- a/src/scheduling/scheduler.py
+++ b/src/scheduling/scheduler.py
@@ -35,7 +35,6 @@ from src.core.resources         import ResourceClass, ResourceUtil
 from src.scheduling.utils       import TimeslotRenderer
 from src.scheduling.programme   import ProgrammeService
 
-from src.scheduling.models import Playlist
 
 
 class AuraScheduler(threading.Thread):
@@ -357,9 +356,19 @@ class AuraScheduler(threading.Thread):
 
     def filter_scheduling_window(self, timeslots):
         """
-        Ignore timeslots which are beyond the scheduling window. The end of the scheduling window
-        is defined by the config option `scheduling_window_end`. This value defines the seconds
-        minus the actual start time of the timeslot.
+        Ignore timeslots which are before the start of scheduling window (start of timeslot - `scheduling_window_start`)
+        or after the end of the scheduling window (end of timeslot -`scheduling_window_end`).
+        
+        Before the scheduling window:
+            - Timeslots can still be deleted in Steering and the playout will respect this
+
+        During the scheduling window:
+            - Timeslots and it's playlists are queued as timed commands
+        
+        After the scheduling window:
+            - Such timeslots are ignored, because it doesn't make sense anymore to schedule them before the next
+              timeslot starts
+        
         """
         if not timeslots:
             return timeslots
@@ -368,9 +377,9 @@ class AuraScheduler(threading.Thread):
         len_before = len(timeslots)
         window_start = self.config.get("scheduling_window_start")
         window_end = self.config.get("scheduling_window_end")
-        timeslots = list(filter(lambda s: (s.start_unix - window_end) > now_unix and (s.start_unix - window_start) < now_unix, timeslots))
+        timeslots = list(filter(lambda t: (t.start_unix - window_start) < now_unix and now_unix < (t.end_unix - window_end), timeslots))
         len_after = len(timeslots)
-        self.logger.info("For now, skipped %s future timeslot(s) which are out of the scheduling window (T-%ss to T-%ss)" % ((len_before - len_after), window_start, window_end))
+        self.logger.info("For now, skipped %s future timeslot(s) which are out of the scheduling window (T¹-%ss to T²-%ss)" % ((len_before - len_after), window_start, window_end))
 
         return timeslots
 
-- 
GitLab