/******************************************************************************* * Copyright (c) 2012-2016 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.everrest.core.impl.async; import com.google.common.util.concurrent.ThreadFactoryBuilder; import org.everrest.core.ApplicationContext; import org.everrest.core.GenericContainerRequest; import org.everrest.core.impl.ContainerRequest; import org.everrest.core.impl.EverrestConfiguration; import org.everrest.core.resource.ResourceMethodDescriptor; import org.everrest.core.tools.EmptyInputStream; import org.slf4j.LoggerFactory; import javax.annotation.PreDestroy; import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.ext.ContextResolver; import javax.ws.rs.ext.Provider; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.ThreadPoolExecutor; import static java.util.concurrent.ThreadPoolExecutor.AbortPolicy; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; /** * Pool of asynchronous jobs. * * @author andrew00x */ @Provider public class AsynchronousJobPool implements ContextResolver<AsynchronousJobPool> { /** Logger. */ private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(AsynchronousJobPool.class); protected final String asynchronousServicePath; /** When timeout (in minutes) reached then an asynchronous job may be removed from the pool. */ protected final int jobTimeout; /** Max cache size. */ protected final int maxCacheSize; /** Maximum number of task in queue. */ protected final int maxQueueSize; /** Number of threads to serve asynchronous jobs. */ protected final int threadPoolSize; private final ExecutorService pool; private final Map<Long, AsynchronousJob> jobs; private final CopyOnWriteArrayList<AsynchronousJobListener> jobListeners; private AsynchronousFutureFactory asynchronousFutureFactory; public AsynchronousJobPool(EverrestConfiguration config) { if (config == null) { config = new EverrestConfiguration(); } this.asynchronousServicePath = config.getAsynchronousServicePath(); this.maxCacheSize = config.getAsynchronousCacheSize(); this.jobTimeout = config.getAsynchronousJobTimeout(); this.maxQueueSize = config.getAsynchronousQueueSize(); this.threadPoolSize = config.getAsynchronousPoolSize(); this.pool = makeExecutorService(); this.jobs = Collections.synchronizedMap(new LinkedHashMap<Long, AsynchronousJob>() { @Override protected boolean removeEldestEntry(Map.Entry<Long, AsynchronousJob> eldest) { AsynchronousJob job = eldest.getValue(); if (size() > maxCacheSize || job.getExpirationDate() < System.currentTimeMillis()) { job.cancel(); return true; } return false; } }); this.jobListeners = new CopyOnWriteArrayList<>(); setAsynchronousFutureFactory(new AsynchronousFutureFactory()); } public String getAsynchronousServicePath() { return asynchronousServicePath; } public int getMaxCacheSize() { return maxCacheSize; } public int getMaxQueueSize() { return maxQueueSize; } public int getThreadPoolSize() { return threadPoolSize; } public int getJobTimeout() { return jobTimeout; } void setAsynchronousFutureFactory(AsynchronousFutureFactory asynchronousFutureFactory) { this.asynchronousFutureFactory = asynchronousFutureFactory; } protected ExecutorService makeExecutorService() { return new ThreadPoolExecutor(threadPoolSize, threadPoolSize, 0L, MILLISECONDS, new LinkedBlockingQueue<>(maxQueueSize), new ThreadFactoryBuilder().setNameFormat("everrest.AsynchronousJobPool-%d").setDaemon(true).build(), new ManyJobsPolicy(new AbortPolicy()) ); } @Override public AsynchronousJobPool getContext(Class<?> type) { return this; } /** * @param resource * object that contains resource method * @param resourceMethod * resource or sub-resource method to invoke * @param params * method parameters * @return asynchronous job * @throws AsynchronousJobRejectedException * if this task cannot be added to pool */ public final AsynchronousJob addJob(Object resource, ResourceMethodDescriptor resourceMethod, Object[] params) throws AsynchronousJobRejectedException { final long expirationDate = System.currentTimeMillis() + MINUTES.toMillis(jobTimeout); final Callable<Object> callable = newCallable(resource, resourceMethod.getMethod(), params); final AsynchronousFuture job = asynchronousFutureFactory.createAsynchronousFuture(callable, expirationDate, resourceMethod, jobListeners); job.setJobURI(getAsynchronousJobUriBuilder(job).build().toString()); final ApplicationContext context = ApplicationContext.getCurrent(); final ContainerRequest request = createRequestCopy(context.getContainerRequest(), context.getSecurityContext()); job.getContext().put("org.everrest.async.request", request); // Save current set of providers. In some environments they can be resource specific. job.getContext().put("org.everrest.async.providers", context.getProviders()); initAsynchronousJobContext(job); final Long jobId = job.getJobId(); jobs.put(jobId, job); try { pool.execute(job); } catch (RejectedExecutionException e) { jobs.remove(jobId); throw new AsynchronousJobRejectedException(e.getMessage()); } LOG.debug("Add asynchronous job, ID: {}", jobId); return job; } private ContainerRequest createRequestCopy(GenericContainerRequest originRequest, SecurityContext securityContext) { // Create copy of request. Need to keep 'Accept' headers to be able determine MessageBodyWriter which can be // used to serialize result of method invocation. Do not copy entity stream. This stream is empty any way. return new ContainerRequest(originRequest.getMethod(), originRequest.getRequestUri(), originRequest.getBaseUri(), new EmptyInputStream(), originRequest.getRequestHeaders(), securityContext); } /** * Configures context of asynchronous job. This method is invoked by thread that adds new job. * <p/> * This implementation does nothing, but may be customized in subclasses. * * @see AsynchronousJob#getContext() */ protected void initAsynchronousJobContext(AsynchronousJob job) { } protected UriBuilder getAsynchronousJobUriBuilder(AsynchronousJob job) { return UriBuilder.fromPath(asynchronousServicePath).path(Long.toString(job.getJobId())); } protected Callable<Object> newCallable(Object resource, Method method, Object[] params) { return new MethodInvokeCallable(resource, method, params); } public AsynchronousJob getJob(Long jobId) { return jobs.get(jobId); } public AsynchronousJob removeJob(Long jobId) { final AsynchronousJob job = jobs.remove(jobId); if (!(job == null || job.isDone())) { job.cancel(); } return job; } public List<AsynchronousJob> getAll() { return new ArrayList<>(jobs.values()); } /** * Registers new listener if it is not registered yet. * * @param listener * listener * @return {@code true} if new listener registered and {@code false} otherwise. * @see AsynchronousJobListener */ public boolean registerListener(AsynchronousJobListener listener) { return jobListeners.addIfAbsent(listener); } /** * Unregisters listener. * * @param listener * listener to unregister * @return {@code true} if listener unregistered and {@code false} otherwise. * @see AsynchronousJobListener */ public boolean unregisterListener(AsynchronousJobListener listener) { return jobListeners.remove(listener); } @PreDestroy public void stop() { pool.shutdown(); try { if (!pool.awaitTermination(5, SECONDS)) { pool.shutdownNow(); } } catch (InterruptedException e) { pool.shutdownNow(); Thread.currentThread().interrupt(); } } public static class ManyJobsPolicy implements RejectedExecutionHandler { private final RejectedExecutionHandler delegate; public ManyJobsPolicy(RejectedExecutionHandler delegate) { this.delegate = delegate; } @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { if (executor.getPoolSize() >= executor.getCorePoolSize()) { throw new RejectedExecutionException( "Can't accept new asynchronous request. Too many asynchronous jobs in progress"); } delegate.rejectedExecution(r, executor); } } }