new poll mechanism
- remove secop.poller and basic poller - regular polls for 'important' parameters done by method doPoll - all other parameters are polled slower (slowInterval) and with lower priority (only one at a time when main poll is due) - nopoll decorator for read_* to disable poll - enablePoll attribute (default True) for disabling polling a module - fast polls may be implemented by means of a statemachine - configurable slow poll interval + allow a Parameter to override a Property (parameter Readable.pollinterval overrides Module.pollinterval) Change-Id: Ib1b3453041a233678b7c4b4add22ac399670e447 Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/27832 Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de> Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch> Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
This commit is contained in:
@@ -33,8 +33,9 @@ from secop.lib.enum import Enum
|
||||
from secop.modules import Attached, Communicator, \
|
||||
Done, Drivable, Module, Readable, Writable
|
||||
from secop.params import Command, Parameter
|
||||
from secop.poller import AUTO, DYNAMIC, REGULAR, SLOW
|
||||
from secop.properties import Property
|
||||
from secop.proxy import Proxy, SecNode, proxy_class
|
||||
from secop.io import HasIO, StringIO, BytesIO, HasIodev # TODO: remove HasIodev (legacy stuff)
|
||||
from secop.persistent import PersistentMixin, PersistentParam
|
||||
from secop.rwhandler import ReadHandler, WriteHandler, CommonReadHandler, \
|
||||
CommonWriteHandler, nopoll
|
||||
|
||||
@@ -35,7 +35,6 @@ from secop.errors import CommunicationFailedError, CommunicationSilentError, \
|
||||
ConfigError, ProgrammingError
|
||||
from secop.modules import Attached, Command, \
|
||||
Communicator, Done, Module, Parameter, Property
|
||||
from secop.poller import REGULAR
|
||||
from secop.lib import generalConfig
|
||||
|
||||
|
||||
@@ -109,7 +108,7 @@ class IOBase(Communicator):
|
||||
uri = Property('hostname:portnumber', datatype=StringType())
|
||||
timeout = Parameter('timeout', datatype=FloatRange(0), default=2)
|
||||
wait_before = Parameter('wait time before sending', datatype=FloatRange(), default=0)
|
||||
is_connected = Parameter('connection state', datatype=BoolType(), readonly=False, default=False, poll=REGULAR)
|
||||
is_connected = Parameter('connection state', datatype=BoolType(), readonly=False, default=False)
|
||||
pollinterval = Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10)
|
||||
|
||||
_reconnectCallbacks = None
|
||||
@@ -133,6 +132,9 @@ class IOBase(Communicator):
|
||||
self._conn = None
|
||||
self.is_connected = False
|
||||
|
||||
def doPoll(self):
|
||||
self.read_is_connected()
|
||||
|
||||
def read_is_connected(self):
|
||||
"""try to reconnect, when not connected
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ class SequencerMixin:
|
||||
self._seq_error = str(e)
|
||||
finally:
|
||||
self._seq_thread = None
|
||||
self.pollParams(0)
|
||||
self.doPoll()
|
||||
|
||||
def _seq_thread_inner(self, seq, store_init):
|
||||
store = Namespace()
|
||||
|
||||
179
secop/modules.py
179
secop/modules.py
@@ -23,8 +23,8 @@
|
||||
"""Define base classes for real Modules implemented in the server"""
|
||||
|
||||
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
from collections import OrderedDict
|
||||
from functools import wraps
|
||||
|
||||
@@ -35,7 +35,6 @@ from secop.errors import BadValueError, ConfigError, \
|
||||
from secop.lib import formatException, mkthread, UniqueObject, generalConfig
|
||||
from secop.lib.enum import Enum
|
||||
from secop.params import Accessible, Command, Parameter
|
||||
from secop.poller import BasicPoller, Poller
|
||||
from secop.properties import HasProperties, Property
|
||||
from secop.logging import RemoteLogHandler, HasComlog
|
||||
|
||||
@@ -90,15 +89,18 @@ class HasAccessibles(HasProperties):
|
||||
else:
|
||||
aobj.merge(merged_properties[aname])
|
||||
accessibles[aname] = aobj
|
||||
|
||||
# rebuild order: (1) inherited items, (2) items from paramOrder, (3) new accessibles
|
||||
# move (2) to the end
|
||||
for aname in list(cls.__dict__.get('paramOrder', ())):
|
||||
paramOrder = cls.__dict__.get('paramOrder', ())
|
||||
for aname in paramOrder:
|
||||
if aname in accessibles:
|
||||
accessibles.move_to_end(aname)
|
||||
# ignore unknown names
|
||||
# move (3) to the end
|
||||
for aname in new_names:
|
||||
accessibles.move_to_end(aname)
|
||||
if aname not in paramOrder:
|
||||
accessibles.move_to_end(aname)
|
||||
# note: for python < 3.6 the order of inherited items is not ensured between
|
||||
# declarations within the same class
|
||||
cls.accessibles = accessibles
|
||||
@@ -147,7 +149,7 @@ class HasAccessibles(HasProperties):
|
||||
setattr(self, pname, value) # important! trigger the setter
|
||||
return value
|
||||
|
||||
new_rfunc.poll = getattr(rfunc, 'poll', True) and pobj.poll
|
||||
new_rfunc.poll = getattr(rfunc, 'poll', True)
|
||||
else:
|
||||
|
||||
def new_rfunc(self, pname=pname):
|
||||
@@ -268,6 +270,9 @@ class Module(HasAccessibles):
|
||||
extname='implementation')
|
||||
interface_classes = Property('offical highest interface-class of the module', ArrayOf(StringType()),
|
||||
extname='interface_classes')
|
||||
pollinterval = Property('poll interval for parameters handled by doPoll', FloatRange(0.1, 120), default=5)
|
||||
slowinterval = Property('poll interval for other parameters', FloatRange(0.1, 120), default=15)
|
||||
enablePoll = True
|
||||
|
||||
# properties, parameters and commands are auto-merged upon subclassing
|
||||
parameters = {}
|
||||
@@ -275,7 +280,6 @@ class Module(HasAccessibles):
|
||||
|
||||
# reference to the dispatcher (used for sending async updates)
|
||||
DISPATCHER = None
|
||||
pollerClass = Poller #: default poller used
|
||||
|
||||
def __init__(self, name, logger, cfgdict, srv):
|
||||
# remember the dispatcher object (for the async callbacks)
|
||||
@@ -289,6 +293,7 @@ class Module(HasAccessibles):
|
||||
self.initModuleDone = False
|
||||
self.startModuleDone = False
|
||||
self.remoteLogHandler = None
|
||||
self.nextPollEvent = threading.Event()
|
||||
errors = []
|
||||
|
||||
# handle module properties
|
||||
@@ -333,13 +338,6 @@ class Module(HasAccessibles):
|
||||
for aname, aobj in self.accessibles.items():
|
||||
# make a copy of the Parameter/Command object
|
||||
aobj = aobj.copy()
|
||||
if isinstance(aobj, Parameter):
|
||||
# fix default properties poll and needscfg
|
||||
if aobj.poll is None:
|
||||
aobj.poll = bool(aobj.handler)
|
||||
if aobj.needscfg is None:
|
||||
aobj.needscfg = not aobj.poll
|
||||
|
||||
if not self.export: # do not export parameters of a module not exported
|
||||
aobj.export = False
|
||||
if aobj.export:
|
||||
@@ -578,16 +576,25 @@ class Module(HasAccessibles):
|
||||
registers it in the server for waiting
|
||||
<timeout> defaults to 30 seconds
|
||||
"""
|
||||
if self.writeDict:
|
||||
mkthread(self.writeInitParams, start_events.get_trigger())
|
||||
if self.enablePoll or self.writeDict:
|
||||
# enablePoll == False: start poll thread for writing values from writeDict only
|
||||
mkthread(self.__pollThread, start_events.get_trigger())
|
||||
self.startModuleDone = True
|
||||
|
||||
def pollOneParam(self, pname):
|
||||
"""poll parameter <pname> with proper error handling"""
|
||||
def doPoll(self):
|
||||
"""polls important parameters like value and status
|
||||
|
||||
all other parameters are polled automatically
|
||||
"""
|
||||
|
||||
def triggerPollEvent(self, *args): # args needed for valueCallback
|
||||
"""interrupts waiting between polls"""
|
||||
self.nextPollEvent.set() # trigger poll loop
|
||||
|
||||
def callPollFunc(self, rfunc):
|
||||
"""call read method with proper error handling"""
|
||||
try:
|
||||
rfunc = getattr(self, 'read_' + pname)
|
||||
if rfunc.poll: # TODO: handle this in poller
|
||||
rfunc()
|
||||
rfunc()
|
||||
except SilentError:
|
||||
pass
|
||||
except SECoPError as e:
|
||||
@@ -595,6 +602,63 @@ class Module(HasAccessibles):
|
||||
except Exception:
|
||||
self.log.error(formatException())
|
||||
|
||||
def __pollThread(self, started_callback):
|
||||
self.writeInitParams()
|
||||
if not self.enablePoll:
|
||||
return
|
||||
polled_parameters = []
|
||||
# collect and call all read functions a first time
|
||||
for pname, pobj in self.parameters.items():
|
||||
rfunc = getattr(self, 'read_' + pname)
|
||||
if rfunc.poll:
|
||||
polled_parameters.append((rfunc, pobj))
|
||||
self.callPollFunc(rfunc)
|
||||
started_callback()
|
||||
last_slow = last_main = 0
|
||||
last_error = None
|
||||
error_count = 0
|
||||
to_poll = ()
|
||||
while True:
|
||||
now = time.time()
|
||||
wait_main = last_main + self.pollinterval - now
|
||||
wait_slow = last_slow + self.slowinterval - now
|
||||
wait_time = min(wait_main, wait_slow)
|
||||
if wait_time > 0:
|
||||
self.nextPollEvent.wait(wait_time)
|
||||
self.nextPollEvent.clear()
|
||||
# remark: if there would be a need to trigger polling all parameters,
|
||||
# we might replace nextPollEvent by a Queue and act depending on the
|
||||
# queued item
|
||||
continue
|
||||
# call doPoll, if due
|
||||
if wait_main <= 0:
|
||||
last_main = (now // self.pollinterval) * self.pollinterval
|
||||
try:
|
||||
self.doPoll()
|
||||
if last_error and error_count > 1:
|
||||
self.log.info('recovered after %d calls to doPoll (%r)', error_count, last_error)
|
||||
last_error = None
|
||||
except Exception as e:
|
||||
if type(e) != last_error:
|
||||
error_count = 0
|
||||
self.log.error('error in doPoll: %r', e)
|
||||
error_count += 1
|
||||
last_error = e
|
||||
now = time.time()
|
||||
# find ONE due slow poll and call it
|
||||
loop = True
|
||||
while loop: # loops max. 2 times, when to_poll is at end
|
||||
for rfunc, pobj in to_poll:
|
||||
if now > pobj.timestamp + self.slowinterval * 0.5:
|
||||
self.callPollFunc(rfunc)
|
||||
loop = False
|
||||
break
|
||||
else:
|
||||
if now < last_slow + self.slowinterval:
|
||||
break
|
||||
last_slow = (now // self.slowinterval) * self.slowinterval
|
||||
to_poll = iter(polled_parameters)
|
||||
|
||||
def writeInitParams(self, started_callback=None):
|
||||
"""write values for parameters with configured values
|
||||
|
||||
@@ -640,55 +704,20 @@ class Readable(Module):
|
||||
UNKNOWN=401,
|
||||
) #: status codes
|
||||
|
||||
value = Parameter('current value of the module', FloatRange(), poll=True)
|
||||
value = Parameter('current value of the module', FloatRange())
|
||||
status = Parameter('current status of the module', TupleOf(EnumType(Status), StringType()),
|
||||
default=(Status.IDLE, ''), poll=True)
|
||||
pollinterval = Parameter('sleeptime between polls', FloatRange(0.1, 120),
|
||||
default=5, readonly=False)
|
||||
default=(Status.IDLE, ''))
|
||||
pollinterval = Parameter('default poll interval', FloatRange(0.1, 120),
|
||||
default=5, readonly=False, export=True)
|
||||
|
||||
def startModule(self, start_events):
|
||||
"""start basic polling thread"""
|
||||
if self.pollerClass and issubclass(self.pollerClass, BasicPoller):
|
||||
# use basic poller for legacy code
|
||||
mkthread(self.__pollThread, start_events.get_trigger(timeout=30))
|
||||
else:
|
||||
super().startModule(start_events)
|
||||
def earlyInit(self):
|
||||
super().earlyInit()
|
||||
# in case pollinterval is reduced a lot, we do not want to wait
|
||||
self.valueCallbacks['pollinterval'].append(self.triggerPollEvent)
|
||||
|
||||
def __pollThread(self, started_callback):
|
||||
while True:
|
||||
try:
|
||||
self.__pollThread_inner(started_callback)
|
||||
except Exception as e:
|
||||
self.log.exception(e)
|
||||
self.status = (self.Status.ERROR, 'polling thread could not start')
|
||||
started_callback()
|
||||
print(formatException(0, sys.exc_info(), verbose=True))
|
||||
time.sleep(10)
|
||||
|
||||
def __pollThread_inner(self, started_callback):
|
||||
"""super simple and super stupid per-module polling thread"""
|
||||
self.writeInitParams()
|
||||
i = 0
|
||||
fastpoll = self.pollParams(i)
|
||||
started_callback()
|
||||
while True:
|
||||
i += 1
|
||||
try:
|
||||
time.sleep(self.pollinterval * (0.1 if fastpoll else 1))
|
||||
except TypeError:
|
||||
time.sleep(min(self.pollinterval)
|
||||
if fastpoll else max(self.pollinterval))
|
||||
fastpoll = self.pollParams(i)
|
||||
|
||||
def pollParams(self, nr=0):
|
||||
# Just poll all parameters regularly where polling is enabled
|
||||
for pname, pobj in self.parameters.items():
|
||||
if not pobj.poll:
|
||||
continue
|
||||
if nr % abs(int(pobj.poll)) == 0:
|
||||
# pollParams every 'pobj.pollParams' iteration
|
||||
self.pollOneParam(pname)
|
||||
return False
|
||||
def doPoll(self):
|
||||
self.read_value()
|
||||
self.read_status()
|
||||
|
||||
|
||||
class Writable(Readable):
|
||||
@@ -739,24 +768,6 @@ class Drivable(Writable):
|
||||
"""
|
||||
return 300 <= (status or self.status)[0] < 390
|
||||
|
||||
# improved polling: may poll faster if module is BUSY
|
||||
def pollParams(self, nr=0):
|
||||
# poll status first
|
||||
self.read_status()
|
||||
fastpoll = self.isBusy()
|
||||
for pname, pobj in self.parameters.items():
|
||||
if not pobj.poll:
|
||||
continue
|
||||
if pname == 'status':
|
||||
# status was already polled above
|
||||
continue
|
||||
if ((int(pobj.poll) < 0) and fastpoll) or (
|
||||
nr % abs(int(pobj.poll))) == 0:
|
||||
# poll always if pobj.poll is negative and fastpoll (i.e. Module is busy)
|
||||
# otherwise poll every 'pobj.poll' iteration
|
||||
self.pollOneParam(pname)
|
||||
return fastpoll
|
||||
|
||||
@Command(None, result=None)
|
||||
def stop(self):
|
||||
"""cease driving, go to IDLE state"""
|
||||
|
||||
@@ -26,10 +26,11 @@
|
||||
import inspect
|
||||
|
||||
from secop.datatypes import BoolType, CommandType, DataType, \
|
||||
DataTypeType, EnumType, IntRange, NoneOr, OrType, \
|
||||
DataTypeType, EnumType, NoneOr, OrType, \
|
||||
StringType, StructOf, TextType, TupleOf, ValueType
|
||||
from secop.errors import BadValueError, ProgrammingError
|
||||
from secop.properties import HasProperties, Property
|
||||
from secop.lib import generalConfig
|
||||
|
||||
|
||||
class Accessible(HasProperties):
|
||||
@@ -132,24 +133,9 @@ class Parameter(Accessible):
|
||||
* True: exported, name automatic.
|
||||
* a string: exported with custom name''', OrType(BoolType(), StringType()),
|
||||
export=False, default=True)
|
||||
poll = Property(
|
||||
'''[internal] polling indicator
|
||||
|
||||
may be:
|
||||
|
||||
* None (omitted): will be converted to True/False if handler is/is not None
|
||||
* False or 0 (never poll this parameter)
|
||||
* True or 1 (AUTO), converted to SLOW (readonly=False)
|
||||
DYNAMIC (*status* and *value*) or REGULAR (else)
|
||||
* 2 (SLOW), polled with lower priority and a multiple of pollinterval
|
||||
* 3 (REGULAR), polled with pollperiod
|
||||
* 4 (DYNAMIC), if BUSY, with a fraction of pollinterval,
|
||||
else polled with pollperiod
|
||||
''', NoneOr(IntRange()),
|
||||
export=False, default=None)
|
||||
needscfg = Property(
|
||||
'[internal] needs value in config', NoneOr(BoolType()),
|
||||
export=False, default=None)
|
||||
export=False, default=False)
|
||||
optional = Property(
|
||||
'[internal] is this parameter optional?', BoolType(),
|
||||
export=False, settable=False, default=False)
|
||||
@@ -169,6 +155,8 @@ class Parameter(Accessible):
|
||||
|
||||
def __init__(self, description=None, datatype=None, inherit=True, **kwds):
|
||||
super().__init__()
|
||||
if 'poll' in kwds and generalConfig.tolerate_poll_property:
|
||||
kwds.pop('poll')
|
||||
if datatype is None:
|
||||
# collect datatype properties. these are not applied, as we have no datatype
|
||||
self.ownProperties = {k: kwds.pop(k) for k in list(kwds) if k not in self.propertyDict}
|
||||
|
||||
278
secop/poller.py
278
secop/poller.py
@@ -1,278 +0,0 @@
|
||||
# -*- 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 'io' with the same value will share the same poller
|
||||
"""
|
||||
|
||||
import time
|
||||
from heapq import heapify, heapreplace
|
||||
from threading import Event
|
||||
|
||||
from secop.errors import ProgrammingError
|
||||
from secop.lib import mkthread
|
||||
|
||||
# poll types:
|
||||
AUTO = 1 #: equivalent to True, converted to REGULAR, SLOW or DYNAMIC
|
||||
SLOW = 2 #: polling with low priority and increased poll interval (used by default when readonly=False)
|
||||
REGULAR = 3 #: polling with standard interval (used by default for read only parameters except status and value)
|
||||
DYNAMIC = 4 #: polling with shorter poll interval when BUSY (used by default for status and value)
|
||||
|
||||
|
||||
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.io.name or module.name, if io is not present
|
||||
"""
|
||||
# for modules with the same io, a common poller is used,
|
||||
# modules without io all get their own poller
|
||||
name = getattr(module, 'io', module).name
|
||||
poller = table.get((cls, name), None)
|
||||
if poller is None:
|
||||
poller = cls(name)
|
||||
table[(cls, 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)
|
||||
|
||||
|
||||
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._event = Event()
|
||||
self._stopped = False
|
||||
self.maxwait = 3600
|
||||
self.name = name
|
||||
self.modules = [] # used for writeInitParams only
|
||||
|
||||
def add_to_poller(self, module):
|
||||
self.modules.append(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
|
||||
handlers = set()
|
||||
# at the beginning, queues are simple lists
|
||||
# later, they will be converted to heaps
|
||||
for pname, pobj in module.parameters.items():
|
||||
polltype = pobj.poll
|
||||
if not polltype:
|
||||
continue
|
||||
if not hasattr(module, 'pollinterval'):
|
||||
raise ProgrammingError("module %s must have a pollinterval"
|
||||
% module.name)
|
||||
if pname == 'is_connected':
|
||||
if hasattr(module, 'registerReconnectCallback'):
|
||||
module.registerReconnectCallback(self.name, self.trigger_all)
|
||||
else:
|
||||
module.log.warning("%r has 'is_connected' but no 'registerReconnectCallback'" % module)
|
||||
if polltype == AUTO: # covers also pobj.poll == True
|
||||
if pname in ('value', 'status'):
|
||||
polltype = DYNAMIC
|
||||
elif pobj.readonly:
|
||||
polltype = REGULAR
|
||||
else:
|
||||
polltype = SLOW
|
||||
if polltype not in factors:
|
||||
raise ProgrammingError("unknown poll type %r for parameter '%s'"
|
||||
% (polltype, pname))
|
||||
if pobj.handler:
|
||||
if pobj.handler in handlers:
|
||||
continue # only one poller per handler
|
||||
handlers.add(pobj.handler)
|
||||
# placeholders 0 are used for due, lastdue and idx
|
||||
self.queues[polltype].append(
|
||||
(0, 0, (0, module, pobj, pname, 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, pname, 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
|
||||
if due == 0:
|
||||
due = now # do not look at timestamp after trigger_all
|
||||
else:
|
||||
due = max(lastdue + interval, pobj.timestamp + interval * 0.5)
|
||||
if now >= due:
|
||||
module.pollOneParam(pname)
|
||||
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 trigger_all(self):
|
||||
for _, queue in sorted(self.queues.items()):
|
||||
for idx, (_, lastdue, pollitem) in enumerate(queue):
|
||||
queue[idx] = (0, lastdue, pollitem)
|
||||
self._event.set()
|
||||
return True
|
||||
|
||||
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.startModule.
|
||||
|
||||
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 time.sleep(float('inf')) might be called below
|
||||
started_callback()
|
||||
return
|
||||
# if writeInitParams is not yet done, we do it here
|
||||
for module in self.modules:
|
||||
module.writeInitParams()
|
||||
# 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, pname, factor)) in enumerate(queue):
|
||||
lastdue = time.time()
|
||||
module.pollOneParam(pname)
|
||||
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, pname, factor))
|
||||
heapify(queue)
|
||||
started_callback() # signal end of startup
|
||||
nregular = len(self.queues[REGULAR])
|
||||
while not self._stopped:
|
||||
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._event.wait(delay)
|
||||
self._event.clear()
|
||||
|
||||
def stop(self):
|
||||
self._event.set()
|
||||
self._stopped = True
|
||||
|
||||
def __bool__(self):
|
||||
"""is there any poll item?"""
|
||||
return any(self.queues.values())
|
||||
|
||||
|
||||
class BasicPoller(PollerBase):
|
||||
"""basic poller
|
||||
|
||||
this is just a dummy, the poller thread is started in Readable.startModule
|
||||
"""
|
||||
# pylint: disable=abstract-method
|
||||
|
||||
@classmethod
|
||||
def add_to_table(cls, table, module):
|
||||
pass
|
||||
@@ -138,17 +138,18 @@ class HasProperties(HasDescriptors):
|
||||
# treat overriding properties with bare values
|
||||
for pn, po in properties.items():
|
||||
value = getattr(cls, pn, po)
|
||||
if not isinstance(value, Property): # attribute is a bare value
|
||||
if not isinstance(value, (Property, HasProperties)): # attribute may be a bare value
|
||||
# HasProperties is a base class of Parameter -> allow a Parameter to override a Property ()
|
||||
po = po.copy()
|
||||
try:
|
||||
# try to apply bare value to Property
|
||||
po.value = po.datatype(value)
|
||||
except BadValueError:
|
||||
if pn in properties:
|
||||
if callable(value):
|
||||
raise ProgrammingError('method %s.%s collides with property of %s' %
|
||||
(cls.__name__, pn, base.__name__)) from None
|
||||
raise ProgrammingError('can not set property %s.%s to %r' %
|
||||
(cls.__name__, pn, value)) from None
|
||||
if callable(value):
|
||||
raise ProgrammingError('method %s.%s collides with property of %s' %
|
||||
(cls.__name__, pn, base.__name__)) from None
|
||||
raise ProgrammingError('can not set property %s.%s to %r' %
|
||||
(cls.__name__, pn, value)) from None
|
||||
cls.propertyDict[pn] = po
|
||||
|
||||
def checkProperties(self):
|
||||
|
||||
@@ -35,9 +35,9 @@ from secop.io import HasIO
|
||||
class ProxyModule(HasIO, Module):
|
||||
module = Property('remote module name', datatype=StringType(), default='')
|
||||
|
||||
pollerClass = None
|
||||
_consistency_check_done = False
|
||||
_secnode = None
|
||||
enablePoll = False
|
||||
|
||||
def ioClass(self, name, logger, opts, srv):
|
||||
opts['description'] = 'secnode %s on %s' % (opts.get('module', name), opts['uri'])
|
||||
@@ -123,7 +123,8 @@ class ProxyModule(HasIO, Module):
|
||||
self.announceUpdate('status', newstatus)
|
||||
|
||||
def checkProperties(self):
|
||||
pass # skip
|
||||
pass # skip
|
||||
|
||||
|
||||
class ProxyReadable(ProxyModule, Readable):
|
||||
pass
|
||||
@@ -184,7 +185,7 @@ def proxy_class(remote_class, name=None):
|
||||
|
||||
for aname, aobj in rcls.accessibles.items():
|
||||
if isinstance(aobj, Parameter):
|
||||
pobj = aobj.merge(dict(poll=False, handler=None, needscfg=False))
|
||||
pobj = aobj.merge(dict(handler=None, needscfg=False))
|
||||
attrs[aname] = pobj
|
||||
|
||||
def rfunc(self, pname=aname):
|
||||
|
||||
@@ -264,7 +264,6 @@ class Server:
|
||||
failure_traceback = traceback.format_exc()
|
||||
errors.append('error creating %s' % modname)
|
||||
|
||||
poll_table = dict()
|
||||
missing_super = set()
|
||||
# all objs created, now start them up and interconnect
|
||||
for modname, modobj in self.modules.items():
|
||||
@@ -276,12 +275,6 @@ class Server:
|
||||
missing_super.add('%s was not called, probably missing super call'
|
||||
% modobj.earlyInit.__qualname__)
|
||||
|
||||
# handle polling
|
||||
for modname, modobj in self.modules.items():
|
||||
if modobj.pollerClass is not None:
|
||||
# a module might be explicitly excluded from polling by setting pollerClass to None
|
||||
modobj.pollerClass.add_to_table(poll_table, modobj)
|
||||
|
||||
# call init on each module after registering all
|
||||
for modname, modobj in self.modules.items():
|
||||
try:
|
||||
@@ -317,17 +310,13 @@ class Server:
|
||||
sys.stderr.write(failure_traceback)
|
||||
sys.exit(1)
|
||||
|
||||
for (_, pollname) , poller in poll_table.items():
|
||||
start_events.name = 'poller %s' % pollname
|
||||
# poller.start must return either a timeout value or None (default 30 sec)
|
||||
poller.start(start_events.get_trigger())
|
||||
self.log.info('waiting for modules and pollers being started')
|
||||
self.log.info('waiting for modules being started')
|
||||
start_events.name = None
|
||||
if not start_events.wait():
|
||||
# some timeout happened
|
||||
for name in start_events.waiting_for():
|
||||
self.log.warning('timeout when starting %s' % name)
|
||||
self.log.info('all modules and pollers started')
|
||||
self.log.info('all modules started')
|
||||
history_path = os.environ.get('FRAPPY_HISTORY')
|
||||
if history_path:
|
||||
from secop_psi.historywriter import FrappyHistoryWriter # pylint: disable=import-outside-toplevel
|
||||
|
||||
@@ -27,13 +27,10 @@ from time import sleep
|
||||
|
||||
from secop.datatypes import FloatRange
|
||||
from secop.lib import mkthread
|
||||
from secop.modules import BasicPoller, Drivable, \
|
||||
Module, Parameter, Readable, Writable, Command
|
||||
from secop.modules import Drivable, Module, Parameter, Readable, Writable, Command
|
||||
|
||||
|
||||
class SimBase:
|
||||
pollerClass = BasicPoller
|
||||
|
||||
def __new__(cls, devname, logger, cfgdict, dispatcher):
|
||||
extra_params = cfgdict.pop('extra_params', '') or cfgdict.pop('.extra_params', '')
|
||||
attrs = {}
|
||||
@@ -120,7 +117,7 @@ class SimDrivable(SimReadable, Drivable):
|
||||
self._value = self.target
|
||||
speed *= self.interval
|
||||
try:
|
||||
self.pollParams(0)
|
||||
self.doPoll()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -133,7 +130,7 @@ class SimDrivable(SimReadable, Drivable):
|
||||
self._value = self.target
|
||||
sleep(self.interval)
|
||||
try:
|
||||
self.pollParams(0)
|
||||
self.doPoll()
|
||||
except Exception:
|
||||
pass
|
||||
self.status = self.Status.IDLE, ''
|
||||
|
||||
Reference in New Issue
Block a user