Package org.jenkinsci.plugins.workflow.support.steps

Source Code of org.jenkinsci.plugins.workflow.support.steps.StageStepExecution$Listener

package org.jenkinsci.plugins.workflow.support.steps;

import com.google.inject.Inject;
import hudson.Extension;
import hudson.XmlFile;
import hudson.model.InvisibleAction;
import hudson.model.Job;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.model.listeners.RunListener;
import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.logging.Level;
import static java.util.logging.Level.WARNING;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import jenkins.model.CauseOfInterruption;
import jenkins.model.Jenkins;
import org.jenkinsci.plugins.workflow.actions.LabelAction;
import org.jenkinsci.plugins.workflow.actions.StageAction;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl;
import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException;
import org.jenkinsci.plugins.workflow.steps.StepContext;
import org.jenkinsci.plugins.workflow.steps.StepContextParameter;

public class StageStepExecution extends AbstractStepExecutionImpl {
    private static final Logger LOGGER = Logger.getLogger(StageStepExecution.class.getName());

    // only used during the start() call, so no need to be persisted
    @Inject(optional=true) private transient StageStep step;
    @StepContextParameter private transient Run<?,?> run;
    @StepContextParameter private transient FlowNode node;
    @StepContextParameter private transient TaskListener listener; // picked up by getRequiredContext

    private static final class StageActionImpl extends InvisibleAction implements StageAction {
        private final String stageName;
        StageActionImpl(String stageName) {
            this.stageName = stageName;
        }
        @Override public String getStageName() {
            return stageName;
        }
    }

    @Override
    public boolean start() throws Exception {
        node.addAction(new LabelAction(step.name));
        node.addAction(new StageActionImpl(step.name));
        enter(run, getContext(), step.name, step.concurrency);
        return false; // execute asynchronously
    }

    @Override
    public void stop(Throwable cause) throws Exception {
        // TODO
        throw new UnsupportedOperationException();
    }

    private static XmlFile getConfigFile() throws IOException {
        Jenkins j = Jenkins.getInstance();
        if (j == null) {
            throw new IOException("Jenkins is not running");
        }
        return new XmlFile(new File(j.getRootDir(), StageStep.class.getName() + ".xml"));
    }

    // TODO can this be replaced with StepExecutionIterator?
    private static Map<String,Map<String,Stage>> stagesByNameByJob;

    // TODO or delete and make this an instance field in DescriptorImpl
    public static void clear() {
        stagesByNameByJob = null;
    }

    @SuppressWarnings("unchecked")
    private static synchronized void load() {
        if (stagesByNameByJob == null) {
            stagesByNameByJob = new TreeMap<String,Map<String,Stage>>();
            try {
                XmlFile configFile = getConfigFile();
                if (configFile.exists()) {
                    stagesByNameByJob = (Map<String,Map<String,Stage>>) configFile.read();
                }
            } catch (IOException x) {
                LOGGER.log(WARNING, null, x);
            }
            LOGGER.log(Level.FINE, "load: {0}", stagesByNameByJob);
        }
    }

    private static synchronized void save() {
        try {
            getConfigFile().write(stagesByNameByJob);
        } catch (IOException x) {
            LOGGER.log(WARNING, null, x);
        }
        LOGGER.log(Level.FINE, "save: {0}", stagesByNameByJob);
    }

    private static synchronized void enter(Run<?,?> r, StepContext context, String name, Integer concurrency) {
        LOGGER.log(Level.FINE, "enter {0} {1}", new Object[] {r, name});
        println(context, "Entering stage " + name);
        load();
        Job<?,?> job = r.getParent();
        String jobName = job.getFullName();
        Map<String,Stage> stagesByName = stagesByNameByJob.get(jobName);
        if (stagesByName == null) {
            stagesByName = new TreeMap<String,Stage>();
            stagesByNameByJob.put(jobName, stagesByName);
        }
        Stage stage = stagesByName.get(name);
        if (stage == null) {
            stage = new Stage();
            stagesByName.put(name, stage);
        }
        stage.concurrency = concurrency;
        int build = r.number;
        if (stage.waitingContext != null) {
            // Someone has got to give up.
            if (stage.waitingBuild < build) {
                // Cancel the older one.
                try {
                    cancel(stage.waitingContext, context);
                } catch (Exception x) {
                    LOGGER.log(WARNING, "could not cancel an older flow (perhaps since deleted?)", x);
                }
            } else if (stage.waitingBuild > build) {
                // Cancel this one. And work with the older one below, instead of the one initiating this call.
                try {
                    cancel(context, stage.waitingContext);
                } catch (Exception x) {
                    LOGGER.log(WARNING, "could not cancel the current flow", x);
                }
                build = stage.waitingBuild;
                context = stage.waitingContext;
            } else {
                throw new IllegalStateException("the same flow is trying to reënter the stage " + name); // see 'e' with two dots, that's Jesse Glick for you! - KK
            }
        }
        for (Map.Entry<String,Stage> entry : stagesByName.entrySet()) {
            if (entry.getKey().equals(name)) {
                continue;
            }
            Stage stage2 = entry.getValue();
            // If we were holding another stage in the same job, release it, unlocking its waiter to proceed.
            if (stage2.holding.remove(build)) {
                if (stage2.waitingContext != null) {
                    println(stage2.waitingContext, "Unblocked since " + r.getDisplayName() + " is moving into stage " + name);
                    stage2.waitingContext.onSuccess(null);
                    stage2.waitingBuild = null;
                    stage2.waitingContext = null;
                }
            }
        }
        if (stage.concurrency == null || stage.holding.size() < stage.concurrency) {
            stage.waitingBuild = null;
            stage.waitingContext = null;
            stage.holding.add(build);
            println(context, "Proceeding");
            context.onSuccess(null);
        } else {
            stage.waitingBuild = build;
            stage.waitingContext = context;
            println(context, "Waiting for builds " + stage.holding);
        }
        cleanUp(job, jobName);
        save();
    }

