snix_castore/chunkstore/
grpc.rs1use futures::TryStreamExt;
4use std::sync::Arc;
5use tonic::{Code, async_trait};
6use tracing::instrument;
7
8use crate::{
9 B3Digest,
10 chunkstore::{Chunk, ChunkStore},
11 composition::{CompositionContext, ServiceBuilder},
12 proto::{self, GetChunkRequest, GetChunkResponse, PutChunkRequest, PutChunkResponse},
13};
14
15const GRPC_CHUNK_SIZE: usize = 2 * 1024 * 1024;
17
18#[derive(Clone)]
20pub struct GrpcChunkStore<T> {
21 instance_name: String,
22 client: proto::chunk_store_service_client::ChunkStoreServiceClient<T>,
25}
26
27impl<T> GrpcChunkStore<T> {
28 pub fn from_client(
30 instance_name: String,
31 client: proto::chunk_store_service_client::ChunkStoreServiceClient<T>,
32 ) -> Self {
33 Self {
34 instance_name,
35 client,
36 }
37 }
38}
39
40#[async_trait]
41impl<T> ChunkStore for GrpcChunkStore<T>
42where
43 T: tonic::client::GrpcService<tonic::body::Body> + Send + Sync + Clone + 'static,
44 T::ResponseBody: tonic::codegen::Body<Data = tonic::codegen::Bytes> + Send + 'static,
45 <T::ResponseBody as tonic::codegen::Body>::Error: Into<tonic::codegen::StdError> + Send,
46 T::Future: Send,
47{
48 #[instrument(skip_all, err, fields(chunk.digest=%digest, instance_name=%self.instance_name))]
49 async fn get(&self, digest: &B3Digest) -> Result<Option<Chunk>, super::Error> {
50 if digest == &B3Digest::EMPTY {
52 return Ok(Some(Chunk::from_static(&[])));
53 }
54
55 let resp = match self
56 .client
57 .clone()
58 .get_chunk(GetChunkRequest::with_digest(*digest))
59 .await
60 {
61 Ok(resp) => resp,
62 Err(err) if err.code() == Code::NotFound => return Ok(None),
63 Err(err) => Err(Error::Tonic(err))?,
64 };
65
66 let mut stream = resp.into_inner();
68 let mut chunk_builder = Chunk::builder();
69 while let Some(GetChunkResponse { data }) = stream.try_next().await.map_err(Error::Tonic)? {
70 chunk_builder.append_bytes(data);
71 }
72 let chunk = chunk_builder.build();
73
74 let actual_digest = chunk.digest();
76 if &chunk.digest() != digest {
77 Err(Error::GetIncorrectDigest {
78 expected: digest.to_owned(),
79 actual: actual_digest,
80 })?;
81 }
82
83 Ok(Some(chunk))
84 }
85
86 #[instrument(skip_all, err, fields(chunk.digest=%digest, instance_name=%self.instance_name))]
87 async fn has(&self, digest: &B3Digest) -> Result<bool, super::Error> {
88 if digest == &B3Digest::EMPTY {
90 return Ok(true);
91 }
92
93 match self
94 .client
95 .clone()
96 .stat_chunk(proto::StatChunkRequest::with_digest(*digest))
97 .await
98 {
99 Ok(_blob_meta) => Ok(true),
100 Err(e) if e.code() == Code::NotFound => Ok(false),
101 Err(err) => Err(Error::Tonic(err))?,
102 }
103 }
104
105 #[instrument(skip_all, err, fields(instance_name=%self.instance_name))]
106 async fn put(&self, chunk: Chunk) -> Result<B3Digest, super::Error> {
107 let chunk_digest: B3Digest = chunk.digest();
108 let chunk_len = chunk.len();
109 let chunk_bytes: bytes::Bytes = chunk.into();
110
111 let resp = self
112 .client
113 .clone()
114 .put_chunk(futures::stream::iter(
115 (0..chunk_len)
116 .step_by(GRPC_CHUNK_SIZE)
117 .map(move |i| PutChunkRequest {
118 data: chunk_bytes.slice(i..(i + GRPC_CHUNK_SIZE).min(chunk_len)),
119 }),
120 ))
121 .await
122 .map_err(Error::Tonic)?;
123
124 let PutChunkResponse { digest } = resp.into_inner();
125 let resp_digest: B3Digest =
126 B3Digest::try_from(digest).map_err(|_| Error::PutInvalidDigest)?;
127
128 if chunk_digest != resp_digest {
129 Err(Error::PutIncorrectDigest {
130 expected: chunk_digest,
131 actual: resp_digest,
132 })?
133 }
134
135 Ok(chunk_digest)
136 }
137}
138
139#[derive(serde::Deserialize, Debug)]
141#[serde(deny_unknown_fields)]
142pub struct GRPCChunkStoreConfig {
143 url: url::Url,
144}
145
146impl TryFrom<url::Url> for GRPCChunkStoreConfig {
147 type Error = Box<dyn std::error::Error + Send + Sync>;
148 fn try_from(url: url::Url) -> Result<Self, Self::Error> {
149 Ok(GRPCChunkStoreConfig { url })
150 }
151}
152
153#[async_trait]
154impl ServiceBuilder for GRPCChunkStoreConfig {
155 type Output = dyn ChunkStore;
156 async fn build<'a>(
157 &'a self,
158 instance_name: &str,
159 _context: &CompositionContext,
160 ) -> Result<Arc<Self::Output>, Box<dyn std::error::Error + Send + Sync>> {
161 let client = proto::chunk_store_service_client::ChunkStoreServiceClient::with_interceptor(
162 crate::tonic::channel_from_url(&self.url).await?,
163 snix_tracing::propagate::tonic::send_trace,
164 );
165 Ok(Arc::new(GrpcChunkStore::from_client(
166 instance_name.to_string(),
167 client,
168 )))
169 }
170}
171
172#[derive(thiserror::Error, Debug)]
173enum Error {
174 #[error(transparent)]
175 Tonic(tonic::Status),
176 #[error("chunk with unexpected digest received, expected: {expected}, got: {actual}")]
177 GetIncorrectDigest {
178 expected: B3Digest,
179 actual: B3Digest,
180 },
181 #[error("got invalid B3Digest in PutChunkResponse")]
182 PutInvalidDigest,
183 #[error("got unexpected digest {actual} after uploading chunk with digest {expected}")]
184 PutIncorrectDigest {
185 expected: B3Digest,
186 actual: B3Digest,
187 },
188}
189
190impl From<Error> for super::Error {
191 fn from(value: Error) -> Self {
192 super::Error(Box::new(value))
193 }
194}