Java tutorial
/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU General Public License, version 2 as published by the Free Software * Foundation. * * You should have received a copy of the GNU General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/gpl-2.0.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * * * Copyright 2006 - 2017 Hitachi Vantara. All rights reserved. */ package org.pentaho.reporting.platform.plugin.async; import java.io.Serializable; import java.util.Collections; import java.util.Map; import java.util.Objects; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.pentaho.platform.api.engine.ILogoutListener; import org.pentaho.platform.api.engine.IPentahoSession; import org.pentaho.platform.engine.core.system.PentahoSystem; import org.pentaho.platform.engine.security.SecurityHelper; import org.pentaho.platform.util.StringUtil; import org.pentaho.reporting.libraries.base.util.ArgumentNullException; import org.pentaho.reporting.libraries.base.util.StringUtils; import org.pentaho.reporting.platform.plugin.staging.AsyncJobFileStagingHandler; import org.pentaho.reporting.platform.plugin.staging.IFixedSizeStreamingContent; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; public class PentahoAsyncExecutor<TReportState extends IAsyncReportState> implements ILogoutListener, IPentahoAsyncExecutor<TReportState> { public static final String BEAN_NAME = "IPentahoAsyncExecutor"; private static final Log log = LogFactory.getLog(PentahoAsyncExecutor.class); private Map<CompositeKey, ListenableFuture<IFixedSizeStreamingContent>> futures = new ConcurrentHashMap<>(); private Map<CompositeKey, IAsyncReportExecution<TReportState>> tasks = new ConcurrentHashMap<>(); private ListeningExecutorService executorService; private final int autoSchedulerThreshold; private final MemorizeSchedulingLocationListener schedulingLocationListener; private Map<CompositeKey, ISchedulingListener> writeToJcrListeners; /** * @param capacity thread pool capacity * @param autoSchedulerThreshold quantity of rows after which reports are automatically scheduled */ public PentahoAsyncExecutor(final int capacity, final int autoSchedulerThreshold) { this.autoSchedulerThreshold = autoSchedulerThreshold; log.info("Initialized reporting async execution fixed thread pool with capacity: " + capacity); executorService = new DelegatedListenableExecutor(new ThreadPoolExecutor(capacity, capacity, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread thread = Executors.defaultThreadFactory().newThread(r); thread.setDaemon(true); thread.setName("PentahoAsyncExecutor Thread Pool"); return thread; } })); PentahoSystem.addLogoutListener(this); this.writeToJcrListeners = new ConcurrentHashMap<>(); this.schedulingLocationListener = new MemorizeSchedulingLocationListener(); } @Deprecated public PentahoAsyncExecutor(final int capacity) { this(capacity, 0); } /** * This executor stores jobs (identified by their id) in a separate partition for each user (identified by the * session-id). We don't let others access our session or job-id, but need to match against the session-id for * onLogout clean-ups. */ public static class CompositeKey { private String sessionId; private String uuid; // default visibility for testing purpose CompositeKey(final IPentahoSession session, final UUID id) { this.uuid = id.toString(); this.sessionId = session.getId(); } public boolean isSameSession(final String sessionId) { return StringUtils.equals(sessionId, this.sessionId); } private String getSessionId() { return sessionId; } @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } final CompositeKey that = (CompositeKey) o; return Objects.equals(sessionId, that.sessionId) && Objects.equals(uuid, that.uuid); } @Override public int hashCode() { return Objects.hash(sessionId, uuid); } } @Override public UUID addTask(final IAsyncReportExecution<TReportState> task, final IPentahoSession session) { return addTask(task, session, UUID.randomUUID()); } @Override public UUID addTask(final IAsyncReportExecution<TReportState> task, final IPentahoSession session, final UUID id) { final CompositeKey key = new CompositeKey(session, id); task.notifyTaskQueued(id, Collections.singletonList(new AutoScheduleListener(id, session, autoSchedulerThreshold, this))); log.debug("register async execution for task: " + task.toString()); final ListenableFuture<IFixedSizeStreamingContent> result = executorService.submit(task); futures.put(key, result); tasks.put(key, task); return id; } @Override public Future<IFixedSizeStreamingContent> getFuture(final UUID id, final IPentahoSession session) { validateParams(id, session); return futures.get(new CompositeKey(session, id)); } @Override public void cleanFuture(final UUID id, final IPentahoSession session) { final CompositeKey key = new CompositeKey(session, id); futures.remove(key); tasks.remove(key); } @Override public void requestPage(final UUID id, final IPentahoSession session, final int page) { validateParams(id, session); final IAsyncReportExecution<TReportState> runningTask = tasks.get(new CompositeKey(session, id)); if (runningTask != null) { runningTask.requestPage(page); } } @Override public boolean preSchedule(final UUID uuid, final IPentahoSession session) { validateParams(uuid, session); final CompositeKey compositeKey = new CompositeKey(session, uuid); final IAsyncReportExecution<? extends TReportState> runningTask = tasks.get(compositeKey); if (runningTask != null) { return runningTask.preSchedule(); } return false; } @SuppressWarnings("unchecked") @Override public UUID recalculate(final UUID uuid, final IPentahoSession session) { validateParams(uuid, session); final CompositeKey compositeKey = new CompositeKey(session, uuid); final IAsyncReportExecution<? extends TReportState> runningTask = tasks.get(compositeKey); if (runningTask == null) { throw new IllegalStateException("We must have a task at this point."); } try { final IAsyncReportExecution<TReportState> recalcTask = (IAsyncReportExecution<TReportState>) new PentahoAsyncReportExecution( (PentahoAsyncReportExecution) runningTask, new AsyncJobFileStagingHandler(session)); return addTask(recalcTask, session); } catch (final Exception e) { log.error("Can't recalculate task: ", e); } return null; } @Override public boolean schedule(final UUID id, final IPentahoSession session) { validateParams(id, session); final CompositeKey compositeKey = new CompositeKey(session, id); final IAsyncReportExecution<TReportState> runningTask = tasks.get(compositeKey); final ListenableFuture<IFixedSizeStreamingContent> future = futures.get(compositeKey); if (runningTask == null || future == null) { // As long as we have a task, we should have a future-object, but checking both does not hurt. throw new IllegalStateException("We must have a task and a future at this point."); } final String userId = session.getName(); final String sessionId = session.getId(); if (!StringUtils.isEmpty(userId)) { if (runningTask.schedule()) { Futures.addCallback(future, new TriggerScheduledContentWritingHandler(userId, sessionId, runningTask, compositeKey), executorService); return true; } } return false; } @Override public void updateSchedulingLocation(final UUID id, final IPentahoSession session, final Serializable folderId, final String newName) { validateParams(id, session); final CompositeKey key = new CompositeKey(session, id); final IAsyncReportExecution<TReportState> runningTask = tasks.get(key); if (runningTask == null) { throw new IllegalStateException("We must have a task at this point."); } final UpdateSchedulingLocationListener listener = getUpdateSchedulingLocationListener(folderId, newName); try { this.schedulingLocationListener.lock(); final Serializable fileId = schedulingLocationListener.lookupOutputFile(key); if (fileId != null) { //Report is already finished and saved to default scheduling directory. // move it to a new location. This operation may move the file multiple times, as the file-id is independent // of the location. listener.onSchedulingCompleted(fileId); } else { //Report is not finished yet. Update the listener list within this synchronized block so that writeToJcrListeners.put(key, listener); } } finally { this.schedulingLocationListener.unlock(); } } protected UpdateSchedulingLocationListener getUpdateSchedulingLocationListener(final Serializable folderId, final String newName) { return new UpdateSchedulingLocationListener(folderId, newName); } @Override public TReportState getReportState(final UUID id, final IPentahoSession session) { validateParams(id, session); // link to running task final IAsyncReportExecution<TReportState> runningTask = tasks.get(new CompositeKey(session, id)); return runningTask == null ? null : runningTask.getState(); } protected void validateParams(final UUID id, final IPentahoSession session) { ArgumentNullException.validate("uuid", id); ArgumentNullException.validate("session", session); } @Override public void onLogout(final IPentahoSession session) { if (log.isDebugEnabled()) { // don't expose full session id. log.debug("killing async report execution cache for user: " + session.getName()); } for (final Map.Entry<CompositeKey, ListenableFuture<IFixedSizeStreamingContent>> entry : futures .entrySet()) { if (ObjectUtils.equals(entry.getKey().getSessionId(), session.getId())) { final IAsyncReportExecution<TReportState> task = tasks.get(entry.getKey()); final ListenableFuture<IFixedSizeStreamingContent> value = entry.getValue(); if (task != null && task.getState() != null && AsyncExecutionStatus.SCHEDULED.equals(task.getState().getStatus())) { //After the session end nobody can poll status, we can remove task //Keep future to have content in place tasks.remove(entry.getKey()); continue; } // attempt to cancel running task value.cancel(true); // remove all links to release GC futures.remove(entry.getKey()); tasks.remove(entry.getKey()); } } //User can't update scheduling directory after logout, so we can clean location locationMap try { this.schedulingLocationListener.lock(); this.schedulingLocationListener.onLogout(session.getId()); } finally { this.schedulingLocationListener.unlock(); } //If some files are still open directory won't be removed AsyncJobFileStagingHandler.cleanSession(session); } @Override public void shutdown() { // attempt to stop all for (final Future<IFixedSizeStreamingContent> entry : futures.values()) { entry.cancel(true); } // forget all this.futures.clear(); this.tasks.clear(); this.writeToJcrListeners.clear(); this.executorService.shutdown(); try { this.schedulingLocationListener.lock(); this.schedulingLocationListener.shutdown(); } finally { this.schedulingLocationListener.unlock(); } AsyncJobFileStagingHandler.cleanStagingDir(); } protected Callable<Serializable> getWriteToJcrTask(final IFixedSizeStreamingContent result, final IAsyncReportExecution<? extends IAsyncReportState> runningTask) { return new WriteToJcrTask(runningTask, result.getStream()); } /** * This class is responsible for writing the content first to a pre-computed location (as specified by the * ISchedulingDirectoryStrategy implementation, and then optionally moves the content to a location specified by the * user (via the UI). */ class TriggerScheduledContentWritingHandler implements FutureCallback<IFixedSizeStreamingContent> { private final IAsyncReportExecution<TReportState> runningTask; private final CompositeKey compositeKey; private final String user; private final String sessionId; TriggerScheduledContentWritingHandler(final String user, final String sessionId, final IAsyncReportExecution<TReportState> runningTask, final CompositeKey compositeKey) { this.user = user; this.sessionId = sessionId; this.runningTask = runningTask; this.compositeKey = compositeKey; } protected IFixedSizeStreamingContent notifyListeners(final IFixedSizeStreamingContent result) throws Exception { final Serializable writtenTo = getWriteToJcrTask(result, runningTask).call(); if (writtenTo == null) { log.debug( "Unable to move scheduled content, due to error while creating content in default location."); return null; } try { PentahoAsyncExecutor.this.schedulingLocationListener.lock(); PentahoAsyncExecutor.this.schedulingLocationListener.recordOutputFile(compositeKey, writtenTo); notifyListeners(writtenTo); } finally { PentahoAsyncExecutor.this.schedulingLocationListener.unlock(); } return null; } protected void notifyListeners(final Serializable writtenTo) { //We can be sure it succeed here and are ready to notify writeToJcrListeners final ISchedulingListener iSchedulingListener = writeToJcrListeners.get(compositeKey); if (iSchedulingListener != null) { iSchedulingListener.onSchedulingCompleted(writtenTo); writeToJcrListeners.remove(compositeKey); } } @Override public void onSuccess(final IFixedSizeStreamingContent result) { try { if (user != null && !StringUtil.isEmpty(user)) { SecurityHelper.getInstance().runAsUser(user, () -> notifyListeners(result)); } } catch (final Exception e) { log.error("Can't execute callback. : ", e); } finally { //Time to remove future - nobody will ask for content at this moment //We need to keep task because status polling may still occur ( or it already has been removed on logout ) //Also we can try to remove directory futures.remove(compositeKey); result.cleanContent(); AsyncJobFileStagingHandler.cleanSession(sessionId); } } @Override public void onFailure(final Throwable t) { log.error("Can't execute callback. Parent task failed: ", t); futures.remove(compositeKey); AsyncJobFileStagingHandler.cleanSession(sessionId); } } }