Package org.jenkinsci.plugins.workflow.cps

Source Code of org.jenkinsci.plugins.workflow.cps.CpsThreadGroup

/*
* 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.Continuable;
import com.cloudbees.groovy.cps.Outcome;
import com.google.common.util.concurrent.Futures;
import groovy.lang.Closure;
import groovy.lang.Script;
import hudson.Util;
import hudson.model.Result;
import org.jenkinsci.plugins.workflow.actions.ErrorAction;
import org.jenkinsci.plugins.workflow.cps.persistence.PersistIn;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException;
import org.jenkinsci.plugins.workflow.support.pickles.serialization.RiverWriter;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;

import static java.util.logging.Level.*;
import static org.jenkinsci.plugins.workflow.cps.CpsFlowExecution.*;
import static org.jenkinsci.plugins.workflow.cps.persistence.PersistenceContext.*;

/**
* List of {@link CpsThread}s that form a single {@link CpsFlowExecution}.
*
* <p>
* To make checkpointing easy, only one {@link CpsThread} runs at any point in time.
*
* @author Kohsuke Kawaguchi
*/
@PersistIn(PROGRAM)
@edu.umd.cs.findbugs.annotations.SuppressWarnings("SE_BAD_FIELD") // bogus warning about closures
public final class CpsThreadGroup implements Serializable {
    /**
     * {@link CpsThreadGroup} always belong to the same {@link CpsFlowExecution}.
     *
     * {@link CpsFlowExecution} and {@link CpsThreadGroup} persist separately,
     * so this field is not persisted, but get fixed up in {@link #readResolve()}
     */
    private /*almost final*/ transient CpsFlowExecution execution;

    /**
     * All the member threads by their {@link CpsThread#id}
     */
    final NavigableMap<Integer,CpsThread> threads = new TreeMap<Integer, CpsThread>();

    /**
     * Unique thread ID generator.
     */
    private int iota;

    /**
     * Ensures only one thread updates CPS VM state at any given time
     * by queueing such tasks in here.
     */
    transient ExecutorService runner;

    /**
     * "Exported" closures that are referenced by live {@link CpsStepContext}s.
     */
    public final Map<Integer,Closure> closures = new HashMap<Integer,Closure>();

    CpsThreadGroup(CpsFlowExecution execution) {
        this.execution = execution;
        setupTransients();
    }

    public CpsFlowExecution getExecution() {
        return execution;
    }

    private Object readResolve() {
        execution = CpsFlowExecution.PROGRAM_STATE_SERIALIZATION.get();
        setupTransients();
        assert execution!=null;
        return this;
    }

    private void setupTransients() {
        runner = new CpsVmExecutorService(this);
    }

    @CpsVmThreadOnly
    public CpsThread addThread(Continuable program, FlowHead head, ContextVariableSet contextVariables) {
        assertVmThread();
        CpsThread t = new CpsThread(this, iota++, program, head, contextVariables);
        threads.put(t.id, t);
        return t;
    }

    /**
     * Ensures that the current thread is running from {@link CpsVmExecutorService}
     *
     * @see CpsVmThreadOnly
     */
    private void assertVmThread() {
        assert current()==this;
    }

    public CpsThread getThread(int id) {
        return threads.get(id);
    }

    @CpsVmThreadOnly("root")
    public BodyReference export(Closure body) {
        assertVmThread();
        if (body==null)     return null;
        int id = iota++;
        closures.put(id, body);
        return new StaticBodyReference(id,body);
    }

    @CpsVmThreadOnly("root")
    public BodyReference export(final Script body) {
        if (body==null)     return null;
        return export(new Closure(null) {
            @Override
            public Object call() {
                return body.run();
            }
        });
    }

    @CpsVmThreadOnly("root")
    public void unexport(BodyReference ref) {
        assertVmThread();
        if (ref==null)      return;
        closures.remove(ref.id);
    }

