Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactored Device wrapping #37

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

IvoVellekoop
Copy link

A completely refactored Device wrapping mechanism, that can wrap openwfs and arbitrary Python objects.

Main changes:

  • ExEngine is no longer a singleton class. Each device explictly takes an ExEngine as input to the constructor. Rationale: the singleton antipattern is problematic for testing and scalability. For example: imagine that a client uses two two packages that both use ExEngine. The first package may shut down its own ExEngine at some point. Of course, this should not shut down the other ExEngine. By dropping the singleton pattern this problem is avoided. Moreover, mocking the ExEngine is simpler and it is clear which parts of the code access the engine and which parts do not.

  • Device base class and wrapping. ExEngine provides a wrapper that makes attribute access thread safe, converts method calls to delayed execution and (not implemented yet) adds metadata, notifications, etc. To wrap an existing object, call engine.register, and make sure never to use the bare (non-wrapped) object. All internal attribute access inside the object itself is unaffected by the wrapper (so no need to monkey patch the threading library). Alternatively, an object can derive from the Device base class. This base class no longer does much, except that it overrides __new__ to call engine.register automatically. Examples are included in test_preferred_thread_annotations.py and test_generic_device.py

  • The option to shutdown queues was implemented through condition variables and complicated the code. In Python 3.13, this functionality is natively present in the Queue and PriorityQueue objects. The new code uses these objects when available, and provides a compatible implementation for Python versions < 3.13

Open issues:

  • Setting thread affinity is not implemented yet: need to rethink?
  • Getting property metadate is not implemented yet: need to rethink the original design with the get_property_limits etc.

@jacopoabramo
Copy link
Contributor

Following up on the discussion, some reference on how you could translate the current ExEngine device APIs into Protocols can be found from bluesky: https://github.com/bluesky/bluesky/blob/main/src/bluesky/protocols.py

I would personally love to switch to a structural typing approach because it's much more versatile than using abstract classes and it would greatly simplify my project implementation as well.

@henrypinkard
Copy link
Member

  • ExEngine is no longer a singleton class. Each device explictly takes an ExEngine as input to the constructor. Rationale: the singleton antipattern is problematic for testing and scalability. For example: imagine that a client uses two two packages that both use ExEngine. The first package may shut down its own ExEngine at some point. Of course, this should not shut down the other ExEngine. By dropping the singleton pattern this problem is avoided. Moreover, mocking the ExEngine is simpler and it is clear which parts of the code access the engine and which parts do not.

Makes sense

  • Device base class and wrapping. ExEngine provides a wrapper that makes attribute access thread safe, converts method calls to delayed execution and (not implemented yet) adds metadata, notifications, etc. To wrap an existing object, call engine.register, and make sure never to use the bare (non-wrapped) object. All internal attribute access inside the object itself is unaffected by the wrapper (so no need to monkey patch the threading library). Alternatively, an object can derive from the Device base class. This base class no longer does much, except that it overrides __new__ to call engine.register automatically. Examples are included in test_preferred_thread_annotations.py and test_generic_device.py

I agree this way of doing it makes much better sense, and I'd support removing the inheritance route and making wrapping the only way of using it. If we wanted to make classes that automatically come wrapped when constructed, easy enough to do it in a __new__ method

self._finished = False
self._initialized = False
# Check for method-level preferred thread name first, then class-level
self._thread_name = getattr(self.execute, '_thread_name', None) or getattr(self.__class__, '_thread_name', None)

def __lt__(self, other) -> bool:
Copy link
Member

Choose a reason for hiding this comment

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

Are you thinking to make the events in a priority queue? So you could submit an event that gets jumped ahead of existing ones? I had a mechanism for this that perhaps you saw in executor.submit, but I think this makes more sense

@@ -0,0 +1,64 @@
import queue
Copy link
Member

Choose a reason for hiding this comment

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

It might be good to give this a more descriptive filename?

@henrypinkard
Copy link
Member

Test setup is failing. I think its because you need to do a relative import here:

    File "D:\a\ExEngine\ExEngine\src\exengine\kernel\queue.py", line 6, in <module>
      from exengine.kernel.ex_event_base import ExecutorEvent
  ModuleNotFoundError: No module named 'exengine'
  [end of output]

I think you need from .kernel... or something similar to that

@henrypinkard
Copy link
Member

I made some more small comments, but looking really good so far!

One issue I've thought about is what the user facing API for a wrapped object should look like:

is any call to device.some_method() in user code going to block until it completes on an executor thread? Is there a way to have a simple imperative API that gets the benefits of asynchronous programming? Or should this necessarily specifically require creating events and submitting them to the executor

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.

3 participants