Garage v1.0 #683
5 changed files with 198 additions and 83 deletions
|
@ -472,3 +472,32 @@ https:// {
|
||||||
|
|
||||||
More information on how this endpoint is implemented in Garage is available
|
More information on how this endpoint is implemented in Garage is available
|
||||||
in the [Admin API Reference](@/documentation/reference-manual/admin-api.md) page.
|
in the [Admin API Reference](@/documentation/reference-manual/admin-api.md) page.
|
||||||
|
|
||||||
|
### Fileserver browser
|
||||||
|
|
||||||
|
Caddy's built-in
|
||||||
|
[file_server](https://caddyserver.com/docs/caddyfile/directives/file_server)
|
||||||
|
browser functionality can be extended with the
|
||||||
|
[caddy-fs-s3](https://github.com/sagikazarmark/caddy-fs-s3) module.
|
||||||
|
|
||||||
|
This can be configured to use Garage as a backend with the following
|
||||||
|
configuration:
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
browse.garage.tld {
|
||||||
|
file_server {
|
||||||
|
fs s3 {
|
||||||
|
bucket test-bucket
|
||||||
|
region garage
|
||||||
|
|
||||||
|
endpoint https://s3.garage.tld
|
||||||
|
use_path_style
|
||||||
|
}
|
||||||
|
|
||||||
|
browse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Caddy must also be configured with the required `AWS_ACCESS_KEY_ID` and
|
||||||
|
`AWS_SECRET_ACCESS_KEY` environment variables to access the bucket.
|
||||||
|
|
|
@ -387,7 +387,10 @@ pub async fn handle_upload_part_copy(
|
||||||
// we need to insert that data as a new block.
|
// we need to insert that data as a new block.
|
||||||
async move {
|
async move {
|
||||||
if must_upload {
|
if must_upload {
|
||||||
garage2.block_manager.rpc_put_block(final_hash, data).await
|
garage2
|
||||||
|
.block_manager
|
||||||
|
.rpc_put_block(final_hash, data, None)
|
||||||
|
.await
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ use hyper::{Request, Response};
|
||||||
use md5::{Digest as Md5Digest, Md5};
|
use md5::{Digest as Md5Digest, Md5};
|
||||||
|
|
||||||
use garage_table::*;
|
use garage_table::*;
|
||||||
use garage_util::async_hash::*;
|
|
||||||
use garage_util::data::*;
|
use garage_util::data::*;
|
||||||
|
|
||||||
use garage_model::bucket_table::Bucket;
|
use garage_model::bucket_table::Bucket;
|
||||||
|
@ -135,17 +134,8 @@ pub async fn handle_put_part(
|
||||||
garage.version_table.insert(&version).await?;
|
garage.version_table.insert(&version).await?;
|
||||||
|
|
||||||
// Copy data to version
|
// Copy data to version
|
||||||
let first_block_hash = async_blake2sum(first_block.clone()).await;
|
let (total_size, data_md5sum, data_sha256sum, _) =
|
||||||
|
read_and_put_blocks(&garage, &version, part_number, first_block, &mut chunker).await?;
|
||||||
let (total_size, data_md5sum, data_sha256sum) = read_and_put_blocks(
|
|
||||||
&garage,
|
|
||||||
&version,
|
|
||||||
part_number,
|
|
||||||
first_block,
|
|
||||||
first_block_hash,
|
|
||||||
&mut chunker,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Verify that checksums map
|
// Verify that checksums map
|
||||||
ensure_checksum_matches(
|
ensure_checksum_matches(
|
||||||
|
|
|
@ -3,10 +3,13 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use base64::prelude::*;
|
use base64::prelude::*;
|
||||||
use futures::prelude::*;
|
use futures::prelude::*;
|
||||||
|
use futures::stream::FuturesOrdered;
|
||||||
use futures::try_join;
|
use futures::try_join;
|
||||||
use md5::{digest::generic_array::*, Digest as Md5Digest, Md5};
|
use md5::{digest::generic_array::*, Digest as Md5Digest, Md5};
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
|
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use hyper::body::Bytes;
|
use hyper::body::Bytes;
|
||||||
use hyper::header::{HeaderMap, HeaderValue};
|
use hyper::header::{HeaderMap, HeaderValue};
|
||||||
use hyper::{Request, Response};
|
use hyper::{Request, Response};
|
||||||
|
@ -17,6 +20,7 @@ use opentelemetry::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use garage_net::bytes_buf::BytesBuf;
|
use garage_net::bytes_buf::BytesBuf;
|
||||||
|
use garage_rpc::rpc_helper::OrderTag;
|
||||||
use garage_table::*;
|
use garage_table::*;
|
||||||
use garage_util::async_hash::*;
|
use garage_util::async_hash::*;
|
||||||
use garage_util::data::*;
|
use garage_util::data::*;
|
||||||
|
@ -35,6 +39,8 @@ use crate::helpers::*;
|
||||||
use crate::s3::api_server::{ReqBody, ResBody};
|
use crate::s3::api_server::{ReqBody, ResBody};
|
||||||
use crate::s3::error::*;
|
use crate::s3::error::*;
|
||||||
|
|
||||||
|
const PUT_BLOCKS_MAX_PARALLEL: usize = 3;
|
||||||
|
|
||||||
pub async fn handle_put(
|
pub async fn handle_put(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
req: Request<ReqBody>,
|
req: Request<ReqBody>,
|
||||||
|
@ -168,17 +174,8 @@ pub(crate) async fn save_stream<S: Stream<Item = Result<Bytes, Error>> + Unpin>(
|
||||||
garage.version_table.insert(&version).await?;
|
garage.version_table.insert(&version).await?;
|
||||||
|
|
||||||
// Transfer data and verify checksum
|
// Transfer data and verify checksum
|
||||||
let first_block_hash = async_blake2sum(first_block.clone()).await;
|
let (total_size, data_md5sum, data_sha256sum, first_block_hash) =
|
||||||
|
read_and_put_blocks(&garage, &version, 1, first_block, &mut chunker).await?;
|
||||||
let (total_size, data_md5sum, data_sha256sum) = read_and_put_blocks(
|
|
||||||
&garage,
|
|
||||||
&version,
|
|
||||||
1,
|
|
||||||
first_block,
|
|
||||||
first_block_hash,
|
|
||||||
&mut chunker,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
ensure_checksum_matches(
|
ensure_checksum_matches(
|
||||||
data_md5sum.as_slice(),
|
data_md5sum.as_slice(),
|
||||||
|
@ -299,84 +296,164 @@ pub(crate) async fn read_and_put_blocks<S: Stream<Item = Result<Bytes, Error>> +
|
||||||
version: &Version,
|
version: &Version,
|
||||||
part_number: u64,
|
part_number: u64,
|
||||||
first_block: Bytes,
|
first_block: Bytes,
|
||||||
first_block_hash: Hash,
|
|
||||||
chunker: &mut StreamChunker<S>,
|
chunker: &mut StreamChunker<S>,
|
||||||
) -> Result<(u64, GenericArray<u8, typenum::U16>, Hash), Error> {
|
) -> Result<(u64, GenericArray<u8, typenum::U16>, Hash, Hash), Error> {
|
||||||
let tracer = opentelemetry::global::tracer("garage");
|
let tracer = opentelemetry::global::tracer("garage");
|
||||||
|
|
||||||
let md5hasher = AsyncHasher::<Md5>::new();
|
let (block_tx, mut block_rx) = mpsc::channel::<Result<Bytes, Error>>(2);
|
||||||
let sha256hasher = AsyncHasher::<Sha256>::new();
|
let read_blocks = async {
|
||||||
|
block_tx.send(Ok(first_block)).await?;
|
||||||
futures::future::join(
|
|
||||||
md5hasher.update(first_block.clone()),
|
|
||||||
sha256hasher.update(first_block.clone()),
|
|
||||||
)
|
|
||||||
.with_context(Context::current_with_span(
|
|
||||||
tracer.start("Hash first block (md5, sha256)"),
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let mut next_offset = first_block.len();
|
|
||||||
let mut put_curr_version_block = put_block_meta(
|
|
||||||
garage,
|
|
||||||
version,
|
|
||||||
part_number,
|
|
||||||
0,
|
|
||||||
first_block_hash,
|
|
||||||
first_block.len() as u64,
|
|
||||||
);
|
|
||||||
let mut put_curr_block = garage
|
|
||||||
.block_manager
|
|
||||||
.rpc_put_block(first_block_hash, first_block);
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let (_, _, next_block) = futures::try_join!(
|
let res = chunker
|
||||||
put_curr_block.map_err(Error::from),
|
.next()
|
||||||
put_curr_version_block.map_err(Error::from),
|
|
||||||
chunker.next(),
|
|
||||||
)?;
|
|
||||||
if let Some(block) = next_block {
|
|
||||||
let (_, _, block_hash) = futures::future::join3(
|
|
||||||
md5hasher.update(block.clone()),
|
|
||||||
sha256hasher.update(block.clone()),
|
|
||||||
async_blake2sum(block.clone()),
|
|
||||||
)
|
|
||||||
.with_context(Context::current_with_span(
|
.with_context(Context::current_with_span(
|
||||||
tracer.start("Hash block (md5, sha256, blake2)"),
|
tracer.start("Read block from client"),
|
||||||
))
|
))
|
||||||
.await;
|
.await;
|
||||||
let block_len = block.len();
|
match res {
|
||||||
put_curr_version_block = put_block_meta(
|
Ok(Some(block)) => block_tx.send(Ok(block)).await?,
|
||||||
garage,
|
Ok(None) => break,
|
||||||
version,
|
Err(e) => {
|
||||||
part_number,
|
block_tx.send(Err(e)).await?;
|
||||||
next_offset as u64,
|
|
||||||
block_hash,
|
|
||||||
block_len as u64,
|
|
||||||
);
|
|
||||||
put_curr_block = garage.block_manager.rpc_put_block(block_hash, block);
|
|
||||||
next_offset += block_len;
|
|
||||||
} else {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
drop(block_tx);
|
||||||
|
Ok::<_, mpsc::error::SendError<_>>(())
|
||||||
|
};
|
||||||
|
|
||||||
let total_size = next_offset as u64;
|
let (block_tx2, mut block_rx2) = mpsc::channel::<Result<Bytes, Error>>(1);
|
||||||
let data_md5sum = md5hasher.finalize().await;
|
let hash_stream = async {
|
||||||
|
let md5hasher = AsyncHasher::<Md5>::new();
|
||||||
|
let sha256hasher = AsyncHasher::<Sha256>::new();
|
||||||
|
while let Some(next) = block_rx.recv().await {
|
||||||
|
match next {
|
||||||
|
Ok(block) => {
|
||||||
|
block_tx2.send(Ok(block.clone())).await?;
|
||||||
|
futures::future::join(
|
||||||
|
md5hasher.update(block.clone()),
|
||||||
|
sha256hasher.update(block.clone()),
|
||||||
|
)
|
||||||
|
.with_context(Context::current_with_span(
|
||||||
|
tracer.start("Hash block (md5, sha256)"),
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
block_tx2.send(Err(e)).await?;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(block_tx2);
|
||||||
|
Ok::<_, mpsc::error::SendError<_>>(futures::join!(
|
||||||
|
md5hasher.finalize(),
|
||||||
|
sha256hasher.finalize()
|
||||||
|
))
|
||||||
|
};
|
||||||
|
|
||||||
|
let (block_tx3, mut block_rx3) = mpsc::channel::<Result<(Bytes, Hash), Error>>(1);
|
||||||
|
let hash_blocks = async {
|
||||||
|
let mut first_block_hash = None;
|
||||||
|
while let Some(next) = block_rx2.recv().await {
|
||||||
|
match next {
|
||||||
|
Ok(block) => {
|
||||||
|
let hash = async_blake2sum(block.clone())
|
||||||
|
.with_context(Context::current_with_span(
|
||||||
|
tracer.start("Hash block (blake2)"),
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
if first_block_hash.is_none() {
|
||||||
|
first_block_hash = Some(hash);
|
||||||
|
}
|
||||||
|
block_tx3.send(Ok((block, hash))).await?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
block_tx3.send(Err(e)).await?;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(block_tx3);
|
||||||
|
Ok::<_, mpsc::error::SendError<_>>(first_block_hash.unwrap())
|
||||||
|
};
|
||||||
|
|
||||||
|
let put_blocks = async {
|
||||||
|
// Structure for handling several concurrent writes to storage nodes
|
||||||
|
let order_stream = OrderTag::stream();
|
||||||
|
let mut write_futs = FuturesOrdered::new();
|
||||||
|
let mut written_bytes = 0u64;
|
||||||
|
loop {
|
||||||
|
// Simultaneously write blocks to storage nodes & await for next block to be written
|
||||||
|
let currently_running = write_futs.len();
|
||||||
|
let write_futs_next = async {
|
||||||
|
if write_futs.is_empty() {
|
||||||
|
futures::future::pending().await
|
||||||
|
} else {
|
||||||
|
write_futs.next().await.unwrap()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let recv_next = async {
|
||||||
|
// If more than a maximum number of writes are in progress, don't add more for now
|
||||||
|
if currently_running >= PUT_BLOCKS_MAX_PARALLEL {
|
||||||
|
futures::future::pending().await
|
||||||
|
} else {
|
||||||
|
block_rx3.recv().await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let (block, hash) = tokio::select! {
|
||||||
|
result = write_futs_next => {
|
||||||
|
result?;
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
recv = recv_next => match recv {
|
||||||
|
Some(next) => next?,
|
||||||
|
None => break,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// For next block to be written: count its size and spawn future to write it
|
||||||
|
let offset = written_bytes;
|
||||||
|
written_bytes += block.len() as u64;
|
||||||
|
write_futs.push_back(put_block_and_meta(
|
||||||
|
garage,
|
||||||
|
version,
|
||||||
|
part_number,
|
||||||
|
offset,
|
||||||
|
hash,
|
||||||
|
block,
|
||||||
|
order_stream.order(written_bytes),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
while let Some(res) = write_futs.next().await {
|
||||||
|
res?;
|
||||||
|
}
|
||||||
|
Ok::<_, Error>(written_bytes)
|
||||||
|
};
|
||||||
|
|
||||||
|
let (_, stream_hash_result, block_hash_result, final_result) =
|
||||||
|
futures::join!(read_blocks, hash_stream, hash_blocks, put_blocks);
|
||||||
|
|
||||||
|
let total_size = final_result?;
|
||||||
|
// unwrap here is ok, because if hasher failed, it is because something failed
|
||||||
|
// later in the pipeline which already caused a return at the ? on previous line
|
||||||
|
let (data_md5sum, data_sha256sum) = stream_hash_result.unwrap();
|
||||||
|
let first_block_hash = block_hash_result.unwrap();
|
||||||
|
|
||||||
let data_sha256sum = sha256hasher.finalize().await;
|
|
||||||
let data_sha256sum = Hash::try_from(&data_sha256sum[..]).unwrap();
|
let data_sha256sum = Hash::try_from(&data_sha256sum[..]).unwrap();
|
||||||
|
|
||||||
Ok((total_size, data_md5sum, data_sha256sum))
|
Ok((total_size, data_md5sum, data_sha256sum, first_block_hash))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn put_block_meta(
|
async fn put_block_and_meta(
|
||||||
garage: &Garage,
|
garage: &Garage,
|
||||||
version: &Version,
|
version: &Version,
|
||||||
part_number: u64,
|
part_number: u64,
|
||||||
offset: u64,
|
offset: u64,
|
||||||
hash: Hash,
|
hash: Hash,
|
||||||
size: u64,
|
block: Bytes,
|
||||||
|
order_tag: OrderTag,
|
||||||
) -> Result<(), GarageError> {
|
) -> Result<(), GarageError> {
|
||||||
let mut version = version.clone();
|
let mut version = version.clone();
|
||||||
version.blocks.put(
|
version.blocks.put(
|
||||||
|
@ -384,7 +461,10 @@ async fn put_block_meta(
|
||||||
part_number,
|
part_number,
|
||||||
offset,
|
offset,
|
||||||
},
|
},
|
||||||
VersionBlock { hash, size },
|
VersionBlock {
|
||||||
|
hash,
|
||||||
|
size: block.len() as u64,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let block_ref = BlockRef {
|
let block_ref = BlockRef {
|
||||||
|
@ -394,6 +474,9 @@ async fn put_block_meta(
|
||||||
};
|
};
|
||||||
|
|
||||||
futures::try_join!(
|
futures::try_join!(
|
||||||
|
garage
|
||||||
|
.block_manager
|
||||||
|
.rpc_put_block(hash, block, Some(order_tag)),
|
||||||
garage.version_table.insert(&version),
|
garage.version_table.insert(&version),
|
||||||
garage.block_ref_table.insert(&block_ref),
|
garage.block_ref_table.insert(&block_ref),
|
||||||
)?;
|
)?;
|
||||||
|
|
|
@ -348,7 +348,12 @@ impl BlockManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send block to nodes that should have it
|
/// Send block to nodes that should have it
|
||||||
pub async fn rpc_put_block(&self, hash: Hash, data: Bytes) -> Result<(), Error> {
|
pub async fn rpc_put_block(
|
||||||
|
&self,
|
||||||
|
hash: Hash,
|
||||||
|
data: Bytes,
|
||||||
|
order_tag: Option<OrderTag>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
let who = self.replication.write_sets(&hash);
|
let who = self.replication.write_sets(&hash);
|
||||||
|
|
||||||
let (header, bytes) = DataBlock::from_buffer(data, self.compression_level)
|
let (header, bytes) = DataBlock::from_buffer(data, self.compression_level)
|
||||||
|
@ -356,6 +361,11 @@ impl BlockManager {
|
||||||
.into_parts();
|
.into_parts();
|
||||||
let put_block_rpc =
|
let put_block_rpc =
|
||||||
Req::new(BlockRpc::PutBlock { hash, header })?.with_stream_from_buffer(bytes);
|
Req::new(BlockRpc::PutBlock { hash, header })?.with_stream_from_buffer(bytes);
|
||||||
|
let put_block_rpc = if let Some(tag) = order_tag {
|
||||||
|
put_block_rpc.with_order_tag(tag)
|
||||||
|
} else {
|
||||||
|
put_block_rpc
|
||||||
|
};
|
||||||
|
|
||||||
self.system
|
self.system
|
||||||
.rpc_helper()
|
.rpc_helper()
|
||||||
|
|
Loading…
Reference in a new issue