org.cloudfoundry.tools.timeout.HotSwappingTimeoutProtectionStrategy.java Source code

Java tutorial

Introduction

Here is the source code for org.cloudfoundry.tools.timeout.HotSwappingTimeoutProtectionStrategy.java

Source

/*
 * Copyright 2010-2012 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.cloudfoundry.tools.timeout;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import javax.servlet.http.HttpServletResponse;

import org.cloudfoundry.tools.timeout.monitor.DuplicatingHttpServletResponseMonitorFactory;
import org.cloudfoundry.tools.timeout.monitor.HttpServletResponseMonitor;
import org.cloudfoundry.tools.timeout.monitor.HttpServletResponseMonitorFactory;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;

/**
 * {@link TimeoutProtectionStrategy} that works by hot-swapping the original request with the subsequent poll request.
 * Requests that take longer than the {@link #setThreshold(long) threshold} to respond will be protected. The threshold
 * should therefore be set to a value slightly lower than the expected gateway timeout. NOTE: once the threshold has
 * been reached the original response will block until a poll occurs. This ensures that none of the response data is
 * lost but can cause a request to take slightly longer to respond than would otherwise be the case. The
 * {@link #setFailTimeout(long)} method should be used to the timeout that will protect against requests that never
 * receive a poll (for example due to network failure). The {@link #setLongPollTime(long)} method can be used to set the
 * long-poll time for the poll request. This value should obviously be less than the gateway timeout.
 * 
 * @author Phillip Webb
 */
public class HotSwappingTimeoutProtectionStrategy implements TimeoutProtectionStrategy, TimeoutValues {

    private long threshold = TimeUnit.SECONDS.toMillis(14);

    private long longPollTime = TimeUnit.SECONDS.toMillis(6);

    private long failTimeout = TimeUnit.SECONDS.toMillis(120);

    private RequestCoordinators requestCoordinators = new RequestCoordinators();

    public HttpServletResponseMonitorFactory handleRequest(final TimeoutProtectionHttpRequest request) {
        final long startTime = System.currentTimeMillis();
        return new HttpServletResponseMonitorFactory() {
            public HttpServletResponseMonitor getMonitor() {
                if ((HotSwappingTimeoutProtectionStrategy.this.threshold != 0) && (System.currentTimeMillis()
                        - startTime < HotSwappingTimeoutProtectionStrategy.this.threshold)) {
                    return null;
                }
                RequestCoordinator requestCoordinator = HotSwappingTimeoutProtectionStrategy.this.requestCoordinators
                        .get(request);
                HttpServletResponse pollResponse;
                synchronized (requestCoordinator) {
                    pollResponse = requestCoordinator.consumePollResponse();
                }
                if (pollResponse == null) {
                    try {
                        requestCoordinator.awaitPollResponse(HotSwappingTimeoutProtectionStrategy.this.failTimeout);
                    } catch (InterruptedException e) {
                        throw new IllegalStateException("Timeout waiting for poll", e);
                    }
                    pollResponse = requestCoordinator.consumePollResponse();
                    Assert.state(pollResponse != null, "Unable to consume poll response");
                }
                return new DuplicatingHttpServletResponseMonitorFactory(pollResponse).getMonitor();
            }
        };
    }

    public void afterRequest(TimeoutProtectionHttpRequest request,
            HttpServletResponseMonitorFactory monitorFactory) {
        RequestCoordinator requestCoordinator = this.requestCoordinators.get(request);
        requestCoordinator.finish();
        synchronized (requestCoordinator) {
            if (!requestCoordinator.isPollResponseConsumed()) {
                this.requestCoordinators.delete(request);
            }
        }
    }

    public void handlePoll(TimeoutProtectionHttpRequest request, HttpServletResponse response) throws IOException {
        RequestCoordinator requestCoordinator = this.requestCoordinators.get(request);
        synchronized (requestCoordinator) {
            requestCoordinator.setPollResponse(response);
        }
        try {
            requestCoordinator.awaitPollReponseConsumed(this.longPollTime);
        } catch (InterruptedException e) {
        }
        synchronized (requestCoordinator) {
            if (requestCoordinator.isPollResponseConsumed()) {
                try {
                    requestCoordinator.awaitFinish(this.failTimeout);
                } catch (InterruptedException e) {
                    throw new IllegalStateException("Timeout waiting for cleanup");
                } finally {
                    this.requestCoordinators.delete(request);
                }
            } else {
                requestCoordinator.clearPollResponse();
                response.setHeader(TimeoutProtectionHttpHeader.POLL, request.getUid());
                response.setStatus(HttpStatus.NO_CONTENT.value());
            }
        }
    }