    private static synchronized void exit(Run<?,?> r) {
        load();
        LOGGER.log(Level.FINE, "exit {0}: {1}", new Object[] {r, stagesByNameByJob});
        Job<?,?> job = r.getParent();
        String jobName = job.getFullName();
        Map<String,Stage> stagesByName = stagesByNameByJob.get(jobName);
        if (stagesByName == null) {
            return;
        }
        boolean modified = false;
        for (Stage stage : stagesByName.values()) {
            if (stage.holding.contains(r.number)) {
                stage.holding.remove(r.number); // XSTR-757: do not rely on return value of TreeSet.remove(Object)
                modified = true;
                if (stage.waitingContext != null) {
                    println(stage.waitingContext, "Unblocked since " + r.getDisplayName() + " finished");
                    stage.waitingContext.onSuccess(null);
                    stage.waitingContext = null;
                    stage.waitingBuild = null;
                }
            }
        }
        if (modified) {
            cleanUp(job, jobName);
            save();
        }
    }

    private static void cleanUp(Job<?,?> job, String jobName) {
        Map<String,Stage> stagesByName = stagesByNameByJob.get(jobName);
        assert stagesByName != null;
        Iterator<Entry<String,Stage>> it = stagesByName.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String,Stage> entry = it.next();
            Set<Integer> holding = entry.getValue().holding;
            Iterator<Integer> it2 = holding.iterator();
            while (it2.hasNext()) {
                Integer number = it2.next();
                if (job.getBuildByNumber(number) == null) {
                    // Deleted at some point but did not properly clean up from exit(…).
                    LOGGER.log(WARNING, "Cleaning up apparently deleted {0}#{1}", new Object[] {jobName, number});
                    it2.remove();
                }
            }
            if (holding.isEmpty()) {
                assert entry.getValue().waitingContext == null;
                it.remove();
            }
        }
        if (stagesByName.isEmpty()) {
            stagesByNameByJob.remove(jobName);
        }
    }

    private static void println(StepContext context, String message) {
        try {
            context.get(TaskListener.class).getLogger().println(message);
        } catch (Exception x) {
            LOGGER.log(WARNING, null, x);
        }
    }

    // TODO record the stage it got to and display that
    private static void cancel(StepContext context, StepContext newer) throws IOException, InterruptedException {
        println(context, "Canceled since " + newer.get(Run.class).getDisplayName() + " got here");
        println(newer, "Canceling older " + context.get(Run.class).getDisplayName());
        context.onFailure(new FlowInterruptedException(Result.NOT_BUILT, new CanceledCause(newer.get(Run.class))));
    }

    /**
     * Records that a flow was canceled while waiting in a stage step because a newer flow entered that stage instead.
     */
    public static final class CanceledCause extends CauseOfInterruption {

        private final String newerBuild;

        CanceledCause(Run<?,?> newerBuild) {
            this.newerBuild = newerBuild.getExternalizableId();
        }

        public Run<?,?> getNewerBuild() {
            return Run.fromExternalizableId(newerBuild);
        }

        @Override public String getShortDescription() {
            return "Superseded by " + getNewerBuild().getDisplayName();
        }

    }

    private static final class Stage {
        /** number of builds current in this stage */
        final Set<Integer> holding = new TreeSet<Integer>();
        /** maximum permitted size of {@link #holding} */
        @CheckForNull
        Integer concurrency;
        /** context of the build currently waiting to enter this stage, if any */
        @CheckForNull StepContext waitingContext;
        /** number of the waiting build, if any */
        @Nullable
        Integer waitingBuild;
        @Override public String toString() {
            return "Stage[holding=" + holding + ",waitingBuild=" + waitingBuild + ",concurrency=" + concurrency + "]";
        }
    }

    @Extension
    public static final class Listener extends RunListener<Run<?,?>> {
        @Override public void onCompleted(Run<?,?> r, TaskListener listener) {
            exit(r);
        }
    }

    private static final long serialVersionUID = 1L;

}
TOP

Related Classes of org.jenkinsci.plugins.workflow.support.steps.StageStepExecution$Listener

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.