algeon-datafusion 0.1.2

DataFusion physical-plan adapter for native GPU execution.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
use super::*;

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn flight_sql_client_can_run_select_one() {
    let (listener, addr) = free_local_addr();
    let mut config = test_server_config();
    declare_test_gpu_ownership(&mut config);
    config.bind_addr = addr;
    let backend = build_injected_test_backend(&mut config);
    let replacement_config = config.clone();
    let cleanup_backend = backend.clone();
    let dir = FlightSqlTempfileBuilder::new().prefix("algeon-datafusion-flight-sql-abort-holder-").tempdir().unwrap();
    let parquet_path = dir.path().join("edges.parquet");
    write_update_edges_parquet(&parquet_path);

    let server = FlightSqlServer::builder().config(config).backend(backend).listener(listener).build().await.unwrap();
    let ctx = Arc::clone(server.session_context());
    ctx.register_parquet("holder_edges", parquet_path.to_str().unwrap(), ParquetReadOptions::default()).await.unwrap();
    let server = tokio::spawn(async move { server.serve().await });
    let mut client = connect_with_retry(addr).await;

    let flight_info = client.execute("select 1 as one".to_string(), None).await.unwrap();
    let schema = flight_info.clone().try_decode_schema().unwrap();
    assert_eq!(schema.fields().len(), 1);
    assert_eq!(schema.field(0).name(), "one");
    let ticket = flight_info.endpoint.first().and_then(|endpoint| endpoint.ticket.clone()).unwrap();
    let batches = client.do_get(ticket).await.unwrap().try_collect::<Vec<_>>().await.unwrap();
    // Only an admitted GPU query retains the resource generation, and with it
    // the device-owner lease, past the close; a bare attempt holds just the
    // observation lifecycle, which close may outlive. The stream is polled once
    // to admit and then left suspended so its permit stays held.
    let holder_attempt = cleanup_backend.begin_attempt(AttemptOptions::default()).expect("holder attempt should begin");
    let mut holder_stream = holder_attempt
        .sql(&ctx, "select src from holder_edges order by src limit 1")
        .await
        .expect("holder should plan");
    holder_stream.try_next().await.expect("holder admits on first poll").expect("holder batch");
    let delayed_cleanup = (holder_attempt, holder_stream);

    drop(client);
    server.abort();
    let error = server.await.expect_err("aborted server task must report cancellation");
    assert!(error.is_cancelled());
    let error = backend_from_server_config(&replacement_config)
        .expect_err("hard abort does not synchronously complete active GPU query cleanup");
    let diagnostic = format!("{error:?}");
    assert!(diagnostic.contains("code: ServiceOverloaded"), "{diagnostic}");
    assert!(diagnostic.contains("operation: \"algeon_datafusion.backend.gpu_core\""), "{diagnostic}");
    assert!(diagnostic.contains("native_operation\", \"query_engine.gpu.owner.claim"), "{diagnostic}");
    drop(delayed_cleanup);
    spawn_blocking(move || cleanup_backend.close_and_wait(Duration::from_secs(10)))
        .await
        .expect("aborted server cleanup waiter must not panic")
        .expect("aborted server must reach query-service quiescence");
    let replacement = backend_from_server_config(&replacement_config)
        .expect("quiescent aborted server must release its closed QueryService governor claim");
    drop(replacement);

    assert_eq!(batches.len(), 1);
    assert_eq!(batches[0].num_rows(), 1);
    assert_eq!(batches[0].num_columns(), 1);
    assert_eq!(int_value(batches[0].column(0)), Some(1));
}

