mz_storage_operators/s3_oneshot_sink/
pgcopy.rs1use anyhow::anyhow;
11use aws_types::sdk_config::SdkConfig;
12use mz_aws_util::s3_uploader::{
13 CompletedUpload, S3MultiPartUploadError, S3MultiPartUploader, S3MultiPartUploaderConfig,
14};
15use mz_ore::assert_none;
16use mz_ore::cast::CastFrom;
17use mz_pgcopy::{CopyFormatParams, encode_copy_format, encode_copy_format_header};
18use mz_pgrepr::TextEncodeSettings;
19use mz_repr::{GlobalId, RelationDesc, Row};
20use mz_storage_types::sinks::s3_oneshot_sink::S3KeyManager;
21use mz_storage_types::sinks::{S3SinkFormat, S3UploadInfo};
22use tracing::info;
23
24use super::{CopyToParameters, CopyToS3Uploader};
25
26pub(super) struct PgCopyUploader {
28 desc: RelationDesc,
30 format: CopyFormatParams<'static>,
32 file_index: usize,
34 key_manager: S3KeyManager,
36 batch: u64,
38 max_file_size: u64,
41 sdk_config: Option<SdkConfig>,
45 current_file_uploader: Option<S3MultiPartUploader>,
48 params: CopyToParameters,
50}
51
52impl CopyToS3Uploader for PgCopyUploader {
53 fn new(
54 sdk_config: SdkConfig,
55 connection_details: S3UploadInfo,
56 sink_id: &GlobalId,
57 batch: u64,
58 params: CopyToParameters,
59 ) -> Result<PgCopyUploader, anyhow::Error> {
60 match connection_details.format {
61 S3SinkFormat::PgCopy(format_params) => Ok(PgCopyUploader {
62 desc: connection_details.desc,
63 sdk_config: Some(sdk_config),
64 format: format_params,
65 key_manager: S3KeyManager::new(sink_id, &connection_details.uri),
66 batch,
67 max_file_size: connection_details.max_file_size,
68 file_index: 0,
69 current_file_uploader: None,
70 params,
71 }),
72 S3SinkFormat::Parquet => anyhow::bail!("Expected PgCopy format"),
73 }
74 }
75
76 async fn finish(&mut self) -> Result<(), anyhow::Error> {
78 if let Some(uploader) = self.current_file_uploader.take() {
79 let handle =
81 mz_ore::task::spawn(|| "s3_uploader::finish", async { uploader.finish().await });
82 let CompletedUpload {
83 part_count,
84 total_bytes_uploaded,
85 bucket,
86 key,
87 } = handle.await?;
88 info!(
89 "finished upload: bucket {}, key {}, bytes_uploaded {}, parts_uploaded {}",
90 bucket, key, total_bytes_uploaded, part_count
91 );
92 }
93 Ok(())
94 }
95
96 async fn append_row(&mut self, row: &Row) -> Result<(), anyhow::Error> {
101 let mut buf: Vec<u8> = vec![];
102 encode_copy_format(
104 &self.format,
105 row,
106 self.desc.typ(),
107 &mut buf,
108 TextEncodeSettings::STABLE,
109 )
110 .map_err(|_| anyhow!("error encoding row"))?;
111
112 if self.current_file_uploader.is_none() {
113 self.start_new_file_upload().await?;
114 }
115 let mut uploader = self.current_file_uploader.as_mut().expect("known exists");
116
117 match uploader.buffer_chunk(&buf) {
118 Ok(_) => Ok(()),
119 Err(S3MultiPartUploadError::UploadExceedsMaxFileLimit(_)) => {
120 self.start_new_file_upload().await?;
122 uploader = self.current_file_uploader.as_mut().expect("known exists");
123 uploader.buffer_chunk(&buf)?;
124 Ok(())
125 }
126 Err(e) => Err(e.into()),
127 }
128 }
129
130 async fn force_new_file(&mut self) -> Result<(), anyhow::Error> {
131 self.start_new_file_upload().await
132 }
133}
134
135impl PgCopyUploader {
136 async fn start_new_file_upload(&mut self) -> Result<(), anyhow::Error> {
138 self.finish().await?;
139 assert_none!(self.current_file_uploader);
140
141 self.file_index += 1;
142 let object_key =
143 self.key_manager
144 .data_key(self.batch, self.file_index, self.format.file_extension());
145 let bucket = self.key_manager.bucket.clone();
146 info!("starting upload: bucket {}, key {}", &bucket, &object_key);
147 let sdk_config = self
148 .sdk_config
149 .take()
150 .expect("sdk_config should always be present");
151 let max_file_size = self.max_file_size;
152 let part_size_limit = u64::cast_from(self.params.s3_multipart_part_size_bytes);
154 let handle = mz_ore::task::spawn(|| "s3_uploader::try_new", async move {
155 let uploader = S3MultiPartUploader::try_new(
156 &sdk_config,
157 bucket,
158 object_key,
159 S3MultiPartUploaderConfig {
160 part_size_limit,
161 file_size_limit: max_file_size,
162 },
163 )
164 .await;
165 (uploader, sdk_config)
166 });
167 let (uploader, sdk_config) = handle.await;
168 self.sdk_config = Some(sdk_config);
169 let mut uploader = uploader?;
170 if self.format.requires_header() {
171 let mut buf: Vec<u8> = vec![];
172 encode_copy_format_header(&self.format, &self.desc, &mut buf)
173 .map_err(|_| anyhow!("error encoding header"))?;
174 uploader.buffer_chunk(&buf)?;
175 }
176 self.current_file_uploader = Some(uploader);
177 Ok(())
178 }
179}
180
181#[cfg(test)]
196mod tests {
197 use bytesize::ByteSize;
198 use mz_pgcopy::CopyFormatParams;
199 use mz_repr::{ColumnName, Datum, SqlColumnType, SqlRelationType};
200 use uuid::Uuid;
201
202 use super::*;
203
204 fn s3_bucket_path_for_test() -> Option<(String, String)> {
205 let bucket = match std::env::var("MZ_S3_UPLOADER_TEST_S3_BUCKET") {
206 Ok(bucket) => bucket,
207 Err(_) => {
208 if mz_ore::env::is_var_truthy("CI") {
209 panic!("CI is supposed to run this test but something has gone wrong!");
210 }
211 return None;
212 }
213 };
214
215 let prefix = Uuid::new_v4().to_string();
216 let path = format!("cargo_test/{}/file", prefix);
217 Some((bucket, path))
218 }
219
220 #[mz_ore::test(tokio::test(flavor = "multi_thread"))]
221 #[cfg_attr(coverage, ignore)] #[cfg_attr(miri, ignore)] #[ignore] async fn test_multiple_files() -> Result<(), anyhow::Error> {
225 let sdk_config = mz_aws_util::defaults().load().await;
226 let (bucket, path) = match s3_bucket_path_for_test() {
227 Some(tuple) => tuple,
228 None => return Ok(()),
229 };
230 let sink_id = GlobalId::User(123);
231 let batch = 456;
232 let typ: SqlRelationType = SqlRelationType::new(vec![SqlColumnType {
233 scalar_type: mz_repr::SqlScalarType::String,
234 nullable: true,
235 }]);
236 let column_names = vec![ColumnName::from("col1")];
237 let desc = RelationDesc::new(typ, column_names.into_iter());
238 let mut uploader = PgCopyUploader::new(
239 sdk_config.clone(),
240 S3UploadInfo {
241 uri: format!("s3://{}/{}", bucket, path),
242 max_file_size: ByteSize::b(6).as_u64(),
244 desc,
245 format: S3SinkFormat::PgCopy(CopyFormatParams::Csv(Default::default())),
246 },
247 &sink_id,
248 batch,
249 CopyToParameters {
250 s3_multipart_part_size_bytes: 10 * 1024 * 1024,
251 arrow_builder_buffer_ratio: 100,
252 parquet_row_group_ratio: 100,
253 },
254 )?;
255 let mut row = Row::default();
256 row.packer().push(Datum::from("1234567"));
258 uploader.append_row(&row).await?;
259
260 row.packer().push(Datum::Null);
262 uploader.append_row(&row).await?;
263
264 row.packer().push(Datum::from("5678"));
265 uploader.append_row(&row).await?;
266
267 uploader.finish().await?;
268
269 let s3_client = mz_aws_util::s3::new_client(&sdk_config);
272 let first_file = s3_client
273 .get_object()
274 .bucket(bucket.clone())
275 .key(format!(
276 "{}/mz-{}-batch-{:04}-0001.csv",
277 path, sink_id, batch
278 ))
279 .send()
280 .await
281 .unwrap();
282
283 let body = first_file.body.collect().await.unwrap().into_bytes();
284 let expected_body: &[u8] = b"1234567\n";
285 assert_eq!(body, *expected_body);
286
287 let second_file = s3_client
288 .get_object()
289 .bucket(bucket)
290 .key(format!(
291 "{}/mz-{}-batch-{:04}-0002.csv",
292 path, sink_id, batch
293 ))
294 .send()
295 .await
296 .unwrap();
297
298 let body = second_file.body.collect().await.unwrap().into_bytes();
299 let expected_body: &[u8] = b"\n5678\n";
300 assert_eq!(body, *expected_body);
301
302 Ok(())
303 }
304}