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

collect errors more reliably from websocket test client #2814

Merged
merged 17 commits into from
Dec 29, 2024

Conversation

graingert
Copy link
Member

@graingert graingert commented Dec 26, 2024

…iddleware' children

Co-authored-by: Thomas Grainger [email protected]
Co-authored-by: Nikita Gashkov [email protected]

Summary

Checklist

  • I understand that this PR may be closed in case there was no previous discussion. (This doesn't apply to typos!)
  • I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
  • I've updated the documentation accordingly.

@graingert
Copy link
Member Author

@Kludex I think this fixed it

@Kludex
Copy link
Member

Kludex commented Dec 26, 2024

What was the issue?

@graingert
Copy link
Member Author

The exception bubbling out of the _run was not being collected properly, and I think the queue EOF/shutdown thing also

@graingert graingert force-pushed the refactor-exit-stack-for-ws-testclient branch from c2049a7 to c210c27 Compare December 27, 2024 09:58
@graingert graingert changed the title refactor exit stack for test client (to be decoupled from Fix unclosed 'MemoryObjectReceiveStream') refactor exit stack for test client Dec 27, 2024
starlette/testclient.py Outdated Show resolved Hide resolved
@graingert graingert changed the title refactor exit stack for test client collect errors more reliably from websocket test client Dec 27, 2024
@graingert graingert marked this pull request as ready for review December 27, 2024 11:16
starlette/testclient.py Outdated Show resolved Hide resolved
@graingert
Copy link
Member Author

I'd recommend reviewing this in side by side view: https://github.com/encode/starlette/pull/2814/files?diff=split

tg.cancel_scope.cancel()
self.should_close.set()
finally:
self._send_queue.put(EOF) # TODO: use self._send_queue.shutdown() on 3.13+
Copy link
Member

Choose a reason for hiding this comment

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

Should we use the if sys.version_info here?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think it's better to stick to the EOF approach until someone puts up a Queue.shutdown backport, or we can use a MemoryObjectStream with portal

message = self._send_queue.get()
if message is EOF:
Copy link
Member

Choose a reason for hiding this comment

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

Is there an analogous to EOF from the standard library on 3.13?

Copy link
Member Author

Choose a reason for hiding this comment

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

It raises an exception

Copy link
Member

@Kludex Kludex left a comment

Choose a reason for hiding this comment

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

As long as this passes the tests, I'm fine with whatever refactor you think is the best here.

starlette/testclient.py Outdated Show resolved Hide resolved
@graingert graingert enabled auto-merge (squash) December 28, 2024 08:51
@graingert graingert requested a review from Kludex December 28, 2024 08:51
Comment on lines +266 to +268
except anyio.get_cancelled_exc_class():
cancelled = True
raise
Copy link
Member

Choose a reason for hiding this comment

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

Why is the cancelled exception here? Is this because this except was removed?

The addition of that except was on purpose, if I recall correctly.

Copy link
Member Author

@graingert graingert Dec 28, 2024

Choose a reason for hiding this comment

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

this is instead of websocket.should_close.is_set() as it no longer exists - so we need to find out that the async function ran and was cancelled

Copy link
Member Author

@graingert graingert Dec 28, 2024

Choose a reason for hiding this comment

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

did you mean to ask about this one: https://github.com/encode/starlette/pull/2814/files#diff-aca25e5f16c4fd49338ccdf3631f72455309335fee1e27f3d3b6016fa94ecedfL145 ?

this is because it's no longer possible to get a intentional cancelled_exc here because it will be caught by the CancelScope
if you do get an cancelled_exc here it's because someone incorrectly issued a native asyncio cancel or manually raised a trio cancel, both of which should be propagated out of the websocket_connect cmgr

Copy link
Member

Choose a reason for hiding this comment

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

Got it.

So the point is: since the WebSocket doesn't call send it can't receive a disconnect exception, and since it doesn't call receive, it doesn't receive a websocket.disconnected message. Then, the TestClient here will propagate the cancellation exception here, but the user shouldn't worry about it because the TestClient will catch it anyway.

Right?

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah, we use a CancelScope passed out with task_status.started(cs), this is then used to cancel the task on completion and collect a result. The CancelScope catches the cancellation that it causes to raise in the coro, unless there's another reason for cancellation which is then propagated, this is a fatal case and so the user should be notified

Copy link
Member

Choose a reason for hiding this comment

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

So if there's a cancelled exception that is not from the TestClient, the user will see it, right?

I'm good with this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah the only way is if the user was calling .cancel() directly or manually raising constructing a trio cancel (bypassing the private constructor protections)

@graingert graingert requested a review from Kludex December 29, 2024 09:33
if isinstance(message, BaseException):
raise message
raise message # pragma: no cover (defensive, should be impossible)
Copy link
Member

Choose a reason for hiding this comment

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

Why it should be impossible?

The except BaseException as exc below doesn't have a pragma: no cover, so I assume it's being hit?

Copy link
Member Author

@graingert graingert Dec 29, 2024

Choose a reason for hiding this comment

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

this is impossible because the exit stack will raise the exception out of fut.result() and so the queue won't be consumed.

This is only possible to be hit if ws.receive() is interrupted (eg with a KI) while waiting for an exception or message to be placed on the queue.

I'm currently sketching out another slight refactor that uses MemoryObjectStreams here instead that should clean this up a bit

@graingert graingert requested a review from Kludex December 29, 2024 10:55
@graingert graingert merged commit 27b6f4c into master Dec 29, 2024
5 checks passed
@graingert graingert deleted the refactor-exit-stack-for-ws-testclient branch December 29, 2024 11:00
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