Skip to content

Commit c405c84

Browse files
committed
Merge remote-tracking branch 'origin/feat/disk-cache' into deployment/wacasoft
2 parents f3b994d + 91eb805 commit c405c84

File tree

10 files changed

+1107
-41
lines changed

10 files changed

+1107
-41
lines changed

Cargo.lock

Lines changed: 344 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ aws-smithy-types = "1.2"
3434
aws-types = "1.3"
3535
axum = { version = "0.6", features = ["headers"] }
3636
axum-server = { version = "0.4.7", features = ["tls-rustls"] }
37+
byte-unit = "5.1.6"
38+
bytes = { version = "1.9.0", features = ["serde"] }
3739
clap = { version = "~4.5", features = ["derive", "env"] }
3840
expanduser = "1.2.2"
3941
flate2 = "1.0"
@@ -42,6 +44,7 @@ http = "1.1"
4244
hyper = { version = "0.14", features = ["full"] }
4345
lazy_static = "1.5"
4446
maligned = "0.2.1"
47+
md5 = "0.7.0"
4548
mime = "0.3"
4649
ndarray = "0.15"
4750
ndarray-stats = "0.5"
@@ -54,6 +57,7 @@ rayon = "1.7"
5457
serde = { version = "1.0", features = ["derive"] }
5558
serde_json = "1.0"
5659
strum_macros = "0.24"
60+
tempdir = "0.3.7"
5761
thiserror = "1.0"
5862
time = "= 0.3.23"
5963
tokio = { version = "1.28", features = ["full"] }
@@ -65,6 +69,7 @@ tracing = "0.1"
6569
tracing-opentelemetry = "0.21"
6670
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
6771
url = { version = "2", features = ["serde"] }
72+
uuid = { version = "1.12.1", features = ["v4"] }
6873
validator = { version = "0.16", features = ["derive"] }
6974
zerocopy = { version = "0.6.1", features = ["alloc", "simd"] }
7075
zune-inflate = "0.2.54"

src/app.rs

Lines changed: 109 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
//! Active Storage server API
22
3+
use crate::chunk_cache::ChunkCache;
34
use crate::cli::CommandLineArgs;
45
use crate::error::ActiveStorageError;
56
use crate::filter_pipeline;
6-
use crate::metrics::{metrics_handler, track_metrics};
7+
use crate::metrics::{metrics_handler, track_metrics, LOCAL_CACHE_MISSES};
78
use crate::models;
89
use crate::operation;
910
use crate::operations;
@@ -14,17 +15,16 @@ use crate::validated_json::ValidatedJson;
1415

1516
use axum::middleware;
1617
use axum::{
17-
body::Bytes,
1818
extract::{Path, State},
1919
headers::authorization::{Authorization, Basic},
2020
http::header,
2121
response::{IntoResponse, Response},
2222
routing::{get, post},
2323
Router, TypedHeader,
2424
};
25+
use bytes::Bytes;
2526

2627
use std::sync::Arc;
27-
use tokio::sync::SemaphorePermit;
2828
use tower::Layer;
2929
use tower::ServiceBuilder;
3030
use tower_http::normalize_path::NormalizePathLayer;
@@ -56,6 +56,9 @@ struct AppState {
5656

5757
/// Resource manager.
5858
resource_manager: ResourceManager,
59+
60+
/// Object chunk cache
61+
chunk_cache: Option<ChunkCache>,
5962
}
6063

