Skip to content

Commit e97ed5d

Browse files
committed
Add ObjectStore spill example
1 parent 4904cd7 commit e97ed5d

5 files changed

Lines changed: 219 additions & 2 deletions

File tree

datafusion-examples/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ futures = { workspace = true }
6060
insta = { workspace = true }
6161
log = { workspace = true }
6262
mimalloc = { version = "0.1", default-features = false }
63-
object_store = { workspace = true, features = ["aws", "http"] }
63+
object_store = { workspace = true, features = ["aws", "fs", "http"] }
6464
prost = { workspace = true }
6565
rand = { workspace = true }
6666
serde = { version = "1", features = ["derive"] }

datafusion-examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ cargo run --example dataframe -- dataframe
9393
| catalog | [`data_io/catalog.rs`](examples/data_io/catalog.rs) | Register tables into a custom catalog |
9494
| in_memory_object_store | [`data_io/in_memory_object_store.rs`](examples/data_io/in_memory_object_store.rs) | Read CSV from an in-memory object store (pattern applies to JSON/Parquet) |
9595
| json_shredding | [`data_io/json_shredding.rs`](examples/data_io/json_shredding.rs) | Implement filter rewriting for JSON shredding |
96+
| object_store_spill | [`data_io/object_store_spill.rs`](examples/data_io/object_store_spill.rs) | Use ObjectStore-backed spill files |
9697
| parquet_adv_idx | [`data_io/parquet_advanced_index.rs`](examples/data_io/parquet_advanced_index.rs) | Create a secondary index across multiple parquet files |
9798
| parquet_emb_idx | [`data_io/parquet_embedded_index.rs`](examples/data_io/parquet_embedded_index.rs) | Store a custom index inside Parquet files |
9899
| parquet_enc | [`data_io/parquet_encrypted.rs`](examples/data_io/parquet_encrypted.rs) | Read & write encrypted Parquet files |

