core-extra/daemon/core/location/event.py

249 lines
6.6 KiB
Python

"""
event.py: event loop implementation using a heap queue and threads.
"""
import heapq
import threading
import time
from functools import total_ordering
from typing import Any, Callable, Optional
class Timer(threading.Thread):
"""
Based on threading.Timer but cancel() returns if the timer was
already running.
"""
def __init__(
self,
interval: float,
func: Callable[..., None],
args: tuple[Any] = None,
kwargs: dict[Any, Any] = None,
) -> None:
"""
Create a Timer instance.
:param interval: time interval
:param func: function to call when timer finishes
:param args: function arguments
:param kwargs: function keyword arguments
"""
super().__init__()
self.interval: float = interval
self.func: Callable[..., None] = func
self.finished: threading.Event = threading.Event()
self._running: threading.Lock = threading.Lock()
# validate arguments were provided
if args is None:
args = ()
self.args: tuple[Any] = args
# validate keyword arguments were provided
if kwargs is None:
kwargs = {}
self.kwargs: dict[Any, Any] = kwargs
def cancel(self) -> bool:
"""
Stop the timer if it hasn't finished yet. Return False if
the timer was already running.
:return: True if canceled, False otherwise
"""
locked = self._running.acquire(False)
if locked:
self.finished.set()
self._running.release()
return locked
def run(self) -> None:
"""
Run the timer.
:return: nothing
"""
self.finished.wait(self.interval)
with self._running:
if not self.finished.is_set():
self.func(*self.args, **self.kwargs)
self.finished.set()
@total_ordering
class Event:
"""
Provides event objects that can be used within the EventLoop class.
"""
def __init__(
self,
eventnum: int,
event_time: float,
func: Callable[..., None],
*args: Any,
**kwds: Any
) -> None:
"""
Create an Event instance.
:param eventnum: event number
:param event_time: event time
:param func: event function
:param args: function arguments
:param kwds: function keyword arguments
"""
self.eventnum: int = eventnum
self.time: float = event_time
self.func: Callable[..., None] = func
self.args: tuple[Any] = args
self.kwds: dict[Any, Any] = kwds
self.canceled: bool = False
def __lt__(self, other: "Event") -> bool:
result = self.time < other.time
if result:
result = self.eventnum < other.eventnum
return result
def run(self) -> None:
"""
Run an event.
:return: nothing
"""
if self.canceled:
return
self.func(*self.args, **self.kwds)
def cancel(self) -> None:
"""
Cancel event.
:return: nothing
"""
self.canceled = True
class EventLoop:
"""
Provides an event loop for running events.
"""
def __init__(self) -> None:
"""
Creates a EventLoop instance.
"""
self.lock: threading.RLock = threading.RLock()
self.queue: list[Event] = []
self.eventnum: int = 0
self.timer: Optional[Timer] = None
self.running: bool = False
self.start: Optional[float] = None
def _run_events(self) -> None:
"""
Run events.
:return: nothing
"""
schedule = False
while True:
with self.lock:
if not self.running or not self.queue:
break
now = time.monotonic()
if self.queue[0].time > now:
schedule = True
break
event = heapq.heappop(self.queue)
if event.time > now:
raise ValueError("invalid event time: %s > %s", event.time, now)
event.run()
with self.lock:
self.timer = None
if schedule:
self._schedule_event()
def _schedule_event(self) -> None:
"""
Schedule event.
:return: nothing
"""
with self.lock:
if not self.running:
raise ValueError("scheduling event while not running")
if not self.queue:
return
delay = self.queue[0].time - time.monotonic()
if self.timer:
raise ValueError("timer was already set")
self.timer = Timer(delay, self._run_events)
self.timer.daemon = True
self.timer.start()
def run(self) -> None:
"""
Start event loop.
:return: nothing
"""
with self.lock:
if self.running:
return
self.running = True
self.start = time.monotonic()
for event in self.queue:
event.time += self.start
self._schedule_event()
def stop(self) -> None:
"""
Stop event loop.
:return: nothing
"""
with self.lock:
if not self.running:
return
self.queue = []
self.eventnum = 0
if self.timer is not None:
self.timer.cancel()
self.timer = None
self.running = False
self.start = None
def add_event(self, delaysec: float, func: Callable, *args: Any, **kwds: Any):
"""
Add an event to the event loop.
:param delaysec: delay in seconds for event
:param func: event function
:param args: event arguments
:param kwds: event keyword arguments
:return: created event
"""
with self.lock:
eventnum = self.eventnum
self.eventnum += 1
evtime = float(delaysec)
if self.running:
evtime += time.monotonic()
event = Event(eventnum, evtime, func, *args, **kwds)
if self.queue:
prevhead = self.queue[0]
else:
prevhead = None
heapq.heappush(self.queue, event)
head = self.queue[0]
if prevhead is not None and prevhead != head:
if self.timer is not None and self.timer.cancel():
self.timer = None
if self.running and self.timer is None:
self._schedule_event()
return event