All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.jetty.server.handler.QoSHandler Maven / Gradle / Ivy

The newest version!
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.server.handler;

import java.time.Duration;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.io.CyclicTimeouts;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.NanoTime;
import org.eclipse.jetty.util.ProcessorUtils;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.thread.Scheduler;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 

A quality of service {@link Handler} that {@link ConditionalHandler conditionally} * limits the number of concurrent requests, to provide more predictable * end-user experience in case descendant {@link Handler}s have limited * capacity.

*

This {@code Handler} limits the number of concurrent requests * to the number configured via {@link #setMaxRequestCount(int)}. * If more requests are received, they are suspended (that is, not * forwarded to the child {@code Handler}) and stored in a priority * queue.

*

The maximum number of suspended request can be set with * {@link #setMaxSuspendedRequestCount(int)} to avoid out of memory errors. * When this limit is reached, the request will fail fast * with status code {@code 503} (not available).

*

Priorities are determined via {@link #getPriority(Request)}, * that should return values between {@code 0} (the lowest priority) * and positive numbers, typically in the range {@code 0-10}.

*

When a request that is being processed completes, the suspended * request that current has the highest priority is resumed.

*

This {@link Handler} is ideal to avoid contending on slow/limited * resources such as a JDBC connection pool, avoiding the situation * where all server threads blocked contending on the limited * resource, therefore leaving threads free to process other * requests that do not require access to the limited resource.

*

Requests are resumed in priority order, so that when the * server is under load, and there are many requests suspended to * be processed, high priority request are processed first. * For example, load balancer "ping" requests may have the highest * priority, followed by requests performed by admin users, etc. * so that regardless of the load, "ping" and "admin" requests will * always be able to access the web application.

*/ @ManagedObject public class QoSHandler extends ConditionalHandler.Abstract { private static final Logger LOG = LoggerFactory.getLogger(QoSHandler.class); private static final String EXPIRED_ATTRIBUTE_NAME = QoSHandler.class.getName() + ".expired"; private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final AtomicInteger state = new AtomicInteger(); private final Map> queues = new ConcurrentHashMap<>(); private final Set priorities = new ConcurrentSkipListSet<>(Comparator.reverseOrder()); private CyclicTimeouts timeouts; private int maxRequests; private int maxSuspendedRequests = 1024; private Duration maxSuspend = Duration.ZERO; public QoSHandler() { this(null); } public QoSHandler(Handler handler) { super(false, handler); } /** * @return the max number of concurrent requests */ @ManagedAttribute(value = "The maximum number of concurrent requests", readonly = true) public int getMaxRequestCount() { return maxRequests; } /** *

Sets the max number of concurrent requests.

*

A negative or zero value indicates to calculate * a value based on heuristics, drawn from the number * the size of the server thread pool and/or the number * of CPU cores.

* * @param maxRequests the max number of concurrent requests */ public void setMaxRequestCount(int maxRequests) { if (isStarted()) throw new IllegalStateException("Cannot change maxRequests: " + this); this.maxRequests = maxRequests; } /** * @return the max number of suspended requests */ @ManagedAttribute(value = "The maximum number of suspended requests", readonly = true) public int getMaxSuspendedRequestCount() { return maxSuspendedRequests; } /** *

Sets the max number of suspended requests.

*

Once the max suspended request limit is reached, * the request is failed with a HTTP status of * {@code 503 Service unavailable}.

*

A negative value indicate an unlimited number * of suspended requests.

* * @param maxSuspendedRequests the max number of suspended requests */ public void setMaxSuspendedRequestCount(int maxSuspendedRequests) { if (isStarted()) throw new IllegalStateException("Cannot change maxSuspendedRequests: " + this); this.maxSuspendedRequests = maxSuspendedRequests; } /** * Get the max duration of time a request may stay suspended. * @return the max duration of time a request may stay suspended */ public Duration getMaxSuspend() { return maxSuspend; } /** *

Sets the max duration of time a request may stay suspended.

*

Once the duration expires, the request is failed with an HTTP * status of {@code 503 Service Unavailable}.

*

{@link Duration#ZERO} means that the request may stay suspended forever.

* * @param maxSuspend the max duration of time a request may stay suspended */ public void setMaxSuspend(Duration maxSuspend) { if (maxSuspend.isNegative()) throw new IllegalArgumentException("Invalid maxSuspend duration"); this.maxSuspend = maxSuspend; } @ManagedAttribute("The number of suspended requests") public int getSuspendedRequestCount() { int permits = state.get(); return Math.max(0, -permits); } @Override protected void doStart() throws Exception { timeouts = new Timeouts(getServer().getScheduler()); addBean(timeouts); int maxRequests = getMaxRequestCount(); if (maxRequests <= 0) { ThreadPool threadPool = getServer().getThreadPool(); if (threadPool instanceof ThreadPool.SizedThreadPool sized) maxRequests = sized.getMaxThreads() / 2; else maxRequests = ProcessorUtils.availableProcessors(); setMaxRequestCount(maxRequests); } state.set(maxRequests); if (LOG.isDebugEnabled()) LOG.debug("{} initialized maxRequests={}", this, maxRequests); super.doStart(); } @Override protected void doStop() throws Exception { super.doStop(); removeBean(timeouts); timeouts.destroy(); } @Override public boolean onConditionsMet(Request request, Response response, Callback callback) throws Exception { return process(request, response, callback); } private boolean process(Request request, Response response, Callback callback) throws Exception { if (LOG.isDebugEnabled()) LOG.debug("{} processing {}", this, request); boolean expired = false; boolean tooManyRequests = false; // The read lock allows concurrency with resume(), // which is the common case, but not with expire(). lock.readLock().lock(); try { int permits = state.decrementAndGet(); if (permits < 0) { int maxSuspended = getMaxSuspendedRequestCount(); if (maxSuspended >= 0 && Math.abs(permits) > maxSuspended) { // Reached the limit of suspended requests, // complete the request with 503 unavailable. state.incrementAndGet(); tooManyRequests = true; } else if (request.getAttribute(EXPIRED_ATTRIBUTE_NAME) == null) { // Cover this race condition: // T1 in this method may find no permits, so it will suspend the request. // T2 in resume() finds no suspended request yet and increments the permits. // T1 suspends the request, despite permits are available. // This is avoided in resume() using a spin loop to wait for the request to be suspended. // See correspondent state machine logic in resume() and expire(). suspend(request, response, callback); return true; } else { // This is a request that was suspended, it expired, and was re-handled. // Do not suspend it again, just complete it with 503 unavailable. state.incrementAndGet(); expired = true; } } } finally { lock.readLock().unlock(); } if (expired || tooManyRequests) { notAvailable(response, callback); return true; } return handleWithPermit(request, response, callback); } @Override protected boolean onConditionsNotMet(Request request, Response response, Callback callback) throws Exception { return nextHandler(request, response, callback); } private void notAvailable(Response response, Callback callback) { if (LOG.isDebugEnabled()) LOG.debug("{} rejecting {}", this, response.getRequest()); response.setStatus(HttpStatus.SERVICE_UNAVAILABLE_503); if (response.isCommitted()) callback.failed(new IllegalStateException("Response already committed")); else response.write(true, null, callback); } /** *

Returns the priority of the given suspended request, * a value greater than or equal to {@code 0}.

*

Priority {@code 0} is the lowest priority.

*

The set of returned priorities should be stable over * time, typically constrained in the range {@code 0-10}.

* * @param request the suspended request to compute the priority for * @return the priority of the given suspended request, a value {@code >= 0} */ protected int getPriority(Request request) { return 0; } /** *

Fails the given suspended request/response with the given error code and failure.

*

This method is called only for suspended requests, in case of timeout while suspended, * or in case of failure when trying to handle a resumed request.

* * @param request the request to fail * @param response the response to fail * @param callback the callback to complete * @param status the failure status code * @param failure the failure */ protected void failSuspended(Request request, Response response, Callback callback, int status, Throwable failure) { Response.writeError(request, response, callback, status, null, failure); } private boolean handleWithPermit(Request request, Response response, Callback callback) throws Exception { if (LOG.isDebugEnabled()) LOG.debug("{} forwarding {}", this, request); Request.addCompletionListener(request, this::resume); return nextHandler(request, response, callback); } private void suspend(Request request, Response response, Callback callback) { int priority = Math.max(0, getPriority(request)); if (LOG.isDebugEnabled()) LOG.debug("{} suspending priority={} {}", this, priority, request); Entry entry = new Entry(request, response, callback, priority); queues.compute(priority, (k, v) -> { if (v == null) { priorities.add(priority); v = new ConcurrentLinkedQueue<>(); } v.offer(entry); return v; }); timeouts.schedule(entry); } private void resume(Throwable x) { // Allows concurrency with process(), but not with expire(). lock.readLock().lock(); try { // See correspondent state machine logic in process() and expire(). int permits = state.incrementAndGet(); if (permits > 0) { if (LOG.isDebugEnabled()) LOG.debug("{} no suspended requests to resume", this, x); return; } while (true) { if (resumeSuspended()) return; // Found no suspended requests yet, but there will be. // This covers the small race window in process(), where // the state is updated and then the request suspended. Thread.onSpinWait(); } } finally { lock.readLock().unlock(); } } private boolean resumeSuspended() { for (Integer priority : priorities) { Queue queue = queues.get(priority); if (queue == null) return false; Entry entry = queue.poll(); if (entry != null) { if (LOG.isDebugEnabled()) LOG.debug("{} resuming {}", this, entry.request); // Always dispatch to avoid StackOverflowError. execute(entry.request, entry); return true; } } return false; } private void execute(Request request, Runnable task) { request.getComponents().getExecutor().execute(task); } private class Entry implements CyclicTimeouts.Expirable, Runnable { private final Request request; private final Response response; private final Callback callback; private final int priority; private final long expireNanoTime; private Entry(Request request, Response response, Callback callback, int priority) { this.request = request; this.response = response; this.callback = callback; this.priority = priority; Duration maxSuspend = getMaxSuspend(); long suspendNanos = NanoTime.now() + maxSuspend.toNanos(); if (suspendNanos == Long.MAX_VALUE) --suspendNanos; this.expireNanoTime = maxSuspend.isZero() ? Long.MAX_VALUE : suspendNanos; } @Override public long getExpireNanoTime() { return expireNanoTime; } private void expire() { boolean removed; // It should be rare that requests expire. // Grab the write lock to atomically operate on the queue and // the state, avoiding concurrency with process() and resume(). lock.writeLock().lock(); try { // The request timed out, therefore it was not handled. removed = queues.get(priority).remove(this); // The remove() may fail to a concurrent resume(). if (removed) { // See correspondent state machine logic in process() and resume(). state.incrementAndGet(); if (LOG.isDebugEnabled()) LOG.debug("{} timeout {}", QoSHandler.this, request); request.setAttribute(EXPIRED_ATTRIBUTE_NAME, true); } } finally { lock.writeLock().unlock(); } if (removed) execute(request, () -> failSuspended(request, response, callback, HttpStatus.SERVICE_UNAVAILABLE_503, new TimeoutException())); } @Override public void run() { try { boolean handled = handleWithPermit(request, response, callback); if (LOG.isDebugEnabled()) LOG.debug("{} handled={} {}", QoSHandler.this, handled, request); if (!handled) failSuspended(request, response, callback, HttpStatus.NOT_FOUND_404, null); } catch (Throwable x) { if (LOG.isDebugEnabled()) LOG.debug("{} failed {}", QoSHandler.this, request, x); failSuspended(request, response, callback, HttpStatus.INTERNAL_SERVER_ERROR_500, x); } } } private class Timeouts extends CyclicTimeouts { private Timeouts(Scheduler scheduler) { super(scheduler); } @Override protected Iterator iterator() { // Use Java streams as this is called infrequently. return queues.values().stream() .flatMap(Queue::stream) .iterator(); } @Override protected boolean onExpired(Entry entry) { entry.expire(); return false; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy