frappy/secop/poller.py
Markus Zolliker c1164568ae further fixes of py3 issues
complaints by pylint are mainly related to
- remove object from base list in class definitions
- unnecessary else/elif after return/raise

Change-Id: I13d15449149cc8bba0562338d0c9c42e97163bdf
Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/21325
Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2019-09-26 14:15:48 +02:00

250 lines
9.3 KiB
Python

# -*- 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 <markus.zolliker@psi.ch>
#
# *****************************************************************************
'''general, advanced frappy poller
Usage examples:
any Module which want to be polled with a specific Poller must define
the pollerClass class variable:
class MyModule(Readable):
...
pollerClass = poller.Poller
...
modules having a parameter 'iodev' with the same value will share the same poller
'''
import time
from threading import Event
from heapq import heapify, heapreplace
from secop.lib import mkthread, formatException
from secop.errors import ProgrammingError
# poll types:
AUTO = 1 # equivalent to True, converted to REGULAR, SLOW or DYNAMIC
SLOW = 2
REGULAR = 3
DYNAMIC = 4
class PollerBase:
startup_timeout = 30 # default timeout for startup
name = 'unknown' # to be overridden in implementors __init__ method
@classmethod
def add_to_table(cls, table, module):
'''sort module into poller table
table is a dict, with (<pollerClass>, <name>) as the key, and the
poller as value.
<name> is module.iodev or module.name, if iodev is not present
'''
try:
pollerClass = module.pollerClass
except AttributeError:
return # no pollerClass -> fall back to simple poller
# for modules with the same iodev, a common poller is used,
# modules without iodev all get their own poller
name = getattr(module, 'iodev', module.name)
poller = table.get((pollerClass, name), None)
if poller is None:
poller = pollerClass(name)
table[(pollerClass, name)] = poller
poller.add_to_poller(module)
def start(self, started_callback):
'''start poller thread
started_callback to be called after all poll items were read at least once
'''
mkthread(self.run, started_callback)
return self.startup_timeout
def run(self, started_callback):
'''poller thread function
started_callback to be called after all poll items were read at least once
'''
raise NotImplementedError
def stop(self):
'''stop polling'''
raise NotImplementedError
def __bool__(self):
'''is there any poll item?'''
raise NotImplementedError
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, self.name)
__nonzero__ = __bool__ # Py2/3 compat
class Poller(PollerBase):
'''a standard poller
parameters may have the following polltypes:
- REGULAR: by default used for readonly parameters with poll=True
- SLOW: by default used for readonly=False parameters with poll=True.
slow polls happen with lower priority, but at least one parameter
is polled with regular priority within self.module.pollinterval.
Scheduled to poll every slowfactor * module.pollinterval
- DYNAMIC: by default used for 'value' and 'status'
When busy, scheduled to poll every fastfactor * module.pollinterval
'''
DEFAULT_FACTORS = {SLOW: 4, DYNAMIC: 0.25, REGULAR: 1}
def __init__(self, name):
'''create a poller'''
self.queues = {polltype: [] for polltype in self.DEFAULT_FACTORS}
self._stopped = Event()
self.maxwait = 3600
self.name = name
def add_to_poller(self, module):
factors = self.DEFAULT_FACTORS.copy()
try:
factors[DYNAMIC] = module.fast_pollfactor
except AttributeError:
pass
try:
factors[SLOW] = module.slow_pollfactor
except AttributeError:
pass
self.maxwait = min(self.maxwait, getattr(module, 'max_polltestperiod', 10))
try:
self.startup_timeout = max(self.startup_timeout, module.startup_timeout)
except AttributeError:
pass
# at the beginning, queues are simple lists
# later, they will be converted to heaps
for pname, pobj in module.parameters.items():
polltype = int(pobj.poll)
rfunc = getattr(module, 'read_' + pname, None)
if not polltype or not rfunc:
continue
if not hasattr(module, 'pollinterval'):
raise ProgrammingError("module %s must have a pollinterval"
% module.name)
if polltype == AUTO: # covers also pobj.poll == True
if pname in ('value', 'status'):
polltype = DYNAMIC
elif pobj.readonly:
polltype = REGULAR
else:
polltype = SLOW
# placeholders 0 are used for due, lastdue and idx
self.queues[polltype].append((0, 0,
(0, module, pobj, rfunc, factors[polltype])))
def poll_next(self, polltype):
'''try to poll next item
advance in queue until
- an item is found which is really due to poll. return 0 in this case
- or until the next item is not yet due. return next due time in this case
'''
queue = self.queues[polltype]
if not queue:
return float('inf') # queue is empty
now = time.time()
done = False
while not done:
due, lastdue, pollitem = queue[0]
if now < due:
return due
_, module, pobj, rfunc, factor = pollitem
if polltype == DYNAMIC and not module.isBusy():
interval = module.pollinterval # effective interval
mininterval = interval * factor # interval for calculating next due
else:
interval = module.pollinterval * factor
mininterval = interval
due = max(lastdue + interval, pobj.timestamp + interval * 0.5)
if now >= due:
try:
rfunc()
except Exception: # really all. errors are handled within rfunc
# TODO: filter repeated errors and log just statistics
module.log.error(formatException())
done = True
lastdue = due
due = max(lastdue + mininterval, now + min(self.maxwait, mininterval * 0.5))
# replace due, lastdue with new values and sort in
heapreplace(queue, (due, lastdue, pollitem))
return 0
def run(self, started_callback):
'''start poll loop
To be called as a thread. After all parameters are polled once first,
started_callback is called. To be called in Module.start_module.
poll strategy:
Slow polls are performed with lower priority than regular and dynamic polls.
If more polls are scheduled than time permits, at least every second poll is a
dynamic poll. After every n regular polls, one slow poll is done, if due
(where n is the number of regular parameters).
'''
if not self:
# nothing to do (else we might call time.sleep(float('inf')) below
started_callback()
return
# do all polls once and, at the same time, insert due info
for _, queue in sorted(self.queues.items()): # do SLOW polls first
for idx, (_, _, (_, module, pobj, rfunc, factor)) in enumerate(queue):
lastdue = time.time()
try:
rfunc()
except Exception: # really all. errors are handled within rfunc
module.log.error(formatException())
due = lastdue + min(self.maxwait, module.pollinterval * factor)
# in python 3 comparing tuples need some care, as not all objects
# are comparable. Inserting a unique idx solves the problem.
queue[idx] = (due, lastdue, (idx, module, pobj, rfunc, factor))
heapify(queue)
started_callback() # signal end of startup
nregular = len(self.queues[REGULAR])
while not self._stopped.is_set():
due = float('inf')
for _ in range(nregular):
due = min(self.poll_next(DYNAMIC), self.poll_next(REGULAR))
if due:
break # no dynamic or regular polls due
due = min(due, self.poll_next(DYNAMIC), self.poll_next(SLOW))
delay = due - time.time()
if delay > 0:
self._stopped.wait(delay)
def stop(self):
self._stopped.set()
def __bool__(self):
'''is there any poll item?'''
return any(self.queues.values())
__nonzero__ = __bool__ # Py2/3 compat