    protected void setRequestCoordinators(RequestCoordinators requestCoordinators) {
        this.requestCoordinators = requestCoordinators;
    }

    @Override
    public long getThreshold() {
        return this.threshold;
    };

    /**
     * Set the threshold that must be passed before timeout protection will be used
     * @param threshold the threshold in milliseconds
     */
    public void setThreshold(long threshold) {
        this.threshold = threshold;
    }

    /**
     * Set the maximum amount of time that a single long poll request can take.
     * @param longPollTime the long poll time in milliseconds
     */
    public void setLongPollTime(long longPollTime) {
        this.longPollTime = longPollTime;
    }

    @Override
    public long getFailTimeout() {
        return this.failTimeout;
    }

    /**
     * Set the amount of time before a request is considered failed.
     * @param failTimeout the fail timeout in milliseconds
     */
    public void setFailTimeout(long failTimeout) {
        this.failTimeout = failTimeout;
    }

    /**
     * Maintains a map of {@link RequestCoordinator}s against {@link TimeoutProtectionHttpRequest}s.
     */
    protected static class RequestCoordinators {

        private Map<String, RequestCoordinator> coordinators = new HashMap<String, RequestCoordinator>();

        public synchronized RequestCoordinator get(TimeoutProtectionHttpRequest request) {
            String uid = request.getUid();
            RequestCoordinator requestCoordinator = this.coordinators.get(uid);
            if (requestCoordinator == null) {
                requestCoordinator = new RequestCoordinator();
                this.coordinators.put(uid, requestCoordinator);
            }
            return requestCoordinator;
        }

        public synchronized void delete(TimeoutProtectionHttpRequest request) {
            this.coordinators.remove(request.getUid());
        }
    }

    protected static enum CoordinatedEvent {
        POLL_RESPONSE, POLL_RESPONSE_CONSUMED, FINISH
    };

    /**
     * A single coordinator used to manage access to a {@link TimeoutProtectionHttpRequest}. The coordinator object
     * should be used as the lock for synchronized blocks to prevent concurrent updates.
     */
    protected static class RequestCoordinator {

        private HttpServletResponse pollResponse;

        private volatile boolean pollResponseConsumed;

        private Map<CoordinatedEvent, CountDownLatch> latches;

        public RequestCoordinator() {
            this.latches = new HashMap<CoordinatedEvent, CountDownLatch>();
            for (CoordinatedEvent event : CoordinatedEvent.values()) {
                this.latches.put(event, new CountDownLatch(1));
            }
        }

        public void setPollResponse(HttpServletResponse pollResponse) {
            Assert.state(!this.pollResponseConsumed, "Unable to set an already consumed poll response");
            this.pollResponse = pollResponse;
            signal(CoordinatedEvent.POLL_RESPONSE);
        }

        public void clearPollResponse() {
            Assert.state(!this.pollResponseConsumed, "Unable to clear an already consumed poll response");
            this.pollResponse = null;
        }

        public HttpServletResponse consumePollResponse() {
            if (this.pollResponse != null) {
                this.pollResponseConsumed = true;
                signal(CoordinatedEvent.POLL_RESPONSE_CONSUMED);
            }
            return this.pollResponse;
        }

        public void finish() {
            signal(CoordinatedEvent.FINISH);
        }

        public boolean isPollResponseConsumed() {
            return this.pollResponseConsumed;
        }

        public void awaitPollResponse(long timeout) throws InterruptedException {
            await(CoordinatedEvent.POLL_RESPONSE, timeout);
        }

        public void awaitPollReponseConsumed(long timeout) throws InterruptedException {
            await(CoordinatedEvent.POLL_RESPONSE_CONSUMED, timeout);
        }

        public void awaitFinish(long timeout) throws InterruptedException {
            await(CoordinatedEvent.FINISH, timeout);
        }

        private void signal(CoordinatedEvent event) {
            this.latches.get(event).countDown();
        }

        private void await(CoordinatedEvent event, long timeout) throws InterruptedException {
            this.latches.get(event).await(timeout, TimeUnit.MILLISECONDS);
        }

    }
}