    /**
     * Schedules the execution of all the runnable threads.
     */
    public Future<?> scheduleRun() {
        final Future<Future<?>> f = runner.submit(new Callable<Future<?>>() {
            public Future<?> call() throws Exception {
                run();
                // we ensure any tasks submitted during run() will complete before we declare us complete
                // those include things like notifying listeners or updating various other states
                // runner is a single-threaded queue, so running a no-op and waiting for its completion
                // ensures that everything submitted in front of us has finished.
                try {
                    return runner.submit(new Runnable() {
                        @Override public void run() {
                            if (threads.isEmpty()) {
                                runner.shutdown();
                            }
                        }
                    });
                } catch (RejectedExecutionException x) {
                    // Was shut down by a prior task?
                    return Futures.immediateFuture(null);
                }
            }
        });

        // unfortunately that means we have to wait for Future of Future,
        // so we need a rather unusual implementation of Future to hide that behind the scene.
        return new Future<Object>() {
            @Override
            public boolean cancel(boolean mayInterruptIfRunning) {
                if (!f.isDone())
                    return f.cancel(mayInterruptIfRunning);

                try {
                    return f.get().cancel(mayInterruptIfRunning);
                } catch (InterruptedException e) {
                    throw new AssertionError(e);
                } catch (ExecutionException e) {
                    return false;
                }
            }

            @Override
            public boolean isCancelled() {
                if (f.isCancelled())    return true;
                if (!f.isDone())        return false;

                try {
                    return f.get().isCancelled();
                } catch (InterruptedException e) {
                    throw new AssertionError(e);
                } catch (ExecutionException e) {
                    return false;
                }
            }

            @Override
            public boolean isDone() {
                if (!f.isDone())    return false;

                try {
                    return f.get().isDone();
                } catch (InterruptedException e) {
                    throw new AssertionError(e);
                } catch (ExecutionException e) {
                    return false;
                }
            }

            @Override
            public Object get() throws InterruptedException, ExecutionException {
                return f.get().get();
            }

            @Override
            public Object get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
                // FIXME: this ends up waiting up to 2x
                return f.get(timeout,unit).get(timeout,unit);
            }
        };
    }

    /**
     * Run all runnable threads as much as possible.
     */
    @CpsVmThreadOnly("root")
    private void run() throws IOException {
        boolean doneSomeWork = false;
        boolean changed;    // used to see if we need to loop over
        do {
            changed = false;
            for (CpsThread t : threads.values().toArray(new CpsThread[threads.size()])) {
                if (t.isRunnable()) {
                    Outcome o = t.runNextChunk();
                    if (o.isFailure()) {
                        assert !t.isAlive();    // failed thread is non-resumable

                        // workflow produced an exception
                        Result result = Result.FAILURE;
                        Throwable error = o.getAbnormal();
                        if (error instanceof FlowInterruptedException) {
                            result = ((FlowInterruptedException) error).getResult();
                        }
                        execution.setResult(result);
                        t.head.get().addAction(new ErrorAction(error));
                    }

                    if (!t.isAlive()) {
                        LOGGER.fine("completed " + t);

                        threads.remove(t.id);
                        if (threads.isEmpty()) {
                            execution.onProgramEnd(o);
                        }
                    }

                    changed = true;
                }
            }

            doneSomeWork |= changed;
        } while (changed);

        if (doneSomeWork) {
            saveProgram();
        }
    }

    /**
     * Notifies listeners of the new {@link FlowHead}.
     *
     * The actual call happens later from a place who owns no lock on any of the CPS objects to avoid deadlock.
     * See https://trello.com/c/7aTFYWM5/26-intermittent-deadlock
     */
    @CpsVmThreadOnly
    /*package*/ void notifyNewHead(final FlowNode head) {
        assertVmThread();
        runner.execute(new Runnable() {
            public void run() {
                execution.notifyListeners(head);
            }
        });
    }

    /**
     * Persists the current state of {@link CpsThreadGroup}.
     */
    @CpsVmThreadOnly
    void saveProgram() throws IOException {
        File f = execution.getProgramDataFile();
        saveProgram(f);
    }

    @CpsVmThreadOnly
    public void saveProgram(File f) throws IOException {
        File dir = f.getParentFile();
        File tmpFile = File.createTempFile("atomic",null, dir);

        assertVmThread();

        CpsFlowExecution old = PROGRAM_STATE_SERIALIZATION.get();
        PROGRAM_STATE_SERIALIZATION.set(execution);

        try {
            RiverWriter w = new RiverWriter(tmpFile, execution.getOwner());
            try {
                w.writeObject(this);
            } finally {
                w.close();
            }
            Util.deleteFile(f);
            if (!tmpFile.renameTo(f)) {
                throw new IOException("rename " + tmpFile + " to " + f + " failed");
            }
            LOGGER.log(FINE, "program state saved");
        } catch (RuntimeException e) {
            LOGGER.log(WARNING, "program state save failed",e);
            propagateErrorToWorkflow(e);
            throw new IOException("Failed to persist "+f,e);
        } catch (IOException e) {
            LOGGER.log(WARNING, "program state save failed",e);
            propagateErrorToWorkflow(e);
            throw new IOException("Failed to persist "+f,e);
        } finally {
            PROGRAM_STATE_SERIALIZATION.set(old);
            Util.deleteFile(tmpFile);
        }
    }

    /**
     * Propagates the failure to the workflow by passing an exception
     */
    @CpsVmThreadOnly
    private void propagateErrorToWorkflow(Throwable t) {
        // it's not obvious which thread to blame, so as a heuristics, pick up the last one,
        // as that's the ony more likely to have caused the problem.
        // TODO: when we start tracking which thread is just waiting for the body, then
        // that information would help. or maybe we should just remember the thread that has run the last time
        threads.lastEntry().getValue().resume(new Outcome(null,t));
    }

    private static final Logger LOGGER = Logger.getLogger(CpsThreadGroup.class.getName());

    private static final long serialVersionUID = 1L;

    /**
     * CPS transformed program runs entirely inside a program execution thread.
     * If we are in that thread executing {@link CpsThreadGroup}, this method returns non-null.
     */
    @CpsVmThreadOnly
    /*package*/ static CpsThreadGroup current() {
        return CpsVmExecutorService.CURRENT.get();
    }
}
TOP

Related Classes of org.jenkinsci.plugins.workflow.cps.CpsThreadGroup

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.