RateLimitlyClients.create(config) creates a reusable client that owns:
Create the client outside the application request path and reuse it. Creating a new client for every resource request discards discovery state, opens new sockets, and creates unnecessary threads.
The client implements AutoCloseable:
try (RateLimitlyClient client = RateLimitlyClients.create(config)) {
runApplication(client);
}
close() is idempotent. It closes sockets, stops router threads, shuts down the
client-owned asynchronous executor, and discards in-flight routing state. It
never shuts down an executor supplied through the configuration builder. Calls
made after close fail. Applications should stop submitting work before closing
the shared client.
checkRateLimit() blocks until it selects a valid response or the configured
request-policy horizon fails. reportLatency() performs discovery if needed,
attempts the datagram sends, and returns without waiting for a response.
Do not call a blocking operation on an event-loop thread. Framework adapters must choose an execution model appropriate for their host environment.
checkRateLimitAsync() and reportLatencyAsync() return CompletionStage
values. A successful stage contains the same result as the blocking method. A
failed stage completes exceptionally with RateLimitlyException; blocking
retrieval methods such as get() and join() wrap that exception according to
the normal CompletableFuture contract.
By default, the client runs each asynchronous operation on a Java 21 virtual
thread owned by the client. It never submits blocking DNS or UDP work to
ForkJoinPool.commonPool().
Applications may call .asyncExecutor(executor) on the configuration builder.
The supplied executor is caller-owned: the client uses it but never shuts it
down. An executor that runs tasks inline can make an asynchronous call block
its caller; that behavior follows from the supplied executor. If task
submission is rejected, the returned stage fails with a RateLimitlyException
whose kind is CONFIGURATION.
One RateLimitlyClient supports concurrent calls to its request, report, and
diagnostics methods. Applications should stop submitting operations and let
them finish before calling close(); a concurrent close terminates transport
resources and may make in-flight operations fail without a usable decision.
The client routes concurrent logical requests by request ID. Every resource request and latency-report send holds a lease on the UDP socket generation it uses. When a valid response requests a source-port change, the client stops new operations from joining that generation and waits for every existing lease to drain.
The replacement port is selected by a per-address-family cursor. It advances by exactly one through ports 49,152 through 65,535, wraps only after 65,535, and skips occupied candidates through the complete range. Steering never falls back to port zero. Sockets bind with address reuse disabled so collision handling remains exclusive on Windows as well as Unix-like systems.
After binding a replacement, the client starts its receive path before making the socket current and closing the drained old socket. Concurrent requests therefore keep receiving on the socket from which they were sent, while the next operation uses the replacement. Multiple steering indications for one draining generation are coalesced into one transition.
Steering is an optimization. If no dynamic-range port can be bound, the client keeps the existing socket and returns the already selected grant or rejection; a local steering failure cannot turn that decision into a request failure.
Cancelling the returned future cancels observation of its result only. It does
not retract a logical request that may already have been transmitted, undo
resource consumption, or suppress best-effort completion delivery. Use
close() to terminate the client’s transport when shutting down the owning
application component.