@@ -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
@@ -1152,10 +1199,22 @@ void Endpoint::Receive(const uv_buf_t& buf,
11521199 DCHECK_NOT_NULL (session);
11531200 if (session->is_destroyed ()) return ;
11541201 size_t len = store.length ();
1155- if (session->Receive (std::move (store), local_address, remote_address)) {
1202+ // Use ReadPacket (no SendPendingDataScope) so that multiple packets
1203+ // received in the same I/O burst are processed before any responses
1204+ // are generated. The deferred flush via BindingData's uv_check
1205+ // callback calls SendPendingData once per dirty session after all
1206+ // packets in the burst have been read.
1207+ if (session->ReadPacket (std::move (store), local_address, remote_address)) {
11561208 STAT_INCREMENT_N (Stats, bytes_received, len);
11571209 STAT_INCREMENT (Stats, packets_received);
11581210 }
1211+ // Schedule the session for deferred SendPendingData if it hasn't
1212+ // been scheduled already in this burst.
1213+ if (!session->is_destroyed () && !session->pending_flush_ ) {
1214+ session->pending_flush_ = true ;
1215+ BindingData::Get (env ()).ScheduleSessionFlush (
1216+ BaseObjectPtr<Session>(session));
1217+ }
11591218 };
11601219
11611220 const auto accept = [&](const Session::Config& config, Store&& store) {
0 commit comments