From 6a7fda6de74a9083ffa55eb0ff43c9837ec8382c Mon Sep 17 00:00:00 2001
From: Georgy Moshkin <gmoshkin@picodata.io>
Date: Tue, 19 Dec 2023 15:43:07 +0300
Subject: [PATCH] test: make Instance.wait_online more robust against slow
 picodata

---
 src/lib.rs       |  1 +
 test/conftest.py | 54 +++++++++++++++++++++++++++++++++++++++++-------
 2 files changed, 47 insertions(+), 8 deletions(-)

diff --git a/src/lib.rs b/src/lib.rs
index 5d4ec8e678..2a03c58e5f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,3 +1,4 @@
+#![allow(unknown_lints)]
 #![allow(clippy::too_many_arguments)]
 #![allow(clippy::let_and_return)]
 #![allow(clippy::needless_return)]
diff --git a/test/conftest.py b/test/conftest.py
index 4892cdb18d..385455dceb 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -990,10 +990,13 @@ class Instance:
     def wait_online(
         self, timeout: int | float = 6, rps: int | float = 5, expected_incarnation=None
     ):
-        """Wait until instance attains Online grade
+        """Wait until instance attains Online grade.
+
+        This function will periodically check the current instance's grade and
+        reset the timeout each time the grade changes.
 
         Args:
-            timeout (int | float, default=6): total time limit
+            timeout (int | float, default=6): time limit since last grade change
             rps (int | float, default=5): retries per second
 
         Raises:
@@ -1006,9 +1009,9 @@ class Instance:
         if self.process is None:
             raise ProcessDead("process was not started")
 
-        def fetch_info():
+        def fetch_current_grade() -> Tuple[str, int]:
             try:
-                exit_code = self.process.wait(timeout=0)
+                exit_code = self.process.wait(timeout=0)  # type: ignore
             except subprocess.TimeoutExpired:
                 # it's fine, the process is still running
                 pass
@@ -1025,11 +1028,46 @@ class Instance:
             myself = self.call("pico.instance_info", self.instance_id)
             assert isinstance(myself, dict)
             assert isinstance(myself["current_grade"], dict)
-            assert myself["current_grade"]["variant"] == "Online"
-            if expected_incarnation is not None:
-                assert myself["current_grade"]["incarnation"] == expected_incarnation
+            return (
+                myself["current_grade"]["variant"],
+                myself["current_grade"]["incarnation"],
+            )
+
+        now = time.monotonic()
+        deadline = now + timeout
+        next_retry = now
+        last_grade = None
+        while True:
+            now = time.monotonic()
+            assert now < deadline, "timeout"
+
+            # Throttling
+            if now < next_retry:
+                time.sleep(next_retry - now)
+            next_retry = time.monotonic() + 1 / rps
+
+            try:
+                # Fetch grade
+                grade = fetch_current_grade()
+                if grade != last_grade:
+                    last_grade = grade
+                    deadline = time.monotonic() + timeout
+
+                # Check grade
+                variant, incarnation = grade
+                assert variant == "Online"
+                if expected_incarnation is not None:
+                    assert incarnation == expected_incarnation
+
+                # Success!
+                break
+
+            except ProcessDead as e:
+                raise e from e
+            except Exception as e:
+                if time.monotonic() > deadline:
+                    raise e from e
 
-        Retriable(timeout, rps, fatal=ProcessDead).call(fetch_info)
         eprint(f"{self} is online")
 
     def raft_term(self) -> int:
-- 
GitLab