/* * (C) Copyright 2012-2016 Nuxeo SA (http://nuxeo.com/) and others. * * 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. * * 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 javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.common.logging.SequenceTracer; 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.NuxeoException; 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.api.Framework; import org.nuxeo.runtime.transaction.TransactionHelper; /** * 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 State state; protected 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; /** * The originating username to use when opening the {@link CoreSession}. * * @since 8.1 */ protected String originatingUsername; protected String status; protected long schedulingTime; protected long startTime; protected long completionTime; protected transient CoreSession session; protected transient LoginContext loginContext; protected WorkSchedulePath schedulePath; protected String callerThread; /** * 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())); callerThread = SequenceTracer.getThreadName(); } public AbstractWork(String id) { this.id = id; progress = PROGRESS_INDETERMINATE; schedulingTime = System.currentTimeMillis(); callerThread = SequenceTracer.getThreadName(); } @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<>(docIds); } /** * @since 8.1 */ public void setOriginatingUsername(String originatingUsername) { this.originatingUsername = originatingUsername; } @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 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) * @deprecated since 8.1. Use {@link #openSystemSession()}. */ @Deprecated public CoreSession initSession() { return initSession(repositoryName); } /** * May be called by implementing classes to open a System session on the repository. * * @since 8.1 */ public void openSystemSession() { session = CoreInstance.openCoreSessionSystem(repositoryName, originatingUsername); } /** * May be called by implementing classes to open a Use session on the repository. * <p> * It uses the set {@link #originatingUsername} to open the session. * * @since 8.1 */ public void openUserSession() { if (originatingUsername == null) { throw new IllegalStateException("Cannot open an user session without an originatingUsername"); } try { loginContext = Framework.loginAsUser(originatingUsername); } catch (LoginException e) { throw new NuxeoException(e); } session = CoreInstance.openCoreSession(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) * @deprecated since 8.1. Use {@link #openSystemSession()} to open a session on the configured repository name, * otherwise use {@link CoreInstance#openCoreSessionSystem(String)}. */ @Deprecated public CoreSession initSession(String repositoryName) { session = CoreInstance.openCoreSessionSystem(repositoryName, originatingUsername); return session; } /** * Closes the session that was opened by {@link #openSystemSession()} or {@link #openUserSession()}. * * @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; } if (SequenceTracer.isEnabled()) { SequenceTracer.startFrom(callerThread, "Work " + getTitleOr("unknown"), " #7acde9"); } RuntimeException 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); } try { runWorkWithTransaction(); SequenceTracer.stop("Work done " + (completionTime - startTime) + " ms"); return; } catch (RuntimeException e) { if (suppressed == null) { suppressed = e; } else { suppressed.addSuppressed(e); } } } // all retries have been done, throw the exception if (suppressed != null) { String msg = "Work failed after " + retryCount + " " + (retryCount == 1 ? "retry" : "retries") + ", class=" + getClass() + " id=" + getId() + " category=" + getCategory() + " title=" + getTitle(); SequenceTracer.destroy("Work failure " + (completionTime - startTime) + " ms"); throw new RuntimeException(msg, suppressed); } } private String getTitleOr(String defaultTitle) { try { return getTitle(); } catch (Exception e) { return defaultTitle; } } /** * Does work under a transaction. * * @since 5.9.4 */ protected void runWorkWithTransaction() { 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 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 || isSuspending()) { log.trace(this + " is suspending, rollbacking"); TransactionHelper.setTransactionRollbackOnly(); } TransactionHelper.commitOrRollbackTransaction(); } } } } @Override public abstract void work(); /** * 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)) { if (!isSuspending()) { log.error("Exception during work: " + this, e); if (WorkSchedulePath.captureStack) { WorkSchedulePath.log.error("Work schedule path", getSchedulePath().getStack()); } } } } } closeSession(); try { // loginContext may be null in tests if (loginContext != null) { loginContext.logout(); } } catch (LoginException le) { throw new NuxeoException(le); } } @Override public String getOriginatingUsername() { return originatingUsername; } @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<>(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(); } }