diff --git a/running_exercise/lib/exercise_loop.py b/running_exercise/lib/exercise_loop.py index a5aeb3784ecddd07e15ad25dc612f7513f1206b1..b40cf44ee3bec616fc52384231827503d63934f4 100644 --- a/running_exercise/lib/exercise_loop.py +++ b/running_exercise/lib/exercise_loop.py @@ -1,5 +1,3 @@ -from typing import Optional - from django.conf import settings from django.utils import timezone @@ -9,15 +7,9 @@ from common_lib.utils import get_model from exercise.models import Exercise from running_exercise.lib.exercise_updater import ExerciseUpdater from running_exercise.lib.loop_thread import LoopThread -from running_exercise.lib.utils import ( - get_running_exercise, -) - -__loop: Optional[LoopThread] = None def _start_exercise(exercise: Exercise): - global __loop if exercise.finished: raise RunningExerciseOperationException( "Cannot start a finished exercise" @@ -28,7 +20,9 @@ def _start_exercise(exercise: Exercise): "Cannot start a running exercise" ) - if (running_exercise := get_running_exercise()) is not None: + if ( + running_exercise := Exercise.objects.filter(running=True).first() + ) is not None: raise RunningExerciseOperationException( f"Cannot start an exercise while exercise id: {running_exercise.id} is running" ) @@ -41,65 +35,12 @@ def _start_exercise(exercise: Exercise): logger.info(f"resuming a previously started exercise id: {exercise.id}") exercise.save() - __loop = LoopThread( + LoopThread( interval_s=settings.UPDATE_INTERVAL, elapsed_s=exercise.elapsed_s, total_time_s=exercise.config.exercise_duration * 60, updater=ExerciseUpdater(exercise), - ) - __loop.start() - - -def _stop_exercise(exercise: Exercise): - global __loop - if not exercise.running: - raise RunningExerciseOperationException( - "Cannot stop a non-running exercise." - ) - - # if the backend crashes while an exercise is running, - # the loop will no longer exist, however the exercise will still - # have the running flag turned on, which would prevent - # starting any exercise, including the one that crashed - if __loop is not None: - __loop.cancel() - logger.info(f"stopping exercise id: {exercise.id}") - exercise.running = False - else: - exercise.running = False - exercise.save() - logger.info( - f"stopping exercise id: {exercise.id} after possible server crash" - ) - - -def _move_time(exercise: Exercise, time_diff: int): - global __loop - - if not exercise.running: - raise RunningExerciseOperationException( - "Exercise must be running to move time" - ) - - # can only happen after the server crashed during a running exercise - if __loop is None: - raise RunningExerciseOperationException( - "Cannot move time when no exercise is running" - ) - __loop.elapsed_s = max(__loop.elapsed_s + time_diff, 0) - exercise.elapsed_s = __loop.elapsed_s - logger.info( - f"moving exercise id: {exercise.id} time by {time_diff} seconds" - ) - - -def _is_running(exercise_id: int) -> bool: - global __loop - return ( - __loop is not None - and __loop.updater.exercise.id == exercise_id - and __loop.is_alive() - ) + ).start() class ExerciseLoop: @@ -112,15 +53,33 @@ class ExerciseLoop: @staticmethod def stop(exercise_id: int) -> Exercise: exercise = get_model(Exercise, id=exercise_id) - _stop_exercise(exercise) + if not exercise.running: + raise RunningExerciseOperationException( + "Cannot stop a non-running exercise." + ) + + # The exercise will stop during the next update interval + exercise.running = False + exercise.save() + logger.info(f"stopping exercise id: {exercise.id}") return exercise @staticmethod def move_time(exercise_id: int, time_diff: int): exercise = get_model(Exercise, id=exercise_id) - _move_time(exercise, time_diff) + if not exercise.running: + raise RunningExerciseOperationException( + "Exercise must be running to move time" + ) + + exercise.elapsed_s = max(exercise.elapsed_s + time_diff, 0) + exercise.save() + logger.info( + f"moving exercise id: {exercise.id} time by {time_diff} seconds" + ) return exercise @staticmethod def is_running(exercise_id: int) -> bool: - return _is_running(exercise_id) + exercise = get_model(Exercise, id=exercise_id) + return exercise.running diff --git a/running_exercise/lib/exercise_updater.py b/running_exercise/lib/exercise_updater.py index c8bdc5d46062d41027758dc5348f0a7c967009bf..69c61f5a98cc663b641f655bbf3b3a49cd1d3335 100644 --- a/running_exercise/lib/exercise_updater.py +++ b/running_exercise/lib/exercise_updater.py @@ -4,6 +4,7 @@ from typing import List, Dict from django.utils import timezone from common_lib.exceptions import RunningExerciseOperationException +from common_lib.logger import logger from common_lib.subscription_handler import SubscriptionHandler from common_lib.utils import get_model from exercise.models import ( @@ -212,7 +213,14 @@ class ExerciseUpdater: self.exercise.elapsed_s = seconds self.exercise.save() - return time.time() - start + update_time = time.time() - start + logger.info( + f"update for exercise id: {self.exercise.id}" + f" elapsed time: {seconds}s" + f" update time: {update_time}s" + ) + + return update_time def finish(self): if not self.exercise.running: @@ -223,13 +231,4 @@ class ExerciseUpdater: self.exercise.running = False self.exercise.finished = True self.exercise.save() - - def stop(self, elapsed_s: int): - if not self.exercise.running: - raise RunningExerciseOperationException( - "Cannot stop a non-running exercise." - ) - - self.exercise.running = False - self.exercise.elapsed_s = elapsed_s - self.exercise.save() + logger.info(f"exercise id: {self.exercise.id} has finished") diff --git a/running_exercise/lib/loop_thread.py b/running_exercise/lib/loop_thread.py index 405db4d0b07b7d424119c6bd96f60bbb241f4b14..3cf68e90294aa6f398f039359ff8406f23b79c5c 100644 --- a/running_exercise/lib/loop_thread.py +++ b/running_exercise/lib/loop_thread.py @@ -1,8 +1,11 @@ -from threading import Thread, Event +import time +from threading import Thread from common_lib.logger import logger from common_lib.schema_types import ExerciseEventTypeEnum from common_lib.subscription_handler import SubscriptionHandler +from common_lib.utils import get_model +from exercise.models import Exercise from running_exercise.lib.exercise_updater import ExerciseUpdater @@ -12,7 +15,6 @@ class LoopThread(Thread): __interval_s: int __total_time_s: int - __finished: Event def __init__( self, @@ -22,81 +24,58 @@ class LoopThread(Thread): updater: ExerciseUpdater, ): super().__init__(daemon=True) - self.__interval_s = interval_s + self.__interval_s = max(interval_s, 1) self.elapsed_s = elapsed_s self.__total_time_s = total_time_s self.updater = updater - self.__finished = Event() - def cancel(self): - self.__finished.set() + def __exercise_loop(self): + while self.elapsed_s <= self.__total_time_s: + exercise = get_model(Exercise, id=self.updater.exercise.id) + if not exercise.running: + logger.info( + f"stopping exercise loop for exercise id: {self.updater.exercise.id}" + ) + break + + # This checks if move_time was called + if self.elapsed_s != exercise.elapsed_s + self.__interval_s: + self.elapsed_s = exercise.elapsed_s + + update_time = self.updater.update(self.elapsed_s) + + wait_time = ( + self.__interval_s - update_time + if update_time < self.__interval_s + else 0 + ) + self.elapsed_s += self.__interval_s + time.sleep(wait_time) def run(self): try: SubscriptionHandler.broadcast_exercise_loop( self.updater.exercise, True ) - self.updater.update(self.elapsed_s) SubscriptionHandler.broadcast_exercises( self.updater.exercise, ExerciseEventTypeEnum.modify() ) - wait_time: float = self.__interval_s - while ( - self.elapsed_s <= self.__total_time_s - and not self.__finished.wait(wait_time) - ): - update_time = self.updater.update(self.elapsed_s) - wait_time = ( - self.__interval_s - update_time - if update_time < self.__interval_s - else 0 - ) - # this is necessary to prevent infinite length loops with 0 interval - # I wouldn't expect this during any real deployment, however it might - # be useful during testing for max speed update times - if self.__interval_s == 0: - self.elapsed_s += 1 - else: - self.elapsed_s += self.__interval_s - logger.info( - f"update loop for exercise id: {self.updater.exercise.id} elapsed time: {self.elapsed_s}s" - ) - SubscriptionHandler.broadcast_exercise_loop( - self.updater.exercise, False - ) - self.__finished.set() + self.__exercise_loop() if self.elapsed_s >= self.__total_time_s: self.updater.finish() - logger.info( - f"finishing exercise loop for exercise id: {self.updater.exercise.id}" - ) - else: - self.updater.stop(self.elapsed_s) - SubscriptionHandler.broadcast_exercises( - self.updater.exercise, ExerciseEventTypeEnum.modify() - ) except Exception as ex: logger.error( f"loop thread exception {type(ex).__name__}: {str(ex)}" ) - for i in range(10): - try: - self.updater.stop(self.elapsed_s - self.__interval_s) - except: - logger.error( - f"failed to stop exercise: {self.updater.exercise.id}" - ) - else: - logger.info( - f"stopped exercise: {self.updater.exercise.id} on the {i}th try" - ) - break - else: - logger.error( - f"failed to stop exercise: {self.updater.exercise.id} 10 times, still running" - ) + finally: + SubscriptionHandler.broadcast_exercises( + self.updater.exercise, ExerciseEventTypeEnum.modify() + ) + SubscriptionHandler.broadcast_exercise_loop( + self.updater.exercise, False + ) def remaining_time_s(self) -> int: return self.__total_time_s - self.elapsed_s