@@ -140,6 +140,8 @@ void TCPServer::shutdown()
140140 }
141141 // This will effectively deactivate the disconnection handler.
142142 client_fds_.clear ();
143+ // Worker thread has been joined above, so the poll set is no longer in use; keep it consistent.
144+ pollfds_.clear ();
143145 ur_close (shutdown_socket);
144146 ur_close (listen_fd_);
145147 listen_fd_ = INVALID_SOCKET ;
@@ -225,6 +227,7 @@ void TCPServer::handleConnect()
225227 if (client_fds_.size () < max_clients_allowed_ || max_clients_allowed_ == 0 )
226228 {
227229 client_fds_.push_back (client_fd);
230+ rebuildPollfds ();
228231 accepted = true ;
229232 }
230233 else
@@ -244,28 +247,32 @@ void TCPServer::handleConnect()
244247 }
245248}
246249
247- void TCPServer::spin ()
250+ void TCPServer::rebuildPollfds ()
248251{
249- // Build the poll set fresh each iteration from the listen socket plus all currently connected
250- // clients. poll() is used on both platforms (WSAPoll() on Windows) because it has no
251- // FD_SETSIZE limit on file descriptor numbers, unlike select(). This matters when the hosting
252- // process holds many file descriptors (e.g. a JVM), pushing socket FDs past FD_SETSIZE (1024).
253- std::vector<struct pollfd > pollfds;
254- pollfds.push_back ({ static_cast <socket_t >(listen_fd_), POLLIN , 0 });
252+ // clear() + push_back() reuses the vector's existing capacity, so this does not allocate once
253+ // the set has reached a given size.
254+ pollfds_.clear ();
255+ pollfds_.push_back ({ static_cast <socket_t >(listen_fd_), POLLIN , 0 });
256+ for (const auto & client_fd : client_fds_)
255257 {
256- std::lock_guard<std::mutex> lk (clients_mutex_);
257- for (const auto & client_fd : client_fds_)
258- {
259- pollfds.push_back ({ client_fd, POLLIN , 0 });
260- }
258+ pollfds_.push_back ({ client_fd, POLLIN , 0 });
261259 }
260+ }
262261
262+ void TCPServer::spin ()
263+ {
264+ // Poll the cached set (pollfds_) directly. It is kept in sync with client_fds_ via
265+ // rebuildPollfds() on every connect/disconnect, so no allocation happens here in steady state.
266+ // poll() is used on both platforms (WSAPoll() on Windows) because it has no FD_SETSIZE limit on
267+ // file descriptor numbers, unlike select(). This matters when the hosting process holds many
268+ // file descriptors (e.g. a JVM), pushing socket FDs past FD_SETSIZE (1024).
269+ //
263270 // Block for up to 1 s waiting for activity on any socket. A shutdown wakes this immediately by
264271 // connecting to the listen socket (see shutdown()).
265272#ifdef _WIN32
266- int ready = ::WSAPoll (pollfds .data (), static_cast <ULONG >(pollfds .size ()), 1000 );
273+ int ready = ::WSAPoll (pollfds_ .data (), static_cast <ULONG >(pollfds_ .size ()), 1000 );
267274#else
268- int ready = ::poll (pollfds .data (), pollfds .size (), 1000 );
275+ int ready = ::poll (pollfds_ .data (), pollfds_ .size (), 1000 );
269276#endif
270277 if (ready < 0 )
271278 {
@@ -279,24 +286,29 @@ void TCPServer::spin()
279286 return ;
280287 }
281288
282- if (pollfds[0 ].revents & POLLIN )
283- {
284- URCL_LOG_DEBUG (" Activity on listen FD %d" , (int )listen_fd_);
285- handleConnect ();
286- }
289+ // Snapshot the poll results into locals BEFORE handleConnect()/handleDisconnect() rebuild
290+ // pollfds_, otherwise activity on an existing client that arrives in the same poll cycle as a
291+ // new connection would be lost.
292+ const bool listen_activity = (pollfds_[0 ].revents & POLLIN ) != 0 ;
287293
288294 std::vector<socket_t > disconnected_clients;
289295 std::vector<socket_t > client_fds_with_activity;
290296
291- // pollfds [0] is the listen socket; client entries start at index 1.
292- for (size_t i = 1 ; i < pollfds .size (); ++i)
297+ // pollfds_ [0] is the listen socket; client entries start at index 1.
298+ for (size_t i = 1 ; i < pollfds_ .size (); ++i)
293299 {
294- if (pollfds [i].revents & (POLLIN | POLLHUP | POLLERR ))
300+ if (pollfds_ [i].revents & (POLLIN | POLLHUP | POLLERR ))
295301 {
296- URCL_LOG_DEBUG (" Activity on client FD %d" , (int )pollfds [i].fd );
297- client_fds_with_activity.push_back (static_cast <socket_t >(pollfds [i].fd ));
302+ URCL_LOG_DEBUG (" Activity on client FD %d" , (int )pollfds_ [i].fd );
303+ client_fds_with_activity.push_back (static_cast <socket_t >(pollfds_ [i].fd ));
298304 }
299305 }
306+
307+ if (listen_activity)
308+ {
309+ URCL_LOG_DEBUG (" Activity on listen FD %d" , (int )listen_fd_);
310+ handleConnect ();
311+ }
300312 // We handle client activity outside the clients_mutex_ lock to avoid holding it during potentially slow I/O and
301313 // message callbacks.
302314 // The clients_mutex_ lock is only needed to protect the client_fds_ vector, but once we have copied the FDs with
@@ -329,6 +341,7 @@ void TCPServer::handleDisconnect(const socket_t fd)
329341 break ;
330342 }
331343 }
344+ rebuildPollfds ();
332345 }
333346
334347 {
@@ -394,6 +407,16 @@ void TCPServer::start()
394407{
395408 URCL_LOG_DEBUG (" Starting worker thread" );
396409 keep_running_ = true ;
410+ // Seed the poll set with the listen socket before the worker thread starts polling it. Reserve
411+ // room for the listen socket plus the expected number of clients up front so the first
412+ // connections don't reallocate. A bounded server reserves its exact client limit; an unbounded
413+ // one (max_clients_allowed_ == 0) reserves DEFAULT_RESERVED_CLIENTS as headroom.
414+ {
415+ std::lock_guard<std::mutex> lk (clients_mutex_);
416+ const uint32_t expected_clients = max_clients_allowed_ > 0 ? max_clients_allowed_ : DEFAULT_RESERVED_CLIENTS ;
417+ pollfds_.reserve (static_cast <size_t >(expected_clients) + 1 );
418+ rebuildPollfds ();
419+ }
397420 worker_thread_ = std::thread (&TCPServer::worker, this );
398421}
399422
0 commit comments