From 5c5dcd53879d5069a62416b817e49e1a1be45bed Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Wed, 24 Dec 2025 21:53:46 +0000 Subject: [PATCH 1/2] fix: Address potential race condition in FeatureStore update_availability --- ldclient/impl/datasystem/fdv2.py | 49 ++++++++++++++++---------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/ldclient/impl/datasystem/fdv2.py b/ldclient/impl/datasystem/fdv2.py index c37b9d7..12c8905 100644 --- a/ldclient/impl/datasystem/fdv2.py +++ b/ldclient/impl/datasystem/fdv2.py @@ -170,38 +170,39 @@ def __wrapper(self, fn: Callable): raise def __update_availability(self, available: bool): - try: - self.__lock.lock() - if available == self.__last_available: - return - self.__last_available = available - finally: + state_changed = False + poller_to_stop = None + task_to_start = None + + self.__lock.lock() + if available == self.__last_available: self.__lock.unlock() + return + + state_changed = True + self.__last_available = available + + if available: + poller_to_stop = self.__poller + self.__poller = None + elif self.__poller is None: + task_to_start = RepeatingTask("ldclient.check-availability", 0.5, 0, self.__check_availability) + self.__poller = task_to_start + self.__lock.unlock() if available: log.warning("Persistent store is available again") + else: + log.warning("Detected persistent store unavailability; updates will be cached until it recovers") status = DataStoreStatus(available, True) self.__store_update_sink.update_status(status) - if available: - try: - self.__lock.lock() - if self.__poller is not None: - self.__poller.stop() - self.__poller = None - finally: - self.__lock.unlock() + if poller_to_stop is not None: + poller_to_stop.stop() - return - - log.warning("Detected persistent store unavailability; updates will be cached until it recovers") - task = RepeatingTask("ldclient.check-availability", 0.5, 0, self.__check_availability) - - self.__lock.lock() - self.__poller = task - self.__poller.start() - self.__lock.unlock() + if task_to_start is not None: + task_to_start.start() def __check_availability(self): try: @@ -487,7 +488,7 @@ def synchronizer_loop(self: 'FDv2'): log.info("Recovery condition met, returning to primary synchronizer") except Exception as e: - log.error("Failed to build primary synchronizer: %s", e) + log.error("Failed to build synchronizer: %s", e) break except Exception as e: From 549dffa039981bdf3ec1b748376653ea19cdddb9 Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Mon, 5 Jan 2026 12:10:23 -0600 Subject: [PATCH 2/2] [sdk-1710] ensure we always release the lock. --- ldclient/impl/datasystem/fdv2.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/ldclient/impl/datasystem/fdv2.py b/ldclient/impl/datasystem/fdv2.py index 12c8905..7482d3a 100644 --- a/ldclient/impl/datasystem/fdv2.py +++ b/ldclient/impl/datasystem/fdv2.py @@ -175,20 +175,21 @@ def __update_availability(self, available: bool): task_to_start = None self.__lock.lock() - if available == self.__last_available: - self.__lock.unlock() - return + try: + if available == self.__last_available: + return - state_changed = True - self.__last_available = available + state_changed = True + self.__last_available = available - if available: - poller_to_stop = self.__poller - self.__poller = None - elif self.__poller is None: - task_to_start = RepeatingTask("ldclient.check-availability", 0.5, 0, self.__check_availability) - self.__poller = task_to_start - self.__lock.unlock() + if available: + poller_to_stop = self.__poller + self.__poller = None + elif self.__poller is None: + task_to_start = RepeatingTask("ldclient.check-availability", 0.5, 0, self.__check_availability) + self.__poller = task_to_start + finally: + self.__lock.unlock() if available: log.warning("Persistent store is available again")