More documentation improvements.
[pyutils.git] / src / pyutils / state_tracker.py
index f83f254f27cce69ecf0d05bbcd740484beac7f65..90580f2d5977fd95abaabdad3e2f3f32558ecd22 100644 (file)
@@ -2,12 +2,16 @@
 
 # © Copyright 2021-2022, Scott Gasch
 
 
 # © Copyright 2021-2022, Scott Gasch
 
-"""Several helpers to keep track of internal state via periodic
-polling.  :class:`StateTracker` expects to be invoked periodically to
-maintain state whereas the others (:class:`AutomaticStateTracker` and
-:class:`WaitableAutomaticStateTracker`) automatically update themselves
-and, optionally, expose an event for client code to wait on state
+"""This module defines several classes (:py:class:`StateTracker`,
+:py:class:`AutomaticStateTracker`, and
+:py:class:`WaitableAutomaticStateTracker`) that can be used as base
+classes by your code.  These class patterns are meant to encapsulate
+and represent some state that dynamically changes and must be updated
+periodically.  These classes update their state (either automatically
+or when invoked to poll) and allow their callers to wait on state
 changes.
 changes.
+
+See also :class:`pyutils.parallelize.thread_utils.periodically_invoke`
 """
 
 import datetime
 """
 
 import datetime
@@ -25,17 +29,20 @@ logger = logging.getLogger(__name__)
 
 
 class StateTracker(ABC):
 
 
 class StateTracker(ABC):
-    """A base class that maintains and updates a global state via an
-    update routine.  Instances of this class should be periodically
-    invoked via the heartbeat() method.  This method, in turn, invokes
-    update() with update_ids according to a schedule / periodicity
-    provided to the c'tor.
+    """A base class that maintains and updates its state via an update
+    routine called :meth:`heartbeat`.  This method is not automatic:
+    instances of this class should be periodically invoked via their
+    :meth:`heartbeat` method by some other thread.
+
+    See also :class:`AutomaticStateTracker` if you'd rather not have
+    to invoke your code regularly.
     """
 
     def __init__(self, update_ids_to_update_secs: Dict[str, float]) -> None:
     """
 
     def __init__(self, update_ids_to_update_secs: Dict[str, float]) -> None:
-        """The update_ids_to_update_secs dict parameter describes one or more
-        update types (unique update_ids) and the periodicity(ies), in
-        seconds, at which it/they should be invoked.
+        """The update_ids_to_update_secs dict parameter describes one
+        or more update types (unique update_ids) and the
+        periodicity(ies), in seconds, at which it/they should be
+        invoked.
 
         .. note::
             When more than one update is overdue, they will be
 
         .. note::
             When more than one update is overdue, they will be
@@ -55,6 +62,7 @@ class StateTracker(ABC):
                 This would indicate that every 10s we would like to
                 refresh local state whereas every 60s we'd like to
                 refresh remote state.
                 This would indicate that every 10s we would like to
                 refresh local state whereas every 60s we'd like to
                 refresh remote state.
+
         """
         self.update_ids_to_update_secs = update_ids_to_update_secs
         self.last_reminder_ts: Dict[str, Optional[datetime.datetime]] = {}
         """
         self.update_ids_to_update_secs = update_ids_to_update_secs
         self.last_reminder_ts: Dict[str, Optional[datetime.datetime]] = {}
@@ -74,7 +82,7 @@ class StateTracker(ABC):
         Args:
             update_id: the string you passed to the c'tor as a key in
                 the update_ids_to_update_secs dict.  :meth:`update` will
         Args:
             update_id: the string you passed to the c'tor as a key in
                 the update_ids_to_update_secs dict.  :meth:`update` will
-                only be invoked on the shoulder, at most, every update_secs
+                only be invoked, at most, every update_secs
                 seconds.
 
             now: the approximate current timestamp at invocation time.
                 seconds.
 
             now: the approximate current timestamp at invocation time.
@@ -85,17 +93,18 @@ class StateTracker(ABC):
         pass
 
     def heartbeat(self, *, force_all_updates_to_run: bool = False) -> None:
         pass
 
     def heartbeat(self, *, force_all_updates_to_run: bool = False) -> None:
