Skip to content

Conversation

Monarda
Copy link

@Monarda Monarda commented Aug 25, 2024

This PR adds three new callback functions for use by SharedPV (p4p.server.raw) handlers. They are:

  • open() - called when a SharedPV is opened
  • post() - called each time a post operation is performed on a SharedPV
  • close() - called when a SharedPV is closed.

The associated changes to the p4p.server.raw.Handler class, additional new decorators, and unit tests (src/p4p/test/test_sharedpv.py) are included. The changes to src/p4p/server/raw.py are relatively minor but the examples and unit tests make this look like a much bigger PR! There was some initial discussion of the idea at #150.

Three examples that make use of the new callback functions are also included:

  • example/auditor.py - demonstrates using open() and close() callbacks along with the existing put() callback to record when an auditing PV that records who's made changes to other PVs is or isn't available. Probably redundant compared to the more complex example/persist.py.
  • example/ntscalar_control.py - implements the Control field logic for an NTScalar. This illustrates how put(), post(), and open() lead to a natural separation of concerns. Logic that does not depend on the previous state of the PV may be implemented in the handler open(), e.g. altering the value dependent on control.limitHigh and control.limitLow. Logic that depends on the current state of the PV and the proposed changes may be implemented in the post(), e.g. applying control.minStep. The put() may then be solely concerned with authorisation.
  • example/persist.py - demonstrates using an SQLite3 database to automatically save and restore the values of PVs. Makes use of the new handler_open_ and handler_post_ prefix arguments to make configuration convenient and to transmit information from a put() to a post() respectively. Makes full use of the new open() callback to automatically restore the value and timeStamp information of a PV and post() to automatically record this information each time it changes.

I believe the example/persist.py file probably makes the strongest case for the kind of new features that these callbacks allow or make easier.

Breaking Changes

I believe these changes may be considered largely backwards compatible. They will only cause compatibility issues:

  • if an existing handler implements open(), close(), or post() functions.
  • if a PV includes top-level fields names beginning with handler_open_ or handler_post_.


# Intercept all arguments that start with 'handler_open_' and remove them from
# the arguments that go to the wrap and send them instead to the handler.open()
post_kws = {x: kws.pop(x) for x in [y for y in kws if y.startswith("handler_open_")]}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This, and the equivalent line 212 for post(), is probably one of the more important changes. It means that any arguments prefixed handler_open_ will be passed to the handler (if it exists) and not to _wrap(). An earlier version of the code added a single new argument that enabled or disabled the handler altogether. But while a smaller change it was also much crude.


@property
def onFirstConnect(self):
def on_open(self):
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a leftover from an earlier version in which I thought I'd needed to supply decorator names that wouldn't clash with the function names. It does make the names PEP8 compliant so I've left the change in for consideration.

# Aliases for decorators to maintain consistent new style
# Required because post is already used and on_post seemed the best
# alternative.
put = on_put
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compatibility for previous decorator names.

except AttributeError:
pass

_SharedPV.post(self, V)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementing the persist.py example has made me think there may be a case to split the post() callback into two. The one implemented in this PR that changes values before they reach this _SharedPV.post(self, V) and one which is called afterwards. The after_post() callback would then only be supplied the updated state of the PV and would not be triggered by unsuccessful posts.

@Monarda
Copy link
Author

Monarda commented Oct 17, 2024

It was suggested at a meeting on 9 Oct that I tag @coretl and @AlexanderWells-diamond for attention or review. Apologies if this comes as a surprise!

Copy link
Contributor

@AlexanderWells-diamond AlexanderWells-diamond left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code changes look fine to me, barring a few things that are little more than nitpicks, although as discussed offline I'm no expert on p4p's design philosophy so can't comment on whether the changes are fundamentally ok. I've tagged @mdavidsaver for further review.

I think the server.rst file needs updating to properly include the newly defined methods for SharedPV; looks like you need to define automethod for each new method.


# Because we have not set use_handler_post=False in the post this
# will automatically trigger evaluation of the post rule and thus
# the application of
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the end of this comment is missing?

class PersistHandler(Handler):
"""
A handler that will allow simple persistence of values and timestamps
across retarts. It requires a post handler in order to persist values
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
across retarts. It requires a post handler in order to persist values
across restarts. It requires a post handler in order to persist values


# Create an SQLite dayabase to function as our persistence store
conn = sqlite3.connect("persist_pvs.db", check_same_thread=False)
# conn.execute("DROP TABLE IF EXISTS pvs")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this comment is unnecessary, looks like you do the same thing in the next line?

class Audited(Handler):
"""Forward information about Put operations to the auditing PV"""

def __init__(self, pv: SharedPV):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I note that this type hint doesn't work in Python2; I don't know how important it is for all the examples to still support Python2, but the overall library still does.

def tearDown(self):
self.server.stop()
_defaultWorkQueue.sync()
#self.pv._handler._pv = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#self.pv._handler._pv = None

pass

def setUp(self):
# gc.set_debug(gc.DEBUG_LEAK)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another comment that can probably be removed

def tearDown(self):
self.server.stop()
_defaultWorkQueue.sync()
#self.pv._handler._pv = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#self.pv._handler._pv = None

Called each time an Open operation is performed on this Channel
:param value: A Value, or appropriate object (see nt= and wrap= of the constructor).
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this (and the post and close methods) call op.done(error='Not supported')), as other methods do?
At the very least for consistency this one should have a pass, to be the same as the other methods that don't call that method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants