001/*
002 * (C) Copyright 2017 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 */
017package org.nuxeo.ecm.platform.web.common.requestcontroller.filter;
018
019import java.io.IOException;
020import java.util.concurrent.atomic.AtomicInteger;
021import java.util.concurrent.locks.Condition;
022import java.util.concurrent.locks.Lock;
023import java.util.concurrent.locks.ReentrantLock;
024
025import javax.servlet.Filter;
026import javax.servlet.FilterChain;
027import javax.servlet.FilterConfig;
028import javax.servlet.ServletException;
029import javax.servlet.ServletRequest;
030import javax.servlet.ServletResponse;
031
032import org.nuxeo.runtime.api.Framework;
033import org.nuxeo.runtime.model.ComponentManager;
034
035/**
036 * Blocks incoming requests when runtime is in standby mode.
037 *
038 * @since 9.2
039 */
040public class NuxeoStandbyFilter implements Filter {
041
042    protected Controller controller;
043
044    @Override
045    public void init(FilterConfig filterConfig) throws ServletException {
046        controller = new Controller();
047        new ComponentManager.Listener() {
048
049            @Override
050            public void beforeStop(ComponentManager mgr, boolean isStandby) {
051                controller.onStandby();
052            }
053
054            @Override
055            public void afterStart(ComponentManager mgr, boolean isResume) {
056                controller.onResumed();
057            }
058
059        }.install();
060    }
061
062    @Override
063    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
064            throws IOException, ServletException {
065        controller.onNewRequest();
066        try {
067            chain.doFilter(request, response);
068        } finally {
069            controller.onRequestEnd();
070        }
071    }
072
073    @Override
074    public void destroy() {
075
076    }
077
078    protected class Controller {
079        protected final Lock lock = new ReentrantLock();
080
081        protected final Condition canStandby = lock.newCondition();
082
083        protected final Condition canProceed = lock.newCondition();
084
085        protected volatile boolean isStandby = !Framework.getRuntime().getComponentManager().isStarted();
086
087        protected final AtomicInteger inProgress = new AtomicInteger();
088
089        /**
090         * This variable is used to determine if the Thread wanting to shutdown/standby the server has gone through this
091         * filter. We need this variable in order to not wait for ourself to end.
092         * <p />
093         * Calls relying on this variable:
094         * <ul>
095         * <li>org.nuxeo.runtime.reload.NuxeoRestart#restart()</li>
096         * <li>org.nuxeo.ecm.admin.operation.HotReloadStudioSnapshot#run()</li>
097         * <li>org.nuxeo.connect.client.jsf.AppCenterViewsManager#installStudioSnapshotAndRedirect()</li>
098         * </ul>
099         */
100        protected final ThreadLocal<Boolean> hasBeenFiltered = ThreadLocal.withInitial(() -> Boolean.FALSE);
101
102        public void onNewRequest() {
103            if (isStandby) {
104                awaitCanProceed();
105            }
106            inProgress.incrementAndGet();
107            hasBeenFiltered.set(Boolean.TRUE);
108        }
109
110        public void onRequestEnd() {
111            hasBeenFiltered.set(Boolean.FALSE);
112            if (inProgress.decrementAndGet() <= 0) {
113                signalBlockedToStandby();
114            }
115        }
116
117        public void onStandby() throws RuntimeException {
118            isStandby = true;
119            if (hasBeenFiltered.get()) {
120                // current thread has gone through this filter, so remove this request from counter
121                // otherwise we will wait for ourself to end
122                inProgress.decrementAndGet();
123            }
124            if (inProgress.get() > 0) {
125                awaitCanStandby();
126            }
127        }
128
129        public void onResumed() {
130            isStandby = false;
131            if (hasBeenFiltered.get()) {
132                // current thread has gone through this filter, so add this request back to counter
133                // as we removed this request just before / proceeding as it makes conditions easier to read
134                inProgress.incrementAndGet();
135            }
136            signalBlockedToProceed();
137        }
138
139        protected void awaitCanProceed() throws RuntimeException {
140            lock.lock();
141            try {
142                canProceed.await();
143            } catch (InterruptedException cause) {
144                Thread.currentThread().interrupt();
145                throw new RuntimeException("Interrupted while locking incoming requests", cause);
146            } finally {
147                lock.unlock();
148            }
149        }
150
151        protected void awaitCanStandby() throws RuntimeException {
152            lock.lock();
153            try {
154                canStandby.await();
155            } catch (InterruptedException cause) {
156                Thread.currentThread().interrupt();
157                throw new RuntimeException("Interrupted while waiting for web requests being drained", cause);
158            } finally {
159                lock.unlock();
160            }
161        }
162
163        protected void signalBlockedToProceed() {
164            lock.lock();
165            try {
166                canProceed.signalAll();
167            } finally {
168                lock.unlock();
169            }
170        }
171
172        protected void signalBlockedToStandby() {
173            lock.lock();
174            try {
175                canStandby.signal();
176            } finally {
177                lock.unlock();
178            }
179        }
180
181    }
182
183}