From c5d228ffc4ba71b6d3114a384eb14037c24f59f9 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 21 Dec 2021 15:44:41 +0100 Subject: [PATCH] add timeouts to MultiEvents to be used for a follow up change for startup events Change-Id: Id8816eb8f561dcd8d1473e25a9685e796fb14953 Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/27364 Tested-by: Jenkins Automated Tests Reviewed-by: Markus Zolliker --- secop/lib/multievent.py | 97 ++++++++++++++++++++++++++++------------- test/test_multievent.py | 60 +++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 31 deletions(-) create mode 100644 test/test_multievent.py diff --git a/secop/lib/multievent.py b/secop/lib/multievent.py index 6b3337e..c743d06 100644 --- a/secop/lib/multievent.py +++ b/secop/lib/multievent.py @@ -21,41 +21,50 @@ # ***************************************************************************** import threading +import time + + +ETERNITY = 1e99 + + +class _SingleEvent: + """Single Event + + remark: :meth:`wait` is not implemented on purpose + """ + def __init__(self, multievent, timeout, name=None): + self.multievent = multievent + self.multievent.clear_(self) + self.name = name + if timeout is None: + self.deadline = ETERNITY + else: + self.deadline = time.monotonic() + timeout + + def clear(self): + self.multievent.clear_(self) + + def set(self): + self.multievent.set_(self) + + def is_set(self): + return self in self.multievent.events class MultiEvent(threading.Event): - """Class implementing multi event objects. + """Class implementing multi event objects.""" - meth:`new` creates Event like objects - meth:'wait` waits for all of them being set - """ - - class SingleEvent: - """Single Event - - remark: :meth:`wait` is not implemented on purpose - """ - def __init__(self, multievent): - self.multievent = multievent - self.multievent._clear(self) - - def clear(self): - self.multievent._clear(self) - - def set(self): - self.multievent._set(self) - - def is_set(self): - return self in self.multievent.events - - def __init__(self): + def __init__(self, default_timeout=None): self.events = set() self._lock = threading.Lock() + self.default_timeout = default_timeout or None # treat 0 as None + self.name = None # default event name super().__init__() - def new(self): - """create a new SingleEvent""" - return self.SingleEvent(self) + def new(self, timeout=None, name=None): + """create a single event like object""" + return _SingleEvent(self, timeout or self.default_timeout, + name or self.name or '') def set(self): raise ValueError('a multievent must not be set directly') @@ -63,7 +72,10 @@ class MultiEvent(threading.Event): def clear(self): raise ValueError('a multievent must not be cleared directly') - def _set(self, event): + def is_set(self): + return not self.events + + def set_(self, event): """internal: remove event from the event list""" with self._lock: self.events.discard(event) @@ -71,13 +83,36 @@ class MultiEvent(threading.Event): return super().set() - def _clear(self, event): + def clear_(self, event): """internal: add event to the event list""" with self._lock: self.events.add(event) super().clear() + def deadline(self): + deadline = 0 + for event in self.events: + deadline = max(event.deadline, deadline) + return None if deadline == ETERNITY else deadline + def wait(self, timeout=None): + """wait for all events being set or timed out""" if not self.events: # do not wait if events are empty - return - super().wait(timeout) + return True + deadline = self.deadline() + if deadline is not None: + deadline -= time.monotonic() + timeout = deadline if timeout is None else min(deadline, timeout) + if timeout <= 0: + return False + return super().wait(timeout) + + def waiting_for(self): + return set(event.name for event in self.events) + + def setfunc(self, timeout=None, name=None): + """create a new single event and return its set method + + as a convenience method + """ + return self.new(timeout, name).set diff --git a/test/test_multievent.py b/test/test_multievent.py new file mode 100644 index 0000000..2c62538 --- /dev/null +++ b/test/test_multievent.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# ***************************************************************************** +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# +# ***************************************************************************** + +import time +from secop.lib.multievent import MultiEvent + + +def test_without_timeout(): + m = MultiEvent() + s1 = m.setfunc(name='s1') + s2 = m.setfunc(name='s2') + assert not m.wait(0) + assert m.deadline() is None + assert m.waiting_for() == {'s1', 's2'} + s2() + assert m.waiting_for() == {'s1'} + assert not m.wait(0) + s1() + assert not m.waiting_for() + assert m.wait(0) + + +def test_with_timeout(monkeypatch): + current_time = 1000 + monkeypatch.setattr(time, 'monotonic', lambda: current_time) + m = MultiEvent() + assert m.deadline() == 0 + m.name = 's1' + s1 = m.setfunc(10) + assert m.deadline() == 1010 + m.name = 's2' + s2 = m.setfunc(20) + assert m.deadline() == 1020 + current_time += 21 + assert not m.wait(0) + assert m.waiting_for() == {'s1', 's2'} + s1() + assert m.waiting_for() == {'s2'} + s2() + assert not m.waiting_for() + assert m.wait(0)