@@ -69,6 +69,9 @@ void channel_resource_dtor(ErlNifEnv *env, void *obj) {
6969 channel -> queue = NULL ;
7070 }
7171
72+ /* Wake any threads waiting on the condition before destroying */
73+ pthread_cond_broadcast (& channel -> data_cond );
74+ pthread_cond_destroy (& channel -> data_cond );
7275 pthread_mutex_destroy (& channel -> mutex );
7376}
7477
@@ -91,8 +94,6 @@ py_channel_t *channel_alloc(size_t max_size) {
9194
9295 channel -> max_size = max_size ;
9396 channel -> current_size = 0 ;
94- channel -> waiting = NULL ;
95- channel -> waiting_callback_id = 0 ;
9697 channel -> waiter_loop = NULL ;
9798 channel -> waiter_callback_id = 0 ;
9899 channel -> has_waiter = false;
@@ -107,6 +108,13 @@ py_channel_t *channel_alloc(size_t max_size) {
107108 return NULL ;
108109 }
109110
111+ if (pthread_cond_init (& channel -> data_cond , NULL ) != 0 ) {
112+ pthread_mutex_destroy (& channel -> mutex );
113+ enif_ioq_destroy (channel -> queue );
114+ enif_release_resource (channel );
115+ return NULL ;
116+ }
117+
110118 return channel ;
111119}
112120
@@ -141,9 +149,6 @@ int channel_send(py_channel_t *channel, const unsigned char *data, size_t size)
141149
142150 channel -> current_size += size ;
143151
144- /* Check if there's a waiting context to resume */
145- bool should_resume = (channel -> waiting != NULL );
146-
147152 /* Check if there's an async waiter to dispatch.
148153 * IMPORTANT: Clear waiter state BEFORE releasing mutex to avoid race condition.
149154 * With task_ready notification, the callback can fire before we re-acquire the mutex.
@@ -171,12 +176,10 @@ int channel_send(py_channel_t *channel, const unsigned char *data, size_t size)
171176 channel -> has_sync_waiter = false;
172177 }
173178
174- pthread_mutex_unlock (& channel -> mutex );
179+ /* Signal any threads waiting on the condition variable */
180+ pthread_cond_signal (& channel -> data_cond );
175181
176- /* Resume happens outside the lock to avoid deadlocks */
177- if (should_resume ) {
178- channel_resume_waiting (channel );
179- }
182+ pthread_mutex_unlock (& channel -> mutex );
180183
181184 /* Dispatch async waiter via timer dispatch (same path as timers) */
182185 if (loop_to_wake != NULL ) {
@@ -225,9 +228,6 @@ int channel_send_owned_binary(py_channel_t *channel, ErlNifBinary *bin) {
225228
226229 channel -> current_size += msg_size ;
227230
228- /* Check if there's a waiting context to resume */
229- bool should_resume = (channel -> waiting != NULL );
230-
231231 /* Check if there's an async waiter to dispatch.
232232 * IMPORTANT: Clear waiter state BEFORE releasing mutex to avoid race condition.
233233 * With task_ready notification, the callback can fire before we re-acquire the mutex.
@@ -255,11 +255,10 @@ int channel_send_owned_binary(py_channel_t *channel, ErlNifBinary *bin) {
255255 channel -> has_sync_waiter = false;
256256 }
257257
258- pthread_mutex_unlock (& channel -> mutex );
258+ /* Signal any threads waiting on the condition variable */
259+ pthread_cond_signal (& channel -> data_cond );
259260
260- if (should_resume ) {
261- channel_resume_waiting (channel );
262- }
261+ pthread_mutex_unlock (& channel -> mutex );
263262
264263 /* Dispatch async waiter via timer dispatch (same path as timers) */
265264 if (loop_to_wake != NULL ) {
@@ -326,10 +325,80 @@ int channel_try_receive(py_channel_t *channel, unsigned char **out_data, size_t
326325 return 0 ;
327326}
328327
328+ int channel_receive_blocking (py_channel_t * channel , unsigned char * * out_data ,
329+ size_t * out_size , long timeout_ms ) {
330+ pthread_mutex_lock (& channel -> mutex );
331+
332+ /* Calculate deadline for timed wait */
333+ struct timespec deadline ;
334+ if (timeout_ms > 0 ) {
335+ clock_gettime (CLOCK_REALTIME , & deadline );
336+ deadline .tv_sec += timeout_ms / 1000 ;
337+ deadline .tv_nsec += (timeout_ms % 1000 ) * 1000000 ;
338+ if (deadline .tv_nsec >= 1000000000 ) {
339+ deadline .tv_sec ++ ;
340+ deadline .tv_nsec -= 1000000000 ;
341+ }
342+ }
343+
344+ /* Wait for data or close */
345+ while (enif_ioq_size (channel -> queue ) == 0 && !channel -> closed ) {
346+ if (timeout_ms == 0 ) {
347+ /* Non-blocking: return immediately if no data */
348+ pthread_mutex_unlock (& channel -> mutex );
349+ return 1 ; /* Empty/timeout */
350+ } else if (timeout_ms < 0 ) {
351+ /* Infinite wait */
352+ pthread_cond_wait (& channel -> data_cond , & channel -> mutex );
353+ } else {
354+ /* Timed wait */
355+ int rc = pthread_cond_timedwait (& channel -> data_cond , & channel -> mutex , & deadline );
356+ if (rc == ETIMEDOUT ) {
357+ pthread_mutex_unlock (& channel -> mutex );
358+ return 1 ; /* Timeout */
359+ }
360+ }
361+ }
362+
363+ /* Check if closed with no data */
364+ if (channel -> closed && enif_ioq_size (channel -> queue ) == 0 ) {
365+ pthread_mutex_unlock (& channel -> mutex );
366+ return -1 ; /* Closed */
367+ }
368+
369+ /* We have data - dequeue it */
370+ SysIOVec * iov ;
371+ int iovcnt ;
372+ iov = enif_ioq_peek (channel -> queue , & iovcnt );
373+
374+ if (iovcnt == 0 || iov == NULL || iov [0 ].iov_len == 0 ) {
375+ pthread_mutex_unlock (& channel -> mutex );
376+ return 1 ; /* Spurious wakeup, no data */
377+ }
378+
379+ size_t msg_size = iov [0 ].iov_len ;
380+
381+ /* Allocate output buffer */
382+ * out_data = enif_alloc (msg_size );
383+ if (* out_data == NULL ) {
384+ pthread_mutex_unlock (& channel -> mutex );
385+ return -1 ; /* Allocation error */
386+ }
387+
388+ /* Copy data and dequeue */
389+ memcpy (* out_data , iov [0 ].iov_base , msg_size );
390+ * out_size = msg_size ;
391+
392+ enif_ioq_deq (channel -> queue , msg_size , NULL );
393+ channel -> current_size -= msg_size ;
394+
395+ pthread_mutex_unlock (& channel -> mutex );
396+ return 0 ;
397+ }
398+
329399void channel_close (py_channel_t * channel ) {
330400 pthread_mutex_lock (& channel -> mutex );
331401 channel -> closed = true;
332- bool should_resume = (channel -> waiting != NULL );
333402
334403 /* Check if there's an async waiter to dispatch.
335404 * For close, we unconditionally clear the waiter since the channel
@@ -354,11 +423,10 @@ void channel_close(py_channel_t *channel) {
354423 channel -> has_sync_waiter = false;
355424 }
356425
357- pthread_mutex_unlock (& channel -> mutex );
426+ /* Wake all threads waiting on the condition variable */
427+ pthread_cond_broadcast (& channel -> data_cond );
358428
359- if (should_resume ) {
360- channel_resume_waiting (channel );
361- }
429+ pthread_mutex_unlock (& channel -> mutex );
362430
363431 /* Dispatch async waiter to signal closure */
364432 if (loop_to_wake != NULL ) {
@@ -377,13 +445,6 @@ void channel_close(py_channel_t *channel) {
377445 }
378446}
379447
380- void channel_resume_waiting (py_channel_t * channel ) {
381- /* This function would trigger resume of the suspended context.
382- * For now, the actual resume logic is handled in the NIF receive function
383- * by checking if data is available before suspending. */
384- (void )channel ;
385- }
386-
387448int channel_init (ErlNifEnv * env ) {
388449 /* Initialize channel-specific atoms */
389450 ATOM_BUSY = enif_make_atom (env , "busy" );
0 commit comments