@@ -492,6 +492,14 @@ int Endpoint::UDP::Send(Packet::Ptr packet) {
492492 return err;
493493}
494494
495+ int Endpoint::UDP::TrySend (Packet* packet) {
496+ DCHECK_NOT_NULL (packet);
497+ if (is_closed_or_closing ()) return UV_EBADF;
498+ uv_buf_t buf = *packet;
499+ return uv_udp_try_send (
500+ &impl_->handle_ , &buf, 1 , packet->destination ().data ());
501+ }
502+
495503void Endpoint::UDP::MemoryInfo (MemoryTracker* tracker) const {
496504 if (impl_) tracker->TrackField (" impl" , impl_);
497505}
@@ -812,6 +820,45 @@ void Endpoint::Send(Packet::Ptr packet) {
812820 STAT_INCREMENT (Stats, packets_sent);
813821}
814822
823+ void Endpoint::SendOrTrySend (Packet::Ptr packet) {
824+ #ifdef DEBUG
825+ if (is_diagnostic_packet_loss (options_.tx_loss )) [[unlikely]] {
826+ return ;
827+ }
828+ #endif
829+
830+ if (is_closed () || is_closing () || packet->length () == 0 ) {
831+ return ;
832+ }
833+
834+ Debug (this , " TrySend %s" , packet->ToString ());
835+ size_t packet_length = packet->length ();
836+
837+ // Attempt synchronous send. On success (returns number of bytes sent),
838+ // the packet is delivered immediately — no callback overhead, no
839+ // waiting for the next poll cycle.
840+ int err = udp_.TrySend (packet.get ());
841+ if (err >= 0 ) {
842+ // Synchronous send succeeded. Release the packet immediately.
843+ STAT_INCREMENT_N (Stats, bytes_sent, packet_length);
844+ STAT_INCREMENT (Stats, packets_sent);
845+ // Ptr destructor releases back to arena pool.
846+ return ;
847+ }
848+
849+ if (err == UV_EAGAIN) {
850+ // Socket not writable or async sends are queued. Fall back to the
851+ // async path — the packet will be queued and flushed on the next
852+ // POLLOUT cycle.
853+ Debug (this , " TrySend got EAGAIN, falling back to async Send" );
854+ return Send (std::move (packet));
855+ }
856+
857+ // Other errors are fatal.
858+ Debug (this , " TrySend failed with error %d" , err);
859+ Destroy (CloseContext::SEND_FAILURE, err);
860+ }
861+
815862void Endpoint::SendRetry (const PathDescriptor& options) {
816863 // Generating and sending retry packets does consume some system resources,
817864 // and it is possible for a malicious peer to trigger sending a large number
@@ -1128,10 +1175,22 @@ void Endpoint::Receive(const uv_buf_t& buf,
11281175 DCHECK_NOT_NULL (session);
11291176 if (session->is_destroyed ()) return ;
11301177 size_t len = store.length ();
1131- if (session->Receive (std::move (store), local_address, remote_address)) {
1178+ // Use ReadPacket (no SendPendingDataScope) so that multiple packets
1179+ // received in the same I/O burst are processed before any responses
1180+ // are generated. The deferred flush via BindingData's uv_check
1181+ // callback calls SendPendingData once per dirty session after all
1182+ // packets in the burst have been read.
1183+ if (session->ReadPacket (std::move (store), local_address, remote_address)) {
11321184 STAT_INCREMENT_N (Stats, bytes_received, len);
11331185 STAT_INCREMENT (Stats, packets_received);
11341186 }
1187+ // Schedule the session for deferred SendPendingData if it hasn't
1188+ // been scheduled already in this burst.
1189+ if (!session->is_destroyed () && !session->pending_flush_ ) {
1190+ session->pending_flush_ = true ;
1191+ BindingData::Get (env ()).ScheduleSessionFlush (
1192+ BaseObjectPtr<Session>(session));
1193+ }
11351194 };
11361195
11371196 const auto accept = [&](const Session::Config& config, Store&& store) {
0 commit comments