mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-15 21:18:11 -07:00
mempool: general improvements
This commit is contained in:
@@ -1,98 +0,0 @@
|
||||
use axum::{
|
||||
body::Bytes,
|
||||
http::{HeaderMap, HeaderValue, header},
|
||||
};
|
||||
|
||||
/// HTTP content encoding for pre-compressed caching.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ContentEncoding {
|
||||
Brotli,
|
||||
Gzip,
|
||||
Zstd,
|
||||
Identity,
|
||||
}
|
||||
|
||||
impl ContentEncoding {
|
||||
/// Negotiate the best encoding from the Accept-Encoding header.
|
||||
/// Priority: zstd > br > gzip > identity.
|
||||
/// zstd is preferred over brotli: ~3-5x faster compression at comparable ratios.
|
||||
/// Respects q=0 (RFC 9110 §12.5.3): encodings explicitly rejected are never selected.
|
||||
pub fn negotiate(headers: &HeaderMap) -> Self {
|
||||
let accept = match headers.get(header::ACCEPT_ENCODING) {
|
||||
Some(v) => v,
|
||||
None => return Self::Identity,
|
||||
};
|
||||
let s = match accept.to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Self::Identity,
|
||||
};
|
||||
|
||||
let mut best = Self::Identity;
|
||||
for part in s.split(',') {
|
||||
let mut iter = part.split(';');
|
||||
let name = iter.next().unwrap_or("").trim();
|
||||
let rejected = iter.any(|p| {
|
||||
let p = p.trim();
|
||||
p == "q=0" || p == "q=0.0" || p == "q=0.00" || p == "q=0.000"
|
||||
});
|
||||
if rejected {
|
||||
continue;
|
||||
}
|
||||
match name {
|
||||
"zstd" => return Self::Zstd,
|
||||
"br" => best = Self::Brotli,
|
||||
"gzip" if matches!(best, Self::Identity) => best = Self::Gzip,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
/// Compress bytes with this encoding. Identity returns bytes unchanged.
|
||||
pub fn compress(self, bytes: Bytes) -> Bytes {
|
||||
match self {
|
||||
Self::Identity => bytes,
|
||||
Self::Brotli => {
|
||||
use std::io::Write;
|
||||
let mut output = Vec::with_capacity(bytes.len() / 2);
|
||||
{
|
||||
let mut writer = brotli::CompressorWriter::new(&mut output, 4096, 4, 22);
|
||||
writer.write_all(&bytes).expect("brotli compression failed");
|
||||
}
|
||||
Bytes::from(output)
|
||||
}
|
||||
Self::Gzip => {
|
||||
use flate2::write::GzEncoder;
|
||||
use std::io::Write;
|
||||
let mut encoder = GzEncoder::new(
|
||||
Vec::with_capacity(bytes.len() / 2),
|
||||
flate2::Compression::new(3),
|
||||
);
|
||||
encoder.write_all(&bytes).expect("gzip compression failed");
|
||||
Bytes::from(encoder.finish().expect("gzip finish failed"))
|
||||
}
|
||||
Self::Zstd => {
|
||||
Bytes::from(zstd::encode_all(bytes.as_ref(), 3).expect("zstd compression failed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wire name used for Content-Encoding header and cache key suffix.
|
||||
#[inline]
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Brotli => "br",
|
||||
Self::Gzip => "gzip",
|
||||
Self::Zstd => "zstd",
|
||||
Self::Identity => "identity",
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn header_value(self) -> Option<HeaderValue> {
|
||||
match self {
|
||||
Self::Identity => None,
|
||||
_ => Some(HeaderValue::from_static(self.as_str())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,6 @@ use axum::http::{
|
||||
header::{self, IF_NONE_MATCH},
|
||||
};
|
||||
|
||||
use super::ContentEncoding;
|
||||
|
||||
pub trait HeaderMapExtended {
|
||||
fn has_etag(&self, etag: &str) -> bool;
|
||||
fn insert_etag(&mut self, etag: &str);
|
||||
@@ -14,8 +12,6 @@ pub trait HeaderMapExtended {
|
||||
|
||||
fn insert_content_disposition_attachment(&mut self, filename: &str);
|
||||
|
||||
fn insert_content_encoding(&mut self, encoding: ContentEncoding);
|
||||
|
||||
fn insert_content_type_application_json(&mut self);
|
||||
fn insert_content_type_text_csv(&mut self);
|
||||
|
||||
@@ -27,18 +23,17 @@ pub trait HeaderMapExtended {
|
||||
impl HeaderMapExtended for HeaderMap {
|
||||
fn has_etag(&self, etag: &str) -> bool {
|
||||
self.get(IF_NONE_MATCH).is_some_and(|v| {
|
||||
let s = v.as_bytes();
|
||||
// Match both quoted and unquoted: "etag" or etag
|
||||
s == etag.as_bytes()
|
||||
|| (s.len() == etag.len() + 2
|
||||
&& s[0] == b'"'
|
||||
&& s[s.len() - 1] == b'"'
|
||||
&& &s[1..s.len() - 1] == etag.as_bytes())
|
||||
let raw = v.as_bytes();
|
||||
let target = etag.as_bytes();
|
||||
raw == b"*"
|
||||
|| raw
|
||||
.split(|&b| b == b',')
|
||||
.any(|entry| normalize_etag(entry) == target)
|
||||
})
|
||||
}
|
||||
|
||||
fn insert_etag(&mut self, etag: &str) {
|
||||
self.insert(header::ETAG, format!("\"{etag}\"").parse().unwrap());
|
||||
self.insert(header::ETAG, format!("W/\"{etag}\"").parse().unwrap());
|
||||
}
|
||||
|
||||
fn insert_cache_control(&mut self, value: &str) {
|
||||
@@ -61,13 +56,6 @@ impl HeaderMapExtended for HeaderMap {
|
||||
);
|
||||
}
|
||||
|
||||
fn insert_content_encoding(&mut self, encoding: ContentEncoding) {
|
||||
if let Some(value) = encoding.header_value() {
|
||||
self.insert(header::CONTENT_ENCODING, value);
|
||||
self.insert_vary_accept_encoding();
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_content_type_application_json(&mut self) {
|
||||
self.insert(header::CONTENT_TYPE, "application/json".parse().unwrap());
|
||||
}
|
||||
@@ -85,3 +73,38 @@ impl HeaderMapExtended for HeaderMap {
|
||||
self.insert("Sunset", sunset.parse().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_etag(entry: &[u8]) -> &[u8] {
|
||||
let s = entry.trim_ascii();
|
||||
let s = s.strip_prefix(b"W/").unwrap_or(s);
|
||||
s.strip_prefix(b"\"")
|
||||
.and_then(|s| s.strip_suffix(b"\""))
|
||||
.unwrap_or(s)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use axum::http::HeaderValue;
|
||||
|
||||
fn map(if_none_match: &str) -> HeaderMap {
|
||||
let mut h = HeaderMap::new();
|
||||
h.insert(IF_NONE_MATCH, HeaderValue::from_str(if_none_match).unwrap());
|
||||
h
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_weak_strong_wildcard_and_list() {
|
||||
assert!(map("W/\"s1-abc\"").has_etag("s1-abc"));
|
||||
assert!(map("\"s1-abc\"").has_etag("s1-abc"));
|
||||
assert!(map("*").has_etag("anything"));
|
||||
assert!(map("W/\"a\", W/\"s1-abc\"").has_etag("s1-abc"));
|
||||
assert!(map(" W/\"s1-abc\" ").has_etag("s1-abc"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_mismatch_and_missing() {
|
||||
assert!(!map("W/\"other\"").has_etag("s1-abc"));
|
||||
assert!(!HeaderMap::new().has_etag("s1-abc"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
mod encoding;
|
||||
mod header_map;
|
||||
mod response;
|
||||
mod transform_operation;
|
||||
|
||||
pub use encoding::*;
|
||||
pub use header_map::*;
|
||||
pub use response::*;
|
||||
pub use transform_operation::*;
|
||||
|
||||
@@ -1,30 +1,18 @@
|
||||
use axum::{
|
||||
body::Body,
|
||||
body::{Body, Bytes},
|
||||
http::{HeaderMap, Response, StatusCode, header},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
use super::header_map::HeaderMapExtended;
|
||||
use crate::cache::CacheParams;
|
||||
|
||||
fn new_json_cached<T: Serialize>(value: T, params: &CacheParams) -> Response<Body> {
|
||||
let bytes = serde_json::to_vec(&value).unwrap();
|
||||
let mut response = Response::builder().body(bytes.into()).unwrap();
|
||||
let h = response.headers_mut();
|
||||
h.insert_content_type_application_json();
|
||||
params.apply_to(h);
|
||||
response
|
||||
}
|
||||
|
||||
pub trait ResponseExtended
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn new_not_modified(params: &CacheParams) -> Self;
|
||||
fn static_json<T>(headers: &HeaderMap, value: T) -> Self
|
||||
where
|
||||
T: Serialize;
|
||||
fn static_json_bytes(headers: &HeaderMap, bytes: Bytes) -> Self;
|
||||
fn static_bytes(
|
||||
headers: &HeaderMap,
|
||||
bytes: &'static [u8],
|
||||
@@ -40,15 +28,16 @@ impl ResponseExtended for Response<Body> {
|
||||
response
|
||||
}
|
||||
|
||||
fn static_json<T>(headers: &HeaderMap, value: T) -> Self
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
fn static_json_bytes(headers: &HeaderMap, bytes: Bytes) -> Self {
|
||||
let params = CacheParams::deploy();
|
||||
if params.matches_etag(headers) {
|
||||
return Self::new_not_modified(¶ms);
|
||||
}
|
||||
new_json_cached(value, ¶ms)
|
||||
let mut response = Response::new(Body::from(bytes));
|
||||
let h = response.headers_mut();
|
||||
h.insert_content_type_application_json();
|
||||
params.apply_to(h);
|
||||
response
|
||||
}
|
||||
|
||||
fn static_bytes(
|
||||
|
||||
Reference in New Issue
Block a user