1
0
mirror of https://github.com/facebookincubator/mvfst.git synced 2025-08-08 09:42:06 +03:00

Move happy eyeballs state to client state.

Summary: This doesn't belong in the generic state. Untangling it is a little difficult, but I think this solution is cleaner than having it in the generic state.

Reviewed By: JunqiWang

Differential Revision: D29856391

fbshipit-source-id: 1042109ed29cd1d20d139e08548d187b469c8398
This commit is contained in:
Matt Joras
2021-07-23 14:19:55 -07:00
committed by Facebook GitHub Bot
parent 7402dbe6c9
commit 612a00c3f9
11 changed files with 94 additions and 81 deletions

View File

@@ -18,7 +18,7 @@ IOBufQuicBatch::IOBufQuicBatch(
folly::AsyncUDPSocket& sock, folly::AsyncUDPSocket& sock,
const folly::SocketAddress& peerAddress, const folly::SocketAddress& peerAddress,
QuicTransportStatsCallback* statsCallback, QuicTransportStatsCallback* statsCallback,
QuicConnectionStateBase::HappyEyeballsState& happyEyeballsState) QuicClientConnectionState::HappyEyeballsState* happyEyeballsState)
: batchWriter_(std::move(batchWriter)), : batchWriter_(std::move(batchWriter)),
threadLocal_(threadLocal), threadLocal_(threadLocal),
sock_(sock), sock_(sock),
@@ -76,38 +76,45 @@ bool IOBufQuicBatch::flushInternal() {
bool written = false; bool written = false;
folly::Optional<int> firstSocketErrno; folly::Optional<int> firstSocketErrno;
if (happyEyeballsState_.shouldWriteToFirstSocket) { if (!happyEyeballsState_ || happyEyeballsState_->shouldWriteToFirstSocket) {
auto consumed = batchWriter_->write(sock_, peerAddress_); auto consumed = batchWriter_->write(sock_, peerAddress_);
firstSocketErrno = errno; if (consumed < 0) {
firstSocketErrno = errno;
}
written = (consumed >= 0); written = (consumed >= 0);
happyEyeballsState_.shouldWriteToFirstSocket = if (happyEyeballsState_) {
(consumed >= 0 || isRetriableError(errno)); happyEyeballsState_->shouldWriteToFirstSocket =
(consumed >= 0 || isRetriableError(errno));
if (!happyEyeballsState_.shouldWriteToFirstSocket) { if (!happyEyeballsState_->shouldWriteToFirstSocket) {
sock_.pauseRead(); sock_.pauseRead();
}
} }
} }
// If error occured on first socket, kick off second socket immediately // If error occured on first socket, kick off second socket immediately
if (!written && happyEyeballsState_.connAttemptDelayTimeout && if (!written && happyEyeballsState_ &&
happyEyeballsState_.connAttemptDelayTimeout->isScheduled()) { happyEyeballsState_->connAttemptDelayTimeout &&
happyEyeballsState_.connAttemptDelayTimeout->timeoutExpired(); happyEyeballsState_->connAttemptDelayTimeout->isScheduled()) {
happyEyeballsState_.connAttemptDelayTimeout->cancelTimeout(); happyEyeballsState_->connAttemptDelayTimeout->timeoutExpired();
happyEyeballsState_->connAttemptDelayTimeout->cancelTimeout();
} }
folly::Optional<int> secondSocketErrno; folly::Optional<int> secondSocketErrno;
if (happyEyeballsState_.shouldWriteToSecondSocket) { if (happyEyeballsState_ && happyEyeballsState_->shouldWriteToSecondSocket) {
auto consumed = batchWriter_->write( auto consumed = batchWriter_->write(
*happyEyeballsState_.secondSocket, *happyEyeballsState_->secondSocket,
happyEyeballsState_.secondPeerAddress); happyEyeballsState_->secondPeerAddress);
secondSocketErrno = errno; if (consumed < 0) {
secondSocketErrno = errno;
}
// written is marked true if either socket write succeeds // written is marked true if either socket write succeeds
written |= (consumed >= 0); written |= (consumed >= 0);
happyEyeballsState_.shouldWriteToSecondSocket = happyEyeballsState_->shouldWriteToSecondSocket =
(consumed >= 0 || isRetriableError(errno)); (consumed >= 0 || isRetriableError(errno));
if (!happyEyeballsState_.shouldWriteToSecondSocket) { if (!happyEyeballsState_->shouldWriteToSecondSocket) {
happyEyeballsState_.secondSocket->pauseRead(); happyEyeballsState_->secondSocket->pauseRead();
} }
} }
@@ -128,8 +135,12 @@ bool IOBufQuicBatch::flushInternal() {
} }
} }
if (!happyEyeballsState_.shouldWriteToFirstSocket && // If we have no happy eyeballs state, we only care if the first socket had
!happyEyeballsState_.shouldWriteToSecondSocket) { // an error. Otherwise we check both.
if ((!happyEyeballsState_ && firstSocketErrno.has_value() &&
!isRetriableError(firstSocketErrno.value())) ||
(happyEyeballsState_ && !happyEyeballsState_->shouldWriteToFirstSocket &&
!happyEyeballsState_->shouldWriteToSecondSocket)) {
auto firstSocketErrorMsg = firstSocketErrno.has_value() auto firstSocketErrorMsg = firstSocketErrno.has_value()
? folly::to<std::string>( ? folly::to<std::string>(
folly::errnoStr(firstSocketErrno.value()), ", ") folly::errnoStr(firstSocketErrno.value()), ", ")

View File

@@ -9,6 +9,7 @@
#pragma once #pragma once
#include <quic/QuicException.h> #include <quic/QuicException.h>
#include <quic/api/QuicBatchWriter.h> #include <quic/api/QuicBatchWriter.h>
#include <quic/client/state/ClientStateMachine.h>
#include <quic/state/QuicTransportStatsCallback.h> #include <quic/state/QuicTransportStatsCallback.h>
namespace quic { namespace quic {
@@ -25,7 +26,7 @@ class IOBufQuicBatch {
folly::AsyncUDPSocket& sock, folly::AsyncUDPSocket& sock,
const folly::SocketAddress& peerAddress, const folly::SocketAddress& peerAddress,
QuicTransportStatsCallback* statsCallback, QuicTransportStatsCallback* statsCallback,
QuicConnectionStateBase::HappyEyeballsState& happyEyeballsState); QuicClientConnectionState::HappyEyeballsState* happyEyeballsState);
~IOBufQuicBatch() = default; ~IOBufQuicBatch() = default;
@@ -55,7 +56,7 @@ class IOBufQuicBatch {
folly::AsyncUDPSocket& sock_; folly::AsyncUDPSocket& sock_;
const folly::SocketAddress& peerAddress_; const folly::SocketAddress& peerAddress_;
QuicTransportStatsCallback* statsCallback_{nullptr}; QuicTransportStatsCallback* statsCallback_{nullptr};
QuicConnectionStateBase::HappyEyeballsState& happyEyeballsState_; QuicClientConnectionState::HappyEyeballsState* happyEyeballsState_;
uint64_t pktSent_{0}; uint64_t pktSent_{0};
}; };

View File

@@ -1320,13 +1320,16 @@ uint64_t writeConnectionDataToSocket(
connection.transportSettings.dataPathType, connection.transportSettings.dataPathType,
connection); connection);
auto happyEyeballsState = connection.nodeType == QuicNodeType::Server
? nullptr
: &static_cast<QuicClientConnectionState&>(connection).happyEyeballsState;
IOBufQuicBatch ioBufBatch( IOBufQuicBatch ioBufBatch(
std::move(batchWriter), std::move(batchWriter),
connection.transportSettings.useThreadLocalBatching, connection.transportSettings.useThreadLocalBatching,
sock, sock,
connection.peerAddress, connection.peerAddress,
connection.statsCallback, connection.statsCallback,
connection.happyEyeballsState); happyEyeballsState);
if (connection.loopDetectorCallback) { if (connection.loopDetectorCallback) {
connection.writeDebugState.schedulerName = scheduler.name().str(); connection.writeDebugState.schedulerName = scheduler.name().str();

View File

@@ -27,7 +27,7 @@ void RunTest(int numBatch) {
folly::SocketAddress peerAddress{"127.0.0.1", 1234}; folly::SocketAddress peerAddress{"127.0.0.1", 1234};
QuicClientConnectionState conn( QuicClientConnectionState conn(
FizzClientQuicHandshakeContext::Builder().build()); FizzClientQuicHandshakeContext::Builder().build());
QuicConnectionStateBase::HappyEyeballsState happyEyeballsState; QuicClientConnectionState::HappyEyeballsState happyEyeballsState;
IOBufQuicBatch ioBufBatch( IOBufQuicBatch ioBufBatch(
std::move(batchWriter), std::move(batchWriter),
@@ -35,7 +35,7 @@ void RunTest(int numBatch) {
sock, sock,
peerAddress, peerAddress,
conn.statsCallback, conn.statsCallback,
happyEyeballsState); nullptr /* happyEyeballsState */);
std::string strTest("Test"); std::string strTest("Test");

View File

@@ -99,8 +99,8 @@ QuicClientTransport::~QuicClientTransport() {
std::string("Closing from client destructor")), std::string("Closing from client destructor")),
false); false);
if (conn_->happyEyeballsState.secondSocket) { if (clientConn_->happyEyeballsState.secondSocket) {
auto sock = std::move(conn_->happyEyeballsState.secondSocket); auto sock = std::move(clientConn_->happyEyeballsState.secondSocket);
sock->pauseRead(); sock->pauseRead();
sock->close(); sock->close();
} }
@@ -213,7 +213,7 @@ void QuicClientTransport::processPacketData(
if (happyEyeballsEnabled_) { if (happyEyeballsEnabled_) {
happyEyeballsOnDataReceived( happyEyeballsOnDataReceived(
*conn_, happyEyeballsConnAttemptDelayTimeout_, socket_, peer); *clientConn_, happyEyeballsConnAttemptDelayTimeout_, socket_, peer);
} }
// Set the destination connection ID to be the value from the source // Set the destination connection ID to be the value from the source
// connection id of the retry packet // connection id of the retry packet
@@ -292,7 +292,7 @@ void QuicClientTransport::processPacketData(
if (happyEyeballsEnabled_) { if (happyEyeballsEnabled_) {
CHECK(socket_); CHECK(socket_);
happyEyeballsOnDataReceived( happyEyeballsOnDataReceived(
*conn_, happyEyeballsConnAttemptDelayTimeout_, socket_, peer); *clientConn_, happyEyeballsConnAttemptDelayTimeout_, socket_, peer);
} }
LongHeader* longHeader = regularOptional->header.asLong(); LongHeader* longHeader = regularOptional->header.asLong();
@@ -1015,7 +1015,7 @@ void QuicClientTransport::errMessage(
// exists, and the second socket is IPv4. Then we basically do the same // exists, and the second socket is IPv4. Then we basically do the same
// thing we would have done if we'd gotten a write error on that socket. // thing we would have done if we'd gotten a write error on that socket.
// If both sockets are not functional we close the connection. // If both sockets are not functional we close the connection.
auto& happyEyeballsState = conn_->happyEyeballsState; auto& happyEyeballsState = clientConn_->happyEyeballsState;
if (!happyEyeballsState.finished) { if (!happyEyeballsState.finished) {
if (cmsg.cmsg_level == SOL_IPV6 && if (cmsg.cmsg_level == SOL_IPV6 &&
happyEyeballsState.shouldWriteToFirstSocket) { happyEyeballsState.shouldWriteToFirstSocket) {
@@ -1485,7 +1485,7 @@ void QuicClientTransport::
happyEyeballsConnAttemptDelayTimeoutExpired() noexcept { happyEyeballsConnAttemptDelayTimeoutExpired() noexcept {
// Declare 0-RTT data as lost so that they will be retransmitted over the // Declare 0-RTT data as lost so that they will be retransmitted over the
// second socket. // second socket.
happyEyeballsStartSecondSocket(conn_->happyEyeballsState); happyEyeballsStartSecondSocket(clientConn_->happyEyeballsState);
// If this gets called from the write path then we haven't added the packets // If this gets called from the write path then we haven't added the packets
// to the outstanding packet list yet. // to the outstanding packet list yet.
runOnEvbAsync([&](auto) { markZeroRttPacketsLost(*conn_, markPacketLoss); }); runOnEvbAsync([&](auto) { markZeroRttPacketsLost(*conn_, markPacketLoss); });
@@ -1495,7 +1495,7 @@ void QuicClientTransport::start(ConnectionCallback* cb) {
if (happyEyeballsEnabled_) { if (happyEyeballsEnabled_) {
// TODO Supply v4 delay amount from somewhere when we want to tune this // TODO Supply v4 delay amount from somewhere when we want to tune this
startHappyEyeballs( startHappyEyeballs(
*conn_, *clientConn_,
evb_, evb_,
happyEyeballsCachedFamily_, happyEyeballsCachedFamily_,
happyEyeballsConnAttemptDelayTimeout_, happyEyeballsConnAttemptDelayTimeout_,
@@ -1558,7 +1558,7 @@ void QuicClientTransport::addNewPeerAddress(folly::SocketAddress peerAddress) {
conn_->udpSendPacketLen, conn_->udpSendPacketLen,
(peerAddress.getFamily() == AF_INET6 ? kDefaultV6UDPSendPacketLen (peerAddress.getFamily() == AF_INET6 ? kDefaultV6UDPSendPacketLen
: kDefaultV4UDPSendPacketLen)); : kDefaultV4UDPSendPacketLen));
happyEyeballsAddPeerAddress(*conn_, peerAddress); happyEyeballsAddPeerAddress(*clientConn_, peerAddress);
return; return;
} }
@@ -1585,7 +1585,7 @@ void QuicClientTransport::setHappyEyeballsCachedFamily(
void QuicClientTransport::addNewSocket( void QuicClientTransport::addNewSocket(
std::unique_ptr<folly::AsyncUDPSocket> socket) { std::unique_ptr<folly::AsyncUDPSocket> socket) {
happyEyeballsAddSocket(*conn_, std::move(socket)); happyEyeballsAddSocket(*clientConn_, std::move(socket));
} }
void QuicClientTransport::setHostname(const std::string& hostname) { void QuicClientTransport::setHostname(const std::string& hostname) {

View File

@@ -71,6 +71,36 @@ struct QuicClientConnectionState : public QuicConnectionStateBase {
uint64_t peerAdvertisedInitialMaxStreamsBidi{0}; uint64_t peerAdvertisedInitialMaxStreamsBidi{0};
uint64_t peerAdvertisedInitialMaxStreamsUni{0}; uint64_t peerAdvertisedInitialMaxStreamsUni{0};
struct HappyEyeballsState {
// Delay timer
folly::HHWheelTimer::Callback* connAttemptDelayTimeout{nullptr};
// IPv6 peer address
folly::SocketAddress v6PeerAddress;
// IPv4 peer address
folly::SocketAddress v4PeerAddress;
// The address that this socket will try to connect to after connection
// attempt delay timeout fires
folly::SocketAddress secondPeerAddress;
// The UDP socket that will be used for the second connection attempt
std::unique_ptr<folly::AsyncUDPSocket> secondSocket;
// Whether should write to the first UDP socket
bool shouldWriteToFirstSocket{true};
// Whether should write to the second UDP socket
bool shouldWriteToSecondSocket{false};
// Whether HappyEyeballs has finished
// The signal of finishing is first successful decryption of a packet
bool finished{false};
};
HappyEyeballsState happyEyeballsState;
// Short header packets we received but couldn't yet decrypt. // Short header packets we received but couldn't yet decrypt.
std::vector<PendingClientData> pendingOneRttData; std::vector<PendingClientData> pendingOneRttData;
// Handshake packets we received but couldn't yet decrypt. // Handshake packets we received but couldn't yet decrypt.

View File

@@ -104,14 +104,13 @@ size_t writePacketsGroup(
auto batchWriter = auto batchWriter =
BatchWriterPtr(new GSOPacketBatchWriter(kDefaultQuicMaxBatchSize)); BatchWriterPtr(new GSOPacketBatchWriter(kDefaultQuicMaxBatchSize));
// This doesn't matter: // This doesn't matter:
QuicConnectionStateBase::HappyEyeballsState happyEyeballsState;
IOBufQuicBatch ioBufBatch( IOBufQuicBatch ioBufBatch(
std::move(batchWriter), std::move(batchWriter),
false /* thread local batching */, false /* thread local batching */,
sock, sock,
reqGroup[0].clientAddress, reqGroup[0].clientAddress,
nullptr /* statsCallback */, nullptr /* statsCallback */,
happyEyeballsState); nullptr /* happyEyeballsState */);
// TODO: Instead of building ciphers every time, we should cache them into a // TODO: Instead of building ciphers every time, we should cache them into a
// CipherMap and look them up. // CipherMap and look them up.
CipherBuilder cipherBuilder; CipherBuilder cipherBuilder;

View File

@@ -42,7 +42,6 @@ class DSRPacketizerSingleWriteTest : public Test {
} }
folly::EventBase evb; folly::EventBase evb;
QuicConnectionStateBase::HappyEyeballsState happyEyeballsState;
folly::SocketAddress peerAddress{"127.0.0.1", 1234}; folly::SocketAddress peerAddress{"127.0.0.1", 1234};
std::unique_ptr<Aead> aead; std::unique_ptr<Aead> aead;
std::unique_ptr<PacketNumberCipher> headerCipher; std::unique_ptr<PacketNumberCipher> headerCipher;
@@ -59,7 +58,7 @@ TEST_F(DSRPacketizerSingleWriteTest, SingleWrite) {
*socket, *socket,
peerAddress, peerAddress,
nullptr /* statsCallback */, nullptr /* statsCallback */,
happyEyeballsState); nullptr /* happyEyeballsState */);
PacketNum packetNum = 20; PacketNum packetNum = 20;
PacketNum largestAckedByPeer = 0; PacketNum largestAckedByPeer = 0;
StreamId streamId = 0; StreamId streamId = 0;
@@ -102,7 +101,7 @@ TEST_F(DSRPacketizerSingleWriteTest, NotEnoughData) {
*socket, *socket,
peerAddress, peerAddress,
nullptr /* statsCallback */, nullptr /* statsCallback */,
happyEyeballsState); nullptr /* happyEyeballsState */);
PacketNum packetNum = 20; PacketNum packetNum = 20;
PacketNum largestAckedByPeer = 0; PacketNum largestAckedByPeer = 0;
StreamId streamId = 0; StreamId streamId = 0;

View File

@@ -28,7 +28,7 @@ namespace fsp = folly::portability::sockets;
namespace quic { namespace quic {
void happyEyeballsAddPeerAddress( void happyEyeballsAddPeerAddress(
QuicConnectionStateBase& connection, QuicClientConnectionState& connection,
const folly::SocketAddress& peerAddress) { const folly::SocketAddress& peerAddress) {
// TODO: Do not wait for both IPv4 and IPv6 addresses to return before // TODO: Do not wait for both IPv4 and IPv6 addresses to return before
// attempting connection establishment. -- RFC8305 // attempting connection establishment. -- RFC8305
@@ -52,13 +52,13 @@ void happyEyeballsAddPeerAddress(
} }
void happyEyeballsAddSocket( void happyEyeballsAddSocket(
QuicConnectionStateBase& connection, QuicClientConnectionState& connection,
std::unique_ptr<folly::AsyncUDPSocket> socket) { std::unique_ptr<folly::AsyncUDPSocket> socket) {
connection.happyEyeballsState.secondSocket = std::move(socket); connection.happyEyeballsState.secondSocket = std::move(socket);
} }
void startHappyEyeballs( void startHappyEyeballs(
QuicConnectionStateBase& connection, QuicClientConnectionState& connection,
folly::EventBase* evb, folly::EventBase* evb,
sa_family_t cachedFamily, sa_family_t cachedFamily,
folly::HHWheelTimer::Callback& connAttemptDelayTimeout, folly::HHWheelTimer::Callback& connAttemptDelayTimeout,
@@ -166,14 +166,14 @@ void happyEyeballsSetUpSocket(
} }
void happyEyeballsStartSecondSocket( void happyEyeballsStartSecondSocket(
QuicConnectionStateBase::HappyEyeballsState& happyEyeballsState) { QuicClientConnectionState::HappyEyeballsState& happyEyeballsState) {
CHECK(!happyEyeballsState.finished); CHECK(!happyEyeballsState.finished);
happyEyeballsState.shouldWriteToSecondSocket = true; happyEyeballsState.shouldWriteToSecondSocket = true;
} }
void happyEyeballsOnDataReceived( void happyEyeballsOnDataReceived(
QuicConnectionStateBase& connection, QuicClientConnectionState& connection,
folly::HHWheelTimer::Callback& connAttemptDelayTimeout, folly::HHWheelTimer::Callback& connAttemptDelayTimeout,
std::unique_ptr<folly::AsyncUDPSocket>& socket, std::unique_ptr<folly::AsyncUDPSocket>& socket,
const folly::SocketAddress& peerAddress) { const folly::SocketAddress& peerAddress) {

View File

@@ -8,7 +8,7 @@
#pragma once #pragma once
#include <quic/state/StateData.h> #include <quic/client/state/ClientStateMachine.h>
#include <folly/io/SocketOptionMap.h> #include <folly/io/SocketOptionMap.h>
#include <folly/io/async/AsyncUDPSocket.h> #include <folly/io/async/AsyncUDPSocket.h>
@@ -26,15 +26,15 @@ namespace quic {
struct TransportSettings; struct TransportSettings;
void happyEyeballsAddPeerAddress( void happyEyeballsAddPeerAddress(
QuicConnectionStateBase& connection, QuicClientConnectionState& connection,
const folly::SocketAddress& peerAddress); const folly::SocketAddress& peerAddress);
void happyEyeballsAddSocket( void happyEyeballsAddSocket(
QuicConnectionStateBase& connection, QuicClientConnectionState& connection,
std::unique_ptr<folly::AsyncUDPSocket> socket); std::unique_ptr<folly::AsyncUDPSocket> socket);
void startHappyEyeballs( void startHappyEyeballs(
QuicConnectionStateBase& connection, QuicClientConnectionState& connection,
folly::EventBase* evb, folly::EventBase* evb,
sa_family_t cachedFamily, sa_family_t cachedFamily,
folly::HHWheelTimer::Callback& connAttemptDelayTimeout, folly::HHWheelTimer::Callback& connAttemptDelayTimeout,
@@ -43,7 +43,7 @@ void startHappyEyeballs(
folly::AsyncUDPSocket::ReadCallback* readCallback, folly::AsyncUDPSocket::ReadCallback* readCallback,
const folly::SocketOptionMap& options); const folly::SocketOptionMap& options);
void resetHappyEyeballs(QuicConnectionStateBase& connection); void resetHappyEyeballs(QuicClientConnectionState& connection);
void happyEyeballsSetUpSocket( void happyEyeballsSetUpSocket(
folly::AsyncUDPSocket& socket, folly::AsyncUDPSocket& socket,
@@ -55,10 +55,10 @@ void happyEyeballsSetUpSocket(
const folly::SocketOptionMap& options); const folly::SocketOptionMap& options);
void happyEyeballsStartSecondSocket( void happyEyeballsStartSecondSocket(
QuicConnectionStateBase::HappyEyeballsState& happyEyeballsState); QuicClientConnectionState::HappyEyeballsState& happyEyeballsState);
void happyEyeballsOnDataReceived( void happyEyeballsOnDataReceived(
QuicConnectionStateBase& connection, QuicClientConnectionState& connection,
folly::HHWheelTimer::Callback& connAttemptDelayTimeout, folly::HHWheelTimer::Callback& connAttemptDelayTimeout,
std::unique_ptr<folly::AsyncUDPSocket>& socket, std::unique_ptr<folly::AsyncUDPSocket>& socket,
const folly::SocketAddress& peerAddress); const folly::SocketAddress& peerAddress);

View File

@@ -685,36 +685,6 @@ struct QuicConnectionStateBase : public folly::DelayedDestruction {
// Track stats for various server events // Track stats for various server events
QuicTransportStatsCallback* statsCallback{nullptr}; QuicTransportStatsCallback* statsCallback{nullptr};
struct HappyEyeballsState {
// Delay timer
folly::HHWheelTimer::Callback* connAttemptDelayTimeout{nullptr};
// IPv6 peer address
folly::SocketAddress v6PeerAddress;
// IPv4 peer address
folly::SocketAddress v4PeerAddress;
// The address that this socket will try to connect to after connection
// attempt delay timeout fires
folly::SocketAddress secondPeerAddress;
// The UDP socket that will be used for the second connection attempt
std::unique_ptr<folly::AsyncUDPSocket> secondSocket;
// Whether should write to the first UDP socket
bool shouldWriteToFirstSocket{true};
// Whether should write to the second UDP socket
bool shouldWriteToSecondSocket{false};
// Whether HappyEyeballs has finished
// The signal of finishing is first successful decryption of a packet
bool finished{false};
};
HappyEyeballsState happyEyeballsState;
// Meta state of d6d, mostly useful for analytics. D6D can operate without it. // Meta state of d6d, mostly useful for analytics. D6D can operate without it.
struct D6DMetaState { struct D6DMetaState {
// Cumulative count of acked packets // Cumulative count of acked packets