From 0aff114a90815e4f6df889a2cc3770c69dbd4dc2 Mon Sep 17 00:00:00 2001 From: Diptesh Roy Date: Sun, 12 Jul 2026 01:55:54 +0530 Subject: [PATCH] fix: replace time.sleep with sleep_until to fix key expiry drift time.sleep() uses the monotonic clock which does not advance when a machine is suspended (e.g. a laptop going to sleep). This caused API key expiry time drift: collection workers would still wait the full original duration even after the machine woke up and the key had already reset. Adds a sleep_until(epoch) helper that uses wall clock time (time.time()) with periodic 60-second wakeups to check if the target time has passed. This also means workers will pick up newly added API keys within 60 seconds rather than waiting out the full original sleep duration. Fixes #410 Signed-off-by: Diptesh Roy --- keyman/KeyClient.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/keyman/KeyClient.py b/keyman/KeyClient.py index ad1a14fdc..d5f70bb4a 100644 --- a/keyman/KeyClient.py +++ b/keyman/KeyClient.py @@ -7,6 +7,30 @@ from collectoss.tasks.init.redis_connection import get_redis_connection from keyman.KeyOrchestrationAPI import spec, WaitKeyTimeout + +def sleep_until(epoch: float, check_interval: int = 60) -> None: + """Sleep until a specific wall-clock epoch time. + + Unlike time.sleep(), which uses the monotonic clock (which does not + advance when a laptop/machine is suspended), this function periodically + wakes up and checks the actual wall clock time. This prevents the + API key expiry time drift issue that occurs when a machine sleeps + while a collection worker is waiting for a rate-limited key to reset. + + As a bonus, this also means that if a new API key is added to keyman + mid-collection, workers will pick it up within check_interval seconds. + + Args: + epoch: Unix timestamp to sleep until + check_interval: How often (in seconds) to wake up and check the + wall clock. Defaults to 60 seconds. + """ + while True: + remaining = epoch - time.time() + if remaining <= 0: + return + time.sleep(min(remaining, check_interval)) + class KeyClient: """Worker-side interface for requesting API keys from the orchestrator. @@ -109,7 +133,7 @@ def request(self, platform: str | None = None) -> str: raise Exception(f"Invalid response type: {msg}") except WaitKeyTimeout as e: self.logger.debug(f"NO FRESH KEYS: sleeping for {e.timeout_seconds} seconds") - time.sleep(e.timeout_seconds) + sleep_until(time.time() + e.timeout_seconds) except Exception as e: self.logger.exception(f"Error during key request: {e}") time.sleep(20)