/* * (C) Copyright 2012 Nuxeo SA (http://nuxeo.com/) and contributors. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser General Public License * (LGPL) version 2.1 which accompanies this distribution, and is available at * http://www.gnu.org/licenses/lgpl.html * * This library 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 * Lesser General Public License for more details. * * Contributors: * Florent Guillaume */ package org.nuxeo.ecm.core.work; import static org.nuxeo.ecm.core.work.api.Work.Progress.PROGRESS_INDETERMINATE; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Random; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.ecm.core.api.ConcurrentUpdateException; import org.nuxeo.ecm.core.api.CoreInstance; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.DocumentLocation; import org.nuxeo.ecm.core.api.IdRef; import org.nuxeo.ecm.core.api.impl.DocumentLocationImpl; import org.nuxeo.ecm.core.work.api.Work; import org.nuxeo.ecm.core.work.api.WorkSchedulePath; import org.nuxeo.runtime.transaction.TransactionHelper; import org.nuxeo.runtime.transaction.TransactionRuntimeException; /** * A base implementation for a {@link Work} instance, dealing with most of the * details around state change. * <p> * It also deals with transaction management, and prevents running work * instances that are suspending. * <p> * Actual implementations must at a minimum implement the {@link #work()} * method. A method {@link #cleanUp} is available. * <p> * To deal with suspension, {@link #work()} should periodically check for * {@link #isSuspending()} and if true save its state and call * {@link #suspended()}. * <p> * Specific information about the work can be returned by {@link #getDocument()} * or {@link #getDocuments()}. * * @since 5.6 */ public abstract class AbstractWork implements Work { private static final long serialVersionUID = 1L; private static final Log log = LogFactory.getLog(AbstractWork.class); protected static final Random RANDOM = new Random(); protected String id; /** Suspend requested by the work manager. */ protected transient volatile boolean suspending; /** Suspend acknowledged by the work instance. */ protected transient volatile boolean suspended; protected transient State state; protected transient Progress progress; /** Repository name for the Work instance, if relevant. */ protected String repositoryName; /** * Doc id for the Work instance, if relevant. This describes for the * WorkManager a document on which this Work instance will act. * <p> * Either docId or docIds is set. Not both. */ protected String docId; /** * Doc ids for the Work instance, if relevant. This describes for the * WorkManager the documents on which this Work instance will act. * <p> * Either docId or docIds is set. Not both. */ protected List<String> docIds; /** * If {@code true}, the docId is only the root of a set of documents on * which this Work instance will act. */ protected boolean isTree; protected String status; protected long schedulingTime; protected long startTime; protected long completionTime; protected transient CoreSession session; protected WorkSchedulePath schedulePath; /** * Constructs a {@link Work} instance with a unique id. */ public AbstractWork() { // we user RANDOM to deal with these cases: // - several calls in the time granularity of nanoTime() // - several concurrent calls on different servers this(System.nanoTime() + "." + Math.abs(RANDOM.nextInt())); } public AbstractWork(String id) { this.id = id; progress = PROGRESS_INDETERMINATE; schedulingTime = System.currentTimeMillis(); } @Override public String getId() { return id; } @Override public WorkSchedulePath getSchedulePath() { // schedulePath is transient so will become null after deserialization return schedulePath == null ? WorkSchedulePath.EMPTY : schedulePath; } @Override public void setSchedulePath(WorkSchedulePath path) { schedulePath = path; } public void setDocument(String repositoryName, String docId, boolean isTree) { this.repositoryName = repositoryName; this.docId = docId; docIds = null; this.isTree = isTree; } public void setDocument(String repositoryName, String docId) { setDocument(repositoryName, docId, false); } public void setDocuments(String repositoryName, List<String> docIds) { this.repositoryName = repositoryName; docId = null; this.docIds = new ArrayList<String>(docIds); } @Override public void setWorkInstanceSuspending() { suspending = true; } @Override public boolean isSuspending() { return suspending; } @Override public void suspended() { suspended = true; } @Override public boolean isWorkInstanceSuspended() { return suspended; } @Override public void setWorkInstanceState(State state) { this.state = state; if (log.isTraceEnabled()) { log.trace(this + " state=" + state); } } @Override public State getWorkInstanceState() { return state; } @Override @Deprecated public State getState() { return state; } @Override public void setProgress(Progress progress) { this.progress = progress; if (log.isTraceEnabled()) { log.trace(String.valueOf(this)); } } @Override public Progress getProgress() { return progress; } /** * Sets a human-readable status for this work instance. * * @param status the status */ public void setStatus(String status) { this.status = status; } @Override public String getStatus() { return status; } /** * May be called by implementing classes to open a session on the * repository. * * @return the session (also available in {@code session} field) */ public CoreSession initSession() throws Exception { return initSession(repositoryName); } /** * May be called by implementing classes to open a session on the given * repository. * * @param repositoryName the repository name * @return the session (also available in {@code session} field) */ public CoreSession initSession(String repositoryName) throws Exception { session = CoreInstance.openCoreSessionSystem(repositoryName); return session; } /** * Closes the session that was opened by {@link #initSession}. * * @since 5.8 */ public void closeSession() { if (session != null) { session.close(); session = null; } } @Override public void run() { if (isSuspending()) { // don't run anything if we're being started while a suspend // has been requested suspended(); return; } Exception suppressed = null; int retryCount = getRetryCount(); // may be 0 for (int i = 0; i <= retryCount; i++) { if (i > 0) { log.debug("Retrying work due to concurrent update (" + i + "): " + this); log.trace("Concurrent update", suppressed); } Exception e = runWorkWithTransactionAndCheckExceptions(); if (e == null) { // no exception, work is done return; } if (suppressed == null) { suppressed = e; } else { suppressed.addSuppressed(e); } } // all retries have been done, throw the exception if (suppressed != null) { if (suppressed instanceof RuntimeException) { throw (RuntimeException) suppressed; } else { throw new RuntimeException(suppressed); } } } /** * Does work under a transaction, and collects exception and suppressed * exceptions that may lead to a retry. * * @since 5.9.4 */ protected Exception runWorkWithTransactionAndCheckExceptions() { List<Exception> suppressed = Collections.emptyList(); try { TransactionHelper.noteSuppressedExceptions(); try { runWorkWithTransaction(); } finally { suppressed = TransactionHelper.getSuppressedExceptions(); } } catch (ConcurrentUpdateException e) { // happens typically during save() return e; } catch (TransactionRuntimeException e) { // error at commit time if (suppressed.isEmpty()) { return e; } } // reached if no catch, or if TransactionRuntimeException caught if (suppressed.isEmpty()) { return null; } // exceptions during commit caused a rollback in SessionImpl#end Exception e = suppressed.get(0); for (int i = 1; i < suppressed.size(); i++) { e.addSuppressed(suppressed.get(i)); } return e; } /** * Does work under a transaction. * * @since 5.9.4 * @throws ConcurrentUpdateException, TransactionRuntimeException */ protected void runWorkWithTransaction() throws ConcurrentUpdateException { TransactionHelper.startTransaction(); boolean ok = false; Exception exc = null; try { WorkSchedulePath.handleEnter(this); // --- do work setStartTime(); work(); // may throw ConcurrentUpdateException ok = true; // --- end work } catch (Exception e) { exc = e; if (e instanceof ConcurrentUpdateException) { throw (ConcurrentUpdateException) e; } else if (e instanceof RuntimeException) { throw (RuntimeException) e; } else if (e instanceof InterruptedException) { // restore interrupted status for the thread pool worker Thread.currentThread().interrupt(); } throw new RuntimeException(e); } finally { WorkSchedulePath.handleReturn(); try { setCompletionTime(); cleanUp(ok, exc); } finally { if (TransactionHelper.isTransactionActiveOrMarkedRollback()) { if (!ok) { TransactionHelper.setTransactionRollbackOnly(); } TransactionHelper.commitOrRollbackTransaction(); } } } } @Override public abstract void work() throws Exception; /** * Gets the number of times that this Work instance can be retried in case * of concurrent update exceptions. * * @return 0 for no retry, or more if some retries are possible * @see #work * @since 5.8 */ public int getRetryCount() { return 0; } /** * This method is called after {@link #work} is done in a finally block, * whether work completed normally or was in error or was interrupted. * * @param ok {@code true} if the work completed normally * @param e the exception, if available */ @Override public void cleanUp(boolean ok, Exception e) { if (!ok) { if (e instanceof InterruptedException) { log.debug("Suspended work: " + this); } else { if (!(e instanceof ConcurrentUpdateException)) { log.error("Exception during work: " + this, e); if (WorkSchedulePath.captureStack) { WorkSchedulePath.log.error("Work schedule path", getSchedulePath().getStack()); } } } } closeSession(); } @Override public String getUserId() { // TODO return null; } @Override public long getSchedulingTime() { return schedulingTime; } @Override public long getStartTime() { return startTime; } @Override public long getCompletionTime() { return completionTime; } @Override public void setStartTime() { startTime = System.currentTimeMillis(); } protected void setCompletionTime() { completionTime = System.currentTimeMillis(); } @Override public String getCategory() { return getClass().getSimpleName(); } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append(getClass().getSimpleName()); buf.append('('); if (docId != null) { buf.append(docId); buf.append(", "); } else if (docIds != null && docIds.size() > 0) { buf.append(docIds.get(0)); buf.append("..., "); } buf.append(getSchedulePath().getParentPath()); buf.append(", "); buf.append(getProgress()); buf.append(", "); buf.append(getStatus()); buf.append(')'); return buf.toString(); } @Override public DocumentLocation getDocument() { if (docId != null) { return newDocumentLocation(docId); } return null; } @Override public List<DocumentLocation> getDocuments() { if (docIds != null) { List<DocumentLocation> res = new ArrayList<DocumentLocation>( docIds.size()); for (String docId : docIds) { res.add(newDocumentLocation(docId)); } return res; } if (docId != null) { return Collections.singletonList(newDocumentLocation(docId)); } return Collections.emptyList(); } protected DocumentLocation newDocumentLocation(String docId) { return new DocumentLocationImpl(repositoryName, new IdRef(docId)); } @Override public boolean isDocumentTree() { return isTree; } /** * Releases the transaction resources by committing the existing transaction * (if any). This is recommended before running a long process. */ public void commitOrRollbackTransaction() { if (TransactionHelper.isTransactionActiveOrMarkedRollback()) { TransactionHelper.commitOrRollbackTransaction(); } } /** * Starts a new transaction. * <p> * Usually called after {@code commitOrRollbackTransaction()}, for instance * for saving back the results of a long process. * * @return true if a new transaction was started */ public boolean startTransaction() { return TransactionHelper.startTransaction(); } @Override public boolean equals(Object other) { if (!(other instanceof Work)) { return false; } return ((Work) other).getId().equals(getId()); } @Override public int hashCode() { return getId().hashCode(); } }