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

fix(multistream-select): don't wait for negotiation in poll_close #4019

Merged
merged 10 commits into from
Jun 6, 2023

Conversation

mxinden
Copy link
Member

@mxinden mxinden commented Jun 2, 2023

Description

With Version::V1Lazy and negotiation of a single protocol, a stream initiator optimistically
sends application data right after proposing its protocol. More specifically an application can
write data via AsyncWrite::poll_write even though the remote has not yet confirmed the stream protocol.

This saves one round-trip.

sequenceDiagram
    A->>B: "/multistream/1.0.0"
    A->>B: "/perf/1.0.0"
    A->>B: <some-perf-protocol-data>
    B->>A: "/multistream/1.0.0"
    B->>A: "/perf/1.0.0"
    B->>A: <some-perf-protocol-data>
Loading

When considering stream closing, i.e. AsyncWrite::poll_close, and using stream closing as an
operation in ones protocol, e.g. using stream closing to signal the end of a request, this becomes tricky.

The behavior without this commit was as following:

sequenceDiagram
    A->>B: "/multistream/1.0.0"
    A->>B: "/perf/1.0.0"
    A->>B: <some-perf-protocol-data>
    Note left of A: Call `AsyncWrite::poll_close` which first waits for the<br/>optimistic multistream-select negotiation to finish, before closing the stream,<br/> i.e. setting the FIN bit.
    B->>A: "/multistream/1.0.0"
    B->>A: "/perf/1.0.0"
    Note right of B: Waiting for A to close the stream (i.e. set the `FIN` bit)<br/>before sending the response.
    A->>B: FIN
    B->>A: <some-perf-protocol-data>
Loading

The above takes 2 round trips:

  1. Send the optimistic multistream-select protocol proposals as well as the initiator protocol
    payload and waits for the confirmation of the protocols.
  2. Close the stream, i.e. sends the FIN bit and waits for the responder protocol payload.

This commit proposes that the stream initiator should not wait for the multistream-select protocol
confirmation when closing the stream, but close the stream within the first round-trip.

sequenceDiagram
    A->>B: "/multistream/1.0.0"
    A->>B: "/perf/1.0.0"
    A->>B: <some-perf-protocol-data>
    A->>B: FIN
    B->>A: "/multistream/1.0.0"
    B->>A: "/perf/1.0.0"
    B->>A: <some-perf-protocol-data>
Loading

This takes 1 round-trip.

The downside of this commit is, that the stream initiator will no longer notice a negotiation error
when closing the stream. They will only notice it when reading from the stream. E.g. say that B does
not support "/perf/1.0.0", A will only notice on AsyncRead::poll_read, not on
AsyncWrite::poll_close. This is problematic for protocols where A only sends data, but never
receives data, i.e. never calls AsyncRead::poll_read. Though one can argue that such protocol is
flawed in the first place. With a response-less protocol, as even if negotiation succceeds, A
doesn't know whether B received the protocol payload.

Notes & open questions

Confirmed against libp2p-perf.max-inden.de/ and with libp2p/test-plans#184.

With `Version::V1Lazy` and negotiation of a singlel protocol only, a stream initiator optimistically
sends application data right after proposing its protocol. More specifically an application can
write data via `AsyncWrite::poll_write` even though the remote has not yet confirmed the stream protocol.
This saves one round-trip.

``` mermaid
sequenceDiagram
    A->>B: "/multistream/1.0.0"
    A->>B: "/perf/1.0.0"
    A->>B: <some-perf-protocol-data>
    B->>A: "/multistream/1.0.0"
    B->>A: "/perf/1.0.0"
    B->>A: <some-perf-protocol-data>
```

When considering stream closing, i.e. `AsyncWrite::poll_close`, and using stream closing as an
operation in ones protocol, e.g. using stream closing to signal the end of a request, this becomes tricky.

The behavior without this commit was as following:

