use std::fmt;
use std::io;
use std::sync::Arc;
use async_trait::async_trait;
use futures::future;
use log::warn;
use object_store::path::Path;
use object_store::{
Attributes, MultipartUpload, ObjectMeta, PutMultipartOptions, PutPayload, PutResult, UploadPart,
};
use slatedb_common::vfs::VfsWriter;
use tokio::sync::{mpsc, oneshot};
use tokio::task::JoinHandle;
use crate::inner::{local_error, Inner, TempFile};
use crate::layout::{LocalName, LocalObject};
use crate::ordering::{OpKind, Ticket};
use crate::MirrorError;
type Part = (PutPayload, oneshot::Sender<Result<(), Arc<io::Error>>>);
fn unshare(err: Arc<io::Error>) -> io::Error {
io::Error::new(err.kind(), err)
}
fn finished() -> io::Error {
io::Error::other("multipart upload already completed or aborted")
}
pub(crate) fn committed(
path: &Path,
source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
) -> object_store::Error {
MirrorError::WriteCommitted {
path: path.clone(),
source: source.into(),
}
.into()
}
pub(crate) async fn start(
inner: Arc<Inner>,
path: &Path,
opts: PutMultipartOptions,
) -> object_store::Result<Box<dyn MultipartUpload>> {
let name = LocalName::new(path);
inner.prefixes.lock().check_or_insert(&name)?;
let ticket = inner
.ordering
.register(path, OpKind::Write)
.expect("writes are always queued");
ticket.ready().await;
let attributes = opts.attributes.clone();
let temp = inner.temp_file(&name);
let writer = match inner.vfs.create(&temp.path).await {
Ok(writer) => writer,
Err(err) => {
temp.remove().await;
return Err(local_error(err).into());
}
};
let upload = match inner.remote.put_multipart_opts(path, opts).await {
Ok(upload) => upload,
Err(err) => {
drop(writer);
temp.remove().await;
return Err(err);
}
};
let (parts, received) = mpsc::unbounded_channel();
let writes = inner.runtime.spawn(write_parts(writer, received));
Ok(Box::new(MirrorUpload {
inner,
upload,
path: path.clone(),
name,
attributes,
parts: Some(parts),
writes: Some(writes),
size: 0,
temp: Some(temp),
ticket: Some(ticket),
}))
}
async fn write_parts(
mut writer: Box<dyn VfsWriter>,
mut parts: mpsc::UnboundedReceiver<Part>,
) -> io::Result<()> {
let mut failed: Option<Arc<io::Error>> = None;
while let Some((data, done)) = parts.recv().await {
let result = match &failed {
Some(err) => Err(Arc::clone(err)),
None => async {
for chunk in &data {
writer.write(chunk.clone()).await?;
}
Ok::<_, io::Error>(())
}
.await
.map_err(Arc::new),
};
if let Err(err) = &result {
failed = Some(Arc::clone(err));
}
let _ = done.send(result);
}
match failed {
Some(err) => Err(unshare(err)),
None => writer.finish().await,
}
}
struct MirrorUpload {
inner: Arc<Inner>,
upload: Box<dyn MultipartUpload>,
path: Path,
name: LocalName,
attributes: Attributes,
parts: Option<mpsc::UnboundedSender<Part>>,
writes: Option<JoinHandle<io::Result<()>>>,
size: u64,
temp: Option<TempFile>,
ticket: Option<Ticket>,
}
impl fmt::Debug for MirrorUpload {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MirrorUpload")
.field("path", &self.path)
.field("size", &self.size)
.finish_non_exhaustive()
}
}
impl MirrorUpload {
async fn discard(&mut self) {
self.parts.take();
if let Some(writes) = self.writes.take() {
writes.abort();
let _ = writes.await;
}
if let Some(temp) = self.temp.take() {
temp.remove().await;
}
self.ticket.take();
}
async fn finish_local(&mut self) -> io::Result<()> {
self.parts.take();
let Some(writes) = self.writes.as_mut() else {
return Ok(());
};
let result = writes
.await
.unwrap_or_else(|err| Err(io::Error::other(err)));
self.writes = None;
result
}
}
impl Drop for MirrorUpload {
fn drop(&mut self) {
if let Some(writes) = self.writes.take() {
writes.abort();
let temp = self.temp.take();
self.inner.runtime.spawn(async move {
let _ = writes.await;
drop(temp);
});
}
}
}
#[async_trait]
impl MultipartUpload for MirrorUpload {
fn put_part(&mut self, data: PutPayload) -> UploadPart {
self.size += data.content_length() as u64;
let (done, written) = oneshot::channel();
if let Some(parts) = &self.parts {
#[allow(clippy::disallowed_methods)]
let _ = parts.send((data.clone(), done));
}
let remote = self.upload.put_part(data);
Box::pin(async move {
let (remote, local) = future::join(remote, written).await;
remote?;
local
.unwrap_or_else(|_| Err(Arc::new(finished())))
.map_err(|err| local_error(unshare(err)))?;
Ok(())
})
}
async fn complete(&mut self) -> object_store::Result<PutResult> {
if self.temp.is_none() {
return Err(local_error(finished()).into());
}
if let Err(err) = self.finish_local().await {
if let Err(abort_err) = self.upload.abort().await {
warn!(
"failed to abort mirror multipart upload [path={}, error={}]",
self.path, abort_err
);
}
self.discard().await;
return Err(local_error(err).into());
}
let put = self.upload.complete().await?;
let object = LocalObject {
meta: ObjectMeta {
location: self.path.clone(),
last_modified: self.inner.clock.now(),
size: self.size,
e_tag: put.e_tag.clone(),
version: put.version.clone(),
},
attributes: self.attributes.clone(),
};
let temp = self.temp.take().expect("checked above");
let installed = self
.inner
.install(&self.path, &self.name, temp, object)
.await;
self.ticket.take();
installed.map_err(|err| committed(&self.path, err))?;
Ok(put)
}
async fn abort(&mut self) -> object_store::Result<()> {
let result = self.upload.abort().await;
self.discard().await;
result
}
}