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

[Yamux] Allow max ACK backlog of 256 streams #340

Merged
merged 10 commits into from
Oct 26, 2023

Conversation

StefanBratanov
Copy link
Collaborator

@StefanBratanov StefanBratanov commented Oct 24, 2023

https://github.com/libp2p/specs/blob/master/yamux/README.md#ack-backlog--backpressure

Implements SHOULD at most allow an ACK backlog of 256 streams.

  • Added new parameter ackBacklogLimit when initializing Yamux
  • Introduced notion of acknowledged when initializing yamux stream
  • throw and don't create new stream if more than 256 streams are not acknowledged

@mxinden
Copy link
Member

mxinden commented Oct 24, 2023

//CC @thomaseizinger who pushed a lot for this to land in rust-libp2p and specs.

@Nashatyrev
Copy link
Collaborator

Just to make sure I'm getting the Yamux spec correctly:

There could be two kinds of unacknowledged streams:

  • outbound: local peer opens a stream, sends SYN, but hasn't received ACK yet
  • inbound: in our current implementation we are sending back ACK immediately upon receiving a SYN from a remote party. In that case there is no way to have unacknowledged inbound streams. However the spec states the following:
    MAY delay acknowledging new streams until the application has received or is about to send the first DATA frame.
    If this statement is implemented at some point then a remotely opened stream may also be unacknowledged. I.e. until the first inbound data frame is received or a local app starts sending a data.

So the questions:

  1. should we account both types as a sum and treat them equally?
  2. In the case of exceeding unacknowledged streams threshold what should we do when ... opening a new stream locally? Should we throw MuxerWriteException? (i think yes) Should we also abort the connection? (probably no)
  3. In the case of exceeding unacknowledged streams threshold what should we do when ... receiving SYN from remote? Abort the stream (by sending back RST)? Abort the connection?

Copy link
Collaborator

@Nashatyrev Nashatyrev left a comment

Choose a reason for hiding this comment

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

Some questions remain

import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.max
import kotlin.properties.Delegates

const val ACK_BACKLOG_ALLOWANCE = 256
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would parametrize it for the YamuxHandler making this constant default value

