Skip to main content

SessionManager

Struct SessionManager 

Source
pub struct SessionManager {
    pub max_connections: usize,
    pub nb_connections: usize,
    pub can_accept: bool,
    pub slab: Slab<Rc<RefCell<dyn ProxySession>>>,
    pub max_connections_per_ip: u64,
    pub retry_after: u32,
    pub max_connections_per_subnet: u64,
    pub subnet_ipv4_prefix: u32,
    pub subnet_ipv6_prefix: u32,
    pub max_connection_attempts: u32,
    /* private fields */
}

Fields§

§max_connections: usize§nb_connections: usize§can_accept: bool§slab: Slab<Rc<RefCell<dyn ProxySession>>>§max_connections_per_ip: u64

Default per-(cluster, source-IP) connection limit. 0 disables the feature; cluster-level overrides take precedence at check time.

§retry_after: u32

Default Retry-After header value (seconds) for HTTP 429 responses emitted on per-(cluster, source-IP) limit hit. 0 omits the header.

§max_connections_per_subnet: u64

Default per-(cluster, source-SUBNET) connection limit. 0 disables the subnet limiter, which is the default; cluster-level overrides take precedence at check time.

This is a SECOND, INDEPENDENT counter standing beside max_connections_per_ip, not a replacement for it. The per-IP cap serves the API-gateway case — one abusive client, one address. The per-subnet cap serves the denial-of-service case, where an attacker holding a routed prefix (an ordinary residential IPv6 /64) defeats a per-address counter outright by taking a fresh address for every connection (sozu-proxy/sozu#1270). Both gates are consulted and BOTH must admit, so “10 per IP AND 100 per /64” is expressible.

§subnet_ipv4_prefix: u32

IPv4 prefix length, in bits, defining a “subnet” for the counter above. Boot-time only: changing a mask would re-key every live counter, so there is no runtime setter for it. 32 masks nothing.

§subnet_ipv6_prefix: u32

IPv6 prefix length, in bits. Boot-time only, as above. 128 masks nothing.

§max_connection_attempts: u32

Default backend connection attempt budget of a request, the first attempt included: ServerConfig::max_connection_attempts, changed at runtime by SetMaxConnectionAttempts. A cluster’s own max_connection_attempts takes precedence (Self::effective_max_connection_attempts). Validated, so between 1 and 255.

Implementations§

Source§

impl SessionManager

Source

pub fn new( slab: Slab<Rc<RefCell<dyn ProxySession>>>, max_connections: usize, max_connections_per_ip: u64, retry_after: u32, max_connections_per_subnet: u64, subnet_ipv4_prefix: u32, subnet_ipv6_prefix: u32, ) -> Rc<RefCell<Self>>

Source

pub fn effective_max_connection_attempts( &self, override_value: Option<u32>, ) -> u32

Resolve the backend connection attempt budget of a request routed to a cluster whose own setting is override_value: the cluster’s value when it sets one, the global default otherwise (sozu-proxy/sozu#1800).

Source

pub fn effective_max_connections_per_ip( &self, override_value: Option<u64>, ) -> u64

Resolve the effective per-(cluster, source-IP) limit. override_value is the cluster-level setting from the proto Cluster message: None inherits the global default, Some(0) is explicit “unlimited”, Some(n > 0) overrides.

Source

pub fn effective_retry_after(&self, override_value: Option<u32>) -> u32

Resolve the effective Retry-After header value. Some(0) (or the global default of 0) signals “omit the header” — caller must skip emission rather than render Retry-After: 0.

Source

pub fn effective_max_connections_per_subnet( &self, override_value: Option<u64>, ) -> u64

Resolve the effective per-(cluster, source-SUBNET) limit. Same three-state contract as effective_max_connections_per_ip: None inherits the global default, Some(0) is explicit “unlimited”, Some(n > 0) overrides.

Source

pub fn subnet_key(&self, ip: &IpAddr) -> IpAddr

Derive the subnet counter key for ip under this manager’s configured prefixes. Allocation-free — IpAddr is Copy.

Source

pub fn cluster_subnet_at_limit( &self, token: Token, cluster_id: &str, ip: &IpAddr, override_value: Option<u64>, ) -> bool

Returns true when admitting token to one more connection for (cluster, subnet_of(ip)) would exceed the resolved subnet limit. Exact mirror of cluster_ip_at_limit on the subnet counter: 0 is unlimited, and a token already holding a slot for this (cluster, subnet) is never at the limit.

The subnet key is derived only AFTER the limit == 0 early return, so a deployment that never enables this feature pays no masking work on the hot path at all.

Source

pub fn cluster_connection_at_limit( &self, token: Token, cluster_id: &str, ip: &IpAddr, ip_override: Option<u64>, subnet_override: Option<u64>, ) -> bool

THE gate. Returns true when EITHER the per-IP cap or the per-subnet cap would be exceeded by admitting token to one more connection for cluster_id from ip.

Both enforcement points — the H1+H2 mux router and raw TCP — call this rather than the two halves, so the conjunction “admit only if BOTH caps allow it” is stated once and cannot drift between protocols. The per-IP half is evaluated first and is byte-for-byte the pre-existing check; the subnet half is the single line this feature adds to the admission path.

Source

pub fn cluster_ip_at_limit( &self, token: Token, cluster_id: &str, ip: &IpAddr, override_value: Option<u64>, ) -> bool

Returns true when admitting token to one more connection for (cluster, ip) would exceed the resolved limit. 0 is treated as unlimited. A token that already holds a slot for this (cluster, ip) is NEVER at the limit — H2 sessions multiplex many streams to the same cluster on a single connection, and the limit governs distinct frontend connections, not streams.

Hot-path: called for every cluster-resolving request from mux/router::plan_connect. The nested-map storage lets both lookups borrow cluster_id and ip; no per-call allocation runs here in steady state.

Source

pub fn track_cluster_ip( &mut self, token: Token, cluster_id: ClusterId, ip: IpAddr, )

Account token’s active connection against (cluster, ip). Idempotent within a token: a second call for the same (cluster, ip) is a no-op so H2 retries / multi-stream opens to the same cluster do not double-count.

Copies no cluster id: entry(cluster_id.clone()) only bumps the ClusterId count. A first (token, cluster) observation allocates the new slot’s IP set; later IPs under it reuse the set, and a repeat call allocates nothing.

── A FAILED backend dial keeps the slot, and that is deliberate ──

Router::plan_connect (lib/src/protocol/mux/router.rs) claims here, after the Self::cluster_ip_at_limit gate but BEFORE backend selection and the dial. Six fallible exits follow the claim and none of them releases. Since sozu-proxy/sozu#1519 they sit in two functions rather than one: Router::plan_connect keeps the two pool-reuse refusals, and Mux::dial_backend with the Mux::attach_dialed it calls (lib/src/protocol/mux/mod.rs) own the four on the fresh-dial path — backend selection, buffer-pool exhaustion, the new connection refusing the stream, and the L7Proxy::register_socket rollback, which restores its gauges and slab entry but not the slot. The split moved no ordering: the claim still precedes all six. Self::untrack_all_cluster_ip is the only release, and session teardown is its only caller — the L7Proxy::remove_session impls on HttpProxy / HttpsProxy for the mux, TcpSession::close for raw TCP. An unpaired claim reads like an oversight; it is not one, and sozu-proxy/sozu#1521 is where the reasoning was measured rather than assumed. Do not pair a release with those exits as part of a refactor — it would LOOSEN a connection limit under exactly the conditions a client can induce.

What makes the unpaired claim cheap on H1 is that a failed dial ENDS the frontend connection, so the slot returns with it:

  • Mux::ready_inner joins the plan and the dial into a single Result with and_then, so every post-claim exit from either function reaches the same BackendConnectionError arm of its pending-link loop, which answers the stream from the template registry instead of retrying. No arm there returns SessionResult::Close of its own.
  • Every built-in template in lib/src/protocol/kawa_h1/answers.rs carries Connection: close, so set_default_answer_with_retry_after (lib/src/protocol/mux/answers.rs) clears HttpContext::keep_alive_frontend. On H1 that one bit is what closes the connection; H2 does not read it.
  • H1: once the answer flushes, the response-complete branch of ConnectionH1::writable (lib/src/protocol/mux/h1.rs) takes ConnectionH1::defer_close_for_tls_flush instead of the keep-alive reset, so the session closes after that one flush.
  • H2: the answer ends its own stream only (RFC 9113 §8.2.2: Connection is not an H2 field, sozu-proxy/sozu#1740). The connection stays open, so the slot is held until the client closes it or it times out.
  • TCP never reaches an answer at all: any error out of TcpSession::connect_to_backend (lib/src/tcp.rs) becomes SessionResult::Close in the same event-loop pass.
  • A registered, Normal backend that is down may not fail on the dial at all. The dial — Mux::dial_backend (lib/src/protocol/mux/mod.rs) for HTTP, Backend::try_connect (lib/src/backends.rs) for TCP — is non-blocking: a refusal the kernel reports synchronously surfaces as BackendError::ConnectionFailures, which the mux re-queues for another backend, while an EINPROGRESS connect returns Ok and its refusal (or its connect_timeout) arrives later as a backend failure, so the stream retries through EndStreamAction::Reconnect. The re-track is the no-op above, so one slot — not one per attempt — is held for at most max_connection_attempts attempts (sozu-proxy/sozu#1800).

The window IS the connection’s whole lifetime in two cases: any H2 connection, and an H1 connection answered by an operator template that omits Connection: close (the opt-out doc/rate-limit-design.md records for the 429 template). Either holds its one slot per (connection, cluster) until the client goes away, as a connection whose dial succeeded does.

Source

pub fn track_cluster_subnet( &mut self, token: Token, cluster_id: ClusterId, ip: IpAddr, override_value: Option<u64>, )

Account token’s active connection against (cluster, subnet_of(ip)). Mirror of track_cluster_ip on the subnet counter, with one deliberate difference: it is a NO-OP while the resolved subnet limit is 0.

That conditional is what keeps the default genuinely free. The per-IP index is populated unconditionally, so an operator who never enables the subnet cap would otherwise pay a second hash, a second allocation and a second per-session entry forever for a feature they do not use. The cost is that enabling the cap at runtime does not retroactively count sessions already in flight; the counter converges as those sessions close. The per-IP path has the same shape of gap after clear_cluster_ip_tracking.

Source

pub fn track_cluster_connection( &mut self, token: Token, cluster_id: ClusterId, ip: IpAddr, subnet_override: Option<u64>, )

Account token’s active connection against BOTH counters. The companion of cluster_connection_at_limit: the two enforcement points take the gate and the slot through one call each, so the per-IP and per-subnet bookkeeping can never fall out of step between the mux and raw TCP.

Source

pub fn untrack_all_cluster_ip(&mut self, token: Token)

Drain every slot held by token — per-IP AND per-subnet — and apply the matching decrements. Called on session teardown only — there is no per-stream untrack because the limit is per-connection, not per-stream. Removes empty inner maps so the outer connections_per_cluster_ip does not retain (cluster_id, empty_map) orphans across cluster lifetimes.

The name is historical: this is the SINGLE release point every protocol already calls on close (HttpSession, HttpsSession, TcpSession), so the subnet counter is drained from here rather than from a second call each of those three sites would have to remember. A missed release leaks a slot until worker restart, which is exactly the failure a second entry point would invite.

Source

pub fn subnet_tracking_is_empty(&self) -> bool

Whether both halves of the subnet accounting are empty.

Exists so a test can assert that a DISABLED subnet limiter leaves no bookkeeping behind at all — “inert” and “free” are different claims, and only the second one is checkable from outside.

Source

pub fn clear_cluster_subnet_tracking(&mut self)

Wipe every per-(cluster, source-SUBNET) accounting bucket. Called by the runtime SetMaxConnectionsPerSubnet(0) path. Deliberately does NOT touch the per-IP buckets: the two limiters are independent, and disabling one must not silently reset the other’s live counters.

Source

pub fn clear_cluster_ip_tracking(&mut self)

Wipe every per-(cluster, source-IP) accounting bucket. Called by the runtime SetMaxConnectionsPerIp(0) path so disabling the feature does not leave dead bookkeeping behind that a future re-enable would consult.

Source

pub fn at_capacity(&self) -> bool

The slab is considered at capacity if it contains more sessions than twice max_connections

Source

pub fn accept_slab_threshold(&self) -> usize

The slab fill level at which at_capacity flips to true and the accept queue is flushed. Reported as slab.accept_threshold so the per-iteration slab.accept_threshold_percent gauge in the run loop can chart proximity to this gate, distinct from raw slab usage.

The constant 10 + 2 * max_connections is the historical pre-knob budget; configured slab capacity is 10 + slab_entries_per_connection * max_connections (see command/src/config.rs) and can be larger, so slab.usage_percent (against slab.capacity()) and slab.accept_threshold_percent (against this gate) are emitted as independent gauges.

Source

pub fn check_limits(&mut self) -> bool

Check the number of connections against max_connections, and the slab capacity. Returns false if limits are reached.

Source

pub fn to_session(token: Token) -> SessionToken

Source

pub fn incr(&mut self)

Source

pub fn decr(&mut self)

Decrements the number of sessions, start accepting new connections if the capacity limit of 90% has not been reached.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<'a, T, E> AsTaggedExplicit<'a, E> for T
where T: 'a,

Source§

fn explicit(self, class: Class, tag: u32) -> TaggedParser<'a, Explicit, Self, E>

Source§

impl<'a, T, E> AsTaggedImplicit<'a, E> for T
where T: 'a,

Source§

fn implicit( self, class: Class, constructed: bool, tag: u32, ) -> TaggedParser<'a, Implicit, Self, E>

Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.