6164
impl AppState {
@@ -64,10 +67,17 @@ impl AppState {
6467
let task_limit = args.thread_limit.or_else(|| Some(num_cpus::get() - 1));
6568
let resource_manager =
6669
ResourceManager::new(args.s3_connection_limit, args.memory_limit, task_limit);
70+
let chunk_cache = if args.use_chunk_cache {
71+
Some(ChunkCache::new(args))
72+
} else {
73+
None
74+
};
75+
6776
Self {
6877
args: args.clone(),
6978
s3_client_map: s3_client::S3ClientMap::new(),
7079
resource_manager,
80+
chunk_cache,
7181
}
7282
}
7383
}
@@ -167,27 +177,94 @@ async fn schema() -> &'static str {
167177
///
168178
/// * `client`: S3 client object
169179
/// * `request_data`: RequestData object for the request
170-
#[tracing::instrument(
171-
level = "DEBUG",
172-
skip(client, request_data, resource_manager, mem_permits)
173-
)]
174-
async fn download_object<'a>(
180+
/// * `resource_manager`: ResourceManager object
181+
async fn download_s3_object<'a>(
175182
client: &s3_client::S3Client,
176183
request_data: &models::RequestData,
177184
resource_manager: &'a ResourceManager,
178-
mem_permits: &mut Option<SemaphorePermit<'a>>,
179185
) -> Result<Bytes, ActiveStorageError> {
186+
187+
// If we're given a size in the request data then use this to
188+
// get an initial guess at the required memory resources.
189+
let memory = request_data.size.unwrap_or(0);
190+
let mut mem_permits = resource_manager.memory(memory).await?;
191+
180192
let range = s3_client::get_range(request_data.offset, request_data.size);
181193
let _conn_permits = resource_manager.s3_connection().await?;
194+
182195
client
196+
.download_object(
197+
&request_data.bucket,
198+
&request_data.object,
199+
range,
200+
resource_manager,
201+
&mut mem_permits,
202+
)
203+
.await
204+
}
205+
206+
/// Download and cache an object from S3
207+
///
208+
/// Requests a byte range if `offset` or `size` is specified in the request.
209+
///
210+
/// # Arguments
211+
///
212+
/// * `client`: S3 client object
213+
/// * `request_data`: RequestData object for the request
214+
/// * `resource_manager`: ResourceManager object
215+
/// * `chunk_cache`: ChunkCache object
216+
async fn download_and_cache_s3_object<'a>(
217+
client: &s3_client::S3Client,
218+
request_data: &models::RequestData,
219+
resource_manager: &'a ResourceManager,
220+
chunk_cache: &ChunkCache,
221+
) -> Result<Bytes, ActiveStorageError> {
222+
223+
let key = format!("{},{:?}", client, request_data);
224+
225+
match chunk_cache.get(&key).await {
226+
Ok(value) => {
227+
if let Some(bytes) = value {
228+
return Ok(bytes);
229+
}
230+
},
231+
Err(e) => {
232+
return Err(e);
233+
}
234+
}
235+
236+
// If we're given a size in the request data then use this to
237+
// get an initial guess at the required memory resources.
238+
let memory = request_data.size.unwrap_or(0);
239+
let mut mem_permits = resource_manager.memory(memory).await?;
240+
241+
let range = s3_client::get_range(request_data.offset, request_data.size);
242+
let _conn_permits = resource_manager.s3_connection().await?;
243+
244+
let data = client
183245
.download_object(
184246
&request_data.bucket,
185247
&request_data.object,
186248
range,
187249
resource_manager,
188-
mem_permits,
250+
&mut mem_permits,
189251
)
190-
.await
252+
.await;
253+
254+
if let Ok(data_bytes) = &data {
255+
// Store the data against this key if the chunk cache is enabled.
256+
match chunk_cache.set(&key, data_bytes.clone()).await {
257+
Ok(_) => {},
258+
Err(e) => {
259+
return Err(e);
260+
}
261+
}
262+
}
263+
264+
// Increment the prometheus metric for cache misses
265+
LOCAL_CACHE_MISSES.with_label_values(&["disk"]).inc();
266+
267+
data
191268
}
192269

193270
/// Handler for Active Storage operations
@@ -209,8 +286,6 @@ async fn operation_handler<T: operation::Operation>(
209286
auth: Option<TypedHeader<Authorization<Basic>>>,
210287
ValidatedJson(request_data): ValidatedJson<models::RequestData>,
211288
) -> Result<models::Response, ActiveStorageError> {
212-
let memory = request_data.size.unwrap_or(0);
213-
let mut _mem_permits = state.resource_manager.memory(memory).await?;
214289
let credentials = if let Some(TypedHeader(auth)) = auth {
215290
s3_client::S3Credentials::access_key(auth.username(), auth.password())
216291
} else {
@@ -221,15 +296,27 @@ async fn operation_handler<T: operation::Operation>(
221296
.get(&request_data.source, credentials)
222297
.instrument(tracing::Span::current())
223298
.await;
224-
let data = download_object(
225-
&s3_client,
226-
&request_data,
227-
&state.resource_manager,
228-
&mut _mem_permits,
229-
)
230-
.instrument(tracing::Span::current())
231-
.await?;
232-
// All remaining work is synchronous. If the use_rayon argument was specified, delegate to the
299+
300+
let data = if state.args.use_chunk_cache {
301+
download_and_cache_s3_object(
302+
&s3_client,
303+
&request_data,
304+
&state.resource_manager,
305+
state.chunk_cache.as_ref().unwrap(),
306+
)
307+
.instrument(tracing::Span::current())
308+
.await?
309+
} else {
310+
download_s3_object(
311+
&s3_client,
312+
&request_data,
313+
&state.resource_manager,
314+
)
315+
.instrument(tracing::Span::current())
316+
.await?
317+
};
318+
319+
// All remaining work i s synchronous. If the use_rayon argument was specified, delegate to the
233320
// Rayon thread pool. Otherwise, execute as normal using Tokio.
234321
if state.args.use_rayon {
235322
tokio_rayon::spawn(move || operation::<T>(request_data, data)).await

0 commit comments

Comments
 (0)