createYamuxStreamHandler(child.id).onLocalOpen()
if (totalUnacknowledgedStreams() < ACK_BACKLOG_ALLOWANCE) {
createYamuxStreamHandler(child.id).onLocalOpen()
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we throw a specific WriteMuxerException here?

@mxinden
Copy link
Member

mxinden commented Oct 25, 2023

should we account both types as a sum and treat them equally?

They should be accounted separately. A should not ACK inbound streams from B, in case A's application layer has not yet read or written to a stream. B should not open a new stream to A in case 256 outbound streams to A have not yet been ACKed. Same vice versa for streams from A to B.

@mxinden
Copy link
Member

mxinden commented Oct 25, 2023

In the case of exceeding unacknowledged streams threshold what should we do when ... opening a new stream locally? Should we throw MuxerWriteException? (i think yes) Should we also abort the connection? (probably no)

Do you have any way to backpressure the application? In Rust and I am assuming in Go as well, we suspend the green thread.

@mxinden
Copy link
Member

mxinden commented Oct 25, 2023

In the case of exceeding unacknowledged streams threshold what should we do when ... receiving SYN from remote? Abort the stream (by sending back RST)? Abort the connection?

In rust-yamux today it is best effort only, enforced at the outbound end for now.

https://github.com/libp2p/rust-yamux/blob/93b70626f7f3d83f97951d9cd5c2951500088f0d/yamux/src/connection.rs#L435-L439

In case you want to enforce it at the receiving side, I would consider resetting the stream exceeding the limit.

@Nashatyrev
Copy link
Collaborator

Do you have any way to backpressure the application?

Not with the present API

In Rust and I am assuming in Go as well, we suspend the green thread.

Yeah, that's a good solution.

In case you want to enforce it at the receiving side, I would consider resetting the stream exceeding the limit.

If both peers has the same ACK_BACKLOG_ALLOWANCE then this situation should be treated as the protocol violation. Am I getting it right?
In that case even connection abort would be appropriate

@mxinden
Copy link
Member

mxinden commented Oct 25, 2023

In case you want to enforce it at the receiving side, I would consider resetting the stream exceeding the limit.

If both peers has the same ACK_BACKLOG_ALLOWANCE then this situation should be treated as the protocol violation. Am I getting it right?

Correct. Though I am not aware of a single libp2p based network out there today that supports the yamux ack backlog feature across all nodes. Thus treating it as best effort everywhere.

@StefanBratanov StefanBratanov marked this pull request as ready for review October 25, 2023 14:30
@@ -240,8 +245,11 @@ open class YamuxHandler(
streamHandlers.remove(child.id)?.dispose()
}

private fun calculateTotalBufferedWrites(): Int {
return streamHandlers.values.sumOf { it.sendBuffer.readableBytes() }
override fun checkCanOpenNewStream() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why not just throw from onLocalOpen() ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So essentially in newStream method in AbstractMuxHandler, if onLocalOpen fails then outboundInitializer is never called and createStream in MuxHandler controller future is never complete. And I am thinking also in order to avoid calling createChild with he new id.

Copy link
Collaborator

Choose a reason for hiding this comment

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

if onLocalOpen fails then outboundInitializer is never called and createStream in MuxHandler controller future is never complete.

Right that probably should be fixed. register() doesn't rethrow exceptions but rather wraps them into ChannelFuture, so the best way is to call sync() to rethrow. register() call shouldn't block so the sync() should be synchronous:

Index: libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/AbstractMuxHandler.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/AbstractMuxHandler.kt b/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/AbstractMuxHandler.kt
--- a/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/AbstractMuxHandler.kt	(revision 1927bcae6804fc5b24662a40221e217b95298944)
+++ b/libp2p/src/main/kotlin/io/libp2p/etc/util/netty/mux/AbstractMuxHandler.kt	(date 1698247944849)
@@ -138,7 +138,7 @@
     ): MuxChannel<TData> {
         val child = MuxChannel(this, id, initializer, initiator)
         streamMap[id] = child
-        ctx!!.channel().eventLoop().register(child)
+        ctx!!.channel().eventLoop().register(child).sync()
         return child
     }

And I am thinking also in order to avoid calling createChild with he new id.

Doesn't look like a big issue: the stream seems to immediately disposed in that case and removed from the stream map

Copy link
Collaborator

Choose a reason for hiding this comment

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

With the fix above throwing from onLocalOpen() worked for me

Copy link
Collaborator Author

@StefanBratanov StefanBratanov Oct 25, 2023

Choose a reason for hiding this comment

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

Nice. I like it. Done.

@Nashatyrev
Copy link
Collaborator

It also looks like we need to treat initiator/responder acknowledgments separately as per comment from @mxinden

@StefanBratanov are you planning to address this here yet or later?
Else the PR looks good to me.

@StefanBratanov
Copy link
Collaborator Author

It also looks like we need to treat initiator/responder acknowledgments separately as per comment from @mxinden

@StefanBratanov are you planning to address this here yet or later? Else the PR looks good to me.

Working on it now. Think it makes sense to be in this PR as well.

@StefanBratanov
Copy link
Collaborator Author

It also looks like we need to treat initiator/responder acknowledgments separately as per comment from @mxinden

@StefanBratanov are you planning to address this here yet or later? Else the PR looks good to me.

Working on it now. Think it makes sense to be in this PR as well.

Done.

Copy link
Collaborator

@Nashatyrev Nashatyrev left a comment

Choose a reason for hiding this comment

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

LGTM, just some nits 👍

@StefanBratanov StefanBratanov enabled auto-merge (squash) October 26, 2023 12:23
@StefanBratanov StefanBratanov merged commit e363c49 into libp2p:develop Oct 26, 2023
@StefanBratanov StefanBratanov deleted the max_ack_backlog branch November 19, 2023 10:20
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