Skip to content

Commit 8e5220a

Browse files
jasnellcjihrig
authored andcommitted
quic: fixup closing/draining period timing
When entering the closing or draining periods, servers should wait three times the current probe timeout before releasing session state. PR-URL: #34283 Reviewed-By: Anna Henningsen <anna@addaleax.net>
1 parent 823fb00 commit 8e5220a

File tree

3 files changed

+84
-85
lines changed

3 files changed

+84
-85
lines changed

src/quic/node_quic_session-inl.h

+6-2
Original file line numberDiff line numberDiff line change
@@ -344,9 +344,13 @@ void QuicSession::InitApplication() {
344344
// the peer. All existing streams are abandoned and closed.
345345
void QuicSession::OnIdleTimeout() {
346346
if (!is_destroyed()) {
347+
if (state_->idle_timeout == 1) {
348+
Debug(this, "Idle timeout");
349+
Close(QuicSessionListener::SESSION_CLOSE_FLAG_SILENT);
350+
return;
351+
}
347352
state_->idle_timeout = 1;
348-
Debug(this, "Idle timeout");
349-
Close(QuicSessionListener::SESSION_CLOSE_FLAG_SILENT);
353+
UpdateClosingTimer();
350354
}
351355
}
352356

src/quic/node_quic_session.cc

+74-82
Original file line numberDiff line numberDiff line change
@@ -1633,7 +1633,8 @@ BaseObjectPtr<QuicStream> QuicSession::CreateStream(int64_t stream_id) {
16331633

16341634
// Initiate a shutdown of the QuicSession.
16351635
void QuicSession::Close(int close_flags) {
1636-
CHECK(!is_destroyed());
1636+
if (is_destroyed())
1637+
return;
16371638
bool silent = close_flags & QuicSessionListener::SESSION_CLOSE_FLAG_SILENT;
16381639
bool stateless_reset = is_stateless_reset();
16391640

@@ -1735,13 +1736,16 @@ bool QuicSession::GetNewConnectionID(
17351736
}
17361737

17371738
void QuicSession::HandleError() {
1738-
if (is_destroyed() || (is_in_closing_period() && !is_server()))
1739+
if (is_destroyed())
17391740
return;
17401741

1741-
if (!SendConnectionClose()) {
1742-
set_last_error(QUIC_ERROR_SESSION, NGTCP2_ERR_INTERNAL);
1743-
Close();
1744-
}
1742+
// If the QuicSession is a server, send a CONNECTION_CLOSE. In either
1743+
// case, the closing timer will be set and the QuicSession will be
1744+
// destroyed.
1745+
if (is_server())
1746+
SendConnectionClose();
1747+
else
1748+
UpdateClosingTimer();
17451749
}
17461750

17471751
// The the retransmit libuv timer fires, it will call MaybeTimeout,
@@ -1751,20 +1755,25 @@ void QuicSession::MaybeTimeout() {
17511755
if (is_destroyed())
17521756
return;
17531757
uint64_t now = uv_hrtime();
1754-
bool transmit = false;
1758+
17551759
if (ngtcp2_conn_loss_detection_expiry(connection()) <= now) {
17561760
Debug(this, "Retransmitting due to loss detection");
1757-
CHECK_EQ(ngtcp2_conn_on_loss_detection_timer(connection(), now), 0);
17581761
IncrementStat(&QuicSessionStats::loss_retransmit_count);
1759-
transmit = true;
1760-
} else if (ngtcp2_conn_ack_delay_expiry(connection()) <= now) {
1762+
}
1763+
1764+
if (ngtcp2_conn_ack_delay_expiry(connection()) <= now) {
17611765
Debug(this, "Retransmitting due to ack delay");
1762-
ngtcp2_conn_cancel_expired_ack_delay_timer(connection(), now);
17631766
IncrementStat(&QuicSessionStats::ack_delay_retransmit_count);
1764-
transmit = true;
17651767
}
1766-
if (transmit)
1767-
SendPendingData();
1768+
1769+
int rv = ngtcp2_conn_handle_expiry(connection(), now);
1770+
if (rv != 0) {
1771+
Debug(this, "Error handling retransmit timeout: %s", ngtcp2_strerror(rv));
1772+
set_last_error(QUIC_ERROR_SESSION, rv);
1773+
HandleError();
1774+
}
1775+
1776+
SendPendingData();
17681777
}
17691778

17701779
bool QuicSession::OpenBidirectionalStream(int64_t* stream_id) {
@@ -1847,16 +1856,7 @@ bool QuicSession::Receive(
18471856
Debug(this, "Receiving QUIC packet");
18481857
IncrementStat(&QuicSessionStats::bytes_received, nread);
18491858

1850-
// Closing period starts once ngtcp2 has detected that the session
1851-
// is being shutdown locally. Note that this is different that the
1852-
// is_graceful_closing() function, which
1853-
// indicates a graceful shutdown that allows the session and streams
1854-
// to finish naturally. When is_in_closing_period is true, ngtcp2 is
1855-
// actively in the process of shutting down the connection and a
1856-
// CONNECTION_CLOSE has already been sent. The only thing we can do
1857-
// at this point is either ignore the packet or send another
1858-
// CONNECTION_CLOSE.
1859-
if (is_in_closing_period()) {
1859+
if (is_in_closing_period() && is_server()) {
18601860
Debug(this, "QUIC packet received while in closing period");
18611861
IncrementConnectionCloseAttempts();
18621862
// For server QuicSession instances, we serialize the connection close
@@ -1866,30 +1866,13 @@ bool QuicSession::Receive(
18661866
// every received packet, however, so we use an exponential
18671867
// backoff, increasing the ratio of packets received to connection
18681868
// close frame sent with every one we send.
1869-
if (!ShouldAttemptConnectionClose()) {
1870-
Debug(this, "Not sending connection close");
1869+
if (UNLIKELY(ShouldAttemptConnectionClose() &&
1870+
!SendConnectionClose())) {
1871+
Debug(this, "Failure trying to send another connection close");
18711872
return false;
18721873
}
1873-
Debug(this, "Sending connection close");
1874-
return SendConnectionClose();
1875-
}
1876-
1877-
// When is_in_draining_period is true, ngtcp2 has received a
1878-
// connection close and we are simply discarding received packets.
1879-
// No outbound packets may be sent. Return true here because
1880-
// the packet was correctly processed, even tho it is being
1881-
// ignored.
1882-
if (is_in_draining_period()) {
1883-
Debug(this, "QUIC packet received while in draining period");
1884-
return true;
18851874
}
18861875

1887-
// It's possible for the remote address to change from one
1888-
// packet to the next so we have to look at the addr on
1889-
// every packet.
1890-
remote_address_ = remote_addr;
1891-
QuicPath path(local_addr, remote_address_);
1892-
18931876
{
18941877
// These are within a scope to ensure that the InternalCallbackScope
18951878
// and HandleScope are both exited before continuing on with the
@@ -1901,38 +1884,28 @@ bool QuicSession::Receive(
19011884
Debug(this, "Processing received packet");
19021885
HandleScope handle_scope(env()->isolate());
19031886
InternalCallbackScope callback_scope(this);
1887+
remote_address_ = remote_addr;
1888+
QuicPath path(local_addr, remote_address_);
19041889
if (!ReceivePacket(&path, data, nread)) {
1905-
Debug(this, "Failure processing received packet (code %" PRIu64 ")",
1906-
last_error().code);
19071890
HandleError();
19081891
return false;
19091892
}
19101893
}
19111894

1912-
if (is_destroyed()) {
1913-
Debug(this, "Session was destroyed while processing the received packet");
1914-
// If the QuicSession has been destroyed but it is not
1915-
// in the closing period, a CONNECTION_CLOSE has not yet
1916-
// been sent to the peer. Let's attempt to send one.
1917-
if (!is_in_closing_period() && !is_in_draining_period()) {
1918-
set_last_error();
1919-
SendConnectionClose();
1920-
}
1921-
return true;
1922-
}
1923-
19241895
// Only send pending data if we haven't entered draining mode.
19251896
// We enter the draining period when a CONNECTION_CLOSE has been
19261897
// received from the remote peer.
19271898
if (is_in_draining_period()) {
19281899
Debug(this, "In draining period after processing packet");
19291900
// If processing the packet puts us into draining period, there's
19301901
// absolutely nothing left for us to do except silently close
1931-
// and destroy this QuicSession.
1902+
// and destroy this QuicSession, which we do by updating the
1903+
// closing timer.
19321904
GetConnectionCloseInfo();
1933-
Close(QuicSessionListener::SESSION_CLOSE_FLAG_SILENT);
1905+
UpdateClosingTimer();
19341906
return true;
19351907
}
1908+
19361909
Debug(this, "Sending pending data after processing packet");
19371910
SendPendingData();
19381911
UpdateIdleTimer();
@@ -1965,20 +1938,32 @@ bool QuicSession::ReceivePacket(
19651938
case NGTCP2_ERR_DRAINING:
19661939
case NGTCP2_ERR_RECV_VERSION_NEGOTIATION:
19671940
break;
1941+
case NGTCP2_ERR_RETRY:
1942+
// This should only ever happen on the server
1943+
CHECK(is_server());
1944+
socket()->SendRetry(scid_, dcid_, local_address_, remote_address_);
1945+
Close(QuicSessionListener::SESSION_CLOSE_FLAG_SILENT);
1946+
break;
1947+
case NGTCP2_ERR_DROP_CONN:
1948+
Close(QuicSessionListener::SESSION_CLOSE_FLAG_SILENT);
1949+
break;
19681950
default:
1969-
// Per ngtcp2: If NGTCP2_ERR_RETRY is returned,
1970-
// QuicSession must be a server and must perform
1971-
// address validation by sending a Retry packet
1972-
// then immediately close the connection.
1973-
if (err == NGTCP2_ERR_RETRY && is_server()) {
1974-
socket()->SendRetry(scid_, dcid_, local_address_, remote_address_);
1975-
Close(QuicSessionListener::SESSION_CLOSE_FLAG_SILENT);
1976-
break;
1977-
}
19781951
set_last_error(QUIC_ERROR_SESSION, err);
19791952
return false;
19801953
}
19811954
}
1955+
1956+
if (is_destroyed()) {
1957+
Debug(this, "Session was destroyed while processing the received packet");
1958+
// If the QuicSession has been destroyed but it is not
1959+
// in the closing period, a CONNECTION_CLOSE has not yet
1960+
// been sent to the peer. Let's attempt to send one. This
1961+
// will have the effect of setting the idle timer to the
1962+
// closing/draining period, after which the QuicSession
1963+
// will be destroyed.
1964+
return is_in_closing_period() ? true : SendConnectionClose();
1965+
}
1966+
19821967
return true;
19831968
}
19841969

@@ -2121,6 +2106,9 @@ bool QuicSession::SendConnectionClose() {
21212106
Debug(this, "Connection Close code: %" PRIu64 " (family: %s)",
21222107
error.code, error.family_name());
21232108

2109+
Debug(this, "Setting the connection/draining period timer");
2110+
UpdateClosingTimer();
2111+
21242112
// If initial keys have not yet been installed, use the alternative
21252113
// ImmediateConnectionClose to send a stateless connection close to
21262114
// the peer.
@@ -2135,11 +2123,12 @@ bool QuicSession::SendConnectionClose() {
21352123
return true;
21362124
}
21372125

2138-
UpdateIdleTimer();
21392126
switch (crypto_context_->side()) {
21402127
case NGTCP2_CRYPTO_SIDE_SERVER: {
2141-
if (!is_in_closing_period() && !StartClosingPeriod())
2128+
if (!is_in_closing_period() && !StartClosingPeriod()) {
2129+
Close(QuicSessionListener::SESSION_CLOSE_FLAG_SILENT);
21422130
return false;
2131+
}
21432132
CHECK_GT(conn_closebuf_->length(), 0);
21442133
return SendPacket(QuicPacket::Copy(conn_closebuf_));
21452134
}
@@ -2157,6 +2146,7 @@ bool QuicSession::SendConnectionClose() {
21572146
if (UNLIKELY(nwrite < 0)) {
21582147
Debug(this, "Error writing connection close: %d", nwrite);
21592148
set_last_error(QUIC_ERROR_SESSION, static_cast<int>(nwrite));
2149+
Close(QuicSessionListener::SESSION_CLOSE_FLAG_SILENT);
21602150
return false;
21612151
}
21622152
packet->set_length(nwrite);
@@ -2330,16 +2320,12 @@ bool QuicSession::StartClosingPeriod() {
23302320
if (is_in_closing_period())
23312321
return true;
23322322

2333-
StopRetransmitTimer();
2334-
UpdateIdleTimer();
2335-
23362323
QuicError error = last_error();
23372324
Debug(this, "Closing period has started. Error %d", error.code);
23382325

23392326
// Once the CONNECTION_CLOSE packet is written,
23402327
// is_in_closing_period will return true.
2341-
conn_closebuf_ = QuicPacket::Create(
2342-
"server connection close");
2328+
conn_closebuf_ = QuicPacket::Create("server connection close");
23432329
ssize_t nwrite =
23442330
SelectCloseFn(error.family)(
23452331
connection(),
@@ -2349,12 +2335,7 @@ bool QuicSession::StartClosingPeriod() {
23492335
error.code,
23502336
uv_hrtime());
23512337
if (nwrite < 0) {
2352-
if (nwrite == NGTCP2_ERR_PKT_NUM_EXHAUSTED) {
2353-
set_last_error(QUIC_ERROR_SESSION, NGTCP2_ERR_PKT_NUM_EXHAUSTED);
2354-
Close(QuicSessionListener::SESSION_CLOSE_FLAG_SILENT);
2355-
} else {
2356-
set_last_error(QUIC_ERROR_SESSION, static_cast<int>(nwrite));
2357-
}
2338+
set_last_error(QUIC_ERROR_SESSION, static_cast<int>(nwrite));
23582339
return false;
23592340
}
23602341
conn_closebuf_->set_length(nwrite);
@@ -2450,6 +2431,8 @@ void QuicSession::UpdateConnectionID(
24502431
// will be silently closed. It is important to update this as activity
24512432
// occurs to keep the idle timer from firing.
24522433
void QuicSession::UpdateIdleTimer() {
2434+
if (is_closing_timer_enabled())
2435+
return;
24532436
uint64_t now = uv_hrtime();
24542437
uint64_t expiry = ngtcp2_conn_get_idle_expiry(connection());
24552438
// nano to millis
@@ -2459,6 +2442,15 @@ void QuicSession::UpdateIdleTimer() {
24592442
idle_.Update(timeout, timeout);
24602443
}
24612444

2445+
void QuicSession::UpdateClosingTimer() {
2446+
set_closing_timer_enabled(true);
2447+
uint64_t timeout =
2448+
is_server() ? (ngtcp2_conn_get_pto(connection()) / 1000000ULL) * 3 : 0;
2449+
Debug(this, "Setting closing timeout to %" PRIu64, timeout);
2450+
retransmit_.Stop();
2451+
idle_.Update(timeout, 0);
2452+
idle_.Ref();
2453+
}
24622454

24632455
// Write any packets current pending for the ngtcp2 connection based on
24642456
// the current state of the QuicSession. If the QuicSession is in the

src/quic/node_quic_session.h

+4-1
Original file line numberDiff line numberDiff line change
@@ -696,7 +696,8 @@ class QuicApplication : public MemoryRetainer,
696696
V(NGTCP2_CALLBACK, in_ngtcp2_callback) \
697697
V(CONNECTION_CLOSE_SCOPE, in_connection_close_scope) \
698698
V(SILENT_CLOSE, silent_closing) \
699-
V(STATELESS_RESET, stateless_reset)
699+
V(STATELESS_RESET, stateless_reset) \
700+
V(CLOSING_TIMER_ENABLED, closing_timer_enabled)
700701

701702
// QUIC sessions are logical connections that exchange data
702703
// back and forth between peer endpoints via UDP. Every QuicSession
@@ -1403,6 +1404,8 @@ class QuicSession final : public AsyncWrap,
14031404

14041405
void UpdateIdleTimer();
14051406

1407+
void UpdateClosingTimer();
1408+
14061409
inline void UpdateRetransmitTimer(uint64_t timeout);
14071410

14081411
inline void StopRetransmitTimer();

0 commit comments

Comments
 (0)