refactor: split sticky-cookie into a separated feature

This commit is contained in:
Jun Kurihara 2023-06-15 15:38:19 +09:00
commit d4040b9b98
No known key found for this signature in database
GPG key ID: 48ADFD173ED22B03
14 changed files with 248 additions and 186 deletions

View file

@ -1,14 +1,13 @@
use super::{load_balance_sticky_cookie::StickyCookieConfig, LbContext, Upstream};
use crate::{constants::STICKY_COOKIE_NAME, log::*};
#[cfg(feature = "sticky-cookie")]
pub use super::{
load_balance_sticky::{LbStickyRoundRobin, LbStickyRoundRobinBuilder},
sticky_cookie::StickyCookie,
};
use derive_builder::Builder;
use rand::Rng;
use rustc_hash::FxHashMap as HashMap;
use std::{
borrow::Cow,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
/// Constants to specify a load balance option
@ -16,6 +15,7 @@ pub(super) mod load_balance_options {
pub const FIX_TO_FIRST: &str = "none";
pub const ROUND_ROBIN: &str = "round_robin";
pub const RANDOM: &str = "random";
#[cfg(feature = "sticky-cookie")]
pub const STICKY_ROUND_ROBIN: &str = "sticky";
}
@ -27,7 +27,7 @@ pub(super) struct PointerToUpstream {
pub context_lb: Option<LbContext>,
}
/// Trait for LB
trait LbWithPointer {
pub(super) trait LbWithPointer {
fn get_ptr(&self, req_info: Option<&LbContext>) -> PointerToUpstream;
}
@ -85,123 +85,6 @@ impl LbWithPointer for LbRandom {
}
}
#[derive(Debug, Clone, Builder)]
/// Round Robin LB object in the sticky cookie manner
pub struct LbStickyRoundRobin {
#[builder(default)]
/// Pointer to the index of the last served upstream destination
ptr: Arc<AtomicUsize>,
#[builder(setter(custom), default)]
/// Number of upstream destinations
num_upstreams: usize,
#[builder(setter(custom))]
/// Information to build the cookie to stick clients to specific backends
pub sticky_config: StickyCookieConfig,
#[builder(setter(custom))]
/// Hashmaps:
/// - Hashmap that maps server indices to server id (string)
/// - Hashmap that maps server ids (string) to server indices, for fast reverse lookup
upstream_maps: UpstreamMap,
}
#[derive(Debug, Clone)]
pub struct UpstreamMap {
/// Hashmap that maps server indices to server id (string)
upstream_index_map: Vec<String>,
/// Hashmap that maps server ids (string) to server indices, for fast reverse lookup
upstream_id_map: HashMap<String, usize>,
}
impl LbStickyRoundRobinBuilder {
pub fn num_upstreams(&mut self, v: &usize) -> &mut Self {
self.num_upstreams = Some(*v);
self
}
pub fn sticky_config(&mut self, server_name: &str, path_opt: &Option<String>) -> &mut Self {
self.sticky_config = Some(StickyCookieConfig {
name: STICKY_COOKIE_NAME.to_string(), // TODO: config等で変更できるように
domain: server_name.to_ascii_lowercase(),
path: if let Some(v) = path_opt {
v.to_ascii_lowercase()
} else {
"/".to_string()
},
duration: 300, // TODO: config等で変更できるように
});
self
}
pub fn upstream_maps(&mut self, upstream_vec: &[Upstream]) -> &mut Self {
let upstream_index_map: Vec<String> = upstream_vec
.iter()
.enumerate()
.map(|(i, v)| v.calculate_id_with_index(i))
.collect();
let mut upstream_id_map = HashMap::default();
for (i, v) in upstream_index_map.iter().enumerate() {
upstream_id_map.insert(v.to_string(), i);
}
self.upstream_maps = Some(UpstreamMap {
upstream_index_map,
upstream_id_map,
});
self
}
}
impl<'a> LbStickyRoundRobin {
fn simple_increment_ptr(&self) -> usize {
// Get a current count of upstream served
let current_ptr = self.ptr.load(Ordering::Relaxed);
if current_ptr < self.num_upstreams - 1 {
self.ptr.fetch_add(1, Ordering::Relaxed)
} else {
// Clear the counter
self.ptr.fetch_and(0, Ordering::Relaxed)
}
}
/// This is always called only internally. So 'unwrap()' is executed.
fn get_server_id_from_index(&self, index: usize) -> String {
self.upstream_maps.upstream_index_map.get(index).unwrap().to_owned()
}
/// This function takes value passed from outside. So 'result' is used.
fn get_server_index_from_id(&self, id: impl Into<Cow<'a, str>>) -> Option<usize> {
let id_str = id.into().to_string();
self.upstream_maps.upstream_id_map.get(&id_str).map(|v| v.to_owned())
}
}
impl LbWithPointer for LbStickyRoundRobin {
fn get_ptr(&self, req_info: Option<&LbContext>) -> PointerToUpstream {
// If given context is None or invalid (not contained), get_ptr() is invoked to increment the pointer.
// Otherwise, get the server index indicated by the server_id inside the cookie
let ptr = match req_info {
None => {
debug!("No sticky cookie");
self.simple_increment_ptr()
}
Some(context) => {
let server_id = &context.sticky_cookie.value.value;
if let Some(server_index) = self.get_server_index_from_id(server_id) {
debug!("Valid sticky cookie: id={}, index={}", server_id, server_index);
server_index
} else {
debug!("Invalid sticky cookie: id={}", server_id);
self.simple_increment_ptr()
}
}
};
// Get the server id from the ptr.
// TODO: This should be simplified and optimized if ptr is not changed (id value exists in cookie).
let upstream_id = self.get_server_id_from_index(ptr);
let new_cookie = self.sticky_config.build_sticky_cookie(upstream_id).unwrap();
let new_context = Some(LbContext {
sticky_cookie: new_cookie,
});
PointerToUpstream {
ptr,
context_lb: new_context,
}
}
}
#[derive(Debug, Clone)]
/// Load Balancing Option
pub enum LoadBalance {
@ -211,6 +94,7 @@ pub enum LoadBalance {
Random(LbRandom),
/// Simple round robin without session persistance
RoundRobin(LbRoundRobin),
#[cfg(feature = "sticky-cookie")]
/// Round robin with session persistance using cookie
StickyRoundRobin(LbStickyRoundRobin),
}
@ -222,7 +106,7 @@ impl Default for LoadBalance {
impl LoadBalance {
/// Get the index of the upstream serving the incoming request
pub(super) fn get_context(&self, context_to_lb: &Option<LbContext>) -> PointerToUpstream {
pub(super) fn get_context(&self, _context_to_lb: &Option<LbContext>) -> PointerToUpstream {
match self {
LoadBalance::FixToFirst => PointerToUpstream {
ptr: 0usize,
@ -230,10 +114,22 @@ impl LoadBalance {
},
LoadBalance::RoundRobin(ptr) => ptr.get_ptr(None),
LoadBalance::Random(ptr) => ptr.get_ptr(None),
#[cfg(feature = "sticky-cookie")]
LoadBalance::StickyRoundRobin(ptr) => {
// Generate new context if sticky round robin is enabled.
ptr.get_ptr(context_to_lb.as_ref())
ptr.get_ptr(_context_to_lb.as_ref())
}
}
}
}
#[derive(Debug, Clone)]
/// Struct to handle the sticky cookie string,
/// - passed from Rp module (http handler) to LB module, manipulated from req, only StickyCookieValue exists.
/// - passed from LB module to Rp module (http handler), will be inserted into res, StickyCookieValue and Info exist.
pub struct LbContext {
#[cfg(feature = "sticky-cookie")]
pub sticky_cookie: StickyCookie,
#[cfg(not(feature = "sticky-cookie"))]
pub sticky_cookie: (),
}

View file

@ -0,0 +1,132 @@
use super::{
load_balance::{LbContext, LbWithPointer, PointerToUpstream},
sticky_cookie::StickyCookieConfig,
Upstream,
};
use crate::{constants::STICKY_COOKIE_NAME, log::*};
use derive_builder::Builder;
use rustc_hash::FxHashMap as HashMap;
use std::{
borrow::Cow,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
#[derive(Debug, Clone, Builder)]
/// Round Robin LB object in the sticky cookie manner
pub struct LbStickyRoundRobin {
#[builder(default)]
/// Pointer to the index of the last served upstream destination
ptr: Arc<AtomicUsize>,
#[builder(setter(custom), default)]
/// Number of upstream destinations
num_upstreams: usize,
#[builder(setter(custom))]
/// Information to build the cookie to stick clients to specific backends
pub sticky_config: StickyCookieConfig,
#[builder(setter(custom))]
/// Hashmaps:
/// - Hashmap that maps server indices to server id (string)
/// - Hashmap that maps server ids (string) to server indices, for fast reverse lookup
upstream_maps: UpstreamMap,
}
#[derive(Debug, Clone)]
pub struct UpstreamMap {
/// Hashmap that maps server indices to server id (string)
upstream_index_map: Vec<String>,
/// Hashmap that maps server ids (string) to server indices, for fast reverse lookup
upstream_id_map: HashMap<String, usize>,
}
impl LbStickyRoundRobinBuilder {
pub fn num_upstreams(&mut self, v: &usize) -> &mut Self {
self.num_upstreams = Some(*v);
self
}
pub fn sticky_config(&mut self, server_name: &str, path_opt: &Option<String>) -> &mut Self {
self.sticky_config = Some(StickyCookieConfig {
name: STICKY_COOKIE_NAME.to_string(), // TODO: config等で変更できるように
domain: server_name.to_ascii_lowercase(),
path: if let Some(v) = path_opt {
v.to_ascii_lowercase()
} else {
"/".to_string()
},
duration: 300, // TODO: config等で変更できるように
});
self
}
pub fn upstream_maps(&mut self, upstream_vec: &[Upstream]) -> &mut Self {
let upstream_index_map: Vec<String> = upstream_vec
.iter()
.enumerate()
.map(|(i, v)| v.calculate_id_with_index(i))
.collect();
let mut upstream_id_map = HashMap::default();
for (i, v) in upstream_index_map.iter().enumerate() {
upstream_id_map.insert(v.to_string(), i);
}
self.upstream_maps = Some(UpstreamMap {
upstream_index_map,
upstream_id_map,
});
self
}
}
impl<'a> LbStickyRoundRobin {
fn simple_increment_ptr(&self) -> usize {
// Get a current count of upstream served
let current_ptr = self.ptr.load(Ordering::Relaxed);
if current_ptr < self.num_upstreams - 1 {
self.ptr.fetch_add(1, Ordering::Relaxed)
} else {
// Clear the counter
self.ptr.fetch_and(0, Ordering::Relaxed)
}
}
/// This is always called only internally. So 'unwrap()' is executed.
fn get_server_id_from_index(&self, index: usize) -> String {
self.upstream_maps.upstream_index_map.get(index).unwrap().to_owned()
}
/// This function takes value passed from outside. So 'result' is used.
fn get_server_index_from_id(&self, id: impl Into<Cow<'a, str>>) -> Option<usize> {
let id_str = id.into().to_string();
self.upstream_maps.upstream_id_map.get(&id_str).map(|v| v.to_owned())
}
}
impl LbWithPointer for LbStickyRoundRobin {
fn get_ptr(&self, req_info: Option<&LbContext>) -> PointerToUpstream {
// If given context is None or invalid (not contained), get_ptr() is invoked to increment the pointer.
// Otherwise, get the server index indicated by the server_id inside the cookie
let ptr = match req_info {
None => {
debug!("No sticky cookie");
self.simple_increment_ptr()
}
Some(context) => {
let server_id = &context.sticky_cookie.value.value;
if let Some(server_index) = self.get_server_index_from_id(server_id) {
debug!("Valid sticky cookie: id={}, index={}", server_id, server_index);
server_index
} else {
debug!("Invalid sticky cookie: id={}", server_id);
self.simple_increment_ptr()
}
}
};
// Get the server id from the ptr.
// TODO: This should be simplified and optimized if ptr is not changed (id value exists in cookie).
let upstream_id = self.get_server_id_from_index(ptr);
let new_cookie = self.sticky_config.build_sticky_cookie(upstream_id).unwrap();
let new_context = Some(LbContext {
sticky_cookie: new_cookie,
});
PointerToUpstream {
ptr,
context_lb: new_context,
}
}
}

View file

@ -1,11 +1,15 @@
mod load_balance;
mod load_balance_sticky_cookie;
#[cfg(feature = "sticky-cookie")]
mod load_balance_sticky;
#[cfg(feature = "sticky-cookie")]
mod sticky_cookie;
mod upstream;
mod upstream_opts;
#[cfg(feature = "sticky-cookie")]
pub use self::sticky_cookie::{StickyCookie, StickyCookieValue};
pub use self::{
load_balance::LoadBalance,
load_balance_sticky_cookie::{LbContext, StickyCookie, StickyCookieBuilder, StickyCookieValue},
load_balance::{LbContext, LoadBalance},
upstream::{ReverseProxy, Upstream, UpstreamGroup, UpstreamGroupBuilder},
upstream_opts::UpstreamOption,
};
@ -270,15 +274,22 @@ impl Backends {
let mut server_config_local = if client_ca_roots_local.is_empty() {
// with no client auth, enable http1.1 -- 3
let mut sc = ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_cert_resolver(Arc::new(resolver_local));
#[cfg(not(feature = "http3"))]
{
ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_cert_resolver(Arc::new(resolver_local))
}
#[cfg(feature = "http3")]
{
let mut sc = ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_cert_resolver(Arc::new(resolver_local));
sc.alpn_protocols = vec![b"h3".to_vec(), b"hq-29".to_vec()]; // TODO: remove hq-29 later?
sc
}
sc
} else {
// with client auth, enable only http1.1 and 2
// let client_certs_verifier = rustls::server::AllowAnyAnonymousOrAuthenticatedClient::new(client_ca_roots);
@ -320,7 +331,7 @@ impl Backends {
}
#[cfg(not(feature = "http3"))]
{
server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
server_crypto_global.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
}
Ok(ServerCrypto {

View file

@ -4,14 +4,6 @@ use crate::error::*;
use chrono::{TimeZone, Utc};
use derive_builder::Builder;
#[derive(Debug, Clone)]
/// Struct to handle the sticky cookie string,
/// - passed from Rp module (http handler) to LB module, manipulated from req, only StickyCookieValue exists.
/// - passed from LB module to Rp module (http handler), will be inserted into res, StickyCookieValue and Info exist.
pub struct LbContext {
pub sticky_cookie: StickyCookie,
}
#[derive(Debug, Clone, Builder)]
/// Cookie value only, used for COOKIE in req
pub struct StickyCookieValue {

View file

@ -1,14 +1,13 @@
use super::{
load_balance::{
load_balance_options as lb_opts, LbRandomBuilder, LbRoundRobinBuilder, LbStickyRoundRobinBuilder, LoadBalance,
},
load_balance_sticky_cookie::LbContext,
BytesName, PathNameBytesExp, UpstreamOption,
};
#[cfg(feature = "sticky-cookie")]
use super::load_balance::LbStickyRoundRobinBuilder;
use super::load_balance::{load_balance_options as lb_opts, LbRandomBuilder, LbRoundRobinBuilder, LoadBalance};
use super::{BytesName, LbContext, PathNameBytesExp, UpstreamOption};
use crate::log::*;
#[cfg(feature = "sticky-cookie")]
use base64::{engine::general_purpose, Engine as _};
use derive_builder::Builder;
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
#[cfg(feature = "sticky-cookie")]
use sha2::{Digest, Sha256};
use std::borrow::Cow;
#[derive(Debug, Clone)]
@ -58,6 +57,7 @@ pub struct Upstream {
pub uri: hyper::Uri,
}
impl Upstream {
#[cfg(feature = "sticky-cookie")]
/// Hashing uri with index to avoid collision
pub fn calculate_id_with_index(&self, index: usize) -> String {
let mut hasher = Sha256::new();
@ -114,8 +114,8 @@ impl UpstreamGroupBuilder {
v: &Option<String>,
// upstream_num: &usize,
upstream_vec: &Vec<Upstream>,
server_name: &str,
path_opt: &Option<String>,
_server_name: &str,
_path_opt: &Option<String>,
) -> &mut Self {
let upstream_num = &upstream_vec.len();
let lb = if let Some(x) = v {
@ -128,10 +128,11 @@ impl UpstreamGroupBuilder {
.build()
.unwrap(),
),
#[cfg(feature = "sticky-cookie")]
lb_opts::STICKY_ROUND_ROBIN => LoadBalance::StickyRoundRobin(
LbStickyRoundRobinBuilder::default()
.num_upstreams(upstream_num)
.sticky_config(server_name, path_opt)
.sticky_config(_server_name, _path_opt)
.upstream_maps(upstream_vec) // TODO:
.build()
.unwrap(),
@ -180,7 +181,10 @@ impl UpstreamGroup {
#[cfg(test)]
mod test {
#[allow(unused)]
use super::*;
#[cfg(feature = "sticky-cookie")]
#[test]
fn calc_id_works() {
let uri = "https://www.rust-lang.org".parse::<hyper::Uri>().unwrap();