/*
* Copyright: (c) 2004-2011 Mayo Foundation for Medical Education and
* Research (MFMER). All rights reserved. MAYO, MAYO CLINIC, and the
* triple-shield Mayo logo are trademarks and service marks of MFMER.
*
* Except as contained in the copyright notice above, or as used to identify
* MFMER as the author of this software, the trade names, trademarks, service
* marks, or product names of the copyright holder shall not be used in
* advertising, promotion or otherwise in connection with this software without
* prior written authorization of the copyright holder.
*
* 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 edu.mayo.cts2.framework.webapp.rest.controller;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.lang.ArrayUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import edu.mayo.cts2.framework.core.timeout.Timeout;
import edu.mayo.cts2.framework.model.exception.ExceptionFactory;
import edu.mayo.cts2.framework.webapp.rest.command.QueryControl;
/**
* The Class MethodTimingAspect.
*
* @author <a href="mailto:kevin.peterson@mayo.edu">Kevin Peterson</a>
*/
@Aspect
@Order(Ordered.LOWEST_PRECEDENCE)
public class MethodTimingAspect {
private ExecutorService executorService = Executors.newCachedThreadPool(new ThreadFactory()
{
@Override
public Thread newThread(Runnable r)
{
Thread t = new Thread(r);
t.setName("ProcessingThread-" + t.getId());
return t;
}
});
private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1, new ThreadFactory()
{
@Override
public Thread newThread(Runnable r)
{
Thread t = new Thread(r);
t.setName("ExecutionTimer-" + t.getId());
return t;
}
});
/**
* Execute.
*
* @param pjp the pjp
* @return the object
* @throws Throwable the throwable
*/
@Around("execution(public *"
+ " edu.mayo.cts2.framework.webapp.rest.controller.*.*(..,edu.mayo.cts2.framework.webapp.rest.command.QueryControl,..))")
public Object execute(final ProceedingJoinPoint pjp) throws Throwable {
QueryControl queryControl = null;
//this should never happen
if(ArrayUtils.isEmpty(pjp.getArgs())){
throw new IllegalStateException("Pointcut failure!");
}
for(Object arg : pjp.getArgs()){
if(arg.getClass() == QueryControl.class){
queryControl = (QueryControl) arg;
break;
}
}
//this also should never happen
if(queryControl == null){
throw new IllegalStateException("Pointcut failure!");
}
final AtomicLong threadId = new AtomicLong(-1);
Future<Object> future = this.executorService.submit(new Callable<Object>(){
@Override
public Object call() {
try {
threadId.set(Thread.currentThread().getId());
/*
* The model here is that we clear any previous timeout before we launch the job. A design flaw is that we
* can't tell if we are clearing a previous timeout that simply hadn't been cleaned up yet, or if we are
* clearing a timeout meant for this thread that happened before this thread even launched. The second scenario
* seems unlikely as the minimum timeout is 1 second - hard to believe it would take more than 1 second to
* launch this thread. Plus, this thread would have to launch in the exact window in between the timeout and
* the future.cancel()
*
* If the above scenario did defy all odds and happen , it shouldn't cause much harm, as the end result would
* be that this thread wouldn't see the cancelled flag - and would churn away for no reason, wasting some cpu
* cycles, but doing no other harm.
*/
Timeout.clearThreadFlag(threadId.get());
return pjp.proceed();
} catch (Throwable e) {
if(e instanceof Error){
throw (Error)e;
}
if(e instanceof RuntimeException){
throw (RuntimeException)e;
}
throw new RuntimeException(e);
}
}
});
long time = queryControl.getTimelimit();
try {
if(time < 0){
return future.get();
} else {
return future.get(time, TimeUnit.SECONDS);
}
} catch (ExecutionException e) {
throw e.getCause();
} catch (TimeoutException e) {
try
{
//Set the flag for the processing thread to read
Timeout.setTimeLimitExceeded(threadId.get());
//Schedule another future to make sure we don't cause a memory leak if the thread IDs aren't being reused (though, they should be)
//and therefore don't get cleared up by the next run. Give the running thread 30 seconds to see the cancelled flag before this
//cleanup takes place.
this.scheduledExecutorService.schedule(new Runnable()
{
@Override
public void run()
{
Timeout.clearThreadFlag(threadId.get());
}
}, 30, TimeUnit.SECONDS);
//Interrupt the processing thread so it has an opportunity to check the flag and stop.
future.cancel(true);
}
catch (Exception e1)
{
// don't think this is possible, but just in case...
}
throw ExceptionFactory.createTimeoutException(e.getMessage());
}
}
}