``` mermaid
sequenceDiagram
    A->>B: "/multistream/1.0.0"
    A->>B: "/perf/1.0.0"
    A->>B: <some-perf-protocol-data>
    Note right of A: Call `AsyncWrite::poll_close` which first waits for the<br/>optimistic multistream-select negotiation to finish, before closing the stream,<br/> i.e. setting the FIN bit.
    B->>A: "/multistream/1.0.0"
    B->>A: "/perf/1.0.0"
    Note right of B: Waiting for A to close the stream (i.e. set the `FIN` bit)<br/>before sending the response.
    A->>B: FIN
    B->>A: <some-perf-protocol-data>
```

The above takes 2 round trips:

1. Send the optimistic multistream-select protocol proposals as well as the initiator protocol
payload and waits for the confirmation of the protocols.
2. Close the stream, i.e. sends the `FIN` bit and waits for the responder protocol payload.

This commit proposes that the stream initiator should not wait for the multistream-select protocol
confirmation when closing the stream, but close the stream within the first round-trip.

``` mermaid
sequenceDiagram
    A->>B: "/multistream/1.0.0"
    A->>B: "/perf/1.0.0"
    A->>B: <some-perf-protocol-data>
    A->>B: FIN
    B->>A: "/multistream/1.0.0"
    B->>A: "/perf/1.0.0"
    B->>A: <some-perf-protocol-data>
```

This takes 1 round-trip.

The downside of this commit is, that the stream initiator will no longer notice a negotiation error
when closing the stream. They will only notice it when reading from the stream. E.g. say that B does
not support "/perf/1.0.0", A will only notice on `AsyncRead::poll_read`, not on
`AsyncWrite::poll_close`. This is problematic for protocols where A only sends data, but never
receives data, i.e. never calls `AsyncRead::poll_read`. Though one can argue that such protocol is
flawed in the first place. With a response-less protocol, as even if negotiation succceeds, A
doesn't know whether B received the protocol payload.
@thomaseizinger
Copy link
Contributor

I think this is a very interesting idea. I wonder if we should/could a log statement somehow that warns the user about this behaviour.

@mxinden
Copy link
Member Author

mxinden commented Jun 5, 2023

I think this is a very interesting idea. I wonder if we should/could a log statement somehow that warns the user about this behaviour.

Good idea. See 22668da.

@mxinden mxinden marked this pull request as ready for review June 5, 2023 06:43
@mxinden
Copy link
Member Author

mxinden commented Jun 5, 2023

Confirmed with libp2p/test-plans#184 that this saves 1 round trip when e.g. using the perf protocol. Thus moving out of draft. Ready for review.

Copy link
Contributor

@thomaseizinger thomaseizinger left a comment

Choose a reason for hiding this comment

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

Can you add a test for this? It shouldn't be too difficult. You should be able to just create a stream and write + close it without polling the other end :)

Ideally, add a timeout to it so it is actually failing and not hanging when you remove this feature.

@mxinden
Copy link
Member Author

mxinden commented Jun 5, 2023

Thank you for pushing for the test @thomaseizinger. Added. Succeeds with and fails without the patch.

@thomaseizinger
Copy link
Contributor

If you didn't want to go through the TCP listener dance, you could use futures_ringbuf for an in-memory alternative.

@mxinden
Copy link
Member Author

mxinden commented Jun 6, 2023

If you didn't want to go through the TCP listener dance, you could use futures_ringbuf for an in-memory alternative.

#4032 updates all multistream-select tests to use futures_ringbuf instead of a plain TCP connection.

@mxinden
Copy link
Member Author

mxinden commented Jun 6, 2023

@thomaseizinger latest commit updates this pull request to use futures-ringbuf as well. Mind giving the patch as a whole another review?

@mergify
Copy link
Contributor

mergify bot commented Jun 6, 2023

This pull request has merge conflicts. Could you please resolve them @mxinden? 🙏

@mxinden mxinden added the send-it label Jun 6, 2023
@mergify mergify bot merged commit 76cb76c into libp2p:master Jun 6, 2023
mxinden added a commit to libp2p/test-plans that referenced this pull request Jun 8, 2023
mxinden added a commit to libp2p/test-plans that referenced this pull request Jun 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants