@@ -101,6 +101,8 @@ struct ImportedStreamState {
101101 abandoned : AtomicBool ,
102102 /// Set to true when handler_release is called (handler was freed by producer).
103103 handler_released : AtomicBool ,
104+ /// Set to true when a producer connects (calls on_schema).
105+ producer_connected : AtomicBool ,
104106 /// Producer state (needs mutex for FFI calls).
105107 producer_state : Mutex < ProducerState > ,
106108}
@@ -119,6 +121,7 @@ impl ImportedStreamState {
119121 pending_requests : AtomicU64 :: new ( 0 ) ,
120122 abandoned : AtomicBool :: new ( false ) ,
121123 handler_released : AtomicBool :: new ( false ) ,
124+ producer_connected : AtomicBool :: new ( false ) ,
122125 producer_state : Mutex :: new ( ProducerState {
123126 producer : null_mut ( ) ,
124127 prefetch_count,
@@ -137,7 +140,10 @@ impl ImportedStreamState {
137140 }
138141
139142 // Lock cannot be poisoned: we never panic while holding it
140- let producer_state = self . producer_state . lock ( ) . expect ( "producer_state mutex poisoned" ) ;
143+ let producer_state = self
144+ . producer_state
145+ . lock ( )
146+ . expect ( "producer_state mutex poisoned" ) ;
141147 if producer_state. producer . is_null ( ) {
142148 return ;
143149 }
@@ -150,26 +156,37 @@ impl ImportedStreamState {
150156 let to_request = prefetch - pending;
151157 if let Some ( request_fn) = unsafe { ( * producer_state. producer ) . request } {
152158 unsafe { request_fn ( producer_state. producer , to_request) } ;
153- self . pending_requests . fetch_add ( to_request, Ordering :: Release ) ;
159+ self . pending_requests
160+ . fetch_add ( to_request, Ordering :: Release ) ;
154161 }
155162 }
156163 }
157164
158165 fn set_producer ( & self , producer : * mut FFI_ArrowAsyncProducer ) {
159166 // Lock cannot be poisoned: we never panic while holding it
160- let mut state = self . producer_state . lock ( ) . expect ( "producer_state mutex poisoned" ) ;
167+ let mut state = self
168+ . producer_state
169+ . lock ( )
170+ . expect ( "producer_state mutex poisoned" ) ;
161171 state. producer = producer;
172+ self . producer_connected . store ( true , Ordering :: Release ) ;
162173 }
163174
164175 fn clear_producer ( & self ) {
165176 // Lock cannot be poisoned: we never panic while holding it
166- let mut state = self . producer_state . lock ( ) . expect ( "producer_state mutex poisoned" ) ;
177+ let mut state = self
178+ . producer_state
179+ . lock ( )
180+ . expect ( "producer_state mutex poisoned" ) ;
167181 state. producer = null_mut ( ) ;
168182 }
169183
170184 fn cancel ( & self ) {
171185 // Lock cannot be poisoned: we never panic while holding it
172- let state = self . producer_state . lock ( ) . expect ( "producer_state mutex poisoned" ) ;
186+ let state = self
187+ . producer_state
188+ . lock ( )
189+ . expect ( "producer_state mutex poisoned" ) ;
173190 if !state. producer . is_null ( ) {
174191 if let Some ( cancel_fn) = unsafe { ( * state. producer ) . cancel } {
175192 unsafe { cancel_fn ( state. producer ) } ;
@@ -211,17 +228,6 @@ unsafe impl Send for ImportedAsyncDeviceStream {}
211228///
212229/// This wrapper ensures the handler is properly cleaned up even if the FFI producer
213230/// never calls `release()`. It provides safe access to the raw pointer for FFI calls.
214- ///
215- /// # Usage
216- ///
217- /// ```ignore
218- /// let (stream, handler) = ImportedAsyncDeviceStream::new(16);
219- ///
220- /// // Pass raw pointer to FFI producer
221- /// ffi_producer_start(handler.as_ptr());
222- ///
223- /// // Handler is automatically cleaned up when dropped (if producer didn't release it)
224- /// ```
225231pub struct AsyncDeviceStreamHandler {
226232 /// Raw pointer to the handler (heap-allocated).
227233 ptr : * mut FFI_ArrowAsyncDeviceStreamHandler ,
@@ -258,8 +264,14 @@ impl Drop for AsyncDeviceStreamHandler {
258264 return ;
259265 }
260266
261- // Handler was never released by producer - clean it up ourselves.
262- // This can happen if the producer never connected or crashed.
267+ // If a producer has connected, it will call release when done.
268+ // We must NOT free the handler or we'll cause a use-after-free.
269+ if self . state . producer_connected . load ( Ordering :: Acquire ) {
270+ return ;
271+ }
272+
273+ // No producer ever connected - clean it up ourselves.
274+ // This can happen if the handler was never passed to FFI code.
263275 //
264276 // We need to:
265277 // 1. Mark the stream as ended
@@ -294,21 +306,6 @@ impl ImportedAsyncDeviceStream {
294306 ///
295307 /// * `prefetch_count` - Number of batches to request ahead for back-pressure.
296308 /// A larger value reduces latency but uses more memory.
297- ///
298- /// # Example
299- ///
300- /// ```ignore
301- /// let (stream, handler) = ImportedAsyncDeviceStream::new(16);
302- ///
303- /// // Pass to FFI producer
304- /// ffi_producer_start(handler.as_ptr());
305- ///
306- /// // Stream the data
307- /// while let Some(batch) = stream.next().await {
308- /// // ...
309- /// }
310- /// // Handler is automatically cleaned up when dropped
311- /// ```
312309 pub fn new ( prefetch_count : u64 ) -> ( Self , AsyncDeviceStreamHandler ) {
313310 let ( sender, receiver) = mpsc:: unbounded ( ) ;
314311 let state = Arc :: new ( ImportedStreamState :: new ( prefetch_count, sender) ) ;
@@ -374,9 +371,7 @@ impl ImportedAsyncDeviceStream {
374371 }
375372 StreamMessage :: Task ( mut task) => {
376373 // Decrement pending (lock-free)
377- self . state
378- . pending_requests
379- . fetch_sub ( 1 , Ordering :: Release ) ;
374+ self . state . pending_requests . fetch_sub ( 1 , Ordering :: Release ) ;
380375 // Maybe request more (acquires lock only if needed)
381376 self . state . maybe_request_more ( ) ;
382377
@@ -507,7 +502,9 @@ unsafe extern "C" fn handler_on_schema(
507502 let result = match Schema :: try_from ( & ffi_schema) {
508503 Ok ( s) => {
509504 // Send through channel (lock-free)
510- let _ = state_arc. sender . unbounded_send ( StreamMessage :: Schema ( Arc :: new ( s) ) ) ;
505+ let _ = state_arc
506+ . sender
507+ . unbounded_send ( StreamMessage :: Schema ( Arc :: new ( s) ) ) ;
511508 state_arc. wake ( ) ;
512509 0
513510 }
@@ -553,7 +550,9 @@ unsafe extern "C" fn handler_on_next_task(
553550 } else {
554551 // Take ownership of the task by copying it
555552 let task_copy = std:: ptr:: read ( task) ;
556- let _ = state_arc. sender . unbounded_send ( StreamMessage :: Task ( task_copy) ) ;
553+ let _ = state_arc
554+ . sender
555+ . unbounded_send ( StreamMessage :: Task ( task_copy) ) ;
557556 }
558557 state_arc. wake ( ) ;
559558
@@ -709,10 +708,7 @@ mod tests {
709708 impl Stream for TestStream {
710709 type Item = Result < RecordBatch > ;
711710
712- fn poll_next (
713- mut self : Pin < & mut Self > ,
714- _cx : & mut Context < ' _ > ,
715- ) -> Poll < Option < Self :: Item > > {
711+ fn poll_next ( mut self : Pin < & mut Self > , _cx : & mut Context < ' _ > ) -> Poll < Option < Self :: Item > > {
716712 let this = self . as_mut ( ) . get_mut ( ) ;
717713 Poll :: Ready ( this. batches . pop_front ( ) )
718714 }
@@ -877,4 +873,114 @@ mod tests {
877873 // Should have received at least 3 batches before cancellation
878874 assert ! ( count >= 3 ) ;
879875 }
876+
877+ #[ tokio:: test]
878+ async fn test_drop_stream_while_producing ( ) {
879+ // Test that dropping the consumer stream while the producer is still producing
880+ // doesn't cause crashes or hangs - the producer should stop gracefully.
881+ let schema = test_schema ( ) ;
882+ let batches: Vec < RecordBatch > = ( 0 ..100 ) . map ( |i| make_batch ( & schema, i * 3 , 3 ) ) . collect ( ) ;
883+ let source_stream = TestStream :: new ( schema. clone ( ) , batches) ;
884+
885+ let ( consumer, handler) = ImportedAsyncDeviceStream :: new ( 2 ) ;
886+ let handler_ptr = handler. as_ptr ( ) ;
887+
888+ // Wrap consumer in Option so we can drop it mid-stream
889+ let consumer = std:: sync:: Arc :: new ( tokio:: sync:: Mutex :: new ( Some ( consumer) ) ) ;
890+ let consumer_clone = consumer. clone ( ) ;
891+
892+ let consumer_future = async move {
893+ let mut count = 0 ;
894+ loop {
895+ let mut guard = consumer_clone. lock ( ) . await ;
896+ let stream = guard. as_mut ( ) . unwrap ( ) ;
897+ match stream. next ( ) . await {
898+ Some ( result) => {
899+ result. expect ( "should not error before drop" ) ;
900+ count += 1 ;
901+ if count >= 2 {
902+ // Drop the stream after receiving 2 batches
903+ drop ( guard. take ( ) ) ;
904+ break ;
905+ }
906+ }
907+ None => break ,
908+ }
909+ }
910+ count
911+ } ;
912+
913+ let producer_future = drive_stream_to_handler ( source_stream, handler_ptr) ;
914+
915+ // Both futures should complete without panic or hang
916+ let ( count, _) = futures:: join!( consumer_future, producer_future) ;
917+ assert_eq ! ( count, 2 ) ;
918+ }
919+
920+ #[ tokio:: test]
921+ async fn test_drop_handler_while_consuming ( ) {
922+ // Test that dropping the handler wrapper while the consumer is still reading
923+ // doesn't cause crashes. The handler should be properly freed by the producer's
924+ // release callback, and the wrapper's Drop should be a no-op.
925+ let schema = test_schema ( ) ;
926+ let batches: Vec < RecordBatch > = ( 0 ..5 ) . map ( |i| make_batch ( & schema, i * 3 , 3 ) ) . collect ( ) ;
927+ let source_stream = TestStream :: new ( schema. clone ( ) , batches) ;
928+
929+ let ( mut consumer, handler) = ImportedAsyncDeviceStream :: new ( 4 ) ;
930+ let handler_ptr = handler. as_ptr ( ) ;
931+
932+ // Start producer
933+ let producer_future = drive_stream_to_handler ( source_stream, handler_ptr) ;
934+
935+ // Consume all batches, but drop the handler wrapper early
936+ let consumer_future = async {
937+ let mut received = vec ! [ ] ;
938+ // Read one batch first
939+ if let Some ( result) = consumer. next ( ) . await {
940+ received. push ( result) ;
941+ }
942+
943+ // Drop the handler wrapper while stream is still active
944+ // This should NOT free the handler since producer hasn't called release yet
945+ drop ( handler) ;
946+
947+ // Continue consuming - should work fine
948+ while let Some ( result) = consumer. next ( ) . await {
949+ received. push ( result) ;
950+ }
951+ received
952+ } ;
953+
954+ let ( received, _) = futures:: join!( consumer_future, producer_future) ;
955+
956+ // All 5 batches should be received
957+ assert_eq ! ( received. len( ) , 5 ) ;
958+ assert ! ( received. iter( ) . all( |r| r. is_ok( ) ) ) ;
959+ }
960+
961+ #[ tokio:: test]
962+ async fn test_handler_cleanup_when_producer_never_connects ( ) {
963+ // Test RAII cleanup when the handler is dropped but the producer never called
964+ // any callbacks (never connected). The handler wrapper should clean up properly.
965+ let ( _consumer, handler) = ImportedAsyncDeviceStream :: new ( 4 ) ;
966+
967+ // Just drop the handler without ever passing it to a producer
968+ // This should NOT panic or leak memory
969+ drop ( handler) ;
970+
971+ // Stream should still be usable (though it will never receive data)
972+ // The ended flag should be set by handler drop
973+ }
974+
975+ #[ tokio:: test]
976+ async fn test_stream_and_handler_both_dropped_before_producer ( ) {
977+ // Test cleanup when both stream and handler are dropped before any producer connects
978+ let ( consumer, handler) = ImportedAsyncDeviceStream :: new ( 4 ) ;
979+
980+ // Drop both without ever starting a producer
981+ drop ( consumer) ;
982+ drop ( handler) ;
983+
984+ // Should not panic or leak
985+ }
880986}
0 commit comments