Skip to content

Add trio implementation #1628

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

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

Add trio implementation #1628

wants to merge 9 commits into from

Conversation

aaugustin
Copy link
Member

No description provided.

@agronholm
Copy link

So, while working on my own version, I started wondering if the Assembler class was actually something that should be left as an implementation detail rather than part of the API? I didn't really understand why it was a separate thing from the connection classes.

Another big change I had to make was the introduction of pytest as a test dependency, as testing with plain unittest would require a custom harness which I feel is pointless when AnyIO's pytest plugin contains all the necessary machinery to support async tests.

@aaugustin aaugustin marked this pull request as draft May 22, 2025 17:06
@aaugustin
Copy link
Member Author

@agronholm The Assembler is an implementation detail: it isn't documented in the API. It could live in the connection module. It happens to be an independent piece with medium complexity, which justifies giving it its own module. That keeps the connection module simpler. This is a personal preference in how I organize the code with no user-visible consequences.

@aaugustin
Copy link
Member Author

aaugustin commented May 22, 2025

websockets got its own async test harness before there was one in the stdlib. The legacy implementation still uses it. I switched to unittest.IsolatedAsyncioTestCase in the new asyncio implementation.

I chose to stick to vanilla choices everywhere. websockets has no required dependencies, even for development. I am clearly biased by:

  • my background in Django, which didn't have any dependencies for an extremely long time;
  • my desire to avoid debugging dependencies :-) I got my share of debugging with asyncio ;-)

@agronholm
Copy link

OK, so is the introduction of pytest as a test dependency going to be a problem? I mean, I have plenty of experience in the best practices front so I can do all the heavy lifting in that regard (packaging, CI etc.).

@agronholm
Copy link

my background in Django, which didn't have any dependencies for an extremely long time;

Incidentally, this is the primary reason why I steer clear of Django.

@aaugustin
Copy link
Member Author

Mastery of pytest isn't the problem. I used it in a professional context before. I know how to configure it. Actually, I spent a lot more time deep in the guts of the fixtures system than I ever wanted...

It's simply not suitable considering my goals and constraints for this project. Keeping complexity and the amount of third-party code that I may have to understand or debug to the bare minimum ranks very high on the list.

You're welcome to use it as a test runner though.

@agronholm
Copy link

Sure, it's your project, your rules. But I've just been thinking how much of the maintenance burden you could shed by relying on external projects rather than inventing everything yourself. Just my two cents.

@aaugustin aaugustin force-pushed the trio branch 4 times, most recently from 62e304c to 09b9947 Compare May 22, 2025 20:37
@agronholm
Copy link

So are you going to continue with your work of adding a Trio-specific back-end? The pushes seem to indicate as much. Would you not rather let me finish my work on the AnyIO back-end?

@aaugustin aaugustin force-pushed the trio branch 2 times, most recently from 1051bfc to 30fdd95 Compare May 22, 2025 21:14
@aaugustin
Copy link
Member Author

Short, directional answer because it's 11:30pm here:

  • I don't see myself reviewing a PR adding an entire implementation. There's a lot of details to get right; I find it quite hard to get them right myself; checking that someone else got them right feels impossible to me. We didn't talk before you started this work and I don't have a plan to integrate it. Of course you can publish websockets-anyio or fork.
  • Since the asyncio backend is already there and there's no good reason to remove it, at this point, it's more straightforward to add just trio than to add anyio + trio. I don't see a benefit in adding anyio and I see a downside - more moving parts going foward.

@agronholm
Copy link

Since the asyncio backend is already there and there's no good reason to remove it, at this point, it's more straightforward to add just trio than to add anyio + trio. I don't see a benefit in adding anyio and I see a downside - more moving parts going foward.

I don't understand. Why even suggest adding anyio AND trio back-ends when the former caters for both Trio and asyncio users? The biggest reason would be fewer parts to maintain, not more!

@aaugustin
Copy link
Member Author

Indeed, if I finish the trio implementation within a reasonable timeframe, there's little point in an anyio implementation.

As all things open-source, completion is highly dependent on everything else in my life; it isn't done until it's done :-)


I see your frustration that I'm not taking advantage of anyio when you built it exactly for people like me. It's fine to disagree. However, I tried to understand where our views diverge to make sure I'm not missing something. Going straight to the point:

1/ I don't have a significant cost of maintenance for code that I control. I invest upfront in getting it right. The cost of maintenance is driven by changes in code outside of my control, whether intentional or not. asyncio is outside of my control and has been painful in this regard. trio and anyio are outside of my control; they may be less painful as asyncio but still it's more code outside of my control. (Of course anyio is under your control so you have the opposite perspective here.)

2/ This is a hobby project for me. I spend time writing code in websockets because I like coding and I don't get the opportunity to write meaningful code in my current job. Some people do crosswords; I do open-source. It's time well spent, even if others have written similar code before. I wrote an unconventional HTTP parser; it does exactly what I want and never caused problems; I found that interesting. Conversely, I get less joy of creation from diagnosing what changed in someone else's code and suddenly caused websockets to misbehave or from reviewing someone else's contribution so I'm not looking to spend more time on these activities.

@agronholm
Copy link

Indeed, if I finish the trio implementation within a reasonable timeframe, there's little point in an anyio implementation.

As all things open-source, completion is highly dependent on everything else in my life; it isn't done until it's done :-)

I see your frustration that I'm not taking advantage of anyio when you built it exactly for people like me. It's fine to disagree. However, I tried to understand where our views diverge to make sure I'm not missing something. Going straight to the point:

1/ I don't have a significant cost of maintenance for code that I control. I invest upfront in getting it right. The cost of maintenance is driven by changes in code outside of my control, whether intentional or not. asyncio is outside of my control and has been painful in this regard. trio and anyio are outside of my control; they may be less painful as asyncio but still it's more code outside of my control. (Of course anyio is under your control so you have the opposite perspective here.)