#[tokio::test(flavor = "current_thread")]
async fn aborting_server_future_never_blocks_current_thread_runtime_drop() {
    let (listener, addr) = free_local_addr();
    let mut config = test_server_config();
    declare_test_gpu_ownership(&mut config);
    config.bind_addr = addr;
    let replacement_config = config.clone();
    let server = tokio::spawn(async move { serve_with_listener(config, listener).await });
    yield_now().await;

    server.abort();
    let error = timeout(Duration::from_secs(1), server)
        .await
        .expect("server task Drop must not block the current-thread runtime")
        .expect_err("aborted server task must report cancellation");
    assert!(error.is_cancelled());

    // The aborted build may already hold the device-owner lease; its close
    // and rollback release it asynchronously. The race between the first yield
    // and the ownership claim depends on the environment, so the loop below
    // polls for that release.
    let deadline = std::time::Instant::now() + Duration::from_secs(30);
    let replacement = loop {
        match backend_from_server_config(&replacement_config) {
            Ok(backend) => break backend,
            Err(error)
                if error.fact_value("native_operation") == Some("query_engine.gpu.owner.claim")
                    && std::time::Instant::now() < deadline =>
            {
                std::thread::sleep(Duration::from_millis(25));
            },
            Err(error) => {
                panic!("nonblocking abort must release a quiescent server governor claim: {error:?}")
            },
        }
    };
    drop(replacement);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn flight_sql_builder_can_run_select_one_and_shutdown_by_token() {
    let (listener, addr) = free_local_addr();
    let mut config = test_server_config();
    declare_test_gpu_ownership(&mut config);
    config.bind_addr = addr;
    let shutdown = CancellationToken::new();
    let server = FlightSqlServer::builder()
        .config(config)
        .listener(listener)
        .shutdown(ServerShutdown::Token(shutdown.clone()))
        .build()
        .await
        .unwrap();
    assert_eq!(server.local_addr(), Some(addr));

    let server = tokio::spawn(async move { server.serve().await });
    let mut client = connect_with_retry(addr).await;

    let flight_info = client.execute("select 1 as one".to_string(), None).await.unwrap();
    let ticket = flight_info.endpoint.first().and_then(|endpoint| endpoint.ticket.clone()).unwrap();
    let batches = client.do_get(ticket).await.unwrap().try_collect::<Vec<_>>().await.unwrap();
    drop(client);
    shutdown.cancel();
    let server_result = timeout(Duration::from_secs(10), server)
        .await
        .expect("token shutdown should drain promptly")
        .expect("server task should not panic");

    server_result.expect("token shutdown should stop the server cleanly");
    assert_eq!(batches.len(), 1);
    assert_eq!(int_value(batches[0].column(0)), Some(1));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn flight_sql_datafusion_baseline_statement_never_enters_gpu_ledger() {
    let (listener, addr) = free_local_addr();
    let mut config = test_server_config();
    declare_test_gpu_ownership(&mut config);
    config.bind_addr = addr;
    let backend = build_injected_test_backend(&mut config);
    let shutdown = CancellationToken::new();
    let server = FlightSqlServer::builder()
        .config(config)
        .backend(backend.clone())
        .listener(listener)
        .shutdown(ServerShutdown::Token(shutdown.clone()))
        .build()
        .await
        .unwrap();

    let server = tokio::spawn(async move { server.serve().await });
    let mut client = connect_with_retry(addr).await;

    // Case-insensitive LIKE remains a structured native expression rejection.
    // Keep this independent of regexp_replace lowering coverage so the test
    // continues to exercise DataFusion-baseline execution that never enters the
    // GPU ledger.
    let attempts_before = backend.attempt_counters_snapshot().attempts_begun;
    let flight_info = client.execute("select 'ABC' ilike 'a%' as label_match".to_string(), None).await.unwrap();
    assert_eq!(
        backend.attempt_counters_snapshot().attempts_begun,
        attempts_before + 1,
        "GetFlightInfo begins the sole invocation attempt before any GPU admission"
    );
    wait_for_empty_gpu_ledger(&backend).await;
    let snapshot = backend.diagnostics().unwrap().admission;
    assert_eq!(snapshot.active_queries, 0, "GetFlightInfo must not occupy GPU admission: {snapshot:?}");
    assert_eq!(snapshot.queued_queries, 0, "GetFlightInfo must not queue GPU admission: {snapshot:?}");
    let ticket = flight_info.endpoint.first().and_then(|endpoint| endpoint.ticket.clone()).unwrap();
    let batches = client.do_get(ticket).await.unwrap().try_collect::<Vec<_>>().await.unwrap();
    // Drain any async GPU pool cleanup path before reading counters so a pure
    // DataFusion-baseline statement cannot race with unrelated pool bookkeeping.
    wait_for_empty_gpu_ledger(&backend).await;
    let snapshot = backend.diagnostics().unwrap().admission;
    assert_eq!(snapshot.active_queries, 0, "DoGet DataFusion baseline must not occupy GPU admission: {snapshot:?}");
    assert_eq!(snapshot.queued_queries, 0, "DoGet DataFusion baseline must not queue GPU admission: {snapshot:?}");

    drop(client);
    shutdown.cancel();
    let server_result = timeout(Duration::from_secs(10), server)
        .await
        .expect("token shutdown should drain promptly")
        .expect("server task should not panic");

    server_result.expect("token shutdown should stop the server cleanly");
    assert_eq!(batches.len(), 1);
    let matched =
        batches[0].column(0).as_any().downcast_ref::<BooleanArray>().expect("ILIKE result must be BooleanArray");
    assert!(matched.value(0));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn flight_sql_builder_shares_injected_backend_with_session_context() {
    let (listener, addr) = free_local_addr();
    let mut config = test_server_config();
    declare_test_gpu_ownership(&mut config);
    config.bind_addr = addr;
    config.gpu_device_profiles[0] = config.gpu_device_profiles[0].clone().with_max_active_attempts(1);
    config.queue_policy = config.queue_policy.clone().with_max_queue_wait(Duration::from_secs(60));
    let backend = build_injected_test_backend(&mut config);
    let dir = FlightSqlTempfileBuilder::new().prefix("algeon-datafusion-flight-sql-shared-backend-").tempdir().unwrap();
    let parquet_path = dir.path().join("edges.parquet");
    write_update_edges_parquet(&parquet_path);

    let shutdown = CancellationToken::new();
    let server = FlightSqlServer::builder()
        .config(config)
        .backend(backend.clone())
        .listener(listener)
        .shutdown(ServerShutdown::Token(shutdown.clone()))
        .build()
        .await
        .unwrap();
    assert!(server.backend().unwrap().same_backend(&backend));
    let ctx = Arc::clone(server.session_context());
    ctx.register_parquet("shared_edges", parquet_path.to_str().unwrap(), ParquetReadOptions::default()).await.unwrap();

    let server = tokio::spawn(async move { server.serve().await });
    let mut client = connect_with_retry(addr).await;
    let flight_info =
        client.execute("select src from shared_edges order by src limit 1".to_string(), None).await.unwrap();
    assert_eq!(
        backend.diagnostics().unwrap().admission.active_queries,
        0,
        "GetFlightInfo must not acquire GPU admission before DoGet"
    );

    // Occupy the shared slot with a live suspended attempt execution. The
    // holder stream is polled once to admit, then left suspended: the slot
    // stays occupied until the stream drops, never reaching EOF.
    let holder_attempt = backend.begin_attempt(AttemptOptions::default()).expect("holder attempt should begin");
    let mut holder_stream = holder_attempt
        .sql(&ctx, "select src from shared_edges order by src limit 1")
        .await
        .expect("holder should plan");
    holder_stream.try_next().await.expect("holder admits on first poll").expect("holder batch");
    let holder = (holder_attempt, holder_stream);
    assert_eq!(backend.diagnostics().unwrap().admission.active_queries, 1);

    let ticket = flight_info.endpoint.first().and_then(|endpoint| endpoint.ticket.clone()).unwrap();
    let flight_query =
        tokio::spawn(async move { client.do_get(ticket).await.unwrap().try_collect::<Vec<_>>().await.unwrap() });

    let session_ctx = Arc::clone(&ctx);
    let session_query = tokio::spawn(async move {
        let df = session_ctx.sql("select src from shared_edges order by src limit 1").await.unwrap();
        df.collect().await
    });
    wait_for_shared_ledger_queue(&backend, 2).await;
    drop(holder);

    // Either queued query may win the one shared slot. Await them together so
    // the assertion does not impose an ordering, and allow the configured
    // 60-second admission window when the shared-GPU lane is fully occupied.
    let (flight_batches, session_batches) =
        timeout(Duration::from_secs(60), async { tokio::join!(flight_query, session_query) })
            .await
            .expect("Flight and session queries should finish after direct backend admission releases");
    let flight_batches = flight_batches.expect("Flight query task should not panic");
    let session_batches = session_batches.expect("session query task should not panic").unwrap();

    shutdown.cancel();
    let server_result = timeout(Duration::from_secs(10), server)
        .await
        .expect("token shutdown should drain promptly")
        .expect("server task should not panic");

    server_result.expect("token shutdown should stop the server cleanly");
    assert_eq!(flight_batches.iter().map(RecordBatch::num_rows).sum::<usize>(), 1);
    assert_eq!(session_batches.iter().map(RecordBatch::num_rows).sum::<usize>(), 1);
    let close =
        backend.close_and_wait(Duration::from_secs(1)).expect("server shutdown must quiesce the shared backend");
    assert!(close.gpu_resource_quiescent);
    assert!(close.observation_quiescent);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn backend_clone_installed_sessions_share_identity_queue_and_worker_pool_records() {
    let mut config = test_server_config();
    declare_test_gpu_ownership(&mut config);
    config.gpu_device_profiles[0] = config.gpu_device_profiles[0].clone().with_max_active_attempts(1);
    config.queue_policy =
        config.queue_policy.clone().with_max_queued_attempts(2).with_max_queue_wait(Duration::from_secs(60));
    let backend = backend_from_server_config(&config).unwrap();
    let first_backend = backend.clone();
    let second_backend = backend.clone();
    let first_session = Arc::new(
        build_session_context_with_backend(&config, &first_backend)
            .await
            .expect("first cloned backend must install a session"),
    );
    let second_session = Arc::new(
        build_session_context_with_backend(&config, &second_backend)
            .await
            .expect("second cloned backend must install a session"),
    );

    assert!(first_backend.same_backend(&backend));
    assert!(second_backend.same_backend(&backend));
    assert!(first_backend.same_backend(&second_backend));
    let backend_generation = backend.backend_metrics_snapshot().backend_generation;
    assert!(backend_generation > 0);
    assert_eq!(first_backend.backend_metrics_snapshot().backend_generation, backend_generation);
    assert_eq!(second_backend.backend_metrics_snapshot().backend_generation, backend_generation);
    let first_startup_records = first_backend.device_startup_records();
    let second_startup_records = second_backend.device_startup_records();
    assert_eq!(first_startup_records, second_startup_records);
    assert_eq!(first_startup_records.len(), 1);
    for (first, second) in first_startup_records.iter().zip(second_startup_records.iter()) {
        assert!(first.backend_health_generation > 0);
        assert_eq!(first.backend_health_generation, second.backend_health_generation);
        assert!(first.device_health_generation > 0);
        assert_eq!(first.device_health_generation, second.device_health_generation);
        assert!(first.memory_manager_installation.identity.is_some());
        assert_eq!(first.memory_manager_installation.identity, second.memory_manager_installation.identity);
        assert!(first.memory_manager_installation.installation_generation.is_some_and(|generation| generation > 0));
        assert_eq!(
            first.memory_manager_installation.installation_generation,
            second.memory_manager_installation.installation_generation
        );
    }
    assert_eq!(first_backend.diagnostics().unwrap().ledgers, second_backend.diagnostics().unwrap().ledgers);
    assert_eq!(
        first_backend.diagnostics().unwrap().memory_failures,
        second_backend.diagnostics().unwrap().memory_failures
    );
    let first_telemetry = first_backend.device_telemetry_snapshots().unwrap();
    let second_telemetry = second_backend.device_telemetry_snapshots().unwrap();
    assert_eq!(first_telemetry.len(), second_telemetry.len());
    for (first, second) in first_telemetry.iter().zip(&second_telemetry) {
        // Telemetry payloads include independently sampled free-memory and
        // wall-clock values; device identity and provider availability are the
        // stable public projections shared by cloned backend sessions.
        assert_eq!(first.device_ordinal, second.device_ordinal);
        assert_eq!(first.cuda_status, second.cuda_status);
        assert_eq!(first.nvml_status, second.nvml_status);
    }

    let first_worker_pools = first_backend.diagnostics().unwrap().execution_pools;
    let second_worker_pools = second_backend.diagnostics().unwrap().execution_pools;
    assert_eq!(first_worker_pools, second_worker_pools);
    assert_eq!(first_worker_pools.len(), 1);
    assert!(first_worker_pools[0].pool_id > 0);
    assert_eq!(first_worker_pools[0].pool_id, second_worker_pools[0].pool_id);
    assert_eq!(first_worker_pools[0].device_ordinal, backend.devices()[0].device_ordinal());
    assert_eq!(first_worker_pools[0].worker_count, 2);

    let dir = FlightSqlTempfileBuilder::new().prefix("algeon-datafusion-cloned-session-backend-").tempdir().unwrap();
    let parquet_path = dir.path().join("edges.parquet");
    write_update_edges_parquet(&parquet_path);
    first_session
        .register_parquet("first_edges", parquet_path.to_str().unwrap(), ParquetReadOptions::default())
        .await
        .unwrap();
    second_session
        .register_parquet("second_edges", parquet_path.to_str().unwrap(), ParquetReadOptions::default())
        .await
        .unwrap();

    // Occupy the shared slot with a live suspended attempt execution, polled
    // once to admit and held without reaching EOF.
    let holder_attempt = backend.begin_attempt(AttemptOptions::default()).expect("holder attempt should begin");
    let mut holder_stream = holder_attempt
        .sql(&first_session, "select src from first_edges order by src limit 1")
        .await
        .expect("holder should plan");
    holder_stream.try_next().await.expect("holder admits on first poll").expect("holder batch");
    let holder = (holder_attempt, holder_stream);
    assert_eq!(first_backend.diagnostics().unwrap().admission.active_queries, 1);
    assert_eq!(second_backend.diagnostics().unwrap().admission.active_queries, 1);

    let first_query = tokio::spawn(async move {
        let frame = first_session.sql("select src from first_edges order by src limit 1").await.unwrap();
        frame.collect().await.unwrap()
    });
    let second_query = tokio::spawn(async move {
        let frame = second_session.sql("select src from second_edges order by src limit 1").await.unwrap();
        frame.collect().await.unwrap()
    });
    wait_for_shared_ledger_queue(&backend, 2).await;
    assert_eq!(
        first_backend.diagnostics().unwrap().admission,
        second_backend.diagnostics().unwrap().admission,
        "both installed sessions must observe one query-service queue and ledger"
    );

    drop(holder);
    let (first_batches, second_batches) =
        timeout(Duration::from_secs(60), async { tokio::join!(first_query, second_query) })
            .await
            .expect("both cloned-session queries should run after the shared slot is released");
    let first_batches = first_batches.expect("first cloned-session task must not panic");
    let second_batches = second_batches.expect("second cloned-session task must not panic");
    assert_eq!(first_batches.iter().map(RecordBatch::num_rows).sum::<usize>(), 1);
    assert_eq!(second_batches.iter().map(RecordBatch::num_rows).sum::<usize>(), 1);
    wait_for_empty_gpu_ledger(&backend).await;

    assert_eq!(
        first_backend.diagnostics().unwrap().execution_pools,
        second_backend.diagnostics().unwrap().execution_pools
    );
    drop(first_backend);
    drop(second_backend);
    let close = backend.close_and_wait(Duration::from_secs(10)).expect("cloned-session backend must close cleanly");
    assert!(close.gpu_resource_quiescent);
    assert!(close.observation_quiescent);
}