Repository: quic-go/quic-go
Pull request #4609 (2024-08-01 06:49:45)

implement qlog JSON-SEQ format, bump qlog version
Closes #3292.


Patch:
diff --git a/.gitignore b/.gitignore
index 3cc06f24..b454729d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@ main
 mockgen_tmp.go
 *.qtr
 *.qlog
+*.sqlog
 *.txt
 race.[0-9]*
 
diff --git a/crypto_stream.go b/crypto_stream.go
index 55700eaa..abc7ddcf 100644
--- a/crypto_stream.go
+++ b/crypto_stream.go
@@ -21,7 +21,8 @@ type cryptoStream interface {
 }
 
 type cryptoStreamImpl struct {
-	queue frameSorter
+	queue  *frameSorter
+	msgBuf []byte
 
 	highestOffset protocol.ByteCount
 	finished      bool
@@ -31,7 +32,7 @@ type cryptoStreamImpl struct {
 }
 
 func newCryptoStream() cryptoStream {
-	return &cryptoStreamImpl{queue: *newFrameSorter()}
+	return &cryptoStreamImpl{queue: newFrameSorter()}
 }
 
 func (s *cryptoStreamImpl) HandleCryptoFrame(f *wire.CryptoFrame) error {
@@ -55,13 +56,23 @@ func (s *cryptoStreamImpl) HandleCryptoFrame(f *wire.CryptoFrame) error {
 		return nil
 	}
 	s.highestOffset = max(s.highestOffset, highestOffset)
-	return s.queue.Push(f.Data, f.Offset, nil)
+	if err := s.queue.Push(f.Data, f.Offset, nil); err != nil {
+		return err
+	}
+	for {
+		_, data, _ := s.queue.Pop()
+		if data == nil {
+			return nil
+		}
+		s.msgBuf = append(s.msgBuf, data...)
+	}
 }
 
 // GetCryptoData retrieves data that was received in CRYPTO frames
 func (s *cryptoStreamImpl) GetCryptoData() []byte {
-	_, data, _ := s.queue.Pop()
-	return data
+	b := s.msgBuf
+	s.msgBuf = nil
+	return b
 }
 
 func (s *cryptoStreamImpl) Finish() error {
diff --git a/internal/flowcontrol/interface.go b/internal/flowcontrol/interface.go
index 57d12a95..befd5c12 100644
--- a/internal/flowcontrol/interface.go
+++ b/internal/flowcontrol/interface.go
@@ -9,6 +9,7 @@ type flowController interface {
 	AddBytesSent(protocol.ByteCount)
 	// for receiving
 	GetWindowUpdate() protocol.ByteCount // returns 0 if no update is necessary
+	IsNewlyBlocked() (bool, protocol.ByteCount)
 }
 
 // A StreamFlowController is a flow controller for a QUIC stream.
@@ -22,7 +23,6 @@ type StreamFlowController interface {
 	// Abandon is called when reading from the stream is aborted early,
 	// and there won't be any further calls to AddBytesRead.
 	Abandon()
-	IsNewlyBlocked() bool
 }
 
 // The ConnectionFlowController is the flow controller for the connection.
@@ -30,7 +30,6 @@ type ConnectionFlowController interface {
 	flowController
 	AddBytesRead(protocol.ByteCount)
 	Reset() error
-	IsNewlyBlocked() (bool, protocol.ByteCount)
 }
 
 type connectionFlowControllerI interface {
diff --git a/internal/flowcontrol/stream_flow_controller.go b/internal/flowcontrol/stream_flow_controller.go
index 2d58351c..6b432c43 100644
--- a/internal/flowcontrol/stream_flow_controller.go
+++ b/internal/flowcontrol/stream_flow_controller.go
@@ -121,11 +121,6 @@ func (c *streamFlowController) SendWindowSize() protocol.ByteCount {
 	return min(c.baseFlowController.sendWindowSize(), c.connection.SendWindowSize())
 }
 
-func (c *streamFlowController) IsNewlyBlocked() bool {
-	blocked, _ := c.baseFlowController.IsNewlyBlocked()
-	return blocked
-}
-
 func (c *streamFlowController) shouldQueueWindowUpdate() bool {
 	return !c.receivedFinalOffset && c.hasWindowUpdate()
 }
diff --git a/internal/mocks/stream_flow_controller.go b/internal/mocks/stream_flow_controller.go
index b5ca05c1..3b63680c 100644
--- a/internal/mocks/stream_flow_controller.go
+++ b/internal/mocks/stream_flow_controller.go
@@ -188,11 +188,12 @@ func (c *MockStreamFlowControllerGetWindowUpdateCall) DoAndReturn(f func() proto
 }
 
 // IsNewlyBlocked mocks base method.
-func (m *MockStreamFlowController) IsNewlyBlocked() bool {
+func (m *MockStreamFlowController) IsNewlyBlocked() (bool, protocol.ByteCount) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "IsNewlyBlocked")
 	ret0, _ := ret[0].(bool)
-	return ret0
+	ret1, _ := ret[1].(protocol.ByteCount)
+	return ret0, ret1
 }
 
 // IsNewlyBlocked indicates an expected call of IsNewlyBlocked.
@@ -208,19 +209,19 @@ type MockStreamFlowControllerIsNewlyBlockedCall struct {
 }
 
 // Return rewrite *gomock.Call.Return
-func (c *MockStreamFlowControllerIsNewlyBlockedCall) Return(arg0 bool) *MockStreamFlowControllerIsNewlyBlockedCall {
-	c.Call = c.Call.Return(arg0)
+func (c *MockStreamFlowControllerIsNewlyBlockedCall) Return(arg0 bool, arg1 protocol.ByteCount) *MockStreamFlowControllerIsNewlyBlockedCall {
+	c.Call = c.Call.Return(arg0, arg1)
 	return c
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *MockStreamFlowControllerIsNewlyBlockedCall) Do(f func() bool) *MockStreamFlowControllerIsNewlyBlockedCall {
+func (c *MockStreamFlowControllerIsNewlyBlockedCall) Do(f func() (bool, protocol.ByteCount)) *MockStreamFlowControllerIsNewlyBlockedCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockStreamFlowControllerIsNewlyBlockedCall) DoAndReturn(f func() bool) *MockStreamFlowControllerIsNewlyBlockedCall {
+func (c *MockStreamFlowControllerIsNewlyBlockedCall) DoAndReturn(f func() (bool, protocol.ByteCount)) *MockStreamFlowControllerIsNewlyBlockedCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
diff --git a/interop/utils/logging.go b/interop/utils/logging.go
index fa076ef4..7e718965 100644
--- a/interop/utils/logging.go
+++ b/interop/utils/logging.go
@@ -39,7 +39,7 @@ func NewQLOGConnectionTracer(_ context.Context, p logging.Perspective, connID qu
 			log.Fatalf("failed to create qlog dir %s: %v", qlogDir, err)
 		}
 	}
-	path := fmt.Sprintf("%s/%s.qlog", strings.TrimRight(qlogDir, "/"), connID)
+	path := fmt.Sprintf("%s/%s.sqlog", strings.TrimRight(qlogDir, "/"), connID)
 	f, err := os.Create(path)
 	if err != nil {
 		log.Printf("Failed to create qlog file %s: %s", path, err.Error())
diff --git a/qlog/qlog_dir.go b/qlog/qlog_dir.go
index 5ea5e3ae..1c7e97fa 100644
--- a/qlog/qlog_dir.go
+++ b/qlog/qlog_dir.go
@@ -19,7 +19,7 @@ func DefaultTracer(ctx context.Context, p logging.Perspective, connID logging.Co
 }
 
 // DefaultConnectionTracer creates a qlog file in the qlog directory specified by the QLOGDIR environment variable.
-// File names are <odcid>_<perspective>.qlog.
+// File names are <odcid>_<perspective>.sqlog.
 // Returns nil if QLOGDIR is not set.
 func DefaultConnectionTracer(_ context.Context, p logging.Perspective, connID logging.ConnectionID) *logging.ConnectionTracer {
 	var label string
@@ -33,7 +33,7 @@ func DefaultConnectionTracer(_ context.Context, p logging.Perspective, connID lo
 }
 
 // qlogDirTracer creates a qlog file in the qlog directory specified by the QLOGDIR environment variable.
-// File names are <odcid>_<label>.qlog.
+// File names are <odcid>_<label>.sqlog.
 // Returns nil if QLOGDIR is not set.
 func qlogDirTracer(p logging.Perspective, connID logging.ConnectionID, label string) *logging.ConnectionTracer {
 	qlogDir := os.Getenv("QLOGDIR")
@@ -45,7 +45,7 @@ func qlogDirTracer(p logging.Perspective, connID logging.ConnectionID, label str
 			log.Fatalf("failed to create qlog dir %s: %v", qlogDir, err)
 		}
 	}
-	path := fmt.Sprintf("%s/%s_%s.qlog", strings.TrimRight(qlogDir, "/"), connID, label)
+	path := fmt.Sprintf("%s/%s_%s.sqlog", strings.TrimRight(qlogDir, "/"), connID, label)
 	f, err := os.Create(path)
 	if err != nil {
 		log.Printf("Failed to create qlog file %s: %s", path, err.Error())
diff --git a/qlog/trace.go b/qlog/trace.go
index c183368d..4c3d33c6 100644
--- a/qlog/trace.go
+++ b/qlog/trace.go
@@ -43,8 +43,8 @@ type topLevel struct {
 
 func (topLevel) IsNil() bool { return false }
 func (l topLevel) MarshalJSONObject(enc *gojay.Encoder) {
-	enc.StringKey("qlog_format", "NDJSON")
-	enc.StringKey("qlog_version", "draft-02")
+	enc.StringKey("qlog_format", "JSON-SEQ")
+	enc.StringKey("qlog_version", "0.3")
 	enc.StringKeyOmitEmpty("title", "quic-go qlog")
 	enc.ObjectKey("configuration", configuration{Version: quicGoVersion})
 	enc.ObjectKey("trace", l.trace)
diff --git a/qlog/writer.go b/qlog/writer.go
index 10cb6499..661c54e8 100644
--- a/qlog/writer.go
+++ b/qlog/writer.go
@@ -12,6 +12,13 @@ import (
 
 const eventChanSize = 50
 
+const recordSeparator = 0x1e
+
+func writeRecordSeparator(w io.Writer) error {
+	_, err := w.Write([]byte{recordSeparator})
+	return err
+}
+
 type writer struct {
 	w io.WriteCloser
 
@@ -44,6 +51,9 @@ func (w *writer) Run() {
 	defer close(w.runStopped)
 	buf := &bytes.Buffer{}
 	enc := gojay.NewEncoder(buf)
+	if err := writeRecordSeparator(buf); err != nil {
+		panic(fmt.Sprintf("qlog encoding into a bytes.Buffer failed: %s", err))
+	}
 	if err := enc.Encode(&topLevel{trace: *w.tr}); err != nil {
 		panic(fmt.Sprintf("qlog encoding into a bytes.Buffer failed: %s", err))
 	}
@@ -58,6 +68,10 @@ func (w *writer) Run() {
 		if w.encodeErr != nil { // if encoding failed, just continue draining the event channel
 			continue
 		}
+		if err := writeRecordSeparator(w.w); err != nil {
+			w.encodeErr = err
+			continue
+		}
 		if err := enc.Encode(ev); err != nil {
 			w.encodeErr = err
 			continue
diff --git a/send_stream.go b/send_stream.go
index c89424e4..f2087592 100644
--- a/send_stream.go
+++ b/send_stream.go
@@ -262,10 +262,10 @@ func (s *sendStream) popNewOrRetransmittedStreamFrame(maxBytes protocol.ByteCoun
 
 	sendWindow := s.flowController.SendWindowSize()
 	if sendWindow == 0 {
-		if s.flowController.IsNewlyBlocked() {
+		if isBlocked, offset := s.flowController.IsNewlyBlocked(); isBlocked {
 			s.sender.queueControlFrame(&wire.StreamDataBlockedFrame{
 				StreamID:          s.streamID,
-				MaximumStreamData: s.writeOffset,
+				MaximumStreamData: offset,
 			})
 			return nil, false
 		}
@@ -379,7 +379,7 @@ func (s *sendStream) isNewlyCompleted() bool {
 	// 1. the application called CancelWrite, or
 	// 2. we received a STOP_SENDING, and
 	// 		* the application consumed the error via Write, or
-	//		* the application called Close
+	//		* the application called CLsoe
 	if s.cancelWriteErr != nil && (s.cancellationFlagged || s.finishedWriting) {
 		s.completed = true
 		return true
@@ -421,17 +421,6 @@ func (s *sendStream) cancelWriteImpl(errorCode qerr.StreamErrorCode, remote bool
 	s.mutex.Lock()
 	if !remote {
 		s.cancellationFlagged = true
-		if s.cancelWriteErr != nil {
-			completed := s.isNewlyCompleted()
-			s.mutex.Unlock()
-			// The user has called CancelWrite. If the previous cancellation was
-			// because of a STOP_SENDING, we don't need to flag the error to the
-			// user any more.
-			if completed {
-				s.sender.onStreamCompleted(s.streamID)
-			}
-			return
-		}
 	}
 	if s.cancelWriteErr != nil {
 		s.mutex.Unlock()


Test patch:
diff --git a/crypto_stream_test.go b/crypto_stream_test.go
index e5ea9186..9a4a2ee5 100644
--- a/crypto_stream_test.go
+++ b/crypto_stream_test.go
@@ -41,30 +41,12 @@ var _ = Describe("Crypto Stream", func() {
 		It("handles out-of-order CRYPTO frames", func() {
 			Expect(str.HandleCryptoFrame(&wire.CryptoFrame{Offset: 3, Data: []byte("bar")})).To(Succeed())
 			Expect(str.HandleCryptoFrame(&wire.CryptoFrame{Data: []byte("foo")})).To(Succeed())
-			var data []byte
-			for {
-				b := str.GetCryptoData()
-				if b == nil {
-					break
-				}
-				data = append(data, b...)
-			}
-			Expect(data).To(Equal([]byte("foobar")))
+			Expect(str.GetCryptoData()).To(Equal([]byte("foobar")))
 			Expect(str.GetCryptoData()).To(BeNil())
 		})
 
 		Context("finishing", func() {
-			It("errors if there's still data to read at the current offset after finishing", func() {
-				Expect(str.HandleCryptoFrame(&wire.CryptoFrame{
-					Data: []byte("foo"),
-				})).To(Succeed())
-				Expect(str.Finish()).To(MatchError(&qerr.TransportError{
-					ErrorCode:    qerr.ProtocolViolation,
-					ErrorMessage: "encryption level changed, but crypto stream has more data to read",
-				}))
-			})
-
-			It("errors if there's still data to read at a higher offset after finishing", func() {
+			It("errors if there's still data to read after finishing", func() {
 				Expect(str.HandleCryptoFrame(&wire.CryptoFrame{
 					Data:   []byte("foobar"),
 					Offset: 10,
@@ -85,8 +67,6 @@ var _ = Describe("Crypto Stream", func() {
 				}
 				Expect(str.HandleCryptoFrame(f2)).To(Succeed())
 				Expect(str.HandleCryptoFrame(f1)).To(Succeed())
-				Expect(str.GetCryptoData()).To(HaveLen(3))
-				Expect(str.GetCryptoData()).To(HaveLen(3))
 				Expect(str.Finish()).To(Succeed())
 				Expect(str.HandleCryptoFrame(f2)).To(Succeed())
 			})
diff --git a/integrationtests/self/qlog_dir_test.go b/integrationtests/self/qlog_dir_test.go
index b81366e5..2adedcba 100644
--- a/integrationtests/self/qlog_dir_test.go
+++ b/integrationtests/self/qlog_dir_test.go
@@ -78,9 +78,10 @@ var _ = Describe("qlog dir tests", Serial, func() {
 		Expect(len(childs)).To(Equal(2))
 		odcids := make([]string, 0)
 		vantagePoints := make([]string, 0)
-		qlogFileNameRegexp := regexp.MustCompile(`^([0-f]+)_(client|server).qlog$`)
+		qlogFileNameRegexp := regexp.MustCompile(`^([0-f]+)_(client|server).sqlog$`)
 		for _, child := range childs {
 			matches := qlogFileNameRegexp.FindStringSubmatch(child.Name())
+			Expect(matches).To(HaveLen(3))
 			odcids = append(odcids, matches[1])
 			vantagePoints = append(vantagePoints, matches[2])
 		}
diff --git a/qlog/connection_tracer_test.go b/qlog/connection_tracer_test.go
index a6207c77..c3b4f0e1 100644
--- a/qlog/connection_tracer_test.go
+++ b/qlog/connection_tracer_test.go
@@ -2,7 +2,6 @@ package qlog
 
 import (
 	"bytes"
-	"encoding/json"
 	"io"
 	"net"
 	"net/netip"
@@ -36,7 +35,7 @@ func exportAndParse(buf *bytes.Buffer) []entry {
 	m := make(map[string]interface{})
 	line, err := buf.ReadBytes('\n')
 	Expect(err).ToNot(HaveOccurred())
-	Expect(json.Unmarshal(line, &m)).To(Succeed())
+	Expect(unmarshal(line, &m)).To(Succeed())
 	Expect(m).To(HaveKey("trace"))
 	var entries []entry
 	trace := m["trace"].(map[string]interface{})
@@ -50,7 +49,7 @@ func exportAndParse(buf *bytes.Buffer) []entry {
 		line, err := buf.ReadBytes('\n')
 		Expect(err).ToNot(HaveOccurred())
 		ev := make(map[string]interface{})
-		Expect(json.Unmarshal(line, &ev)).To(Succeed())
+		Expect(unmarshal(line, &ev)).To(Succeed())
 		Expect(ev).To(HaveLen(3))
 		Expect(ev).To(HaveKey("time"))
 		Expect(ev).To(HaveKey("name"))
@@ -89,8 +88,8 @@ var _ = Describe("Tracing", func() {
 		tracer.Close()
 
 		m := make(map[string]interface{})
-		Expect(json.Unmarshal(buf.Bytes(), &m)).To(Succeed())
-		Expect(m).To(HaveKeyWithValue("qlog_version", "draft-02"))
+		Expect(unmarshal(buf.Bytes(), &m)).To(Succeed())
+		Expect(m).To(HaveKeyWithValue("qlog_version", "0.3"))
 		Expect(m).To(HaveKey("title"))
 		Expect(m).To(HaveKey("trace"))
 		trace := m["trace"].(map[string]interface{})
diff --git a/qlog/qlog_suite_test.go b/qlog/qlog_suite_test.go
index 53ebb363..984211da 100644
--- a/qlog/qlog_suite_test.go
+++ b/qlog/qlog_suite_test.go
@@ -26,6 +26,13 @@ func scaleDuration(t time.Duration) time.Duration {
 	return time.Duration(scaleFactor) * t
 }
 
+func unmarshal(data []byte, v interface{}) error {
+	if data[0] == recordSeparator {
+		data = data[1:]
+	}
+	return json.Unmarshal(data, v)
+}
+
 func checkEncoding(data []byte, expected map[string]interface{}) {
 	// unmarshal the data
 	m := make(map[string]interface{})
diff --git a/qlog/tracer_test.go b/qlog/tracer_test.go
index 8aa8faa5..04e79b65 100644
--- a/qlog/tracer_test.go
+++ b/qlog/tracer_test.go
@@ -2,7 +2,6 @@ package qlog
 
 import (
 	"bytes"
-	"encoding/json"
 	"net"
 	"time"
 
@@ -28,12 +27,12 @@ var _ = Describe("Tracing", func() {
 		tracer.Close()
 
 		m := make(map[string]interface{})
-		Expect(json.Unmarshal(buf.Bytes(), &m)).To(Succeed())
-		Expect(m).To(HaveKeyWithValue("qlog_version", "draft-02"))
+		Expect(unmarshal(buf.Bytes(), &m)).To(Succeed())
+		Expect(m).To(HaveKeyWithValue("qlog_version", "0.3"))
 		Expect(m).To(HaveKey("title"))
 		Expect(m).To(HaveKey("trace"))
 		trace := m["trace"].(map[string]interface{})
-		Expect(trace).To(HaveKey(("common_fields")))
+		Expect(trace).To(HaveKey("common_fields"))
 		commonFields := trace["common_fields"].(map[string]interface{})
 		Expect(commonFields).ToNot(HaveKey("ODCID"))
 		Expect(commonFields).ToNot(HaveKey("group_id"))
diff --git a/send_stream_test.go b/send_stream_test.go
index 4c8df1c4..cd800a10 100644
--- a/send_stream_test.go
+++ b/send_stream_test.go
@@ -324,13 +324,11 @@ var _ = Describe("Send Stream", func() {
 
 		Context("flow control blocking", func() {
 			It("queues a BLOCKED frame if the stream is flow control blocked", func() {
-				mockFC.EXPECT().SendWindowSize().Return(protocol.ByteCount(3))
-				mockFC.EXPECT().AddBytesSent(protocol.ByteCount(3))
 				mockFC.EXPECT().SendWindowSize().Return(protocol.ByteCount(0))
-				mockFC.EXPECT().IsNewlyBlocked().Return(true)
+				mockFC.EXPECT().IsNewlyBlocked().Return(true, protocol.ByteCount(12))
 				mockSender.EXPECT().queueControlFrame(&wire.StreamDataBlockedFrame{
 					StreamID:          streamID,
-					MaximumStreamData: 3,
+					MaximumStreamData: 12,
 				})
 				done := make(chan struct{})
 				go func() {
@@ -341,10 +339,41 @@ var _ = Describe("Send Stream", func() {
 					Expect(err).ToNot(HaveOccurred())
 				}()
 				waitForWrite()
-				f, ok, hasMoreData := str.popStreamFrame(1000, protocol.Version1)
+				_, ok, hasMoreData := str.popStreamFrame(1000, protocol.Version1)
+				Expect(ok).To(BeFalse())
+				Expect(hasMoreData).To(BeFalse())
+				// make the Write go routine return
+				str.closeForShutdown(nil)
+				Eventually(done).Should(BeClosed())
+			})
+
+			It("says that it doesn't have any more data, when it is flow control blocked", func() {
+				done := make(chan struct{})
+				go func() {
+					defer GinkgoRecover()
+					defer close(done)
+					mockSender.EXPECT().onHasStreamData(streamID, str)
+					_, err := str.Write([]byte("foobar"))
+					Expect(err).ToNot(HaveOccurred())
+				}()
+				waitForWrite()
+
+				// first pop a STREAM frame of the maximum size allowed by flow control
+				mockFC.EXPECT().SendWindowSize().Return(protocol.ByteCount(3))
+				mockFC.EXPECT().AddBytesSent(protocol.ByteCount(3))
+				f, ok, hasMoreData := str.popStreamFrame(expectedFrameHeaderLen(0)+3, protocol.Version1)
 				Expect(ok).To(BeTrue())
+				Expect(f).ToNot(BeNil())
 				Expect(hasMoreData).To(BeTrue())
-				Expect(f.Frame.Data).To(HaveLen(3))
+
+				// try to pop again, this time noticing that we're blocked
+				mockFC.EXPECT().SendWindowSize()
+				// don't use offset 3 here, to make sure the BLOCKED frame contains the number returned by the flow controller
+				mockFC.EXPECT().IsNewlyBlocked().Return(true, protocol.ByteCount(10))
+				mockSender.EXPECT().queueControlFrame(&wire.StreamDataBlockedFrame{
+					StreamID:          streamID,
+					MaximumStreamData: 10,
+				})
 				_, ok, hasMoreData = str.popStreamFrame(1000, protocol.Version1)
 				Expect(ok).To(BeFalse())
 				Expect(hasMoreData).To(BeFalse())
@@ -880,20 +909,6 @@ var _ = Describe("Send Stream", func() {
 				})
 			})
 
-			It("discards the stream when CancelWrite is called after receiving STOP_SENDING", func() {
-				mockSender.EXPECT().queueControlFrame(&wire.ResetStreamFrame{
-					StreamID:  streamID,
-					ErrorCode: 101,
-				})
-				str.handleStopSendingFrame(&wire.StopSendingFrame{
-					StreamID:  streamID,
-					ErrorCode: 101,
-				})
-
-				mockSender.EXPECT().onStreamCompleted(gomock.Any())
-				str.CancelWrite(101)
-			})
-
 			It("unblocks Write", func() {
 				mockSender.EXPECT().onHasStreamData(streamID, str)
 				mockSender.EXPECT().queueControlFrame(gomock.Any())