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

com.outbrain.ob1k.server.jetty.handler.RequestTimeoutHandler Maven / Gradle / Ivy

The newest version!
package com.outbrain.ob1k.server.jetty.handler;

import com.google.common.base.Preconditions;
import com.outbrain.swinfra.metrics.api.Counter;
import com.outbrain.swinfra.metrics.api.MetricFactory;
import org.eclipse.jetty.server.HttpChannelState;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * A Jetty Handler that cancels (interrupts) long running requests.
 *
 * @author Eran Harel
 */
public class RequestTimeoutHandler extends HandlerWrapper {
  private static final Logger log = LoggerFactory.getLogger(RequestTimeoutHandler.class);

  private final ScheduledExecutorService requestTimeoutExecutor = Executors.newScheduledThreadPool(1);
  private final long timeoutMillis;
  private final Counter timeouts;

  public RequestTimeoutHandler(final long timeoutMillis, final MetricFactory metricFactory) {
    Preconditions.checkArgument(0 <= timeoutMillis, "timeoutMillis must be positive");
    this.timeoutMillis = timeoutMillis;
    Preconditions.checkNotNull(metricFactory, "metricFactory must not be null");

    timeouts = metricFactory.createCounter(getClass().getSimpleName(), "requestTimeouts");
  }

  /**
   * the timer thread needs to interrupt the jetty thread only if it is still running the original request
   * when timeout happens. the timer thread and jetty's thread can be in a race condition if the request finishes
   * around the time the timer starts to run. is such a case the timer can check to see if the request is still running
   * and decide to interrupt it, meanwhile the jetty thread finishes the original request and starts to execute
   * another one.
   * to avoid that condition we use a simple three state, state machine. the first thread(usually jetty) move the machine
   * to the END state. if the timer thread starts to run and the request hasn't finished yet it moves the machine to the
   * BEFORE_INTERRUPT state to signal the jetty thread that it is about to interrupt so that the jetty thread should avoid
   * continue to the next request. after interrupting the state moves to END.
   * the jetty thread once encountered the BEFORE_INTERRUPT spin waits until the timer will interrupt him and set the state
   * to END.
   */
  private static enum TimerState {
    BEGIN, BEFORE_INTERRUPT, END
  }

  @Override
  public void handle(final String target, final Request baseRequest, final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException {
    final Thread requestThread = Thread.currentThread();

    final AtomicReference timerState = new AtomicReference<>(TimerState.BEGIN);
    requestTimeoutExecutor.schedule(new Runnable() {
      @Override
      public void run() {
        if (!timerState.compareAndSet(TimerState.BEGIN, TimerState.BEFORE_INTERRUPT)) {
          log.debug("request completed before. serving other request now...");
          return;
        }

        final HttpChannelState.State state = baseRequest.getHttpChannelState().getState();
        response.setStatus(HttpServletResponse.SC_REQUEST_TIMEOUT);
        requestThread.interrupt();
        timerState.set(TimerState.END);

        timeouts.inc();
        log.warn("Request timed out!!! state={} url=[{}] queryString=[{}]; Request thread was interrupted...",
            state, request.getRequestURI(), request.getQueryString());
      }
    }, timeoutMillis, TimeUnit.MILLISECONDS);

    try {
      super.handle(target, baseRequest, request, response);
    } finally {
      if (!timerState.compareAndSet(TimerState.BEGIN, TimerState.END)) {
        long counter = 0;
        // spin wait until the timer thread will interrupt us and set the state to END.
        // if we're here the request took the same time as the request timeout so there is a race condition
        // between the executing thread and the timer thread.
        while(!(timerState.get() == TimerState.END)) {
          counter++;
        }
        // reset the interrupt flag on the thread before moving to the next request.
        Thread.interrupted();

        log.info("took {} rounds to finish race between jetty and the timer", counter);
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy