Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions src/http/cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//! HTTP cache helpers.
//!
//! Only available on nginx builds where `--with-http_cache` (the
//! default for the stock distribution) is enabled.

use crate::ffi::{
NGX_HTTP_CACHE_BYPASS, NGX_HTTP_CACHE_EXPIRED, NGX_HTTP_CACHE_HIT, NGX_HTTP_CACHE_MISS,
NGX_HTTP_CACHE_REVALIDATED, NGX_HTTP_CACHE_SCARCE, NGX_HTTP_CACHE_STALE,
NGX_HTTP_CACHE_UPDATING, ngx_uint_t,
};

/// Outcome of nginx's cache lookup for a request, mirroring the
/// `$upstream_cache_status` variable. Variants line up with the
/// `NGX_HTTP_CACHE_*` constants nginx core publishes.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum CacheStatus {
/// The request was a cache miss; nginx forwarded it to upstream
/// and stored the response.
Miss,
/// The request bypassed the cache (e.g. `proxy_cache_bypass`).
Bypass,
/// The cached response had expired and was refreshed from
/// upstream.
Expired,
/// A stale cached response was served (e.g. while upstream
/// was unreachable).
Stale,
/// A stale cached response was served while the cache entry is
/// being refreshed in the background.
Updating,
/// The cached response was revalidated against upstream and
/// served from cache.
Revalidated,
/// The cached response was served directly without contacting
/// upstream.
Hit,
/// `proxy_cache_min_uses` not yet reached; the response was not
/// cached on this miss.
Scarce,
}

