@@ -513,11 +513,18 @@ static void suspended_state_destructor(ErlNifEnv *env, void *obj) {
513513 (void )env ;
514514 suspended_state_t * state = (suspended_state_t * )obj ;
515515
516+ /* Release the worker resource kept alive in create_suspended_state_ex. */
517+ if (state -> worker != NULL ) {
518+ enif_release_resource (state -> worker );
519+ state -> worker = NULL ;
520+ }
521+
516522 /* Clean up Python objects if Python is still initialized.
517523 * suspended_state_t is used with the worker-based API which runs in
518524 * the main interpreter, so we always use PyGILState_Ensure. */
519525 if (runtime_is_running () && state -> callback_args != NULL ) {
520526 if (PyGILState_GetThisThreadState () != NULL || PyGILState_Check ()) {
527+ Py_XDECREF (state -> callback_args );
521528 state -> callback_args = NULL ;
522529 } else {
523530 PyGILState_STATE gstate = PyGILState_Ensure ();
@@ -1700,6 +1707,11 @@ static ERL_NIF_TERM nif_set_callback_handler(ErlNifEnv *env, int argc, const ERL
17001707 if (pipe (worker -> callback_pipe ) < 0 ) {
17011708 return make_error (env , "pipe_failed" );
17021709 }
1710+ /* Non-blocking write end so write_all_with_deadline can bound the write. */
1711+ {
1712+ int wfl = fcntl (worker -> callback_pipe [1 ], F_GETFL , 0 );
1713+ if (wfl >= 0 ) (void )fcntl (worker -> callback_pipe [1 ], F_SETFL , wfl | O_NONBLOCK );
1714+ }
17031715
17041716 worker -> has_callback_handler = true;
17051717
@@ -1708,6 +1720,10 @@ static ERL_NIF_TERM nif_set_callback_handler(ErlNifEnv *env, int argc, const ERL
17081720 enif_make_int (env , worker -> callback_pipe [1 ]));
17091721}
17101722
1723+ /* Bound for callback-response pipe writes: a stalled reader must not block a
1724+ * dirty scheduler forever (the pipe write ends are set non-blocking). */
1725+ #define CALLBACK_RESPONSE_IO_TIMEOUT_MS 30000
1726+
17111727static ERL_NIF_TERM nif_send_callback_response (ErlNifEnv * env , int argc , const ERL_NIF_TERM argv []) {
17121728 (void )argc ;
17131729 int fd ;
@@ -1721,15 +1737,16 @@ static ERL_NIF_TERM nif_send_callback_response(ErlNifEnv *env, int argc, const E
17211737 return make_error (env , "invalid_response" );
17221738 }
17231739
1724- /* Write length then data */
1740+ /* Write length then data with a timed, non-blocking writer (the pipe write
1741+ * end is O_NONBLOCK) so a stalled reader or a large payload can't block a
1742+ * dirty scheduler forever or desync the length-framed protocol on EINTR. */
17251743 uint32_t len = (uint32_t )response .size ;
1726- ssize_t n = write ( fd , & len , sizeof (len ));
1727- if ( n != sizeof ( len ) ) {
1744+ if ( write_all_with_deadline ( fd , & len , sizeof (len ),
1745+ CALLBACK_RESPONSE_IO_TIMEOUT_MS ) != WRITE_OK ) {
17281746 return make_error (env , "write_length_failed" );
17291747 }
1730-
1731- n = write (fd , response .data , response .size );
1732- if (n != (ssize_t )response .size ) {
1748+ if (write_all_with_deadline (fd , response .data , response .size ,
1749+ CALLBACK_RESPONSE_IO_TIMEOUT_MS ) != WRITE_OK ) {
17331750 return make_error (env , "write_data_failed" );
17341751 }
17351752
@@ -4702,6 +4719,11 @@ static ERL_NIF_TERM nif_context_create(ErlNifEnv *env, int argc, const ERL_NIF_T
47024719 enif_release_resource (ctx );
47034720 return make_error (env , "pipe_create_failed" );
47044721 }
4722+ /* Non-blocking write end so write_all_with_deadline can bound the write. */
4723+ {
4724+ int wfl = fcntl (ctx -> callback_pipe [1 ], F_GETFL , 0 );
4725+ if (wfl >= 0 ) (void )fcntl (ctx -> callback_pipe [1 ], F_SETFL , wfl | O_NONBLOCK );
4726+ }
47054727
47064728#ifdef HAVE_SUBINTERPRETERS
47074729 ctx -> uses_own_gil = false;
@@ -6579,15 +6601,17 @@ static ERL_NIF_TERM nif_context_write_callback_response(ErlNifEnv *env, int argc
65796601 return make_error (env , "pipe_not_initialized" );
65806602 }
65816603
6582- /* Write length prefix (4 bytes, native endianness - must match read_length_prefixed_data) */
6604+ /* Write length prefix + data with a timed, non-blocking writer (the pipe
6605+ * write end is O_NONBLOCK) so a stalled reader or large payload can't block a
6606+ * dirty scheduler forever or desync the framed protocol. 4-byte native-endian
6607+ * length must match read_length_prefixed_data. */
65836608 uint32_t len = (uint32_t )data .size ;
6584- ssize_t written = write ( ctx -> callback_pipe [1 ], & len , sizeof (len ));
6585- if ( written != sizeof ( len ) ) {
6609+ if ( write_all_with_deadline ( ctx -> callback_pipe [1 ], & len , sizeof (len ),
6610+ CALLBACK_RESPONSE_IO_TIMEOUT_MS ) != WRITE_OK ) {
65866611 return make_error (env , "write_failed" );
65876612 }
6588-
6589- written = write (ctx -> callback_pipe [1 ], data .data , data .size );
6590- if (written != (ssize_t )data .size ) {
6613+ if (write_all_with_deadline (ctx -> callback_pipe [1 ], data .data , data .size ,
6614+ CALLBACK_RESPONSE_IO_TIMEOUT_MS ) != WRITE_OK ) {
65916615 return make_error (env , "write_failed" );
65926616 }
65936617
@@ -6639,7 +6663,13 @@ static ERL_NIF_TERM nif_context_resume(ErlNifEnv *env, int argc, const ERL_NIF_T
66396663 return make_error (env , "context_mismatch" );
66406664 }
66416665
6642- /* Store the callback result */
6666+ /* Store the callback result. Free any prior result first to avoid leaking it
6667+ * on a duplicate/raced resume (result_data, not the toggling has_result flag,
6668+ * is the real pending-result indicator). */
6669+ if (state -> result_data != NULL ) {
6670+ enif_free (state -> result_data );
6671+ state -> result_data = NULL ;
6672+ }
66436673 state -> result_data = enif_alloc (result_bin .size );
66446674 if (state -> result_data == NULL ) {
66456675 return make_error (env , "alloc_failed" );
@@ -7812,7 +7842,7 @@ static ErlNifFunc nif_funcs[] = {
78127842
78137843 /* Callback support */
78147844 {"set_callback_handler" , 2 , nif_set_callback_handler , 0 },
7815- {"send_callback_response" , 2 , nif_send_callback_response , 0 },
7845+ {"send_callback_response" , 2 , nif_send_callback_response , ERL_NIF_DIRTY_JOB_IO_BOUND },
78167846 {"resume_callback" , 2 , nif_resume_callback , 0 },
78177847
78187848 /* Async worker management */
@@ -7961,7 +7991,7 @@ static ErlNifFunc nif_funcs[] = {
79617991 {"context_interp_id" , 1 , nif_context_interp_id , 0 },
79627992 {"context_set_callback_handler" , 2 , nif_context_set_callback_handler , 0 },
79637993 {"context_get_callback_pipe" , 1 , nif_context_get_callback_pipe , 0 },
7964- {"context_write_callback_response" , 2 , nif_context_write_callback_response , 0 },
7994+ {"context_write_callback_response" , 2 , nif_context_write_callback_response , ERL_NIF_DIRTY_JOB_IO_BOUND },
79657995 {"context_resume" , 3 , nif_context_resume , ERL_NIF_DIRTY_JOB_CPU_BOUND },
79667996 {"context_cancel_resume" , 2 , nif_context_cancel_resume , 0 },
79677997 {"context_get_event_loop" , 1 , nif_context_get_event_loop , 0 },
0 commit comments