Funnily enough, you're making my point for me. If you proceed with your current plan of adding a Trio backend, you will end up with two async back-ends, either one of which may break when the upstream makes changes. But if there was only the AnyIO back-end, then websockets would "just work" on both asyncio and Trio (and any other event loop it will support in the future; there have been talks of that), and you'd only have to worry about breakages occurring in AnyIO (which are quite rare these days), as it will shield you against most upstream changes.

@aaugustin
Copy link
Member Author

aaugustin commented May 23, 2025

Funnily enough, you're making my point for me.

This sounds like you're talking down to me. Please, let's have a constructive conversation or leave it there.

I'm clear on where I disagree with you. I'm trying — and, for now, failing — to land this conversation on a respectful agreement to disagree.

You're saying that using anyio will result in a lower maintenance cost. I don't buy it. I don't think there's a clear cut winner between:

  1. Depending directly on asyncio and trio
  • allows providing native APIs for both -> better UX IMO
  • vulnerable to changes in asyncio and trio but debugging involves only one layer (debugging usually means reading the source)
  • when appropriate, I can push back with "it's not me; it's asyncio / it's trio" as the user is actively choosing asyncio or trio
  1. Depending directly on anyio and indirectly on asyncio and trio
  • allows providing a unified API for both -> some might prefer this
  • users are exposed structured concurrency (when many are just learning asyncio) -> leads to questions in the issue tracker
  • vulnerable to changes in anyio and asyncio and trio, although anyio might shield some of them, but debugging involves two layers

(This isn't a full decision matrix; that's not my point; my point is that you have pros and cons on both sides.)

It's similar building a Swift and Kotlin apps or a React Native app: both options are valid, depending on your preferences and constraints.


Based on how this conversation went, it doesn't look like there's a path for smooth collaboration. Therefore, I won't consider a PR adding an anyio backend to websockets.

@agronholm
Copy link

agronholm commented May 23, 2025

Alright, thank you for the elaborate response. At least I tried. And sorry if I sounded like I was talking down to you – it was never my intention.

@aaugustin
Copy link
Member Author

aaugustin commented May 23, 2025

No worries. Feels like we fell into a typical trap of open-source collaboration :-(

If you have a WIP version of your anyio implementation somewhere, I'm genuinely interested in reading it because it will help me get a better grasp of anyio and structured concurrency (which I have no experience with).

@agronholm
Copy link

If you have a WIP version of your anyio implementation somewhere, I'm genuinely interested in reading it because it will help me get a better grasp of anyio and structured concurrency (which I have no experience with).

Here it is: https://github.com/agronholm/websockets/tree/anyio

@aaugustin aaugustin force-pushed the trio branch 6 times, most recently from c224f28 to 1c346bd Compare May 26, 2025 20:01
@aaugustin aaugustin force-pushed the trio branch 2 times, most recently from 635cb5d to 9888470 Compare July 22, 2025 07:36
@aaugustin
Copy link
Member Author

Client is now feature-complete with 100% test coverage, although there's a bit of proof-reading and cleanup left. Moving on to server.

@aaugustin
Copy link
Member Author

aaugustin commented Aug 6, 2025

I am debating whether the method to close i/ connections and ii/ servers should be called close or aclose. The choice should be made according to the Principle of Least Astonishment.

Consistency with Trio

Trio usually provides a close method and an aclose coroutine for closing basic objects such as SocketStream. (Actually, closing a socket requires I/O, but that's handled asynchronously by the kernel so we don't see it in Python.) It provides only aclose complex objects that cannot always be closed synchronously such as SSLStream.

Trio doesn't provide an abstraction for servers. Servers are just lists of listeners accepting requests. You close them by cancelling listeners. There's no prior art here. However, we can reasonably assume that a server class would implement the AsyncResource interface, meaning it has an aclose method.

Relying on cancellation to close a server is impractical for us because that would cancel every coroutine, including the coroutine pumping data from the network to the Sans-I/O stack, which must continue running until the TCP connection is closed to achieve a graceful shutdown. Implementing a clean shutdown in a cancellation context would require an extensive amount of shielding and create too much risk to introduce bugs where cancellation wouldn't work anymore. That's why we need an (a)close method for servers.

Consistency with WebSocket

The argument that close is the method that sends a CLOSE frame is very weak because close does more than just that and because I/O layers provide a single send method for sending all three kinds of data frames, showing there's no 1-1 mapping of method names to WebSocket frame types.

In the asyncio implementation, Connection.close is a coroutine while Server.close is a function (and there's a Server.wait_closed coroutine). I don't see any meaningful conclusion from this situation.

Conclusion

Given our focus on providing I/O layers that feel native, using aclose() in Trio seems best.

We should implement the semantics of AsyncResource i.e. "failure is failure to achieve grace, not failure to close".

UPDATE

Connection.aclose accepts code and reason arguments, deviating from the standard AsyncResource API, but those arguments have default values so that's probably OK.

@aaugustin aaugustin force-pushed the trio branch 8 times, most recently from 4ca3810 to 56fde96 Compare August 8, 2025 06:11
@aaugustin aaugustin marked this pull request as ready for review August 8, 2025 06:11
@aaugustin aaugustin force-pushed the trio branch 3 times, most recently from 3341352 to 6f7bf08 Compare August 9, 2025 20:26
@aaugustin
Copy link
Member Author

Green build. Woohoo. Still a few fixes + full proof-reading necessary.

Indeed, trio has no equivalent of asyncio.wait.
Also uniformize code & tests with other implementations.
Uniformize other implementations.
Also uniformize code & tests with other implementations.
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