Skip to content

Discussion - Add CompositeHandler class? #175

@Monarda

Description

@Monarda

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 a put() method which checks op.account to allow write access to a PV. It is given a list of users allowed to change the PV.
  • ChangeRateHandler- implements only a post() 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions