Skip to content

Commit 0832933

Browse files
committed
test some drop behaviour
1 parent 8cc758d commit 0832933

1 file changed

Lines changed: 148 additions & 42 deletions

File tree

c/sedona-extension/src/import_sendable_record_batch_stream.rs

Lines changed: 148 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -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-
/// ```
225231
pub 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

Comments
 (0)