-
Notifications
You must be signed in to change notification settings - Fork 47
Description
Use Case
Consider a set of Handlers, each derived from the version of p4p.server.raw.Handler discussed in PR #172, that apply to an NTScalar:
PermissionHandler- implements only aput()method which checksop.accountto allow write access to a PV. It is given a list of users allowed to change the PV.ChangeRateHandler- implements only apost()method which aborts changes to a PV if the absolute difference between the old and new value is greater than or equal to some threshold, e.g.ChangeRateHandler(10)would allow a change from 4 to 6 but abort a change from 4 to -6.ControlHandler- implements methods necessary to implement the logic in the NTScalar control fields, i.e. limits the value of a PV to a defined range.TimestampHandler- updates the timestamp of a PV when its value changes.
(These examples rely on PR #172, but I don't think the discussion needs to.)
We can imagine that we'd like to mix and match these Handlers. For example, some PVs might only need the TimestampHandler, most would need the PermissionHandler and TimestampHandler, and a few would need the full set.
Trivial Solution
We can create a trivial way to compose these Handlers, e.g.
from p4p.server.raw import Handler
class CompositeHandler(Handler):
"""Allows multiple handlers to be combined and called in a defined sequence"""
def __init__(self, handlers: list[Handler]) -> None:
self.handlers = handlers
def put(self, pv, op) -> None:
for handler in self.handlers:
handler.put(pv,op)
op.done()
def post(self, pv, value) -> None:
for handler in self.handlers:
handler.post(pv, value)
def open(self, value) -> None:
for handler in self.handlers:
handler.open(value)We can then use
example1_pv = SharedPV(nt=NTScalar("d"), handler=CompositeHandler([TimestampHandler()]))
example2_pv = SharedPV(
nt=NTScalar("d"),
handler=CompositeHandler([PermissionHandler(["alice", "bob"]), TimestampHandler()]),
)
example3_pv = SharedPV(
nt=NTScalar("d"),
handler=CompositeHandler(
[
PermissionHandler(["alice"]),
ControlHandler(-10, 15),
ChangeRateHandler(10),
TimestampHandler(),
]
),
)Problem
example2_pv and example3_pv above won't work as expected.
In both those PVs, what will happen if user "Carol" attempts to put a value to example2_pv? How does the CompositeHandler.put() method stop without calling further handlers, and how does it set a sensible error in op.done()?
In the case of example3_pv above with the ControlHandler limiting the range of allowed values to between -10 and 15 and the ChangeRateHandler limiting the absolute change to <=10, consider what will happen if the PV starts at -4 and an authorised user attempts to put it to 25. The ControlHandler will change the value to 15 but the ChangeRateHandler will abort the put. We need to ensure the value remains -4 and not 15 or 25.
Essentially the CompositeHandler needs to implement some kind of simple control flow and information needs to be passed from the component / encapsulated Handlers to the CompositeHandler.
I can see immediately see several different ways of doing this:
- Exceptions
- Return values
- Message passing systems via a common object, ZeroMQ, etc.
There are probably others! None of the ones above require changes to p4p, they can be implemented in a Handler derived class.
However, if two users choose different options their Handlers will no longer be directly compatible / easy to share.
Experience
Having made a first pass at implementing a CompositeHandler to apply all the rules necessary for NTScalars, I implemented the following scenarios using return values:
- CONTINUE - Continue rules processing
- TERMINATE - Do not process later handlers but apply timestamp and complete
- TERMINATE_WO_TIMESTAMP - Do not process further handlers; do not apply timestamp rule
- ABORT - Stop handler processing and abort put
However, in practice only CONTINUE and ABORT were used.
Discussion
Is it worth adding a default CompositeHandler to p4p? If so, should it implement any kind of control flow logic?
It, or something like it, will need to be implemented in any code which implements the logic of the Normative Types. But perhaps it is sufficiently general to be useful outside that scenario?
I think there is a case for it to be in the main library, but it's perhaps only a weak case. I am happy to do the development work based on the outcome of any discussion.