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}