-        """Invoke this method to cause the StateTracker instance to identify
-        and invoke any overdue updates based on the schedule passed to
-        the c'tor.  In the base :class:`StateTracker` class, this method must
-        be invoked manually by a thread from external code.  Other subclasses
-        are available that create their own updater threads (see below).
-
-        If more than one type of update (update_id) are overdue,
-        they will be invoked in order based on their update_ids.
-
-        Setting force_all_updates_to_run will invoke all updates
-        (ordered by update_id) immediately ignoring whether or not
+        """Invoke this method periodically to cause the :class:`StateTracker`
+        instance to identify and invoke any overdue updates based on the
+        schedule passed to the c'tor.  In the base :class:`StateTracker` class,
+        this method must be invoked manually by a thread from external code.
+        Other subclasses (e.g. :class:`AutomaticStateTracker`) are available
+        that create their own updater threads (see below).
+
+        If more than one type of update (`update_id`) is overdue,
+        overdue updates will be invoked in order based on their `update_id`.
+
+        Setting `force_all_updates_to_run` will invoke all updates
+        (ordered by `update_id`) immediately ignoring whether or not
         they are due.
         """
 
         they are due.
         """
 
@@ -133,17 +142,20 @@ class AutomaticStateTracker(StateTracker):
     """
 
     @background_thread
     """
 
     @background_thread
-    def pace_maker(self, should_terminate: threading.Event) -> None:
+    def _pace_maker(self, should_terminate: threading.Event) -> None:
         """Entry point for a background thread to own calling :meth:`heartbeat`
         at regular intervals so that the main thread doesn't need to
         do so.
         """Entry point for a background thread to own calling :meth:`heartbeat`
         at regular intervals so that the main thread doesn't need to
         do so.
+
+        Args:
+            should_terminate: an event which, when set, indicates we should terminate.
         """
         while True:
             if should_terminate.is_set():
         """
         while True:
             if should_terminate.is_set():
-                logger.debug('pace_maker noticed event; shutting down')
+                logger.debug('_pace_maker noticed event; shutting down')
                 return
             self.heartbeat()
                 return
             self.heartbeat()
-            logger.debug('pace_maker is sleeping for %.1fs', self.sleep_delay)
+            logger.debug('_pace_maker is sleeping for %.1fs', self.sleep_delay)
             time.sleep(self.sleep_delay)
 
     def __init__(
             time.sleep(self.sleep_delay)
 
     def __init__(
@@ -185,13 +197,13 @@ class AutomaticStateTracker(StateTracker):
             periods_list = list(update_ids_to_update_secs.values())
             self.sleep_delay = math_utils.gcd_float_sequence(periods_list)
             logger.info('Computed sleep_delay=%.1f', self.sleep_delay)
             periods_list = list(update_ids_to_update_secs.values())
             self.sleep_delay = math_utils.gcd_float_sequence(periods_list)
             logger.info('Computed sleep_delay=%.1f', self.sleep_delay)
-        (thread, stop_event) = self.pace_maker()
+        (thread, stop_event) = self._pace_maker()
         self.should_terminate = stop_event
         self.updater_thread = thread
 
     def shutdown(self):
         """Terminates the background thread and waits for it to tear down.
         self.should_terminate = stop_event
         self.updater_thread = thread
 
     def shutdown(self):
         """Terminates the background thread and waits for it to tear down.
-        This may block for as long as self.sleep_delay.
+        This may block for as long as `self.sleep_delay`.
         """
         logger.debug('Setting shutdown event and waiting for background thread.')
         self.should_terminate.set()
         """
         logger.debug('Setting shutdown event and waiting for background thread.')
         self.should_terminate.set()
@@ -211,10 +223,10 @@ class WaitableAutomaticStateTracker(AutomaticStateTracker):
 
         detector = waitable_presence.WaitableAutomaticStateSubclass()
         while True:
 
         detector = waitable_presence.WaitableAutomaticStateSubclass()
         while True:
-            changed = detector.wait(timeout=60 * 5)
+            changed = detector.wait(timeout=60)
             if changed:
                 detector.reset()
             if changed:
                 detector.reset()
-                # Figure out what changed and react
+                # Figure out what changed and react somehow
             else:
                 # Just a timeout; no need to reset.  Maybe do something
                 # else before looping up into wait again.
             else:
                 # Just a timeout; no need to reset.  Maybe do something
                 # else before looping up into wait again.
@@ -267,10 +279,10 @@ class WaitableAutomaticStateTracker(AutomaticStateTracker):
         self._something_changed.clear()
 
     def wait(self, *, timeout=None):
         self._something_changed.clear()
 
     def wait(self, *, timeout=None):
-        """Wait for something to change or a timeout to lapse.
+        """Blocking wait for something to change or a timeout to lapse.
 
         Args:
             timeout: maximum amount of time to wait.  If None, wait
 
         Args:
             timeout: maximum amount of time to wait.  If None, wait
-                forever (until something changes).
+                forever (until something changes or shutdown).
         """
         return self._something_changed.wait(timeout=timeout)
         """
         return self._something_changed.wait(timeout=timeout)