88#pragma comment(lib, "ws2_32.lib")
99#endif
1010
11+ #include < cerrno>
1112#include < mutex>
1213#include < condition_variable>
1314#include < csp/core/Time.h>
@@ -61,6 +62,10 @@ class QueueWaiter
6162// FdWaiter provides file descriptor based signaling for integration with
6263// external event loops like asyncio. The read fd can be registered with
6364// select/poll/epoll and will become readable when notify() is called.
65+ //
66+ // Consumers must clear() BEFORE draining the event queue they are guarding, never after.
67+ // Clearing afterwards discards the signal for any event queued between the drain and the clear,
68+ // which strands that event until the next unrelated wakeup.
6469class FdWaiter
6570{
6671public:
@@ -78,9 +83,15 @@ class FdWaiter
7883 {
7984 m_readFd = fds[0 ];
8085 m_writeFd = fds[1 ];
81- // Set non-blocking
82- fcntl ( m_readFd, F_SETFL , O_NONBLOCK );
83- fcntl ( m_writeFd, F_SETFL , O_NONBLOCK );
86+ // notify() writes on every call and must never block, so non-blocking is mandatory
87+ if ( fcntl ( m_readFd, F_SETFL , O_NONBLOCK ) == -1 ||
88+ fcntl ( m_writeFd, F_SETFL , O_NONBLOCK ) == -1 )
89+ {
90+ close ( m_readFd );
91+ close ( m_writeFd );
92+ m_readFd = -1 ;
93+ m_writeFd = -1 ;
94+ }
8495 }
8596 else
8697 {
@@ -121,43 +132,61 @@ class FdWaiter
121132 int readFd () const { return m_readFd; }
122133#endif
123134
124- // Signal the fd (makes it readable)
135+ // Signal the fd ( makes it readable ). Callable from any producer thread.
136+ // Deliberately lock-free: this sits on the push-event hot path. EAGAIN means the eventfd
137+ // counter saturated or the pipe buffer is full, ie the fd is already readable, which is the
138+ // state notify() is trying to reach - so it needs no handling.
125139 void notify ()
126140 {
127- std::lock_guard<std::mutex> guard ( m_lock );
128- if ( m_notified )
129- return ; // Already notified, avoid filling buffer
130-
131- m_notified = true ;
132-
133141#ifdef __linux__
134142 uint64_t val = 1 ;
135- [[maybe_unused]] auto rv = write ( m_eventfd, &val, sizeof ( val ) );
143+ ssize_t rv;
144+ do { rv = write ( m_eventfd, &val, sizeof ( val ) ); } while ( rv < 0 && errno == EINTR );
136145#elif defined(__APPLE__)
137146 char c = 1 ;
138- [[maybe_unused]] auto rv = write ( m_writeFd, &c, 1 );
147+ ssize_t rv;
148+ do { rv = write ( m_writeFd, &c, 1 ); } while ( rv < 0 && errno == EINTR );
139149#elif defined(_WIN32)
140150 char c = 1 ;
141- send ( m_writeFd, &c, 1 , 0 );
151+ int rv;
152+ do { rv = send ( m_writeFd, &c, 1 , 0 ); } while ( rv == SOCKET_ERROR && WSAGetLastError () == WSAEINTR );
142153#endif
154+ ( void ) rv;
143155 }
144156
145- // Clear the notification (call after processing)
157+ // Drain the fd. See the class comment: call this before draining the guarded event queue.
158+ // The drain is bounded - producers are unsynchronized and can refill faster than we read, and
159+ // leaving bytes behind is harmless: the fd simply stays readable and costs one extra cycle.
146160 void clear ()
147161 {
148- std::lock_guard<std::mutex> guard ( m_lock );
149- m_notified = false ;
150-
151162#ifdef __linux__
152163 uint64_t val;
153- [[maybe_unused]] auto rv = read ( m_eventfd, &val, sizeof ( val ) );
164+ ssize_t rv;
165+ do { rv = read ( m_eventfd, &val, sizeof ( val ) ); } while ( rv < 0 && errno == EINTR );
154166#elif defined(__APPLE__)
155- char buf[64 ];
156- while ( read ( m_readFd, buf, sizeof ( buf ) ) > 0 ) {}
167+ char buf[ DRAIN_BUFFER_SIZE ];
168+ ssize_t rv = 0 ;
169+ for ( size_t i = 0 ; i < DRAIN_MAX_READS ; ++i )
170+ {
171+ rv = read ( m_readFd, buf, sizeof ( buf ) );
172+ if ( rv < 0 && errno == EINTR )
173+ continue ;
174+ if ( rv <= 0 )
175+ break ;
176+ }
157177#elif defined(_WIN32)
158- char buf[64 ];
159- while ( recv ( m_readFd, buf, sizeof ( buf ), 0 ) > 0 ) {}
178+ char buf[ DRAIN_BUFFER_SIZE ];
179+ int rv = 0 ;
180+ for ( size_t i = 0 ; i < DRAIN_MAX_READS ; ++i )
181+ {
182+ rv = recv ( m_readFd, buf, sizeof ( buf ), 0 );
183+ if ( rv == SOCKET_ERROR && WSAGetLastError () == WSAEINTR )
184+ continue ;
185+ if ( rv <= 0 )
186+ break ;
187+ }
160188#endif
189+ ( void ) rv;
161190 }
162191
163192 bool isValid () const
@@ -170,6 +199,9 @@ class FdWaiter
170199 }
171200
172201private:
202+ static constexpr size_t DRAIN_BUFFER_SIZE = 64 ;
203+ static constexpr size_t DRAIN_MAX_READS = 64 ;
204+
173205#ifdef _WIN32
174206 void createSocketPair ()
175207 {
@@ -230,10 +262,16 @@ class FdWaiter
230262 return ;
231263 }
232264
233- // Set non-blocking
265+ // Set non-blocking; notify() must never block on a full buffer
234266 u_long mode = 1 ;
235- ioctlsocket ( m_readFd, FIONBIO , &mode );
236- ioctlsocket ( m_writeFd, FIONBIO , &mode );
267+ if ( ioctlsocket ( m_readFd, FIONBIO , &mode ) == SOCKET_ERROR ||
268+ ioctlsocket ( m_writeFd, FIONBIO , &mode ) == SOCKET_ERROR )
269+ {
270+ closesocket ( m_readFd );
271+ closesocket ( m_writeFd );
272+ m_readFd = INVALID_SOCKET ;
273+ m_writeFd = INVALID_SOCKET ;
274+ }
237275 }
238276
239277 SOCKET m_readFd;
@@ -245,8 +283,6 @@ class FdWaiter
245283 int m_eventfd;
246284#endif
247285#endif
248- std::mutex m_lock;
249- bool m_notified = false ;
250286};
251287
252288}
0 commit comments