impl CacheStatus {
/// Convert the raw `r->upstream->cache_status` value reported by
/// nginx into a typed variant. Returns `None` for the
/// "no cache lookup performed" sentinel (`0`) and for any value
/// outside the documented range, so callers can distinguish
/// "request had nothing to do with cache" from a known outcome.
pub fn from_raw(raw: ngx_uint_t) -> Option<Self> {
match raw as u32 {
NGX_HTTP_CACHE_MISS => Some(Self::Miss),
NGX_HTTP_CACHE_BYPASS => Some(Self::Bypass),
NGX_HTTP_CACHE_EXPIRED => Some(Self::Expired),
NGX_HTTP_CACHE_STALE => Some(Self::Stale),
NGX_HTTP_CACHE_UPDATING => Some(Self::Updating),
NGX_HTTP_CACHE_REVALIDATED => Some(Self::Revalidated),
NGX_HTTP_CACHE_HIT => Some(Self::Hit),
NGX_HTTP_CACHE_SCARCE => Some(Self::Scarce),
_ => None,
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn from_raw_maps_known_values() {
assert_eq!(CacheStatus::from_raw(NGX_HTTP_CACHE_MISS as _), Some(CacheStatus::Miss));
assert_eq!(CacheStatus::from_raw(NGX_HTTP_CACHE_BYPASS as _), Some(CacheStatus::Bypass));
assert_eq!(CacheStatus::from_raw(NGX_HTTP_CACHE_EXPIRED as _), Some(CacheStatus::Expired));
assert_eq!(CacheStatus::from_raw(NGX_HTTP_CACHE_STALE as _), Some(CacheStatus::Stale));
assert_eq!(
CacheStatus::from_raw(NGX_HTTP_CACHE_UPDATING as _),
Some(CacheStatus::Updating)
);
assert_eq!(
CacheStatus::from_raw(NGX_HTTP_CACHE_REVALIDATED as _),
Some(CacheStatus::Revalidated)
);
assert_eq!(CacheStatus::from_raw(NGX_HTTP_CACHE_HIT as _), Some(CacheStatus::Hit));
assert_eq!(CacheStatus::from_raw(NGX_HTTP_CACHE_SCARCE as _), Some(CacheStatus::Scarce));
}

#[test]
fn from_raw_rejects_no_cache_sentinel_and_unknown_values() {
assert_eq!(CacheStatus::from_raw(0), None);
assert_eq!(CacheStatus::from_raw(9), None);
assert_eq!(CacheStatus::from_raw(ngx_uint_t::MAX), None);
}
}
4 changes: 4 additions & 0 deletions src/http/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
#[cfg(ngx_feature = "http_cache")]
mod cache;
mod conf;
mod module;
mod request;
mod status;
mod upstream;

#[cfg(ngx_feature = "http_cache")]
pub use cache::*;
pub use conf::*;
pub use module::*;
pub use request::*;
Expand Down
268 changes: 268 additions & 0 deletions src/http/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,114 @@ impl Request {
Some(self.0.upstream)
}

/// Cache lookup outcome for this request, mirroring
/// `$upstream_cache_status`. `None` when the request did not
/// consult any cache (no `proxy_cache` / `fastcgi_cache` /
/// configured, or the request was processed before nginx
/// classified it).
#[cfg(ngx_feature = "http_cache")]
pub fn cache_status(&self) -> Option<crate::http::CacheStatus> {
if self.0.upstream.is_null() {
return None;
}
// SAFETY: `upstream` is non-null per the check above and is
// populated by nginx core for the lifetime of the request.
let status = unsafe { (*self.0.upstream).cache_status() };
crate::http::CacheStatus::from_raw(status as ngx_uint_t)
}

/// Name of the `proxy_cache_path` / `fastcgi_cache_path`
/// keys-zone consulted for this request, or `None` when no
/// cache lookup happened.
///
/// Useful as the `zone=` label for cache metrics: `nginx_vts`,
/// `vts`, statsd exporters, etc.
#[cfg(ngx_feature = "http_cache")]
pub fn cache_zone_name(&self) -> Option<&NgxStr> {
if self.0.cache.is_null() {
return None;
}
// SAFETY: `cache` is non-null per the check above; the
// chained pointers are populated by `ngx_http_file_cache_init`
// in the master before workers fork and remain valid for the
// lifetime of the process.
unsafe {
let file_cache = (*self.0.cache).file_cache;
if file_cache.is_null() {
return None;
}
let shm_zone = (*file_cache).shm_zone;
if shm_zone.is_null() {
return None;
}
Some(NgxStr::from_ngx_str((*shm_zone).shm.name))
}
}

/// The `max_size` of the cache consulted for this request, in bytes,
/// or `None` when no cache lookup happened.
///
/// nginx does not keep this figure in bytes:
/// `ngx_http_file_cache_init` divides the configured size by the
/// filesystem block size, so that it can be compared against the
/// running total directly. Reading `max_size` off the struct
/// therefore gives a number some thousands of times too small,
/// with nothing to suggest anything is wrong. Both this and
/// [`Request::cache_zone_used_size`] undo that, and
/// [`Request::cache_zone_block_size`] is there for callers that
/// want the raw unit.
#[cfg(ngx_feature = "http_cache")]
pub fn cache_zone_max_size(&self) -> Option<u64> {
let file_cache = self.file_cache()?;
// SAFETY: an initialized file cache holds both fields.
Some(unsafe { (*file_cache).max_size as u64 * (*file_cache).bsize as u64 })
}

/// What the cache consulted for this request currently holds on
/// disk, in bytes, or `None` when no cache lookup happened.
///
/// Kept in filesystem blocks for the reason given on
/// [`Request::cache_zone_max_size`], and converted here for the
/// same reason.
#[cfg(ngx_feature = "http_cache")]
pub fn cache_zone_used_size(&self) -> Option<u64> {
let file_cache = self.file_cache()?;
// SAFETY: an initialized file cache holds its shared state,
// which the cache manager keeps up to date.
unsafe {
let sh = (*file_cache).sh;
if sh.is_null() {
return None;
}
Some((*sh).size as u64 * (*file_cache).bsize as u64)
}
}

/// The filesystem block size the cache accounts in, or `None` when
/// no cache lookup happened.
#[cfg(ngx_feature = "http_cache")]
pub fn cache_zone_block_size(&self) -> Option<usize> {
let file_cache = self.file_cache()?;
// SAFETY: an initialized file cache holds the field.
Some(unsafe { (*file_cache).bsize })
}

/// The file cache behind this request, if it consulted one.
#[cfg(ngx_feature = "http_cache")]
fn file_cache(&self) -> Option<*mut crate::ffi::ngx_http_file_cache_t> {
if self.0.cache.is_null() {
return None;
}
// SAFETY: `cache` is non-null per the check above; the pointer
// it holds is populated by `ngx_http_file_cache_init` in the
// master before workers fork.
let file_cache = unsafe { (*self.0.cache).file_cache };
if file_cache.is_null() {
return None;
}
Some(file_cache)
}

/// Pointer to a [`ngx_connection_t`] client connection object.
///
/// [`ngx_connection_t`]: https://nginx.org/en/docs/dev/development_guide.html#connection
Expand Down Expand Up @@ -800,3 +908,163 @@ enum MethodInner {
Trace,
Connect,
}

#[cfg(test)]
mod tests {
use core::mem::MaybeUninit;

use super::*;

fn zeroed_request() -> ngx_http_request_t {
// SAFETY: `ngx_http_request_t` is `#[repr(C)]` and tests only
// read the fields they populate below.
unsafe { MaybeUninit::zeroed().assume_init() }
}

fn request_from(r: &mut ngx_http_request_t) -> &mut Request {
// SAFETY: `Request` is `#[repr(transparent)]` over `ngx_http_request_t`.
unsafe { Request::from_ngx_http_request(r) }
}

#[cfg(ngx_feature = "http_cache")]
mod cache {
use super::*;
use crate::ffi::{
NGX_HTTP_CACHE_HIT, NGX_HTTP_CACHE_MISS, ngx_http_cache_t, ngx_http_file_cache_sh_t,
ngx_http_file_cache_t, ngx_http_upstream_t, ngx_shm_zone_t, ngx_str_t,
};
use crate::http::CacheStatus;

#[test]
fn cache_status_none_when_upstream_null() {
let mut r = zeroed_request();
let req = request_from(&mut r);
assert_eq!(req.cache_status(), None);
}

#[test]
fn cache_status_none_when_field_is_zero_sentinel() {
// `cache_status == 0` is the "no cache lookup" sentinel
// nginx leaves on upstreams created for non-cached
// requests. Must surface as `None`, not a fake variant.
let mut upstream: ngx_http_upstream_t = unsafe { MaybeUninit::zeroed().assume_init() };
upstream.set_cache_status(0);
let mut r = zeroed_request();
r.upstream = &raw mut upstream;
let req = request_from(&mut r);
assert_eq!(req.cache_status(), None);
}

#[test]
fn cache_status_maps_hit_and_miss() {
let mut upstream: ngx_http_upstream_t = unsafe { MaybeUninit::zeroed().assume_init() };
upstream.set_cache_status(NGX_HTTP_CACHE_HIT);
let mut r = zeroed_request();
r.upstream = &raw mut upstream;
assert_eq!(request_from(&mut r).cache_status(), Some(CacheStatus::Hit));

upstream.set_cache_status(NGX_HTTP_CACHE_MISS);
assert_eq!(request_from(&mut r).cache_status(), Some(CacheStatus::Miss));
}

#[test]
fn cache_zone_name_none_when_cache_null() {
let mut r = zeroed_request();
let req = request_from(&mut r);
assert!(req.cache_zone_name().is_none());
}

#[test]
fn cache_zone_name_none_when_file_cache_null() {
let mut cache: ngx_http_cache_t = unsafe { MaybeUninit::zeroed().assume_init() };
// file_cache stays null after zero-init.
let mut r = zeroed_request();
r.cache = &raw mut cache;
assert!(request_from(&mut r).cache_zone_name().is_none());
}

#[test]
fn cache_zone_name_none_when_shm_zone_null() {
let mut file_cache: ngx_http_file_cache_t =
unsafe { MaybeUninit::zeroed().assume_init() };
// shm_zone stays null after zero-init.
let mut cache: ngx_http_cache_t = unsafe { MaybeUninit::zeroed().assume_init() };
cache.file_cache = &raw mut file_cache;
let mut r = zeroed_request();
r.cache = &raw mut cache;
assert!(request_from(&mut r).cache_zone_name().is_none());
}

#[test]
fn zone_sizes_none_when_no_cache_lookup() {
let mut r = zeroed_request();
let req = request_from(&mut r);
assert_eq!(req.cache_zone_max_size(), None);
assert_eq!(req.cache_zone_used_size(), None);
assert_eq!(req.cache_zone_block_size(), None);
}

#[test]
fn zone_sizes_come_back_in_bytes_not_blocks() {
// What nginx holds: max_size and sh->size counted in
// blocks of bsize, the division done once in
// ngx_http_file_cache_init. Reading either field
// directly would answer 256 and 64 here.
let mut sh: ngx_http_file_cache_sh_t = unsafe { MaybeUninit::zeroed().assume_init() };
sh.size = 64;

let mut file_cache: ngx_http_file_cache_t =
unsafe { MaybeUninit::zeroed().assume_init() };
file_cache.max_size = 256;
file_cache.bsize = 4096;
file_cache.sh = &raw mut sh;

let mut cache: ngx_http_cache_t = unsafe { MaybeUninit::zeroed().assume_init() };
cache.file_cache = &raw mut file_cache;

let mut r = zeroed_request();
r.cache = &raw mut cache;
let req = request_from(&mut r);

assert_eq!(req.cache_zone_max_size(), Some(1024 * 1024));
assert_eq!(req.cache_zone_used_size(), Some(256 * 1024));
assert_eq!(req.cache_zone_block_size(), Some(4096));
}

#[test]
fn used_size_none_when_shared_state_null() {
// sh stays null after zero-init: a cache the manager has
// not brought up yet has no running total to report.
let mut file_cache: ngx_http_file_cache_t =
unsafe { MaybeUninit::zeroed().assume_init() };
file_cache.bsize = 4096;

let mut cache: ngx_http_cache_t = unsafe { MaybeUninit::zeroed().assume_init() };
cache.file_cache = &raw mut file_cache;

let mut r = zeroed_request();
r.cache = &raw mut cache;
assert_eq!(request_from(&mut r).cache_zone_used_size(), None);
}

#[test]
fn cache_zone_name_returns_zone_name() {
let bytes = b"my_cache";
let mut shm_zone: ngx_shm_zone_t = unsafe { MaybeUninit::zeroed().assume_init() };
shm_zone.shm.name = ngx_str_t { len: bytes.len(), data: bytes.as_ptr().cast_mut() };

let mut file_cache: ngx_http_file_cache_t =
unsafe { MaybeUninit::zeroed().assume_init() };
file_cache.shm_zone = &raw mut shm_zone;

let mut cache: ngx_http_cache_t = unsafe { MaybeUninit::zeroed().assume_init() };
cache.file_cache = &raw mut file_cache;

let mut r = zeroed_request();
r.cache = &raw mut cache;

let name = request_from(&mut r).cache_zone_name().expect("should resolve");
assert_eq!(name.as_bytes(), bytes);
}
}
}