Repos / pytaku / 7f6cdad769
commit 7f6cdad769ce2d3eb19f4a2a14f1b5bca158241e
Author: Bùi Thành Nhân <hi@imnhan.com>
Date:   Sat Aug 29 15:43:06 2020 +0700

    protect scheduler against exceptions
    
    A failed worker is now put back into the queue, for a maximum of 3
    retries before its current turn is skipped.
    
    Also, in the update title worker: just skip title if source site is
    currently acting up.

diff --git a/pyproject.toml b/pyproject.toml
index 54f5d77..f71d4ea 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "pytaku"
-version = "0.3.9"
+version = "0.3.10"
 description = "Self-hostable web-based manga reader"
 authors = ["Bùi Thành Nhân <hi@imnhan.com>"]
 license = "AGPL-3.0-only"
diff --git a/src/pytaku/scheduler.py b/src/pytaku/scheduler.py
index 1617c8d..c64cb21 100644
--- a/src/pytaku/scheduler.py
+++ b/src/pytaku/scheduler.py
@@ -1,8 +1,13 @@
 import time
+import traceback
 from abc import ABC, abstractmethod
 from datetime import datetime, timedelta
 from pathlib import Path
 
+from requests.exceptions import ReadTimeout
+
+from mangoapi.exceptions import SourceSite5xxError
+
 from .conf import config
 from .persistence import delete_expired_tokens, find_outdated_titles, save_title
 from .source_sites import get_title
@@ -17,8 +22,13 @@ def main_loop():
         for worker in workers:
             if worker.should_run():
                 print("Running", worker.__class__.__name__)
-                worker.run()
-                worker.after_run()
+                try:
+                    worker.run()
+                    worker.after_run()
+                except Exception:
+                    stacktrace = traceback.format_exc()
+                    print(stacktrace)
+                    worker.after_error(stacktrace)
         time.sleep(5)
 
 
@@ -27,6 +37,7 @@ class Worker(ABC):
 
     def __init__(self):
         self.last_run = datetime(1, 1, 1)
+        self.error_count = 0
 
     def should_run(self):
         return now() - self.last_run >= self.interval
@@ -38,6 +49,15 @@ def run(self):
     def after_run(self):
         self.last_run = now()
 
+    def after_error(self, stacktrace):
+        # TODO: email or send stacktrace to an ops chat channel or something
+        self.error_count += 1
+
+        # If failed repeatedly: give up this run
+        if self.error_count > 3:
+            self.last_run = now()
+            self.error_count = 0
+
 
 class UpdateOutdatedTitles(Worker):
     interval = timedelta(hours=2)
@@ -45,9 +65,12 @@ class UpdateOutdatedTitles(Worker):
     def run(self):
         for title in find_outdated_titles():
             print(f"Updating title {title['id']} from {title['site']}...", end="")
-            updated_title = get_title(title["site"], title["id"])
-            save_title(updated_title)
-            print(" done")
+            try:
+                updated_title = get_title(title["site"], title["id"])
+                save_title(updated_title)
+                print(" done")
+            except (SourceSite5xxError, ReadTimeout) as e:
+                print(" skipped because of server error:", str(e))
 
 
 class DeleteExpiredTokens(Worker):