/*
* The MIT License
*
* Copyright (c) 2013-2014, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.jenkinsci.plugins.workflow.cps;
import com.cloudbees.groovy.cps.Outcome;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import groovy.lang.Closure;
import hudson.model.Descriptor;
import hudson.model.Result;
import hudson.util.DaemonThreadFactory;
import hudson.util.NamingThreadFactory;
import jenkins.model.Jenkins;
import org.jenkinsci.plugins.workflow.cps.nodes.StepEndNode;
import org.jenkinsci.plugins.workflow.cps.nodes.StepStartNode;
import org.jenkinsci.plugins.workflow.cps.persistence.PersistIn;
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
import org.jenkinsci.plugins.workflow.graph.AtomNode;
import org.jenkinsci.plugins.workflow.graph.BlockEndNode;
import org.jenkinsci.plugins.workflow.graph.BlockStartNode;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException;
import org.jenkinsci.plugins.workflow.steps.Step;
import org.jenkinsci.plugins.workflow.steps.StepContext;
import org.jenkinsci.plugins.workflow.steps.StepDescriptor;
import org.jenkinsci.plugins.workflow.steps.StepExecution;
import org.jenkinsci.plugins.workflow.support.DefaultStepContext;
import org.jenkinsci.plugins.workflow.support.concurrent.Futures;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.GuardedBy;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.CauseOfInterruption;
import jenkins.util.ContextResettingExecutorService;
import org.codehaus.groovy.runtime.InvokerInvocationException;
import org.jenkinsci.plugins.workflow.cps.nodes.StepNode;
import static org.jenkinsci.plugins.workflow.cps.persistence.PersistenceContext.*;
/**
* {@link StepContext} implementation for CPS.
*
* <p>
* This context behaves in two modes. It starts in the synchronous mode, where if a result is set (or exception
* is thrown), it just gets recoded. When passed into {@link Step#start(StepContext)}, it's in this mode.
*
* <p>
* When {@link Step#start(StepContext)} method returns, we'll atomically check if the result is set or not
* and then switch to the asynchronous mode. In this mode, if the result is set, it'll trigger the rehydration
* of the workflow. If a {@link CpsStepContext} gets serialized, it'll be deserialized in the asynchronous mode.
*
* <p>
* This object must be serializable on its own without sucking in any of the {@link CpsFlowExecution} object
* graph. Wherever we need {@link CpsFlowExecution} we do that by following {@link FlowExecutionOwner}, and
* when we need pointers to individual objects inside, we use IDs (such as {@link #id}}.
*
* @author Kohsuke Kawaguchi
* @see Step#start(StepContext)
*/
@PersistIn(ANYWHERE)
@edu.umd.cs.findbugs.annotations.SuppressWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") // bodyInvokers, syncMode handled specially
public class CpsStepContext extends DefaultStepContext { // TODO add XStream class mapper
private static final Logger LOGGER = Logger.getLogger(CpsStepContext.class.getName());
@GuardedBy("this")
private transient Outcome outcome;
private transient Throwable whenOutcomeDelivered;
// see class javadoc.
// transient because if it's serialized and deserialized, it should come back in the async mode.
private transient boolean syncMode = true;
/**
* This object gets serialized independently from the rest of {@link CpsFlowExecution}
* and {@link DSL}, so it needs to use a handle to refer back to {@link CpsFlowExecution}
*/
private final FlowExecutionOwner executionRef;
/**
* {@link FlowNode#id} that points to the atom node created for this step.
*/
private final String id;
/**
* Keeps an in-memory reference to {@link FlowNode} to speed up the synchronous execution.
*
* If there's a body, this field is {@link BlockStartNode}. If there's no body, then this
* field is {@link AtomNode}
*
* @see #getNode()
*/
/*package*/ transient FlowNode node;
/*
TODO: parallel step implementation
when forking off another branch of parallel, call the 3-arg version of the start() method,
and have its callback insert the ID of the new head at the end of the thread
*/
/**
* {@link FlowHead#getId()} that should become
* the parents of the {@link BlockEndNode} when we create one. Only used when this context has the body.
*/
final List<Integer> bodyHeads = new ArrayList<Integer>();
/**
* If the invocation of the body is requested, this object remembers how to start it.
*
* <p>
* Only used in the synchronous mode while {@link CpsFlowExecution} is in the RUNNABLE state,
* so this need not be persisted. To preserve the order of invocation in the flow graph,
* this needs to be a list and not set.
*/
transient List<CpsBodyInvoker> bodyInvokers = Collections.synchronizedList(new ArrayList<CpsBodyInvoker>());
/**
* While {@link CpsStepContext} has not received teh response, maintains the body closure.
*
* This is the implicit closure block passed to the step invocation.
*/
private @CheckForNull BodyReference body;
private final int threadId;
/**
* {@linkplain Descriptor#getId() step descriptor ID}.
*/
private final String stepDescriptorId;
/**
* Resolved result of {@link #stepDescriptorId} to make the look up faster.
*/
private transient volatile StepDescriptor stepDescriptor;
/**
* Cached value of {@link #getThreadGroupSynchronously}.
* Never null once set (might be overwritten).
*/
private transient volatile CpsThreadGroup threadGroup;
private transient volatile boolean loadingThreadGroup;
@CpsVmThreadOnly
CpsStepContext(StepDescriptor step, CpsThread thread, FlowExecutionOwner executionRef, FlowNode node, @CheckForNull Closure body) {
this.threadId = thread.id;
this.executionRef = executionRef;
this.id = node.getId();
this.node = node;
this.body = body != null ? thread.group.export(body) : null;
this.stepDescriptorId = step.getId();
}
/**
* Obtains {@link StepDescriptor} that represents the step this context is invoking.
*
* @return
* This method returns null if the step descriptor used is not recoverable in the current VM session,
* such as when the plugin that implements this was removed. So the caller should defend against null.
*/
@SuppressFBWarnings(value="RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", justification="TODO 1.653+ switch to Jenkins.getInstanceOrNull")
public @CheckForNull StepDescriptor getStepDescriptor() {
Jenkins j = Jenkins.getInstance();
if (j == null) {
return null;
}
if (stepDescriptor==null)
stepDescriptor = (StepDescriptor) j.getDescriptor(stepDescriptorId);
return stepDescriptor;
}
public String getDisplayName() {
StepDescriptor d = getStepDescriptor();
return d!=null ? d.getDisplayName() : stepDescriptorId;
}
@Override protected CpsFlowExecution getExecution() throws IOException {
return (CpsFlowExecution)executionRef.get();
}
/**
* Returns the thread that is executing this step.
* Needs to take {@link CpsThreadGroup} as a parameter to prove that the caller is in CpsVmThread.
*
* @return
* null if the thread has finished executing.
*/
@CheckForNull CpsThread getThread(CpsThreadGroup g) {
CpsThread thread = g.threads.get(threadId);
if (thread == null) {
LOGGER.log(Level.FINE, "no thread " + threadId + " among " + g.threads.keySet(), new IllegalStateException());
}
return thread;
}
/**
* Synchronously resolve the current thread.
*
* This can block for the entire duration of the PREPARING state.
*/
private @CheckForNull CpsThread getThreadSynchronously() throws InterruptedException, IOException {
return getThread(getThreadGroupSynchronously());
}
private @Nonnull CpsThreadGroup getThreadGroupSynchronously() throws InterruptedException, IOException {
if (threadGroup == null) {
ListenableFuture<CpsThreadGroup> pp;
CpsFlowExecution flowExecution = getFlowExecution();
while ((pp = flowExecution.programPromise) == null) {
Thread.sleep(100); // TODO does JENKINS-33005 remove the need for this?
}
try {
threadGroup = pp.get();
} catch (ExecutionException e) {
throw new IOException(e);
}
}
return threadGroup;
}
// As in c16a522, using jenkins.util.Timer for this could deadlock. TODO would like a standard unbounded executor service.
private static final ExecutorService isReadyExecutorService = new ContextResettingExecutorService(Executors.newCachedThreadPool(new NamingThreadFactory(new DaemonThreadFactory(), "CpsStepContext.isReady")));
@SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_BAD_PRACTICE")
@Override public boolean isReady() {
if (threadGroup == null) {
if (!loadingThreadGroup) { // but start computing it
isReadyExecutorService.submit(new Callable<Void>() {
@Override public Void call() throws Exception {
getThreadGroupSynchronously();
return null;
}
});
loadingThreadGroup = true;
}
return false;
} else {
return true;
}
}
@Override public boolean hasBody() {
return body != null;
}
@Override
public CpsBodyInvoker newBodyInvoker() {
if (body == null) {
throw new IllegalStateException("There is no body to invoke");
}
return newBodyInvoker(body);
}
public @Nonnull CpsBodyInvoker newBodyInvoker(@Nonnull BodyReference body) {
return new CpsBodyInvoker(this,body);
}
@Override
protected <T> T doGet(Class<T> key) throws IOException, InterruptedException {
CpsThread t = getThreadSynchronously();
if (t == null) {
throw new IOException("cannot find current thread");
}
T v = t.getContextVariable(key);
if (v!=null) return v;
if (FlowNode.class.isAssignableFrom(key)) {
return key.cast(getNode());
}
if (key == CpsThread.class) {
return key.cast(t);
}
if (key == CpsThreadGroup.class) {
return key.cast(t.group);
}
return null;
}
@Override protected FlowNode getNode() throws IOException {
if (node == null) {
node = getFlowExecution().getNode(id);
if (node == null) {
throw new IOException("no node found for " + id);
}
}
return node;
}
@Override public synchronized void onFailure(Throwable t) {
if (t == null) {
throw new IllegalArgumentException();
}
completed(new Outcome(null, t));
}
@Override public synchronized void onSuccess(Object returnValue) {
completed(new Outcome(returnValue, null));
}
private void completed(@Nonnull Outcome newOutcome) {
if (outcome == null) {
outcome = newOutcome;
scheduleNextRun();
whenOutcomeDelivered = new Throwable();
} else {
Throwable failure = newOutcome.getAbnormal();
if (failure instanceof FlowInterruptedException) {
for (CauseOfInterruption cause : ((FlowInterruptedException) failure).getCauses()) {
if (cause instanceof BodyFailed) {
LOGGER.log(Level.FINE, "already completed " + this + " and now received body failure", failure);
// Predictable that the error would be thrown up here; quietly ignore it.
return;
}
}
}
LOGGER.log(Level.WARNING, "already completed " + this, new IllegalStateException("delivered here"));
if (failure != null) {
LOGGER.log(Level.INFO, "new failure", failure);
} else {
LOGGER.log(Level.INFO, "new success: {0}", outcome.getNormal());
}
if (whenOutcomeDelivered != null) {
LOGGER.log(Level.INFO, "previously delivered here", whenOutcomeDelivered);
}
failure = outcome.getAbnormal();
if (failure != null) {
LOGGER.log(Level.INFO, "earlier failure", failure);
} else {
LOGGER.log(Level.INFO, "earlier success: {0}", outcome.getNormal());
}
}
}
/**
* When this step context has completed execution (successful or otherwise), plan the next action.
*/
private void scheduleNextRun() {
if (syncMode) {
// if we get the result set before the start method returned, then DSL.invokeMethod() will
// plan the next action.
return;
}
try {
final FlowNode n = getNode();
final CpsFlowExecution flow = getFlowExecution();
final List<FlowNode> parents = new ArrayList<FlowNode>();
for (int head : bodyHeads) {
FlowHead flowHead = flow.getFlowHead(head);
if (flowHead != null) {
parents.add(flowHead.get());
} else {
LOGGER.log(Level.WARNING, "Could not find flow head #{0}", head);
}
}
flow.runInCpsVmThread(new FutureCallback<CpsThreadGroup>() {
@CpsVmThreadOnly
@Override
public void onSuccess(CpsThreadGroup g) {
g.unexport(body);
body = null;
CpsThread thread = getThread(g);
if (thread != null) {
CpsThread nit = thread.getNextInner();
if (nit!=null) {
// can't mark this done until the inner thread is done.
// defer the processing until the inner thread is done
nit.addCompletionHandler(new ScheduleNextRun());
if (getOutcome().isFailure()) {
// if the step with a currently running body reported a failure,
// make some effort to try to interrupt the running body
StepExecution s = nit.getStep();
if (s != null) {
// TODO: ideally this needs to work like interrupt, in that
// if s==null the next StepExecution gets interrupted when it happen
FlowInterruptedException cause = new FlowInterruptedException(Result.FAILURE, new BodyFailed());
cause.initCause(getOutcome().getAbnormal());
try {
// TODO JENKINS-26148/JENKINS-34637 this is probably wrong: should interrupt the innermost execution
// (the “next” one could be block-scoped, and we would want to interrupt all parallel heads)
s.stop(cause);
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Failed to stop the body execution in response to the failure of the parent");
}
}
}
return;
}
if (n instanceof StepStartNode) {
// if there's no body to invoke, we want the current thread to be the sole head
if (parents.isEmpty())
parents.add(thread.head.get());
// clear all the subsumed heads that are joining. thread that owns parents.get(0) lives on
for (int i=1; i<parents.size(); i++)
g.getExecution().subsumeHead(parents.get(i));
thread.head.setNewHead(new StepEndNode(flow, (StepStartNode) n, parents));
}
thread.head.markIfFail(getOutcome());
thread.setStep(null);
thread.resume(getOutcome());
}
}
/**
* Program state failed to load.
*/
@Override
public void onFailure(Throwable t) {
}
});
} catch (IOException x) {
LOGGER.log(Level.FINE, null, x);
}
}
private static class BodyFailed extends CauseOfInterruption {
@Override public String getShortDescription() {
return "Body of block-scoped step failed";
}
}
@Override
public void setResult(Result r) {
try {
getFlowExecution().setResult(r);
} catch (IOException x) {
LOGGER.log(Level.FINE, null, x);
}
}
private @Nonnull CpsFlowExecution getFlowExecution() throws IOException {
return (CpsFlowExecution)executionRef.get();
}
synchronized boolean isCompleted() {
return outcome!=null;
}
synchronized boolean isSyncMode() {
return syncMode;
}
/**
* Simulates the result of the {@link StepContext call} by either throwing an exception
* or returning the value.
*/
synchronized Object replay() {
try {
return getOutcome().replay();
} catch (Throwable failure) {
// Cf. CpsBodyExecution.FailureAdapter:
if (failure instanceof RuntimeException)
throw (RuntimeException) failure;
if (failure instanceof Error)
throw (Error) failure;
// Any GroovyRuntimeException is treated magically by ScriptBytecodeAdapter.unwrap (from PogoMetaClassSite):
throw new InvokerInvocationException(failure);
}
}
synchronized Outcome getOutcome() {
return outcome;
}
/**
* Atomically switch this context into the asynchronous mode.
* Any results set beyond this point will trigger callback.
*
* @return
* true if the result was not available prior to this call and the context was successfully switched to the
* async mode.
*
* false if the result is already available. The caller should use {@link #getOutcome()} to obtain that.
*/
synchronized boolean switchToAsyncMode() {
if (!syncMode) throw new AssertionError();
syncMode = false;
return !isCompleted();
}
@Override public ListenableFuture<Void> saveState() {
try {
final SettableFuture<Void> f = SettableFuture.create();
getFlowExecution().runInCpsVmThread(new FutureCallback<CpsThreadGroup>() {
@Override public void onSuccess(CpsThreadGroup result) {
try {
// TODO keep track of whether the program was saved anyway after saveState was called but before now, and do not bother resaving it in that case
result.saveProgram();
f.set(null);
} catch (IOException x) {
f.setException(x);
}
}
@Override public void onFailure(Throwable t) {
f.setException(t);
}
});
return f;
} catch (IOException x) {
return Futures.immediateFailedFuture(x);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CpsStepContext that = (CpsStepContext) o;
return executionRef.equals(that.executionRef) && id.equals(that.id);
}
@Override
public int hashCode() {
int result = executionRef.hashCode();
result = 31 * result + id.hashCode();
return result;
}
@Override public String toString() {
String function = null;
if (node instanceof StepNode) {
StepDescriptor d = ((StepNode) node).getDescriptor();
if (d != null) {
function = d.getFunctionName();
}
}
return "CpsStepContext[" + id + ":" + function + "]:" + executionRef;
}
private static final long serialVersionUID = 1L;
@edu.umd.cs.findbugs.annotations.SuppressWarnings("SE_INNER_CLASS")
private class ScheduleNextRun implements FutureCallback<Object>, Serializable {
public void onSuccess(Object _) { scheduleNextRun(); }
public void onFailure(Throwable _) { scheduleNextRun(); }
private static final long serialVersionUID = 1L;
}
}