Skip to content

Commit d3f9b36

Browse files
committed
Fix 4 critical issues
Signed-off-by: Tim Paine <3105306+timkpaine@users.noreply.github.com>
1 parent 1ffd009 commit d3f9b36

11 files changed

Lines changed: 275 additions & 583 deletions

File tree

cpp/csp/core/QueueWaiter.h

Lines changed: 63 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
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.
6469
class FdWaiter
6570
{
6671
public:
@@ -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

172201
private:
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
}

cpp/csp/engine/RootEngine.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ RootEngine::RootEngine( const Dictionary & settings ) : Engine( m_cycleStepTable
9393

9494
RootEngine::~RootEngine()
9595
{
96+
// Defense in depth for callers of the decomposed API that abandon an engine without finish()
97+
m_fdWaiterEnabled.store( false, std::memory_order_relaxed );
9698
}
9799

98100
bool RootEngine::interrupted() const
@@ -115,6 +117,8 @@ void RootEngine::preRun( DateTime start, DateTime end )
115117
void RootEngine::postRun()
116118
{
117119
m_state = State::SHUTDOWN;
120+
// Disarm before stopping adapters so producer threads stop reaching for the fd on their way out
121+
m_fdWaiterEnabled.store( false, std::memory_order_relaxed );
118122
stop();
119123
}
120124

cpp/csp/engine/RootEngine.h

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <csp/engine/PushEvent.h>
1515
#include <csp/engine/PushPullEvent.h>
1616
#include <csp/engine/Scheduler.h>
17+
#include <atomic>
1718
#include <memory>
1819

1920
namespace csp
@@ -81,8 +82,8 @@ class RootEngine : public Engine
8182

8283
void cancelCallback( Scheduler::Handle handle );
8384

84-
void schedulePushEvent( PushEvent * event ) { m_pushEventQueue.push( event ); m_fdWaiter.notify(); }
85-
void schedulePushBatch( PushEventQueue::Batch & batch ) { m_pushEventQueue.push( batch ); m_fdWaiter.notify(); }
85+
void schedulePushEvent( PushEvent * event ) { m_pushEventQueue.push( event ); notifyFdWaiter(); }
86+
void schedulePushBatch( PushEventQueue::Batch & batch ) { m_pushEventQueue.push( batch ); notifyFdWaiter(); }
8687

8788
bool scheduleEndCycleListener( EndCycleListener * l );
8889

@@ -106,8 +107,15 @@ class RootEngine : public Engine
106107
PushPullEventQueue & pushPullEventQueue() { return m_pushPullEventQueue; }
107108

108109
// Native fd-based wakeup for external event loops (asyncio, etc.)
109-
// Returns a file descriptor that becomes readable when events are queued
110-
int getWakeupFd() const { return m_fdWaiter.readFd(); }
110+
// Returns a file descriptor that becomes readable when events are queued. Asking for the fd
111+
// is what arms the signalling: until then the push path does no fd work at all. Arming is
112+
// one-way for the life of the run; it is disarmed only at teardown.
113+
int getWakeupFd()
114+
{
115+
if( m_fdWaiter.isValid() )
116+
m_fdWaiterEnabled.store( true, std::memory_order_relaxed );
117+
return m_fdWaiter.readFd();
118+
}
111119
void clearWakeupFd() { m_fdWaiter.clear(); }
112120

113121
protected:
@@ -123,6 +131,15 @@ class RootEngine : public Engine
123131

124132
void processEndCycle();
125133

134+
// Signals only once a consumer has armed the fd, keeping the push hot path syscall-free.
135+
// The flag carries no data, hence relaxed; it is a hint, not a synchronization point, and it
136+
// does not make teardown safe - adapters must still be stopped before the engine is destroyed.
137+
void notifyFdWaiter()
138+
{
139+
if( m_fdWaiterEnabled.load( std::memory_order_relaxed ) ) [[unlikely]]
140+
m_fdWaiter.notify();
141+
}
142+
126143
struct Settings
127144
{
128145
Settings( const Dictionary & );
@@ -161,6 +178,9 @@ class RootEngine : public Engine
161178
std::mutex m_exception_mutex;
162179
std::unique_ptr<csp::Profiler> m_profiler;
163180
mutable FdWaiter m_fdWaiter; // For native fd-based event loop integration
181+
// Only armed once a consumer asks for the fd, and disarmed at teardown, so the default
182+
// engine.run() path never touches the fd from the push hot path
183+
std::atomic<bool> m_fdWaiterEnabled{ false };
164184

165185
};
166186

cpp/csp/python/CMakeLists.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ set(CSPIMPL_PUBLIC_HEADERS
3232
NumpyConversions.h
3333
NumpyInputAdapter.h
3434
PyAdapterManagerWrapper.h
35-
PyEventLoop.h
3635
PyBasketInputProxy.h
3736
PyBasketOutputProxy.h
3837
PyCppNode.h
@@ -57,7 +56,6 @@ add_library(cspimpl SHARED
5756
NumpyConversions.cpp
5857
PyAdapterManager.cpp
5958
PyAdapterManagerWrapper.cpp
60-
PyEventLoop.cpp
6159
PyConstAdapter.cpp
6260
PyCppNode.cpp
6361
PyEngine.cpp

cpp/csp/python/PyEngine.cpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ static PyObject * PyEngine_start( PyEngine * self, PyObject * args )
135135
CSP_TRUE_OR_THROW_RUNTIME( self -> engine() -> isRootEngine(), "engine is not root engine" );
136136
self -> rootEngine() -> start( start, end );
137137

138+
if( PyErr_Occurred() )
139+
return nullptr;
140+
138141
Py_RETURN_NONE;
139142
CSP_RETURN_NONE;
140143
}
@@ -155,6 +158,10 @@ static PyObject * PyEngine_processOneCycle( PyEngine * self, PyObject * args )
155158
TimeDelta maxWait = TimeDelta::fromNanoseconds( maxWaitNanos );
156159
bool hasMore = self -> rootEngine() -> processOneCycle( maxWait );
157160

161+
// dialectLockGIL runs PyErr_CheckSignals, so a KeyboardInterrupt may be pending here
162+
if( PyErr_Occurred() )
163+
return nullptr;
164+
158165
return PyBool_FromLong( hasMore );
159166
CSP_RETURN_NONE;
160167
}
@@ -166,7 +173,7 @@ static PyObject * PyEngine_finish( PyEngine * self, PyObject * args )
166173
CSP_TRUE_OR_THROW_RUNTIME( self -> engine() -> isRootEngine(), "engine is not root engine" );
167174
self -> rootEngine() -> finish();
168175

169-
return self -> collectOutputs();
176+
return PyErr_Occurred() ? nullptr : self -> collectOutputs();
170177
CSP_RETURN_NONE;
171178
}
172179

@@ -233,7 +240,7 @@ static PyMethodDef PyEngine_methods[] = {
233240
{ "now", ( PyCFunction ) PyEngine_now, METH_NOARGS, "get current engine time" },
234241
{ "next_scheduled_time", ( PyCFunction ) PyEngine_nextScheduledTime, METH_NOARGS, "get next scheduled event time" },
235242
{ "get_wakeup_fd", ( PyCFunction ) PyEngine_getWakeupFd, METH_NOARGS, "get fd that becomes readable when events are queued" },
236-
{ "clear_wakeup_fd", ( PyCFunction ) PyEngine_clearWakeupFd, METH_NOARGS, "clear the wakeup fd after processing events" },
243+
{ "clear_wakeup_fd", ( PyCFunction ) PyEngine_clearWakeupFd, METH_NOARGS, "drain the wakeup fd; call before processing events, never after" },
237244
{ NULL }
238245
};
239246

0 commit comments

Comments
 (0)