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

MAINT: add main thread check for ui dispatch, solve no ui failure #1740

Merged
merged 18 commits into from
Apr 24, 2023

Conversation

homosapien-lcy
Copy link
Contributor

@homosapien-lcy homosapien-lcy commented Apr 17, 2023

Previously the ui_dispatch(handler, *args, **kw) function in traits/traits/trait_notifiers.py only checks current_thread().ident with ui_thread. But supposedly, this current thread id should be compared with the main thread (threading.main_thread()). This discrepancy caused a no ui failure in ui dispatch as mentioned in issue #1732. This PR attempts to resolve this failure. Closes #1732

Checklist

  • Tests
  • Update API reference (docs/source/traits_api_reference) (seems no need to change for this PR)
  • Update User manual (docs/source/traits_user_manual) (seems no need to change for this PR)
  • Update type annotation hints in stub files (seems no need to change for this PR)

@mdickinson
Copy link
Member

@homosapien-lcy Thank you for the PR. Please:

  • add a PR description
  • uncheck the 'tests' checkbox in the checklist
  • add tests

We should also check that the documentation makes the behaviour clear.

Copy link
Member

@mdickinson mdickinson left a comment

Choose a reason for hiding this comment

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

Like any behaviour-changing PR, this should have tests.

@@ -44,7 +44,7 @@ def set_ui_handler(handler):


def ui_dispatch(handler, *args, **kw):
if threading.current_thread().ident == ui_thread:
if threading.current_thread().ident == threading.main_thread().ident:
Copy link
Member

Choose a reason for hiding this comment

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

No need for the .ident here: you can just compare the thread objects directly.

Suggested change
if threading.current_thread().ident == threading.main_thread().ident:
if threading.current_thread() == threading.main_thread():

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the suggestion. Are directly comparing threads vs comparing their ids completely equivalent?

Copy link
Member

Choose a reason for hiding this comment

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

