🚧 More work on resilient managers

This commit is contained in:
GeoffreyCoulaud
2023-05-31 16:54:51 +02:00
parent 0b188136a2
commit d204737339
8 changed files with 40 additions and 30 deletions

View File

@@ -241,9 +241,20 @@ class CartridgesApplication(Adw.Application):
def main(version): # pylint: disable=unused-argument def main(version): # pylint: disable=unused-argument
# Initiate logger # Initiate logger
default_log_level = "DEBUG" if shared.PROFILE == "development" else "WARNING" # (silence debug info from external libraries)
log_level = os.environ.get("LOGLEVEL", default_log_level).upper() profile_base_log_level = "DEBUG" if shared.PROFILE == "development" else "WARNING"
logging.basicConfig(level=log_level) profile_lib_log_level = "INFO" if shared.PROFILE == "development" else "WARNING"
base_log_level = os.environ.get("LOGLEVEL", profile_base_log_level).upper()
lib_log_level = os.environ.get("LIBLOGLEVEL", profile_lib_log_level).upper()
log_levels = {
__name__: base_log_level,
"PIL": lib_log_level,
"urllib3": lib_log_level,
}
logging.basicConfig()
for logger, level in log_levels.items():
logging.getLogger(logger).setLevel(level)
# Start app # Start app
app = CartridgesApplication() app = CartridgesApplication()
return app.run(sys.argv) return app.run(sys.argv)

View File

@@ -26,7 +26,8 @@ class AsyncManager(Manager):
Already scheduled Tasks will no longer be cancellable.""" Already scheduled Tasks will no longer be cancellable."""
self.cancellable = Gio.Cancellable() self.cancellable = Gio.Cancellable()
def run(self, game: Game, callback: Callable[["Manager"], Any]) -> None: def process_game(self, game: Game, callback: Callable[["Manager"], Any]) -> None:
"""Create a task to process the game in a separate thread"""
task = Task.new(None, self.cancellable, self._task_callback, (callback,)) task = Task.new(None, self.cancellable, self._task_callback, (callback,))
task.set_task_data((game,)) task.set_task_data((game,))
task.run_in_thread(self._task_thread_func) task.run_in_thread(self._task_thread_func)
@@ -34,9 +35,9 @@ class AsyncManager(Manager):
def _task_thread_func(self, _task, _source_object, data, cancellable): def _task_thread_func(self, _task, _source_object, data, cancellable):
"""Task thread entry point""" """Task thread entry point"""
game, *_rest = data game, *_rest = data
self.final_run(game) self.execute_resilient_manager_logic(game)
def _task_callback(self, _source_object, _result, data): def _task_callback(self, _source_object, _result, data):
"""Method run after the async task is done""" """Method run after the task is done"""
callback, *_rest = data callback, *_rest = data
callback(self) callback(self)

View File

@@ -10,7 +10,7 @@ class DisplayManager(Manager):
run_after = set((SteamAPIManager, SGDBManager)) run_after = set((SteamAPIManager, SGDBManager))
def final_run(self, game: Game) -> None: def manager_logic(self, game: Game) -> None:
# TODO decouple a game from its widget # TODO decouple a game from its widget
shared.win.games[game.game_id] = game shared.win.games[game.game_id] = game
game.update() game.update()

View File

@@ -8,5 +8,5 @@ class FileManager(AsyncManager):
run_after = set((SteamAPIManager,)) run_after = set((SteamAPIManager,))
def final_run(self, game: Game) -> None: def manager_logic(self, game: Game) -> None:
game.save() game.save()

View File

@@ -40,40 +40,38 @@ class Manager:
return errors return errors
@abstractmethod @abstractmethod
def final_run(self, game: Game) -> None: def manager_logic(self, game: Game) -> None:
""" """
Manager specific logic triggered by the run method Manager specific logic triggered by the run method
* Implemented by final child classes * Implemented by final child classes
* Called by the run method, not used directly
* May block its thread * May block its thread
* May raise retryable exceptions that will be be retried if possible * May raise retryable exceptions that will trigger a retry if possible
* May raise other exceptions that will be reported * May raise other exceptions that will be reported
""" """
def run(self, game: Game, callback: Callable[["Manager"], Any]) -> None: def execute_resilient_manager_logic(self, game: Game) -> None:
""" """Execute the manager logic and handle its errors by reporting them or retrying"""
Pass the game through the manager
* Public method called by a pipeline
* In charge of calling the final_run method and handling its errors
"""
for remaining_tries in range(self.max_tries, -1, -1): for remaining_tries in range(self.max_tries, -1, -1):
try: try:
self.final_run(game, self.max_tries) self.manager_logic(game)
except Exception as error: except Exception as error:
# Handle unretryable errors
log_args = (type(error).__name__, self.name, game.game_id)
if type(error) in self.retryable_on: if type(error) in self.retryable_on:
# Handle unretryable errors logging.error("Unretryable %s in %s for %s", *log_args)
logging.error("Unretryable error in %s", self.name, exc_info=error)
self.report_error(error) self.report_error(error)
break break
# Handle being out of retries
elif remaining_tries == 0: elif remaining_tries == 0:
# Handle being out of retries logging.error("Too many retries due to %s in %s for %s", *log_args)
logging.error("Out of retries in %s", self.name, exc_info=error)
self.report_error(error) self.report_error(error)
break break
# Retry
else: else:
# Retry logging.debug("Retry caused by %s in %s for %s", *log_args)
logging.debug("Retrying %s (%s)", self.name, type(error).__name__)
continue continue
def process_game(self, game: Game, callback: Callable[["Manager"], Any]) -> None:
"""Pass the game through the manager"""
self.execute_resilient_manager_logic(game, tries=0)
callback(self) callback(self)

View File

@@ -12,11 +12,11 @@ class SGDBManager(AsyncManager):
run_after = set((SteamAPIManager,)) run_after = set((SteamAPIManager,))
retryable_on = set((HTTPError,)) retryable_on = set((HTTPError,))
def final_run(self, game: Game) -> None: def manager_logic(self, game: Game) -> None:
try: try:
sgdb = SGDBHelper() sgdb = SGDBHelper()
sgdb.conditionaly_update_cover(game) sgdb.conditionaly_update_cover(game)
except SGDBAuthError as error: except SGDBAuthError:
# If invalid auth, cancel all SGDBManager tasks # If invalid auth, cancel all SGDBManager tasks
self.cancellable.cancel() self.cancellable.cancel()
self.report_error(error) raise

View File

@@ -13,7 +13,7 @@ class SteamAPIManager(AsyncManager):
retryable_on = set((HTTPError,)) retryable_on = set((HTTPError,))
def final_run(self, game: Game) -> None: def manager_logic(self, game: Game) -> None:
# Skip non-steam games # Skip non-steam games
if not game.source.startswith("steam_"): if not game.source.startswith("steam_"):
return return

View File

@@ -61,7 +61,7 @@ class Pipeline(GObject.Object):
for manager in (*parallel, *blocking): for manager in (*parallel, *blocking):
self.waiting.remove(manager) self.waiting.remove(manager)
self.running.add(manager) self.running.add(manager)
manager.run(self.game, self.manager_callback) manager.process_game(self.game, self.manager_callback)
def manager_callback(self, manager: Manager) -> None: def manager_callback(self, manager: Manager) -> None:
"""Method called by a manager when it's done""" """Method called by a manager when it's done"""