/build/source/nativelink-store/src/common_s3_utils.rs
Line | Count | Source |
1 | | // Copyright 2025 The NativeLink Authors. All rights reserved. |
2 | | // |
3 | | // Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License"); |
4 | | // you may not use this file except in compliance with the License. |
5 | | // You may obtain a copy of the License at |
6 | | // |
7 | | // See LICENSE file for details |
8 | | // |
9 | | // Unless required by applicable law or agreed to in writing, software |
10 | | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | | // See the License for the specific language governing permissions and |
13 | | // limitations under the License. |
14 | | |
15 | | use core::future::Future; |
16 | | use core::pin::Pin; |
17 | | use core::task::{Context, Poll}; |
18 | | use std::sync::Arc; |
19 | | |
20 | | use aws_config::retry::ErrorKind::TransientError; |
21 | | use aws_smithy_runtime_api::client::http::{ |
22 | | HttpClient as SmithyHttpClient, HttpConnector as SmithyHttpConnector, HttpConnectorFuture, |
23 | | HttpConnectorSettings, SharedHttpConnector, |
24 | | }; |
25 | | use aws_smithy_runtime_api::client::orchestrator::HttpRequest; |
26 | | use aws_smithy_runtime_api::client::result::ConnectorError; |
27 | | use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents; |
28 | | use aws_smithy_runtime_api::http::Response; |
29 | | use aws_smithy_types::body::SdkBody; |
30 | | use bytes::{Bytes, BytesMut}; |
31 | | use futures::Stream; |
32 | | use http_body::{Frame, SizeHint}; |
33 | | use http_body_util::BodyExt; |
34 | | use hyper::{Method, Request}; |
35 | | use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; |
36 | | use hyper_util::client::legacy::Client as LegacyClient; |
37 | | use hyper_util::client::legacy::connect::HttpConnector as LegacyHttpConnector; |
38 | | use hyper_util::rt::TokioExecutor; |
39 | | use nativelink_config::stores::CommonObjectSpec; |
40 | | // Note: S3 store should be very careful about the error codes it returns |
41 | | // when in a retryable wrapper. Always prefer Code::Aborted or another |
42 | | // retryable code over Code::InvalidArgument or make_input_err!(). |
43 | | // ie: Don't import make_input_err!() to help prevent this. |
44 | | use nativelink_error::{Code, make_err}; |
45 | | use nativelink_util::buf_channel::DropCloserReadHalf; |
46 | | use nativelink_util::fs; |
47 | | use nativelink_util::retry::{Retrier, RetryResult}; |
48 | | use tokio::time::sleep; |
49 | | |
50 | 33 | pub(crate) fn install_default_rustls_crypto_provider() { |
51 | 33 | drop(rustls::crypto::ring::default_provider().install_default()); |
52 | 33 | } |
53 | | |
54 | | #[derive(Clone)] |
55 | | pub struct TlsClient { |
56 | | client: LegacyClient<HttpsConnector<LegacyHttpConnector>, SdkBody>, |
57 | | retrier: Retrier, |
58 | | } |
59 | | |
60 | | impl TlsClient { |
61 | | #[must_use] |
62 | 3 | pub fn new(common: &CommonObjectSpec) -> Self { |
63 | 3 | install_default_rustls_crypto_provider(); |
64 | | |
65 | 3 | let connector_with_roots = HttpsConnectorBuilder::new().with_platform_verifier(); |
66 | | |
67 | 3 | let connector_with_schemes = if common.insecure_allow_http { |
68 | 0 | connector_with_roots.https_or_http() |
69 | | } else { |
70 | 3 | connector_with_roots.https_only() |
71 | | }; |
72 | | |
73 | 3 | let connector = if common.disable_http2 { |
74 | 0 | connector_with_schemes.enable_http1().build() |
75 | | } else { |
76 | 3 | connector_with_schemes.enable_http1().enable_http2().build() |
77 | | }; |
78 | | |
79 | 3 | Self::with_https_connector(common, connector) |
80 | 3 | } |
81 | | |
82 | 6 | pub fn with_https_connector( |
83 | 6 | common: &CommonObjectSpec, |
84 | 6 | connector: HttpsConnector<LegacyHttpConnector>, |
85 | 6 | ) -> Self { |
86 | 6 | install_default_rustls_crypto_provider(); |
87 | | |
88 | 6 | let client = LegacyClient::builder(TokioExecutor::new()).build(connector); |
89 | | |
90 | | Self { |
91 | 6 | client, |
92 | 6 | retrier: Retrier::new( |
93 | 6 | Arc::new(|duration| Box::pin0 (sleep0 (duration0 ))), |
94 | 6 | common.retry.make_jitter_fn(), |
95 | 6 | common.retry.clone(), |
96 | | ), |
97 | | } |
98 | 6 | } |
99 | | } |
100 | | |
101 | | impl core::fmt::Debug for TlsClient { |
102 | 42 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { |
103 | 42 | f.debug_struct("TlsClient").finish_non_exhaustive() |
104 | 42 | } |
105 | | } |
106 | | |
107 | | impl SmithyHttpClient for TlsClient { |
108 | 6 | fn http_connector( |
109 | 6 | &self, |
110 | 6 | _settings: &HttpConnectorSettings, |
111 | 6 | _components: &RuntimeComponents, |
112 | 6 | ) -> SharedHttpConnector { |
113 | 6 | SharedHttpConnector::new(self.clone()) |
114 | 6 | } |
115 | | } |
116 | | |
117 | | struct RequestBuilder<'a> { |
118 | | components: &'a RequestComponents, |
119 | | } |
120 | | |
121 | | impl<'a> RequestBuilder<'a> { |
122 | | #[inline] |
123 | 6 | const fn new(components: &'a RequestComponents) -> Self { |
124 | 6 | Self { components } |
125 | 6 | } |
126 | | |
127 | | #[inline] |
128 | | #[allow(unused_qualifications, reason = "false positive on hyper::http::Error")] |
129 | 6 | fn build(&self) -> Result<Request<SdkBody>, hyper::http::Error> { |
130 | 6 | let mut req_builder = Request::builder() |
131 | 6 | .method(self.components.method.clone()) |
132 | 6 | .uri(self.components.uri.clone()) |
133 | 6 | .version(self.components.version); |
134 | | |
135 | 6 | let headers_map = req_builder.headers_mut().unwrap(); |
136 | 18 | for (name, value) in &self.components.headers6 { |
137 | 18 | headers_map.insert(name, value.clone()); |
138 | 18 | } |
139 | | |
140 | 6 | match &self.components.body_data { |
141 | 6 | BufferedBodyState::Cloneable(body) => { |
142 | 6 | let cloned_body = body.try_clone().expect("Body should be cloneable"); |
143 | 6 | req_builder.body(cloned_body) |
144 | | } |
145 | 0 | BufferedBodyState::Buffered(bytes) => req_builder.body(SdkBody::from(bytes.clone())), |
146 | 0 | BufferedBodyState::Empty => req_builder.body(SdkBody::empty()), |
147 | | } |
148 | 6 | } |
149 | | } |
150 | | |
151 | | mod execution { |
152 | | use super::conversions::ResponseExt; |
153 | | use super::{ |
154 | | Code, HttpsConnector, LegacyClient, LegacyHttpConnector, RequestBuilder, RequestComponents, |
155 | | Response, RetryResult, SdkBody, fs, make_err, |
156 | | }; |
157 | | |
158 | | #[inline] |
159 | 6 | pub(crate) async fn execute_request( |
160 | 6 | client: LegacyClient<HttpsConnector<LegacyHttpConnector>, SdkBody>, |
161 | 6 | components: &RequestComponents, |
162 | 6 | ) -> RetryResult<Response<SdkBody>> { |
163 | 6 | let _permit = match fs::get_permit().await { |
164 | 6 | Ok(permit) => permit, |
165 | 0 | Err(e) => { |
166 | 0 | return RetryResult::Retry(make_err!( |
167 | 0 | Code::Unavailable, |
168 | 0 | "Failed to acquire permit: {e}" |
169 | 0 | )); |
170 | | } |
171 | | }; |
172 | | |
173 | 6 | let request = match RequestBuilder::new(components).build() { |
174 | 6 | Ok(req) => req, |
175 | 0 | Err(e) => { |
176 | 0 | return RetryResult::Err(make_err!( |
177 | 0 | Code::Internal, |
178 | 0 | "Failed to create request: {e}", |
179 | 0 | )); |
180 | | } |
181 | | }; |
182 | | |
183 | 6 | match client.request(request).await { |
184 | 0 | Ok(resp) => RetryResult::Ok(resp.into_smithy_response()), |
185 | 6 | Err(e) => RetryResult::Retry(make_err!( |
186 | 6 | Code::Unavailable, |
187 | 6 | "Failed request in S3Store: {e}" |
188 | 6 | )), |
189 | | } |
190 | 6 | } |
191 | | |
192 | | #[inline] |
193 | 6 | pub(crate) fn create_retry_stream( |
194 | 6 | client: LegacyClient<HttpsConnector<LegacyHttpConnector>, SdkBody>, |
195 | 6 | components: RequestComponents, |
196 | 6 | ) -> impl futures::Stream<Item = RetryResult<Response<SdkBody>>> { |
197 | 6 | futures::stream::unfold(components, move |components| { |
198 | 6 | let client_clone = client.clone(); |
199 | 6 | async move { |
200 | 6 | let result = execute_request(client_clone, &components).await; |
201 | | |
202 | 6 | Some((result, components)) |
203 | 6 | } |
204 | 6 | }) |
205 | 6 | } |
206 | | } |
207 | | |
208 | | enum BufferedBodyState { |
209 | | Cloneable(SdkBody), |
210 | | Buffered(Bytes), |
211 | | Empty, |
212 | | } |
213 | | |
214 | | mod body_processing { |
215 | | use super::{BodyExt, BufferedBodyState, BytesMut, ConnectorError, SdkBody, TransientError}; |
216 | | |
217 | | /// Buffer a request body fully into memory. |
218 | | /// |
219 | | /// TODO(aaronmondal): This could lead to OOMs in extremely constrained |
220 | | /// environments. Probably better to implement something |
221 | | /// like a rewindable stream logic. |
222 | | #[inline] |
223 | 0 | pub(crate) async fn buffer_body(body: SdkBody) -> Result<BufferedBodyState, ConnectorError> { |
224 | 0 | let mut bytes = BytesMut::new(); |
225 | 0 | let mut body_stream = body; |
226 | 0 | while let Some(frame) = body_stream.frame().await { |
227 | 0 | match frame { |
228 | 0 | Ok(frame) => { |
229 | 0 | if let Some(data) = frame.data_ref() { |
230 | 0 | bytes.extend_from_slice(data); |
231 | 0 | } |
232 | | } |
233 | 0 | Err(e) => { |
234 | 0 | return Err(ConnectorError::other( |
235 | 0 | format!("Failed to read request body: {e}").into(), |
236 | 0 | Some(TransientError), |
237 | 0 | )); |
238 | | } |
239 | | } |
240 | | } |
241 | | |
242 | 0 | Ok(BufferedBodyState::Buffered(bytes.freeze())) |
243 | 0 | } |
244 | | } |
245 | | |
246 | | pub(crate) struct RequestComponents { |
247 | | method: Method, |
248 | | uri: hyper::Uri, |
249 | | version: hyper::Version, |
250 | | headers: hyper::HeaderMap, |
251 | | body_data: BufferedBodyState, |
252 | | } |
253 | | |
254 | | mod conversions { |
255 | | use super::{ |
256 | | BufferedBodyState, ConnectorError, Future, HttpRequest, Method, RequestComponents, |
257 | | Response, SdkBody, TransientError, body_processing, |
258 | | }; |
259 | | |
260 | | pub(crate) trait RequestExt { |
261 | | fn into_components(self) |
262 | | -> impl Future<Output = Result<RequestComponents, ConnectorError>>; |
263 | | } |
264 | | |
265 | | impl RequestExt for HttpRequest { |
266 | 6 | async fn into_components(self) -> Result<RequestComponents, ConnectorError> { |
267 | | // Note: This does *not* refer the the HTTP protocol, but to the |
268 | | // version of the http crate. |
269 | 6 | let hyper_req = self.try_into_http1x().map_err(|e| {0 |
270 | 0 | ConnectorError::other( |
271 | 0 | format!("Failed to convert to HTTP request: {e}").into(), |
272 | 0 | Some(TransientError), |
273 | | ) |
274 | 0 | })?; |
275 | | |
276 | 6 | let method = hyper_req.method().clone(); |
277 | 6 | let uri = hyper_req.uri().clone(); |
278 | 6 | let version = hyper_req.version(); |
279 | 6 | let headers = hyper_req.headers().clone(); |
280 | | |
281 | 6 | let body = hyper_req.into_body(); |
282 | | |
283 | | // Only buffer bodies for methods likely to have payloads. |
284 | 6 | let needs_buffering = matches!0 (method, Method::POST | Method::PUT); |
285 | | |
286 | | // Preserve the body in case we need to retry. |
287 | 6 | let body_data = if needs_buffering { |
288 | 6 | if let Some(cloneable_body) = body.try_clone() { |
289 | 6 | BufferedBodyState::Cloneable(cloneable_body) |
290 | | } else { |
291 | 0 | body_processing::buffer_body(body).await? |
292 | | } |
293 | | } else { |
294 | 0 | BufferedBodyState::Empty |
295 | | }; |
296 | | |
297 | 6 | Ok(RequestComponents { |
298 | 6 | method, |
299 | 6 | uri, |
300 | 6 | version, |
301 | 6 | headers, |
302 | 6 | body_data, |
303 | 6 | }) |
304 | 6 | } |
305 | | } |
306 | | |
307 | | pub(crate) trait ResponseExt { |
308 | | fn into_smithy_response(self) -> Response<SdkBody>; |
309 | | } |
310 | | |
311 | | impl ResponseExt for hyper::Response<hyper::body::Incoming> { |
312 | 0 | fn into_smithy_response(self) -> Response<SdkBody> { |
313 | 0 | let (parts, body) = self.into_parts(); |
314 | 0 | let sdk_body = SdkBody::from_body_1_x(body); |
315 | 0 | let mut smithy_resp = Response::new(parts.status.into(), sdk_body); |
316 | 0 | let header_pairs: Vec<(String, String)> = parts |
317 | 0 | .headers |
318 | 0 | .iter() |
319 | 0 | .filter_map(|(name, value)| { |
320 | 0 | value |
321 | 0 | .to_str() |
322 | 0 | .ok() |
323 | 0 | .map(|value_str| (name.as_str().to_owned(), value_str.to_owned())) |
324 | 0 | }) |
325 | 0 | .collect(); |
326 | | |
327 | 0 | for (name, value) in header_pairs { |
328 | 0 | smithy_resp.headers_mut().insert(name, value); |
329 | 0 | } |
330 | | |
331 | 0 | smithy_resp |
332 | 0 | } |
333 | | } |
334 | | } |
335 | | |
336 | | impl SmithyHttpConnector for TlsClient { |
337 | 6 | fn call(&self, req: HttpRequest) -> HttpConnectorFuture { |
338 | | use conversions::RequestExt; |
339 | | |
340 | 6 | let client = self.client.clone(); |
341 | 6 | let retrier = self.retrier.clone(); |
342 | | |
343 | 6 | HttpConnectorFuture::new(Box::pin(async move { |
344 | 6 | let components = req.into_components().await?0 ; |
345 | | |
346 | 6 | let retry_stream = execution::create_retry_stream(client, components); |
347 | | |
348 | 6 | match retrier.retry(retry_stream).await { |
349 | 0 | Ok(response) => Ok(response), |
350 | 6 | Err(e) => Err(ConnectorError::other( |
351 | 6 | format!("Connection failed after retries: {e}").into(), |
352 | 6 | Some(TransientError), |
353 | 6 | )), |
354 | | } |
355 | 6 | })) |
356 | 6 | } |
357 | | } |
358 | | |
359 | | #[derive(Debug)] |
360 | | pub struct BodyWrapper { |
361 | | pub reader: DropCloserReadHalf, |
362 | | pub size: u64, |
363 | | } |
364 | | |
365 | | impl http_body::Body for BodyWrapper { |
366 | | type Data = Bytes; |
367 | | type Error = std::io::Error; |
368 | | |
369 | 83 | fn poll_frame( |
370 | 83 | self: Pin<&mut Self>, |
371 | 83 | cx: &mut Context<'_>, |
372 | 83 | ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> { |
373 | 83 | let reader = Pin::new(&mut Pin::get_mut(self).reader); |
374 | 83 | reader |
375 | 83 | .poll_next(cx) |
376 | 83 | .map(|maybe_bytes_res| maybe_bytes_res58 .map58 (|res| res50 .map50 (Frame::data))) |
377 | 83 | } |
378 | | |
379 | 4 | fn size_hint(&self) -> SizeHint { |
380 | 4 | SizeHint::with_exact(self.size) |
381 | 4 | } |
382 | | } |