server: Set a custom header if the cache is public

This can be used as a signal to improve caching. Only done for
the Nix Binary Cache APIs for now.
This commit is contained in:
Zhaofeng Li 2023-01-08 00:57:22 -07:00
parent 6c4d04da74
commit 05a5e9cca8
5 changed files with 50 additions and 3 deletions

View file

@ -0,0 +1,7 @@
//! Nix Binary Cache server.
//!
//! This module contains Attic-specific extensions to the
//! Nix Binary Cache API.
/// Header indicating a cache's visibility.
pub const ATTIC_CACHE_VISIBILITY: &str = "X-Attic-Cache-Visibility";

View file

@ -1 +1,2 @@
pub mod binary_cache;
pub mod v1; pub mod v1;

View file

@ -85,6 +85,8 @@ async fn get_nix_cache_info(
}) })
.await?; .await?;
req_state.set_public_cache(cache.is_public);
let info = NixCacheInfo { let info = NixCacheInfo {
want_mass_query: true, want_mass_query: true,
store_dir: cache.store_dir.into(), store_dir: cache.store_dir.into(),
@ -137,6 +139,8 @@ async fn get_store_path_info(
.get_permission_for_cache(&cache_name, cache.is_public); .get_permission_for_cache(&cache_name, cache.is_public);
permission.require_pull()?; permission.require_pull()?;
req_state.set_public_cache(cache.is_public);
let mut narinfo = object.to_nar_info(&nar)?; let mut narinfo = object.to_nar_info(&nar)?;
if narinfo.signature().is_none() { if narinfo.signature().is_none() {
@ -189,6 +193,8 @@ async fn get_nar(
.get_permission_for_cache(&cache_name, cache.is_public); .get_permission_for_cache(&cache_name, cache.is_public);
permission.require_pull()?; permission.require_pull()?;
req_state.set_public_cache(cache.is_public);
database.bump_object_last_accessed(object.id).await?; database.bump_object_last_accessed(object.id).await?;
let remote_file = nar.remote_file.0; let remote_file = nar.remote_file.0;

View file

@ -26,6 +26,7 @@ pub mod oobe;
mod storage; mod storage;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@ -46,7 +47,7 @@ use attic::cache::CacheName;
use config::{Config, StorageConfig}; use config::{Config, StorageConfig};
use database::migration::{Migrator, MigratorTrait}; use database::migration::{Migrator, MigratorTrait};
use error::{ErrorKind, ServerError, ServerResult}; use error::{ErrorKind, ServerError, ServerResult};
use middleware::{init_request_state, restrict_host}; use middleware::{init_request_state, restrict_host, set_visibility_header};
use storage::{LocalBackend, S3Backend, StorageBackend}; use storage::{LocalBackend, S3Backend, StorageBackend};
type State = Arc<StateInner>; type State = Arc<StateInner>;
@ -79,6 +80,12 @@ struct RequestStateInner {
/// Whether the client claims the connection is HTTPS or not. /// Whether the client claims the connection is HTTPS or not.
client_claims_https: bool, client_claims_https: bool,
/// Whether the cache the client's interacting with is public.
///
/// This is purely informational and used to add the `X-Attic-Cache-Visibility`.
/// header in responses.
public_cache: AtomicBool,
} }
impl StateInner { impl StateInner {
@ -167,6 +174,11 @@ impl RequestStateInner {
fn substituter_endpoint(&self, cache: CacheName) -> ServerResult<String> { fn substituter_endpoint(&self, cache: CacheName) -> ServerResult<String> {
Ok(format!("{}{}", self.api_endpoint()?, cache.as_str())) Ok(format!("{}{}", self.api_endpoint()?, cache.as_str()))
} }
/// Indicates whether the cache the client is interacting with is public.
fn set_public_cache(&self, public: bool) {
self.public_cache.store(public, Ordering::Relaxed);
}
} }
/// The fallback route. /// The fallback route.
@ -192,6 +204,7 @@ pub async fn run_api_server(cli_listen: Option<SocketAddr>, config: Config) -> R
.fallback(fallback) .fallback(fallback)
// middlewares // middlewares
.layer(axum::middleware::from_fn(apply_auth)) .layer(axum::middleware::from_fn(apply_auth))
.layer(axum::middleware::from_fn(set_visibility_header))
.layer(axum::middleware::from_fn(init_request_state)) .layer(axum::middleware::from_fn(init_request_state))
.layer(axum::middleware::from_fn(restrict_host)) .layer(axum::middleware::from_fn(restrict_host))
.layer(Extension(state.clone())) .layer(Extension(state.clone()))

View file

@ -1,15 +1,17 @@
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use anyhow::anyhow; use anyhow::anyhow;
use axum::{ use axum::{
extract::{Extension, Host}, extract::{Extension, Host},
http::Request, http::{HeaderValue, Request},
middleware::Next, middleware::Next,
response::Response, response::Response,
}; };
use super::{AuthState, RequestStateInner, State}; use super::{AuthState, RequestState, RequestStateInner, State};
use crate::error::{ErrorKind, ServerResult}; use crate::error::{ErrorKind, ServerResult};
use attic::api::binary_cache::ATTIC_CACHE_VISIBILITY;
/// Initializes per-request state. /// Initializes per-request state.
pub async fn init_request_state<B>( pub async fn init_request_state<B>(
@ -31,6 +33,7 @@ pub async fn init_request_state<B>(
api_endpoint: state.config.api_endpoint.to_owned(), api_endpoint: state.config.api_endpoint.to_owned(),
host, host,
client_claims_https, client_claims_https,
public_cache: AtomicBool::new(false),
}); });
req.extensions_mut().insert(req_state); req.extensions_mut().insert(req_state);
@ -55,3 +58,20 @@ pub async fn restrict_host<B>(
Ok(next.run(req).await) Ok(next.run(req).await)
} }
/// Sets the `X-Attic-Cache-Visibility` header in responses.
pub(crate) async fn set_visibility_header<B>(
Extension(req_state): Extension<RequestState>,
req: Request<B>,
next: Next<B>,
) -> ServerResult<Response> {
let mut response = next.run(req).await;
if req_state.public_cache.load(Ordering::Relaxed) {
response
.headers_mut()
.append(ATTIC_CACHE_VISIBILITY, HeaderValue::from_static("public"));
}
Ok(response)
}