Package org.microproxy.extras
Class ConcurrencyLimiter
java.lang.Object
org.microproxy.extras.ConcurrencyLimiter
- All Implemented Interfaces:
HttpFiltersSource
Limits how many exchanges run at once per key: per client address by default, or per user,
per target host, or anything else a key function derives from the
request and its client connection. Requests over the limit wait in a bounded queue, or are
answered with
429 Too Many Requests.
ConcurrencyLimiter limiter = ConcurrencyLimiter.builder()
.key(ConcurrencyLimiter.byUser()) // default: the client's IP address
.permits(8) // per key
.permits(user -> user.equals("batch") ? 32 : null) // null: the default
.queue(16, Duration.ofSeconds(2)) // up to 16 wait up to 2 s each
.build();
MicroProxy.bootstrap().plusFiltersSource(limiter).start();
How a permit is held:
- It is taken in
HttpFilters.clientToProxyRequest(org.microproxy.http.HttpObject)for the request head, which runs after proxy authentication, sobyUser()sees the authenticated user and requests refused with407are never counted. Place the limiter first among the filters (HttpFiltersChain.of(limiter, ...)) so that later filters do no work for refused requests; an earlier filter that answers a request itself means the limiter never sees it. - It is released exactly once, in
HttpFilters.exchangeEnded(boolean): after the response has been written, or when the exchange was abandoned (client disconnected, server failed, a filter answered or aborted, the proxy stopped). - A
CONNECTthat becomes a byte tunnel, and a request upgraded with101(WebSocket), hold their permit until the tunnel closes. WithcountTunnels(false)CONNECTs are not counted at all and an upgraded request gives its permit back once the101has been sent. An intercepted (MITM)CONNECTholds its permit only until interception starts; the requests inside the session are counted one by one. - As a safety net, a permit held for longer than
ConcurrencyLimiter.Builder.permitTimeout(10 minutes by default) is reclaimed and logged atWARNING. Tunnels are exempt once established: they end when either side closes or goes idle.
A key's bookkeeping is dropped as soon as none of its permits are in use and nobody waits,
so the number of keys tracked is bounded by the number of exchanges in progress. Per-key
rejection counts in snapshot() therefore cover the key's current busy period; the
totals cover the limiter's lifetime.
The limiter only reads request heads (SelectiveFilters), so bodies keep the proxy's
fast path. Waiting blocks only the client connection's virtual thread.
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic final classConfigures aConcurrencyLimiter.static final recordOne key's state.static interfaceCalled for every request over the limit, refused or (in shadow mode) let through.static interfaceMakes the answer to a refused request;nullsends the default429.static final recordThe limiter's state at one moment. -
Method Summary
Modifier and TypeMethodDescriptionstatic ConcurrencyLimiter.Builderbuilder()Starts a builder with the default settings.static BiFunction<HttpRequest, FlowContext, String> Keys requests by the client's IP address (the default).static BiFunction<HttpRequest, FlowContext, String> Keys requests by the host they go to, lower-cased and without the port: theCONNECTtarget, the absolute URI's host, or theHostheader (inside intercepted sessions).static BiFunction<HttpRequest, FlowContext, String> byUser()Keys requests by the authenticated user (flow.getClientDetails().getUserName()), or by the client's IP address for clients that did not authenticate.booleanReturns whetherCONNECTtunnels hold a permit (see the class documentation).filterRequest(HttpRequest originalRequest, FlowContext flowContext) Returns the filters for a request, ornullto proxy it unfiltered.intpermits()Returns the permits per key unlessConcurrencyLimiter.Builder.permits(Function)says otherwise.voidregisterStats(StatsRegistry registry, String prefix) Registers the limiter's state withregistryas<prefix>.in_useand<prefix>.waiting(gauges), and<prefix>.acquired,<prefix>.rq_blocked(requests over the limit, refused or let through in shadow mode) and<prefix>.reclaimed(counters), read when the metrics are shown.booleanshadow()Returns whether requests over the limit are only counted and reported, never refused.snapshot()The current state, per key and in total.Methods inherited from class java.lang.Object
clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, waitMethods inherited from interface org.microproxy.HttpFiltersSource
getMaximumRequestBufferSizeInBytes, getMaximumResponseBufferSizeInBytes
-
Method Details
-
builder
Starts a builder with the default settings.- Returns:
- a new builder with default settings
-
byClientIp
Keys requests by the client's IP address (the default).- Returns:
- a function extracting the client IP key
-
byUser
Keys requests by the authenticated user (flow.getClientDetails().getUserName()), or by the client's IP address for clients that did not authenticate.- Returns:
- a function extracting the authenticated-user or client-IP key
-
byTargetHost
Keys requests by the host they go to, lower-cased and without the port: theCONNECTtarget, the absolute URI's host, or theHostheader (inside intercepted sessions).- Returns:
- a function extracting the lower-case destination host key
-
permits
public int permits()Returns the permits per key unlessConcurrencyLimiter.Builder.permits(Function)says otherwise.- Returns:
- the permits per key unless
ConcurrencyLimiter.Builder.permits(Function)says otherwise
-
shadow
public boolean shadow()Returns whether requests over the limit are only counted and reported, never refused.- Returns:
- whether requests over the limit are only counted and reported, never refused
-
countTunnels
public boolean countTunnels()Returns whetherCONNECTtunnels hold a permit (see the class documentation).- Returns:
- whether
CONNECTtunnels hold a permit (see the class documentation)
-
filterRequest
Description copied from interface:HttpFiltersSourceReturns the filters for a request, ornullto proxy it unfiltered.- Specified by:
filterRequestin interfaceHttpFiltersSource- Parameters:
originalRequest- a copy of the request as received from the clientflowContext- the client connection the request arrived on- Returns:
- the per-request filters, or
nullto leave the request unfiltered
-
snapshot
The current state, per key and in total. Also reclaims expired permits.- Returns:
- the current state, per key and in total
-
registerStats
Registers the limiter's state withregistryas<prefix>.in_useand<prefix>.waiting(gauges), and<prefix>.acquired,<prefix>.rq_blocked(requests over the limit, refused or let through in shadow mode) and<prefix>.reclaimed(counters), read when the metrics are shown. A proxy with metrics registers the limiters among its filters itself, asconcurrency_limit.- Parameters:
registry- the registry to add the metrics toprefix- the names' prefix, such asconcurrency_limit
-