I think so, yes. The == check is essentially an is check (there's no __eq__ special method created), and there should be exactly one Thread object per OS thread.

@homosapien-lcy
Copy link
Contributor Author

Like any behaviour-changing PR, this should have tests.

So, for the test, I'm thinking of checking whether the handler triggered in the main thread is a "handler" and not in the main thread is a "ui_handler". Does that sound reasonable to you? And any suggested test that I can look at for reference?

@mdickinson
Copy link
Member

For the test, I suggest using something like Corran's example from the linked issue. The issue is about the behaviour of ui_dispatch, and we should focus on testing that behaviour rather than testing implementation details. Ideally, your test should fail before this change and pass afterwards.

@mdickinson
Copy link
Member

I've unchecked the "Update User manual" entry in the checklist: we should check whether the current documentation needs to be updated.

In particular, the documentation should make it clear what the behaviour of ui dispatch is when there's no event loop running.

@mdickinson
Copy link
Member

@homosapien-lcy I'd suggest merging main into this branch so that the CI can run properly. You should be able to do that by clicking the "Update branch" button in the PR status block.

@homosapien-lcy
Copy link
Contributor Author

homosapien-lcy commented Apr 19, 2023

I found that the error raised by the ui_dispatch is happened not in the main thread, thus unable to be caught in the main thread, even if it says typeError, the unittest will still past...:

(py311) (base) cyliu@aus552cyliu traits % python3.11 -m unittest traits/tests/test_trait_notifiers.py
Exception occurred in traits notification handler for event object: TraitChangeEvent(object=<traits.tests.test_trait_notifiers.TestTraitNotifiers.test_ui_dispatch.<locals>.DispatchTest object at 0x10929e840>, name='test_param', old=0, new=1)
Traceback (most recent call last):
  File "/Users/cyliu/Documents/3.11_test/traits/traits/observation/_trait_event_notifier.py", line 122, in __call__
    self.dispatcher(handler, event)
  File "/Users/cyliu/Documents/3.11_test/traits/traits/trait_notifiers.py", line 50, in ui_dispatch
    ui_handler(handler, *args, **kw)
TypeError: 'NoneType' object is not callable
.
----------------------------------------------------------------------
Ran 1 test in 0.004s

OK

So, what I also did is add a try except in ui_dispatch to throw a systemExit that will also notify the main thread.

@homosapien-lcy
Copy link
Contributor Author

I've unchecked the "Update User manual" entry in the checklist: we should check whether the current documentation needs to be updated.

In particular, the documentation should make it clear what the behaviour of ui dispatch is when there's no event loop running.

Both test and documentation are added

@homosapien-lcy
Copy link
Contributor Author

Some tests failed (for instance, Tests/tests (ubuntu-latest, 3.8)) due to unable to download certain packages:

1m 19s
Run sudo apt-get update
Get:[1](https://github.com/enthought/traits/actions/runs/4740125152/jobs/8415614428?pr=1740#step:3:1) https://packages.microsoft.com/ubuntu/22.04/prod jammy InRelease [3611 B]
Get:2 https://packages.microsoft.com/ubuntu/22.04/prod jammy/main amd64 Packages [60.2 kB]
Hit:3 https://ppa.launchpadcontent.net/ubuntu-toolchain-r/test/ubuntu jammy InRelease
Ign:4 http://azure.archive.ubuntu.com/ubuntu jammy InRelease
Ign:5 http://azure.archive.ubuntu.com/ubuntu jammy-updates InRelease
Ign:6 http://azure.archive.ubuntu.com/ubuntu jammy-backports InRelease
Ign:7 http://azure.archive.ubuntu.com/ubuntu jammy-security InRelease
Ign:4 http://azure.archive.ubuntu.com/ubuntu jammy InRelease
Ign:5 http://azure.archive.ubuntu.com/ubuntu jammy-updates InRelease
Ign:6 http://azure.archive.ubuntu.com/ubuntu jammy-backports InRelease
Ign:7 http://azure.archive.ubuntu.com/ubuntu jammy-security InRelease
Ign:4 http://azure.archive.ubuntu.com/ubuntu jammy InRelease
Ign:5 http://azure.archive.ubuntu.com/ubuntu jammy-updates InRelease
Ign:6 http://azure.archive.ubuntu.com/ubuntu jammy-backports InRelease
Ign:7 http://azure.archive.ubuntu.com/ubuntu jammy-security InRelease
Err:4 http://azure.archive.ubuntu.com/ubuntu jammy InRelease
  Could not connect to azure.archive.ubuntu.com:80 (52.147.219.192), connection timed out
Err:5 http://azure.archive.ubuntu.com/ubuntu jammy-updates InRelease
  Unable to connect to azure.archive.ubuntu.com:http:
Err:6 http://azure.archive.ubuntu.com/ubuntu jammy-backports InRelease
  Unable to connect to azure.archive.ubuntu.com:http:
Err:7 http://azure.archive.ubuntu.com/ubuntu jammy-security InRelease
  Unable to connect to azure.archive.ubuntu.com:http:
Fetched 63.8 kB in 37s ([17](https://github.com/enthought/traits/actions/runs/4740125152/jobs/8415614428?pr=1740#step:3:18)09 B/s)
Reading package lists...
W: Failed to fetch http://azure.archive.ubuntu.com/ubuntu/dists/jammy/InRelease  Could not connect to azure.archive.ubuntu.com:80 (52.147.2[19](https://github.com/enthought/traits/actions/runs/4740125152/jobs/8415614428?pr=1740#step:3:20).192), connection timed out
W: Failed to fetch http://azure.archive.ubuntu.com/ubuntu/dists/jammy-updates/InRelease  Unable to connect to azure.archive.ubuntu.com:http:
W: Failed to fetch http://azure.archive.ubuntu.com/ubuntu/dists/jammy-backports/InRelease  Unable to connect to azure.archive.ubuntu.com:http:
W: Failed to fetch http://azure.archive.ubuntu.com/ubuntu/dists/jammy-security/InRelease  Unable to connect to azure.archive.ubuntu.com:http:
W: Some index files failed to download. They have been ignored, or old ones used instead.
Reading package lists...
Building dependency tree...
Reading state information...
The following additional packages will be installed:
  libegl-mesa0
The following NEW packages will be installed:
  libegl-mesa0 libegl1
0 upgraded, 2 newly installed, 0 to remove and [20](https://github.com/enthought/traits/actions/runs/4740125152/jobs/8415614428?pr=1740#step:3:21) not upgraded.
Need to get 140 kB of archives.
After this operation, 465 kB of additional disk space will be used.
Ign:1 http://azure.archive.ubuntu.com/ubuntu jammy-updates/main amd64 libegl-mesa0 amd64 22.2.5-0ubuntu0.1~22.04.1
Ign:2 http://azure.archive.ubuntu.com/ubuntu jammy/main amd64 libegl1 amd64 1.4.0-1
Ign:1 http://azure.archive.ubuntu.com/ubuntu jammy-updates/main amd64 libegl-mesa0 amd64 22.2.5-0ubuntu0.1~22.04.1
Ign:2 http://azure.archive.ubuntu.com/ubuntu jammy/main amd64 libegl1 amd64 1.4.0-1
Ign:1 http://azure.archive.ubuntu.com/ubuntu jammy-updates/main amd64 libegl-mesa0 amd64 22.2.5-0ubuntu0.1~22.04.1
Ign:2 http://azure.archive.ubuntu.com/ubuntu jammy/main amd64 libegl1 amd64 1.4.0-1
Err:1 http://azure.archive.ubuntu.com/ubuntu jammy-updates/main amd64 libegl-mesa0 amd64 22.2.5-0ubuntu0.1~22.04.1
  Could not connect to azure.archive.ubuntu.com:80 (52.147.[21](https://github.com/enthought/traits/actions/runs/4740125152/jobs/8415614428?pr=1740#step:3:22)9.192), connection timed out
Err:2 http://azure.archive.ubuntu.com/ubuntu jammy/main amd64 libegl1 amd64 1.4.0-1
  Unable to connect to azure.archive.ubuntu.com:http:
E: Failed to fetch http://azure.archive.ubuntu.com/ubuntu/pool/main/m/mesa/libegl-mesa0_[22](https://github.com/enthought/traits/actions/runs/4740125152/jobs/8415614428?pr=1740#step:3:23).2.5-0ubuntu0.1%7e22.04.1_amd64.deb  Could not connect to azure.archive.ubuntu.com:80 (52.147.219.192), connection timed out
E: Failed to fetch http://azure.archive.ubuntu.com/ubuntu/pool/main/libg/libglvnd/libegl1_1.4.0-1_amd64.deb  Unable to connect to azure.archive.ubuntu.com:http:
E: Unable to fetch some archives, maybe run apt-get update or try with --fix-missing?
Error: Process completed with exit code 100.

@mdickinson
Copy link
Member

@homosapien-lcy Thanks for the updates!

For the test: this looks like a great start - the setup looks like exactly what we need. For the test itself, the behaviour we want to test is that the observer is executed when the trait value is changed. You should be able to change the observer to something that has an observable side-effect, and then test that side-effect to verify that the observer was called. (For example, set up an empty events list and use events.append as the observer.)

For the business logic: please could you explain the reason for the try / except and the raise SystemExit?

I haven't reviewed the documentation yet.

@mdickinson
Copy link
Member

Some tests failed [...]

This looks like an upstream problem that we've seen a few times before. I've restarted the test run.

@mdickinson
Copy link
Member

Some tests failed [...]

Looks like it was indeed an intermittent upstream problem - the re-run seems fine.

@mdickinson
Copy link
Member

For the business logic: please could you explain the reason for the try / except and the raise SystemExit?

Apologies - I see you already explained this in an earlier comment. Thank you.

It's important to think through the consequences of any change to the business logic. A question for you: with this change, if I have a Traits-using application and some part of that application happens to cause an exception to be raised in a handler, what then happens to that application? What would have happened before this change?

@homosapien-lcy
Copy link
Contributor Author

For the business logic: please could you explain the reason for the try / except and the raise SystemExit?

Apologies - I see you already explained this in an earlier comment. Thank you.

It's important to think through the consequences of any change to the business logic. A question for you: with this change, if I have a Traits-using application and some part of that application happens to cause an exception to be raised in a handler, what then happens to that application? What would have happened before this change?

Thanks for the comments, I found a work around by adding an event list in the main thread and has the test handler add item to the event list, when the side thread failed, the list will be empty and cause an exception in the main thread.

# Then
try:
t.test_param = 1
except Exception:
Copy link
Member

Choose a reason for hiding this comment

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

Let's remove this try/except - it's just extra boilerplate and we don't really need it. If an exception is raised, the test will already fail, and there's no particular reason to expect an exception to be raised either before or after the change in this PR.

Copy link
Member

Choose a reason for hiding this comment

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

I'd also move the t.test_param = 1 to the "When" block - it's the main part of the behaviour we're testing. The "Then" block is for testing the effects of that behaviour.

Copy link
Member

@mdickinson mdickinson Apr 20, 2023

Choose a reason for hiding this comment

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

It's often helpful to think about expanding out the "When/Then" to full sentence fragments: E.g., "When we change the value of a trait with a dispatch='ui' observer, then that observer is run as expected.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! thats a great advice

self.fail("test_ui_dispatch raised an Exception unexpectedly!")

# also check the observer is called (test_handler function)
self.assertTrue(event_list)
Copy link
Member

Choose a reason for hiding this comment

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

I'd suggest a slightly stronger check, that the event list has length one. It would be even better to extract the event from the event list and check that the "new" has the expected value of 1.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Stronger tests are added

Copy link
Member

Choose a reason for hiding this comment

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

Thanks. Please could you check that the event list has length one, too? e.g.,, replace self.assertTrue(event_list) with self.assertEqual(len(event_list), 1)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the suggestion, just updated

@mdickinson
Copy link
Member

when the side thread failed

Just for clarity, there is no side thread here. Everything is happening in the main thread.

@mdickinson
Copy link
Member

mdickinson commented Apr 20, 2023

Just for clarity, there is no side thread here. Everything is happening in the main thread.

Actually, this is a good point. It would be useful to have tests that check that we get the expected result when dispatch='ui' is used from a background thread, too. That's independent of solving this particular issue, though - we can leave it for this PR and do it in a separate PR.

@homosapien-lcy
Copy link
Contributor Author

I found that the error raised by the ui_dispatch is happened not in the main thread, thus unable to be caught in the main thread, even if it says typeError, the unittest will still past...:

(py311) (base) cyliu@aus552cyliu traits % python3.11 -m unittest traits/tests/test_trait_notifiers.py
Exception occurred in traits notification handler for event object: TraitChangeEvent(object=<traits.tests.test_trait_notifiers.TestTraitNotifiers.test_ui_dispatch.<locals>.DispatchTest object at 0x10929e840>, name='test_param', old=0, new=1)
Traceback (most recent call last):
  File "/Users/cyliu/Documents/3.11_test/traits/traits/observation/_trait_event_notifier.py", line 122, in __call__
    self.dispatcher(handler, event)
  File "/Users/cyliu/Documents/3.11_test/traits/traits/trait_notifiers.py", line 50, in ui_dispatch
    ui_handler(handler, *args, **kw)
TypeError: 'NoneType' object is not callable
.
----------------------------------------------------------------------
Ran 1 test in 0.004s

OK

So, what I also did is add a try except in ui_dispatch to throw a systemExit that will also notify the main thread.

Thanks for the explanation, but I have a question then: if everything is happening in the main thread, why the "TypeError" here does not cause the test to fail? The original test looks like this:

class TestTraitNotifiers(unittest.TestCase):
    def test_ui_dispatch(self):
        class DispatchTest(HasTraits):
            test_param = Int()
        t = DispatchTest()

        event_list = []

        # create handler
        def test_handler(event):
            event_list.append(event)

        t.observe(test_handler, 'test_param', dispatch='ui')
        try:
            t.test_param = 1
        except:
            self.fail("Test failed")

@mdickinson
Copy link
Member

if everything is happening in the main thread, why the "TypeError" here does not cause the test to fail?

By default, the exception is caught and logged. What you're seeing here in the output is the logged message. See the code here:

event = self.event_factory(*args, **kwargs)
if self.prevent_event(event):
return
try:
self.dispatcher(handler, event)
except Exception:
handle_exception(event)

and here:

def _log_exception(self, event):
""" A handler that logs the exception with the given event.
Parameters
----------
event : object
An event object emitted by the notification.
"""
_logger.exception(
"Exception occurred in traits notification handler "
"for event object: %r",
event,
)

In cases like these, a useful technique is to search for the error message in the codebase - a code search for "Exception occurred in traits notification handler" would have brought up the above location.

You can easily verify that everything is happening in the main thread by adding a print(threading.current_thread()) debugging call.

@homosapien-lcy
Copy link
Contributor Author

homosapien-lcy commented Apr 24, 2023

if everything is happening in the main thread, why the "TypeError" here does not cause the test to fail?

By default, the exception is caught and logged. What you're seeing here in the output is the logged message. See the code here:

event = self.event_factory(*args, **kwargs)
if self.prevent_event(event):
return
try:
self.dispatcher(handler, event)
except Exception:
handle_exception(event)

and here:

def _log_exception(self, event):
""" A handler that logs the exception with the given event.
Parameters
----------
event : object
An event object emitted by the notification.
"""
_logger.exception(
"Exception occurred in traits notification handler "
"for event object: %r",
event,
)

In cases like these, a useful technique is to search for the error message in the codebase - a code search for "Exception occurred in traits notification handler" would have brought up the above location.

You can easily verify that everything is happening in the main thread by adding a print(threading.current_thread()) debugging call.

If I under stand you correctly: The exception already happened in the main thread but it is caught thus the test is not failed?

And the suggestions in your comments are implemented

@mdickinson
Copy link
Member

If I under stand you correctly: The exception already happened in the main thread but it is caught thus the test is not failed?

Yes! But you don't need to take my word for it - you can verify this for yourself.

And your comments are addressed

Please could you address this one? https://github.com/enthought/traits/pull/1740/files#r1173395668

@mdickinson
Copy link
Member

@homosapien-lcy Please ignore my comments on the documentation. There are a number of changes needed there, but I think the easiest path is for me to make a commit to this branch with the necessary changes.

@mdickinson
Copy link
Member

I've added new documentation, and removed the documentation that was added to the migration section.

Copy link
Member

@mdickinson mdickinson left a comment

Choose a reason for hiding this comment

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

I think everything has now been addressed. I'll merge when the CI completes.

@homosapien-lcy Thank you for the fix.

@mdickinson mdickinson merged commit 872def3 into main Apr 24, 2023
@mdickinson mdickinson deleted the fix_no_ui_failure branch April 24, 2023 08:40
mdickinson added a commit that referenced this pull request May 7, 2024
Since #1788, we have only one test module that makes use of the Qt event
loop. That test module contains tests for the behaviour of handlers that
use `dispatch='ui'` mechanism to redispatch off-thread notifications to
the ui thread.

This PR reworks that test module, with some significant collateral
damage along the way.

In detail:

- reworks that test module (`test_ui_notifiers`) to avoid the need for
the Qt event loop; instead, it tests against a `ui_handler` based on
asyncio, which redispatches to the running asyncio event loop
- adds a `get_ui_handler` counterpart to `set_ui_handler`, and exposes
both functions in `traits.api`
- adds type hints for `get_ui_handler` and `set_ui_handler`
- removes two public module globals from `trait_notifiers`: `ui_handler`
has been made private, while `ui_thread` is removed altogether
- fixes a bug where ui dispatch didn't do the right thing (PR #1740 was
incomplete; this bug should have been caught at review time on that PR)
- makes another couple of drive-by cleanups, removing a very old check
for `threading.local()` being a dict (which it hasn't been in living
memory), and tidying up some uses of thread identity.
This pull request was closed.
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.

Unexpected failure mode for dispatch="ui" when there is no UI
2 participants