From d283a58188c0d0267ef5c4ff78ed1b071e673343 Mon Sep 17 00:00:00 2001
From: Daniel K Lyons <dlyons@nrao.edu>
Date: Tue, 13 Sep 2022 15:30:54 -0600
Subject: [PATCH] Retry the validation as well as the fetch

---
 .../productfetcher/productfetcher/fetchers.py | 15 +----
 .../productfetcher/interfaces.py              |  4 +-
 .../productfetcher/productfetcher/retry.py    | 57 +++++++++++++++++++
 3 files changed, 63 insertions(+), 13 deletions(-)
 create mode 100644 apps/cli/executables/pexable/productfetcher/productfetcher/retry.py

diff --git a/apps/cli/executables/pexable/productfetcher/productfetcher/fetchers.py b/apps/cli/executables/pexable/productfetcher/productfetcher/fetchers.py
index 33785253f..4f6d7af51 100644
--- a/apps/cli/executables/pexable/productfetcher/productfetcher/fetchers.py
+++ b/apps/cli/executables/pexable/productfetcher/productfetcher/fetchers.py
@@ -41,6 +41,7 @@ from .exceptions import (
 )
 from .interfaces import FetchProgressReporter, FileFetcher, LocatedFile
 from .locations import NgasFile, OracleXml
+from .retry import retry
 
 # pylint: disable=E0401, E0402, W0221
 
@@ -245,18 +246,8 @@ class RetryableFileFetcher(FileFetcherDecorator):
         super().__init__(underlying)
         self.retries = retries
 
-    def do_fetch(self, attempt=1) -> Path:
-        try:
-            return self.underlying.do_fetch()
-        except RetryableFetchError as r_err:
-            if attempt < self.retries:
-                # sleep for 2, 4, 8 seconds between attempts
-                time.sleep(2**attempt)
-                return self.do_fetch(attempt + 1)
-            else:
-                # let's annotate the error with how many times we tried
-                r_err.retries = attempt
-                raise r_err
+    def do_fetch(self) -> Path:
+        return retry(self.underlying.do_fetch, [2**attempt for attempt in range(1, 4)])
 
     def __str__(self):
         return f"{self.underlying} (with up to {self.retries} retries)"
diff --git a/apps/cli/executables/pexable/productfetcher/productfetcher/interfaces.py b/apps/cli/executables/pexable/productfetcher/productfetcher/interfaces.py
index f26c6358c..1ae735eb2 100644
--- a/apps/cli/executables/pexable/productfetcher/productfetcher/interfaces.py
+++ b/apps/cli/executables/pexable/productfetcher/productfetcher/interfaces.py
@@ -42,6 +42,8 @@ from abc import ABC, abstractmethod
 from pathlib import Path
 from typing import List, NamedTuple, Union
 
+from .retry import retry
+
 
 class LocatedFile(abc.ABC):
     """
@@ -170,7 +172,7 @@ class FileFetcher(ABC):
         # now validate with our validators, if we fetched a file
         if result is not None:
             for validator in self.validators:
-                validator.validate(result)
+                retry(lambda: validator.validate(result), [1, 4, 9])
 
         # if we made it here, we have a result to report
         reporter.complete(self.file, result)
diff --git a/apps/cli/executables/pexable/productfetcher/productfetcher/retry.py b/apps/cli/executables/pexable/productfetcher/productfetcher/retry.py
new file mode 100644
index 000000000..97fc0c2ce
--- /dev/null
+++ b/apps/cli/executables/pexable/productfetcher/productfetcher/retry.py
@@ -0,0 +1,57 @@
+#
+# Copyright (C) 2021 Associated Universities, Inc. Washington DC, USA.
+#
+# This file is part of NRAO Workspaces.
+#
+# Workspaces is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Workspaces is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Workspaces.  If not, see <https://www.gnu.org/licenses/>.
+import time
+from typing import Callable, List, TypeVar
+
+T = TypeVar("T")
+
+
+def retry(f: Callable[[], T], backoff_times: List[int] = None, retry_number=1) -> T:
+    """
+    Calls f() and returns its result. However, if f() throws an exception, we will retry.
+    Each entry in backoff_times is a length of time (in seconds) to sleep using `time.sleep`.
+    So if the backoff_times = [1, 4, 9], we will sleep 1 second, then 4 seconds, then 9 seconds between retries.
+
+    Also, if an exception should be raised on the last attempt, we will attempt to add a "retries" attribute to
+    it with the number of retries that were attempted.
+
+    :param f: the function to call
+    :param backoff_times: a list of times to wait
+    :param retry_number: the current "retry" (used only for the retries attribute)
+    :return: whatever the result of `f` is
+    """
+    exception = None
+    try:
+        return f()
+    except Exception as ex:
+        if backoff_times:
+            # if we have, say, [1, 4, 9] in our list of backoff times,
+            # we will delay for 1 second and then have [4, 9] remaining backoff times
+            delay, *remaining_backoff_times = backoff_times
+
+            # sleep
+            time.sleep(delay)
+
+            return retry(f, remaining_backoff_times, retry_number + 1)
+        else:
+            # if this exception has a slot for "retry count" we should set it
+            if hasattr(ex, "retries"):
+                ex.retries = retry_number
+
+            # now raise the exception
+            raise exception
-- 
GitLab