-
Notifications
You must be signed in to change notification settings - Fork 180
Expand file tree
/
Copy pathwrite.rs
More file actions
159 lines (127 loc) · 4.73 KB
/
Copy pathwrite.rs
File metadata and controls
159 lines (127 loc) · 4.73 KB
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
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright the Vortex contributors
use std::io;
use std::sync::Arc;
use bytes::BytesMut;
use futures::TryStreamExt;
use futures::stream::FuturesUnordered;
use object_store::MultipartUpload;
use object_store::ObjectStore;
use object_store::ObjectStoreExt;
use object_store::PutPayload;
use object_store::PutResult;
use object_store::path::Path;
use vortex_error::VortexResult;
use crate::IoBuf;
use crate::VortexWrite;
/// Adapter type to write data through a [`ObjectStore`] instance.
///
/// After writing, the caller must make sure to call `shutdown`, in order to ensure the data is actually persisted.
pub struct ObjectStoreWrite {
upload: Box<dyn MultipartUpload>,
buffer: BytesMut,
put_result: Option<PutResult>,
}
const CHUNK_SIZE: usize = 16 * 1024 * 1024;
const BUFFER_SIZE: usize = 128 * 1024 * 1024;
impl ObjectStoreWrite {
pub async fn new(object_store: Arc<dyn ObjectStore>, location: &Path) -> VortexResult<Self> {
let upload = object_store.put_multipart(location).await?;
Ok(Self {
upload,
buffer: BytesMut::with_capacity(CHUNK_SIZE),
put_result: None,
})
}
pub fn put_result(&self) -> Option<&PutResult> {
self.put_result.as_ref()
}
}
impl VortexWrite for ObjectStoreWrite {
async fn write_all<B: IoBuf>(&mut self, buffer: B) -> io::Result<B> {
self.buffer.extend_from_slice(buffer.as_slice());
let parts = FuturesUnordered::new();
// If the buffer is full
if self.buffer.len() > BUFFER_SIZE {
// Split off chunks while buffer is larger than CHUNKS_SIZE
while self.buffer.len() > CHUNK_SIZE {
let payload = self.buffer.split_to(CHUNK_SIZE).freeze();
let part_fut = self.upload.put_part(PutPayload::from_bytes(payload));
parts.push(part_fut);
}
}
parts.try_collect::<Vec<_>>().await?;
Ok(buffer)
}
async fn flush(&mut self) -> io::Result<()> {
let parts = FuturesUnordered::new();
while self.buffer.len() > CHUNK_SIZE {
let payload = self.buffer.split_to(CHUNK_SIZE).freeze();
let part_fut = self.upload.put_part(PutPayload::from_bytes(payload));
parts.push(part_fut);
}
parts.try_collect::<Vec<_>>().await?;
Ok(())
}
async fn shutdown(&mut self) -> io::Result<()> {
self.flush().await?;
if !self.buffer.is_empty() {
let payload = std::mem::take(&mut self.buffer).freeze();
self.upload
.put_part(PutPayload::from_bytes(payload))
.await?;
}
self.put_result = Some(self.upload.complete().await?);
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use object_store::ObjectStore;
use object_store::local::LocalFileSystem;
use object_store::memory::InMemory;
use object_store::path::Path;
use rstest::rstest;
use tempfile::tempdir;
use super::*;
// Note: Concurrent writes test removed because &mut self in write_all already ensures
// exclusive access. Multiple writers would need to be created with separate buffers,
// which is not the intended use case.
#[tokio::test]
#[rstest]
#[case(100)]
#[case(8 * 1024 * 1024)]
#[case(25 * 1024 * 1024)]
#[case(26 * 1024 * 1024)]
async fn test_object_store_writer_multiple_flushes(
#[case] chunk_size: usize,
) -> anyhow::Result<()> {
let temp_dir = tempdir()?;
let local_store =
Arc::new(LocalFileSystem::new_with_prefix(temp_dir.path())?) as Arc<dyn ObjectStore>;
let memory_store = Arc::new(InMemory::new()) as Arc<dyn ObjectStore>;
let location = Path::from("test.bin");
for test_store in [memory_store, local_store] {
let mut writer = ObjectStoreWrite::new(test_store.clone(), &location).await?;
#[expect(clippy::cast_possible_truncation)]
let data = (0..3)
.map(|i| vec![i as u8; chunk_size])
.collect::<Vec<_>>();
// Write and flush multiple times
for i in 0..3 {
let data = data[i].clone();
writer.write_all(data).await?;
writer.flush().await?;
}
// Shutdown the writer to make sure data actually gets persisted.
writer.shutdown().await?;
// Verify all data was written
let result = test_store.get(&location).await?;
let bytes = result.bytes().await?;
let expected_data = itertools::concat(data.into_iter());
assert_eq!(bytes, expected_data);
}
Ok(())
}
}