introduce a StreamLimitReachedError for Connection.Open{Uni}Stream (#4579)

Using a concrete type is preferable to relying on interpreting the
return values from net.Error.Timeout and net.Error.Temporary, especially
since the latter has been deprecated since Go 1.18.
This commit is contained in:
Marten Seemann
2024-06-28 06:58:28 -07:00
committed by GitHub
parent 7379f1fd5e
commit b52c33939d
5 changed files with 35 additions and 23 deletions

View File

@@ -150,7 +150,7 @@ type SendStream interface {
// * TransportError: for errors triggered by the QUIC transport (in many cases a misbehaving peer) // * TransportError: for errors triggered by the QUIC transport (in many cases a misbehaving peer)
// * IdleTimeoutError: when the peer goes away unexpectedly (this is a net.Error timeout error) // * IdleTimeoutError: when the peer goes away unexpectedly (this is a net.Error timeout error)
// * HandshakeTimeoutError: when the cryptographic handshake takes too long (this is a net.Error timeout error) // * HandshakeTimeoutError: when the cryptographic handshake takes too long (this is a net.Error timeout error)
// * StatelessResetError: when we receive a stateless reset (this is a net.Error temporary error) // * StatelessResetError: when we receive a stateless reset
// * VersionNegotiationError: returned by the client, when there's no version overlap between the peers // * VersionNegotiationError: returned by the client, when there's no version overlap between the peers
type Connection interface { type Connection interface {
// AcceptStream returns the next stream opened by the peer, blocking until one is available. // AcceptStream returns the next stream opened by the peer, blocking until one is available.
@@ -163,28 +163,29 @@ type Connection interface {
AcceptUniStream(context.Context) (ReceiveStream, error) AcceptUniStream(context.Context) (ReceiveStream, error)
// OpenStream opens a new bidirectional QUIC stream. // OpenStream opens a new bidirectional QUIC stream.
// There is no signaling to the peer about new streams: // There is no signaling to the peer about new streams:
// The peer can only accept the stream after data has been sent on the stream. // The peer can only accept the stream after data has been sent on the stream,
// If the error is non-nil, it satisfies the net.Error interface. // or the stream has been reset or closed.
// When reaching the peer's stream limit, err.Temporary() will be true. // When reaching the peer's stream limit, it is not possible to open a new stream until the
// If the connection was closed due to a timeout, Timeout() will be true. // peer raises the stream limit. In that case, a StreamLimitReachedError is returned.
OpenStream() (Stream, error) OpenStream() (Stream, error)
// OpenStreamSync opens a new bidirectional QUIC stream. // OpenStreamSync opens a new bidirectional QUIC stream.
// It blocks until a new stream can be opened. // It blocks until a new stream can be opened.
// There is no signaling to the peer about new streams: // There is no signaling to the peer about new streams:
// The peer can only accept the stream after data has been sent on the stream, // The peer can only accept the stream after data has been sent on the stream,
// or the stream has been reset or closed. // or the stream has been reset or closed.
// If the error is non-nil, it satisfies the net.Error interface.
// If the connection was closed due to a timeout, Timeout() will be true.
OpenStreamSync(context.Context) (Stream, error) OpenStreamSync(context.Context) (Stream, error)
// OpenUniStream opens a new outgoing unidirectional QUIC stream. // OpenUniStream opens a new outgoing unidirectional QUIC stream.
// If the error is non-nil, it satisfies the net.Error interface. // There is no signaling to the peer about new streams:
// When reaching the peer's stream limit, Temporary() will be true. // The peer can only accept the stream after data has been sent on the stream,
// If the connection was closed due to a timeout, Timeout() will be true. // or the stream has been reset or closed.
// When reaching the peer's stream limit, it is not possible to open a new stream until the
// peer raises the stream limit. In that case, a StreamLimitReachedError is returned.
OpenUniStream() (SendStream, error) OpenUniStream() (SendStream, error)
// OpenUniStreamSync opens a new outgoing unidirectional QUIC stream. // OpenUniStreamSync opens a new outgoing unidirectional QUIC stream.
// It blocks until a new stream can be opened. // It blocks until a new stream can be opened.
// If the error is non-nil, it satisfies the net.Error interface. // There is no signaling to the peer about new streams:
// If the connection was closed due to a timeout, Timeout() will be true. // The peer can only accept the stream after data has been sent on the stream,
// or the stream has been reset or closed.
OpenUniStreamSync(context.Context) (SendStream, error) OpenUniStreamSync(context.Context) (SendStream, error)
// LocalAddr returns the local address. // LocalAddr returns the local address.
LocalAddr() net.Addr LocalAddr() net.Addr

View File

@@ -38,11 +38,21 @@ type streamOpenErr struct{ error }
var _ net.Error = &streamOpenErr{} var _ net.Error = &streamOpenErr{}
func (e streamOpenErr) Temporary() bool { return e.error == errTooManyOpenStreams } func (streamOpenErr) Timeout() bool { return false }
func (streamOpenErr) Timeout() bool { return false } func (e streamOpenErr) Unwrap() error { return e.error }
// errTooManyOpenStreams is used internally by the outgoing streams maps. func (e streamOpenErr) Temporary() bool {
var errTooManyOpenStreams = errors.New("too many open streams") // In older versions of quic-go, the stream limit error was documented to be a net.Error.Temporary.
// This function was since deprecated, but we keep the existing behavior.
return errors.Is(e, &StreamLimitReachedError{})
}
// StreamLimitReachedError is returned from Connection.OpenStream and Connection.OpenUniStream
// when it is not possible to open a new stream because the number of opens streams reached
// the peer's stream limit.
type StreamLimitReachedError struct{}
func (e StreamLimitReachedError) Error() string { return "too many open streams" }
type streamsMap struct { type streamsMap struct {
ctx context.Context // not used for cancellations, but carries the values associated with the connection ctx context.Context // not used for cancellations, but carries the values associated with the connection

View File

@@ -60,7 +60,7 @@ func (m *outgoingStreamsMap[T]) OpenStream() (T, error) {
// if there are OpenStreamSync calls waiting, return an error here // if there are OpenStreamSync calls waiting, return an error here
if len(m.openQueue) > 0 || m.nextStream > m.maxStream { if len(m.openQueue) > 0 || m.nextStream > m.maxStream {
m.maybeSendBlockedFrame() m.maybeSendBlockedFrame()
return *new(T), streamOpenErr{errTooManyOpenStreams} return *new(T), streamOpenErr{&StreamLimitReachedError{}}
} }
return m.openStream(), nil return m.openStream(), nil
} }

View File

@@ -361,8 +361,7 @@ var _ = Describe("Streams Map (outgoing)", func() {
Expect(bf.StreamLimit).To(BeEquivalentTo(6)) Expect(bf.StreamLimit).To(BeEquivalentTo(6))
}) })
_, err := m.OpenStream() _, err := m.OpenStream()
Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(&StreamLimitReachedError{}))
Expect(err.Error()).To(Equal(errTooManyOpenStreams.Error()))
}) })
It("only sends one STREAMS_BLOCKED frame for one stream ID", func() { It("only sends one STREAMS_BLOCKED frame for one stream ID", func() {
@@ -452,8 +451,7 @@ var _ = Describe("Streams Map (outgoing)", func() {
} }
str, err := m.OpenStream() str, err := m.OpenStream()
if limit <= n { if limit <= n {
Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(&StreamLimitReachedError{}))
Expect(err.Error()).To(Equal(errTooManyOpenStreams.Error()))
} else { } else {
Expect(str.num).To(Equal(protocol.StreamNum(n + 1))) Expect(str.num).To(Equal(protocol.StreamNum(n + 1)))
} }

View File

@@ -33,11 +33,14 @@ type streamMapping struct {
} }
func expectTooManyStreamsError(err error) { func expectTooManyStreamsError(err error) {
ExpectWithOffset(1, err).To(HaveOccurred()) ExpectWithOffset(1, err).To(MatchError(&StreamLimitReachedError{}))
ExpectWithOffset(1, err.Error()).To(Equal(errTooManyOpenStreams.Error()))
nerr, ok := err.(net.Error) nerr, ok := err.(net.Error)
ExpectWithOffset(1, ok).To(BeTrue()) ExpectWithOffset(1, ok).To(BeTrue())
ExpectWithOffset(1, nerr.Timeout()).To(BeFalse()) ExpectWithOffset(1, nerr.Timeout()).To(BeFalse())
//nolint:staticcheck // SA1019
// In older versions of quic-go, the stream limit error was documented to be a net.Error.Temporary.
// This function was since deprecated, but we keep the existing behavior.
ExpectWithOffset(1, nerr.Temporary()).To(BeTrue())
} }
var _ = Describe("Streams Map", func() { var _ = Describe("Streams Map", func() {