Skip to content

Refactor to introduce a sans-I/O layer? #466

Closed

Description

@aaugustin

https://sans-io.readthedocs.io/how-to-sans-io.html

Bonus: write curio- or trio-compatible versions of the main APIs?

Activity

  1. added this to the milestone on Sep 1, 2018
  2. aaugustin commented on Sep 2, 2018

    @aaugustin
    MemberAuthor

    Backpressure may be a bit tricky to handle.

    Sans-I/O requires an implementation that is "defined entirely in terms of synchronous functions returning synchronous results, and that does not block or wait for any form of I/O."

    Probably the best way to tell the I/O layer to stop the flow of input bytes without infringing these requirements is to send stop / resume events in the output stream.

    Conversely, there should be a way to let the I/O layer tell the protocol to stop the flow of output bytes.

  3. aaugustin commented on Sep 2, 2018

    @aaugustin
    MemberAuthor

    Websockets needs a way to reassemble incoming data chunks and read lines (for parsing the handshake) or N bytes (for parsing frames). asyncio.StreamReader and ohneio implement this part.

  4. aaugustin commented on Sep 2, 2018

    @aaugustin
    MemberAuthor

    hyper-h2 simply attempts to parse HTTP2 frames, giving up if there isn't enough data and retrying when more data is pushed. This results in repeated work: the header is re-parsed every time.

  5. aaugustin commented on Sep 2, 2018

    @aaugustin
    MemberAuthor

    Another concern is that WebSockets specifies some TCP-specific behavior, mostly related to half-closing and closing connections. This behavior cannot be encoded into the abstraction of a sans-I/O protocol.

  6. changed the title [-]Refactor to introduce a sans-I/O layer[/-] [+]Refactor to introduce a sans-I/O layer?[/+] on Sep 2, 2018
  7. aaugustin commented on Sep 2, 2018

    @aaugustin
    MemberAuthor

    The websockets.framing module is already almost sans-I/O compliant: dropping the @asyncio.coroutine decorator (which mostly serves as documentation) and injecting reader = ohneio.read and writer = ohneio.write should do the job.

    The websockets.http module doesn't support injecting reader but that would be easy to change.

    Then, with some surgery, about half of websockets.protocol could be extracted in the same way.

    We'd end up with the protocol logic split across a sans-I/O part and a with-I/O part, which may or may not be easier to maintain. I don't remember bugs in the would-be sans-I/O part, all the complexity is in the with-I/O part...

  8. pgjones commented on Sep 23, 2018

    @pgjones

    There is a wsproto library that is already sans-I/O that could be helpful here.

  9. aaugustin commented on Sep 23, 2018

    @aaugustin
    MemberAuthor

    Yes, I'm aware of wsproto.

    It can be an inspiration for designing the API. Similar APIs will make it easier for third party integrators to support either websockets or wsproto (like uvicorn does).

    I considered depending on the third-party implementation in wsproto and decided it wouldn't be a good trade-off.

    websockets predates wsproto by more than three years. I'd rather continue building on the code base that I know very well and that received lots of production use than discover a new set of behaviors.

    My problem isn't about writing a Sans-I/O implementation. I almost have one, as explained in the previous comment. It's more about plugging that implementation in the high-level APIs and determining whether this is an improvement — including in terms of performance.

    Currently websockets doesn't have any dependencies. That's a good thing. It gives me complete control over behavior and performance. For example, websockets is more than 30 times faster than wsaccel, which wsproto uses, for masking 10kB of data:

    >>> import timeit
    >>> timeit.timeit(
    ...     """apply_mask(data, mask)""",
    ...     """from websockets.speedups import apply_mask; data = b'abcdefghij' * 1024; mask = b'xyzt'; """,
    ... )
    0.46926557800000523
    >>> timeit.timeit(
    ...     """masker.process(data)""",
    ...     """from wsaccel.xormask import createXorMasker; data = b'abcdefghij' * 1024; masker = createXorMasker(b'xyzt'); """,
    ... )
    15.354961824000014
    >>> 15.354961824000014 / 0.46926557800000523
    32.721261784089016

    EDIT: to be completely fair, wsaccel is faster for messages shorter than about 50 bytes. But xormask speed is negligible for such small messages compared to everything else Python does for each message, like round-tripping to the event loop.

  10. lgrahl commented on Sep 23, 2018

    @lgrahl
    Collaborator

    Do I understand correctly that this is primarily about being able to use a protocol without tightly binding it to some specific source or sink (e.g. file descriptors) and secondarily about the issue with red/blue functions? (Haven't watched the talk yet but it's now on my to do list.)

  11. aaugustin commented on Sep 23, 2018

    @aaugustin
    MemberAuthor

    Yes, these are the main design principles. https://sans-io.readthedocs.io/ is a reasonably short read.

  12. removed this from the milestone on Jun 22, 2019
  13. aaugustin commented on Jul 7, 2019

    @aaugustin
    MemberAuthor

    The low-level API I'd like to expose should look like this: https://aioquic.readthedocs.io/en/latest/http3.html /Cc @jlaine

  14. jlaine commented on Jul 7, 2019

    @jlaine

    If you want a more mature example I recommend looking at h11 and h2.

    h2 is interesting in the way it handles backpressure: the API consumer "acknowledges" data.

    Ping @sethmlarson ;)

  15. aaugustin commented on Jul 7, 2019

    @aaugustin
    MemberAuthor

    I'll take a look, thanks!

  16. aaugustin commented on Jul 19, 2020

    @aaugustin
    MemberAuthor

    I wrote about the implementation strategy here: https://fractalideas.com/blog/sans-io-when-rubber-meets-road/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions