🚧 More work on resilient managers
This commit is contained in:
@@ -26,7 +26,8 @@ class AsyncManager(Manager):
|
||||
Already scheduled Tasks will no longer be 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.set_task_data((game,))
|
||||
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):
|
||||
"""Task thread entry point"""
|
||||
game, *_rest = data
|
||||
self.final_run(game)
|
||||
self.execute_resilient_manager_logic(game)
|
||||
|
||||
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(self)
|
||||
|
||||
@@ -10,7 +10,7 @@ class DisplayManager(Manager):
|
||||
|
||||
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
|
||||
shared.win.games[game.game_id] = game
|
||||
game.update()
|
||||
|
||||
@@ -8,5 +8,5 @@ class FileManager(AsyncManager):
|
||||
|
||||
run_after = set((SteamAPIManager,))
|
||||
|
||||
def final_run(self, game: Game) -> None:
|
||||
def manager_logic(self, game: Game) -> None:
|
||||
game.save()
|
||||
|
||||
@@ -40,40 +40,38 @@ class Manager:
|
||||
return errors
|
||||
|
||||
@abstractmethod
|
||||
def final_run(self, game: Game) -> None:
|
||||
def manager_logic(self, game: Game) -> None:
|
||||
"""
|
||||
Manager specific logic triggered by the run method
|
||||
* Implemented by final child classes
|
||||
* Called by the run method, not used directly
|
||||
* 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
|
||||
"""
|
||||
|
||||
def run(self, game: Game, callback: Callable[["Manager"], Any]) -> None:
|
||||
"""
|
||||
Pass the game through the manager
|
||||
* Public method called by a pipeline
|
||||
* In charge of calling the final_run method and handling its errors
|
||||
"""
|
||||
|
||||
def execute_resilient_manager_logic(self, game: Game) -> None:
|
||||
"""Execute the manager logic and handle its errors by reporting them or retrying"""
|
||||
for remaining_tries in range(self.max_tries, -1, -1):
|
||||
try:
|
||||
self.final_run(game, self.max_tries)
|
||||
self.manager_logic(game)
|
||||
except Exception as error:
|
||||
# Handle unretryable errors
|
||||
log_args = (type(error).__name__, self.name, game.game_id)
|
||||
if type(error) in self.retryable_on:
|
||||
# Handle unretryable errors
|
||||
logging.error("Unretryable error in %s", self.name, exc_info=error)
|
||||
logging.error("Unretryable %s in %s for %s", *log_args)
|
||||
self.report_error(error)
|
||||
break
|
||||
# Handle being out of retries
|
||||
elif remaining_tries == 0:
|
||||
# Handle being out of retries
|
||||
logging.error("Out of retries in %s", self.name, exc_info=error)
|
||||
logging.error("Too many retries due to %s in %s for %s", *log_args)
|
||||
self.report_error(error)
|
||||
break
|
||||
# Retry
|
||||
else:
|
||||
# Retry
|
||||
logging.debug("Retrying %s (%s)", self.name, type(error).__name__)
|
||||
logging.debug("Retry caused by %s in %s for %s", *log_args)
|
||||
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)
|
||||
|
||||
@@ -12,11 +12,11 @@ class SGDBManager(AsyncManager):
|
||||
run_after = set((SteamAPIManager,))
|
||||
retryable_on = set((HTTPError,))
|
||||
|
||||
def final_run(self, game: Game) -> None:
|
||||
def manager_logic(self, game: Game) -> None:
|
||||
try:
|
||||
sgdb = SGDBHelper()
|
||||
sgdb.conditionaly_update_cover(game)
|
||||
except SGDBAuthError as error:
|
||||
except SGDBAuthError:
|
||||
# If invalid auth, cancel all SGDBManager tasks
|
||||
self.cancellable.cancel()
|
||||
self.report_error(error)
|
||||
raise
|
||||
|
||||
@@ -13,7 +13,7 @@ class SteamAPIManager(AsyncManager):
|
||||
|
||||
retryable_on = set((HTTPError,))
|
||||
|
||||
def final_run(self, game: Game) -> None:
|
||||
def manager_logic(self, game: Game) -> None:
|
||||
# Skip non-steam games
|
||||
if not game.source.startswith("steam_"):
|
||||
return
|
||||
|
||||
@@ -61,7 +61,7 @@ class Pipeline(GObject.Object):
|
||||
for manager in (*parallel, *blocking):
|
||||
self.waiting.remove(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:
|
||||
"""Method called by a manager when it's done"""
|
||||
|
||||
Reference in New Issue
Block a user