datafusion-examples/examples/data_io/main.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
//!
2222
//! ## Usage
2323
//! ```bash
24-
//! cargo run --example data_io -- [all|catalog|in_memory_object_store|json_shredding|parquet_adv_idx|parquet_emb_idx|parquet_enc_with_kms|parquet_enc|parquet_exec_visitor|parquet_idx|query_http_csv|remote_catalog]
24+
//! cargo run --example data_io -- [all|catalog|in_memory_object_store|json_shredding|object_store_spill|parquet_adv_idx|parquet_emb_idx|parquet_enc_with_kms|parquet_enc|parquet_exec_visitor|parquet_idx|query_http_csv|remote_catalog]
2525
//! ```
2626
//!
2727
//! Each subcommand runs a corresponding example:
@@ -36,6 +36,9 @@
3636
//! - `json_shredding`
3737
//! (file: json_shredding.rs, desc: Implement filter rewriting for JSON shredding)
3838
//!
39+
//! - `object_store_spill`
40+
//! (file: object_store_spill.rs, desc: Use ObjectStore-backed spill files)
41+
//!
3942
//! - `parquet_adv_idx`
4043
//! (file: parquet_advanced_index.rs, desc: Create a secondary index across multiple parquet files)
4144
//!
@@ -66,6 +69,7 @@
6669
mod catalog;
6770
mod in_memory_object_store;
6871
mod json_shredding;
72+
mod object_store_spill;
6973
mod parquet_advanced_index;
7074
mod parquet_embedded_index;
7175
mod parquet_encrypted;
@@ -87,6 +91,7 @@ enum ExampleKind {
8791
Catalog,
8892
InMemoryObjectStore,
8993
JsonShredding,
94+
ObjectStoreSpill,
9095
ParquetAdvIdx,
9196
ParquetEmbIdx,
9297
ParquetEnc,
@@ -118,6 +123,9 @@ impl ExampleKind {
118123
in_memory_object_store::in_memory_object_store().await?
119124
}
120125
ExampleKind::JsonShredding => json_shredding::json_shredding().await?,
126+
ExampleKind::ObjectStoreSpill => {
127+
object_store_spill::object_store_spill().await?
128+
}
121129
ExampleKind::ParquetAdvIdx => {
122130
parquet_advanced_index::parquet_advanced_index().await?
123131
}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
//! See `main.rs` for how to run it.
19+
//!
20+
//! [`object_store_spill`] shows how to use the [`TempFileFactory]` API to configure
21+
//! DataFusion to spill intermediate results to remote storage when it exceeds
22+
//! its memory limits.
23+
use std::future::Future;
24+
use std::io::Write;
25+
use std::path::Path as StdPath;
26+
use std::pin::Pin;
27+
use std::sync::Arc;
28+
use std::sync::atomic::{AtomicU64, Ordering};
29+
30+
use bytes::Bytes;
31+
use datafusion::common::Result;
32+
use datafusion::error::DataFusionError;
33+
use datafusion::execution::disk_manager::DiskManagerBuilder;
34+
use datafusion::execution::runtime_env::RuntimeEnvBuilder;
35+
use datafusion::execution::{SpillFile, SpillWriter, TempFileFactory};
36+
use datafusion::prelude::{SessionConfig, SessionContext};
37+
use futures::{Stream, StreamExt, stream};
38+
use object_store::local::LocalFileSystem;
39+
use object_store::path::Path;
40+
use object_store::{ObjectStore, ObjectStoreExt, PutPayload};
41+
42+
/// Demonstrates configuring DataFusion with spill files backed by an ObjectStore.
43+
pub async fn object_store_spill() -> Result<()> {
44+
let tmp_dir = tempfile::tempdir().map_err(DataFusionError::IoError)?;
45+
let store: Arc<dyn ObjectStore> =
46+
Arc::new(LocalFileSystem::new_with_prefix(tmp_dir.path())?);
47+
48+
// A spill factory defines how data will be spilled.
49+
let spill_factory = Arc::new(ObjectStoreTempFileFactory::new(store));
50+
let runtime = RuntimeEnvBuilder::new()
51+
.with_disk_manager_builder(
52+
DiskManagerBuilder::default().with_spill_factory(spill_factory),
53+
)
54+
.build_arc()?;
55+
56+
// Queries run with this context use the custom factory whenever an operator spills.
57+
let _ctx =
58+
SessionContext::new_with_config_rt(SessionConfig::new(), Arc::clone(&runtime));
59+
60+
// Exercise the same spill file path directly so the example does not depend on
61+
// a particular query plan spilling for this small input.
62+
let spill_file = runtime
63+
.disk_manager
64+
.create_tmp_file("object store spill example")?;
65+
let mut writer = spill_file.open_writer()?;
66+
writer.write_all(b"spill data stored through ObjectStore")?;
67+
writer.finish()?;
68+
69+
let chunks: Vec<Bytes> = spill_file
70+
.read_stream()?
71+
.collect::<Vec<_>>()
72+
.await
73+
.into_iter()
74+
.collect::<Result<_>>()?;
75+
assert_eq!(chunks.concat(), b"spill data stored through ObjectStore");
76+
77+
Ok(())
78+
}
79+
80+
struct ObjectStoreTempFileFactory {
81+
store: Arc<dyn ObjectStore>,
82+
counter: AtomicU64,
83+
}
84+
85+
impl ObjectStoreTempFileFactory {
86+
fn new(store: Arc<dyn ObjectStore>) -> Self {
87+
Self {
88+
store,
89+
counter: AtomicU64::new(0),
90+
}
91+
}
92+
}
93+
94+
impl std::panic::RefUnwindSafe for ObjectStoreTempFileFactory {}
95+
impl std::panic::UnwindSafe for ObjectStoreTempFileFactory {}
96+
97+
impl TempFileFactory for ObjectStoreTempFileFactory {
98+
fn create_temp_file(&self, description: &str) -> Result<Arc<dyn SpillFile>> {
99+
let id = self.counter.fetch_add(1, Ordering::Relaxed);
100+
let description = sanitize_path_part(description);
101+
let location = Path::from(format!("spill/{description}-{id}.bin"));
102+
103+
Ok(Arc::new(ObjectStoreSpillFile {
104+
store: Arc::clone(&self.store),
105+
location,
106+
size: Arc::new(AtomicU64::new(0)),
107+
}))
108+
}
109+
}
110+
111+
struct ObjectStoreSpillFile {
112+
store: Arc<dyn ObjectStore>,
113+
location: Path,
114+
size: Arc<AtomicU64>,
115+
}
116+
117+
impl SpillFile for ObjectStoreSpillFile {
118+
fn path(&self) -> Option<&StdPath> {
119+
None
120+
}
121+
122+
fn size(&self) -> Option<u64> {
123+
Some(self.size.load(Ordering::Relaxed))
124+
}
125+
126+
fn read_stream(&self) -> Result<Pin<Box<dyn Stream<Item = Result<Bytes>> + Send>>> {
127+
let store = Arc::clone(&self.store);
128+
let location = self.location.clone();
129+
130+
Ok(Box::pin(stream::once(async move {
131+
Ok(store.get(&location).await?.bytes().await?)
132+
})))
133+
}
134+
135+
fn open_writer(&self) -> Result<Box<dyn SpillWriter>> {
136+
Ok(Box::new(ObjectStoreSpillWriter {
137+
store: Arc::clone(&self.store),
138+
location: self.location.clone(),
139+
size: Arc::clone(&self.size),
140+
buffer: Vec::new(),
141+
}))
142+
}
143+
}
144+
145+
struct ObjectStoreSpillWriter {
146+
store: Arc<dyn ObjectStore>,
147+
location: Path,
148+
size: Arc<AtomicU64>,
149+
buffer: Vec<u8>,
150+
}
151+
152+
impl Write for ObjectStoreSpillWriter {
153+
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
154+
self.buffer.extend_from_slice(buf);
155+
Ok(buf.len())
156+
}
157+
158+
fn flush(&mut self) -> std::io::Result<()> {
159+
Ok(())
160+
}
161+
}
162+
163+
impl SpillWriter for ObjectStoreSpillWriter {
164+
fn finish(&mut self) -> Result<()> {
165+
let store = Arc::clone(&self.store);
166+
let location = self.location.clone();
167+
let data = std::mem::take(&mut self.buffer);
168+
let size = data.len() as u64;
169+
170+
// This simple example buffers the spill and uploads it on finish.
171+
// Production remote stores should consider multipart or streaming uploads.
172+
block_on_object_store(async move {
173+
store
174+
.put(&location, PutPayload::from_bytes(data.into()))
175+
.await?;
176+
Ok(())
177+
})?;
178+
179+
self.size.store(size, Ordering::Relaxed);
180+
Ok(())
181+
}
182+
}
183+
184+
fn block_on_object_store<T>(future: impl Future<Output = Result<T>>) -> Result<T> {
185+
if let Ok(handle) = tokio::runtime::Handle::try_current() {
186+
tokio::task::block_in_place(|| handle.block_on(future))
187+
} else {
188+
tokio::runtime::Runtime::new()
189+
.map_err(DataFusionError::IoError)?
190+
.block_on(future)
191+
}
192+
}
193+
194+
fn sanitize_path_part(value: &str) -> String {
195+
value
196+
.chars()
197+
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
198+
.collect()
199+
}

datafusion/execution/src/disk_manager.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ impl DiskManagerBuilder {
6969
self
7070
}
7171

72+
pub fn set_spill_factory(&mut self, spill_factory: Arc<dyn TempFileFactory>) {
73+
self.mode = DiskManagerMode::Custom(spill_factory);
74+
}
75+
76+
pub fn with_spill_factory(mut self, spill_factory: Arc<dyn TempFileFactory>) -> Self {
77+
self.set_spill_factory(spill_factory);
78+
self
79+
}
80+
7281
pub fn set_max_temp_directory_size(&mut self, value: u64) {
7382
self.max_temp_directory_size = value;
7483
}

0 commit comments

Comments
 (0)