Skip to content

Commit 25298d7

Browse files
authored
Merge branch 'master' into CpsVmExecutorService.shutdown
2 parents 960a3f7 + 7bc717e commit 25298d7

File tree

5 files changed

+152
-43
lines changed

5 files changed

+152
-43
lines changed

pom.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
<dependency>
7676
<groupId>io.jenkins.tools.bom</groupId>
7777
<artifactId>bom-2.332.x</artifactId>
78-
<version>1289.v5c4b_1c43511b_</version>
78+
<version>1382.v7d694476f340</version>
7979
<scope>import</scope>
8080
<type>pom</type>
8181
</dependency>
@@ -89,6 +89,7 @@
8989
<dependency>
9090
<groupId>org.jenkins-ci.plugins.workflow</groupId>
9191
<artifactId>workflow-api</artifactId>
92+
<version>1162.va_1e49062a_00e</version>
9293
</dependency>
9394
<dependency>
9495
<groupId>org.jenkins-ci.plugins.workflow</groupId>

src/main/java/org/jenkinsci/plugins/workflow/cps/CpsFlowExecution.java

Lines changed: 81 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import hudson.model.Action;
5353
import hudson.model.Result;
5454
import hudson.util.Iterators;
55+
import io.jenkins.lib.versionnumber.JavaSpecificationVersion;
5556
import jenkins.model.CauseOfInterruption;
5657
import jenkins.model.Jenkins;
5758
import org.jboss.marshalling.Unmarshaller;
@@ -276,6 +277,13 @@ public class CpsFlowExecution extends FlowExecution implements BlockableResume {
276277

277278
boolean resumeBlocked = false;
278279

280+
/**
281+
* Whether {@link CpsThreadGroup#isPaused} when loaded from disk.
282+
* @see #loadProgramAsync
283+
* @see #afterStepExecutionsResumed
284+
*/
285+
private transient boolean pausedWhenLoaded;
286+
279287
/** Subdirectory string where we store {@link FlowNode}s */
280288
private String storageDir = null;
281289

@@ -774,17 +782,8 @@ public void onSuccess(Unmarshaller u) {
774782
try {
775783
CpsThreadGroup g = (CpsThreadGroup) u.readObject();
776784
result.set(g);
777-
try {
778-
if (g.isPaused()) {
779-
owner.getListener().getLogger().println("Still paused");
780-
} else {
781-
owner.getListener().getLogger().println("Ready to run at " + new Date());
782-
// In case we last paused execution due to Jenkins.isQuietingDown, make sure we do something after we restart.
783-
g.scheduleRun();
784-
}
785-
} catch (IOException x) {
786-
LOGGER.log(Level.WARNING, null, x);
787-
}
785+
pausedWhenLoaded = g.isPaused();
786+
g.pause();
788787
} catch (Throwable t) {
789788
onFailure(t);
790789
} finally {
@@ -870,6 +869,28 @@ void croak(Throwable t) {
870869
}
871870
}
872871

872+
@Override protected void afterStepExecutionsResumed() {
873+
runInCpsVmThread(new FutureCallback<CpsThreadGroup>() {
874+
@Override public void onSuccess(CpsThreadGroup g) {
875+
try {
876+
if (pausedWhenLoaded) {
877+
owner.getListener().getLogger().println("Still paused");
878+
} else {
879+
owner.getListener().getLogger().println("Ready to run at " + new Date());
880+
// In case we last paused execution due to Jenkins.isQuietingDown, make sure we do something after we restart.
881+
g.unpause();
882+
g.saveProgramIfPossible(false); // ensure pausedWhenLoaded=false is persisted
883+
}
884+
} catch (IOException x) {
885+
LOGGER.log(Level.WARNING, null, x);
886+
}
887+
}
888+
@Override public void onFailure(Throwable t) {
889+
LOGGER.log(Level.WARNING, "could not resume " + this, t);
890+
}
891+
});
892+
}
893+
873894
/**
874895
* Where we store {@link CpsThreadGroup}.
875896
*/
@@ -1298,6 +1319,7 @@ private static void cleanUpLoader(ClassLoader loader, Set<ClassLoader> encounter
12981319
if (encounteredClasses.add(clazz)) {
12991320
LOGGER.log(Level.FINER, "found {0}", clazz.getName());
13001321
Introspector.flushFromCaches(clazz);
1322+
cleanUpClassInfoCache(clazz);
13011323
cleanUpGlobalClassSet(clazz);
13021324
cleanUpClassHelperCache(clazz);
13031325
cleanUpObjectStreamClassCaches(clazz);
@@ -1358,6 +1380,44 @@ private static void cleanUpGlobalClassValue(@NonNull ClassLoader loader) throws
13581380
}
13591381
}
13601382

1383+
private static void cleanUpClassInfoCache(Class<?> clazz) {
1384+
JavaSpecificationVersion current = JavaSpecificationVersion.forCurrentJVM();
1385+
if (current.isNewerThan(new JavaSpecificationVersion("1.8"))
1386+
&& current.isOlderThan(new JavaSpecificationVersion("16"))) {
1387+
try {
1388+
// TODO Work around JDK-8231454.
1389+
Class<?> classInfoC = Class.forName("com.sun.beans.introspect.ClassInfo");
1390+
Field cacheF = classInfoC.getDeclaredField("CACHE");
1391+
try {
1392+
cacheF.setAccessible(true);
1393+
} catch (RuntimeException e) { // TODO Java 9+ InaccessibleObjectException
1394+
/*
1395+
* Not running with "--add-opens java.desktop/com.sun.beans.introspect=ALL-UNNAMED".
1396+
* Until core adds this to its --add-opens configuration, and until that core
1397+
* change is widely adopted, avoid unnecessary log spam and return early.
1398+
*/
1399+
if (LOGGER.isLoggable(Level.FINER)) {
1400+
LOGGER.log(Level.FINER, "Failed to clean up " + clazz.getName() + " from ClassInfo#CACHE. A metaspace leak may have occurred.", e);
1401+
}
1402+
return;
1403+
}
1404+
Object cache = cacheF.get(null);
1405+
Class<?> cacheC = Class.forName("com.sun.beans.util.Cache");
1406+
if (LOGGER.isLoggable(Level.FINER)) {
1407+
LOGGER.log(Level.FINER, "Cleaning up " + clazz.getName() + " from ClassInfo#CACHE.");
1408+
}
1409+
Method removeM = cacheC.getMethod("remove", Object.class);
1410+
removeM.invoke(cache, clazz);
1411+
} catch (ReflectiveOperationException e) {
1412+
/*
1413+
* Should never happen, but if it does, ensure the failure is isolated to this
1414+
* method and does not prevent other cleanup logic from executing.
1415+
*/
1416+
LOGGER.log(Level.WARNING, "Failed to clean up " + clazz.getName() + " from ClassInfo#CACHE. A metaspace leak may have occurred.", e);
1417+
}
1418+
}
1419+
}
1420+
13611421
private static void cleanUpGlobalClassSet(@NonNull Class<?> clazz) throws Exception {
13621422
Class<?> classInfoC = Class.forName("org.codehaus.groovy.reflection.ClassInfo"); // or just ClassInfo.class, but unclear whether this will always be there
13631423
Field globalClassSetF = classInfoC.getDeclaredField("globalClassSet");
@@ -1405,13 +1465,16 @@ private static void cleanUpObjectStreamClassCaches(@NonNull Class<?> clazz) thro
14051465
for (String cacheFName : new String[] {"localDescs", "reflectors"}) {
14061466
Field cacheF = cachesC.getDeclaredField(cacheFName);
14071467
cacheF.setAccessible(true);
1408-
ConcurrentMap<Reference<Class<?>>, ?> cache = (ConcurrentMap) cacheF.get(null);
1409-
Iterator<? extends Entry<Reference<Class<?>>, ?>> iterator = cache.entrySet().iterator();
1410-
while (iterator.hasNext()) {
1411-
if (iterator.next().getKey().get() == clazz) {
1412-
iterator.remove();
1413-
LOGGER.log(Level.FINER, "cleaning up {0} from ObjectStreamClass.Caches.{1}", new Object[] {clazz.getName(), cacheFName});
1414-
break;
1468+
Object cache = cacheF.get(null);
1469+
if (cache instanceof ConcurrentMap) {
1470+
// Prior to JDK-8277072
1471+
Iterator<? extends Entry<Reference<Class<?>>, ?>> iterator = ((ConcurrentMap) cache).entrySet().iterator();
1472+
while (iterator.hasNext()) {
1473+
if (iterator.next().getKey().get() == clazz) {
1474+
iterator.remove();
1475+
LOGGER.log(Level.FINER, "cleaning up {0} from ObjectStreamClass.Caches.{1}", new Object[]{clazz.getName(), cacheFName});
1476+
break;
1477+
}
14151478
}
14161479
}
14171480
}

src/main/java/org/jenkinsci/plugins/workflow/cps/CpsStepContext.java

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import com.google.common.util.concurrent.SettableFuture;
3131
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
3232
import groovy.lang.Closure;
33-
import hudson.Main;
3433
import hudson.model.Descriptor;
3534
import hudson.model.Result;
3635
import hudson.util.DaemonThreadFactory;
@@ -66,6 +65,7 @@
6665
import java.util.function.Function;
6766
import java.util.logging.Level;
6867
import java.util.logging.Logger;
68+
import java.util.stream.Stream;
6969
import jenkins.model.CauseOfInterruption;
7070
import jenkins.util.ContextResettingExecutorService;
7171
import org.codehaus.groovy.runtime.InvokerInvocationException;
@@ -328,39 +328,33 @@ private void completed(@NonNull Outcome newOutcome) {
328328
whenOutcomeDelivered = new Throwable();
329329
} else {
330330
Throwable failure = newOutcome.getAbnormal();
331-
Level level;
332-
if (Main.isUnitTest) {
333-
if (failure instanceof FlowInterruptedException && ((FlowInterruptedException) failure).getCauses().stream().anyMatch(BodyFailed.class::isInstance)) {
334-
// Very common and generally uninteresting.
335-
level = Level.FINE;
336-
} else {
337-
// Possibly a minor bug.
338-
level = Level.INFO;
339-
}
340-
} else {
341-
// Typically harmless; do not alarm users.
342-
level = Level.FINE;
343-
}
344-
if (LOGGER.isLoggable(level)) {
345-
LOGGER.log(level, "already completed " + this, new IllegalStateException("delivered here"));
331+
Throwable earlierFailure = outcome.getAbnormal();
332+
if (LOGGER.isLoggable(Level.FINE)) {
333+
LOGGER.log(Level.FINE, "already completed " + this, new IllegalStateException("delivered here"));
346334
if (failure != null) {
347-
LOGGER.log(level, "new failure", failure);
335+
LOGGER.log(Level.FINE, "new failure", failure);
348336
} else {
349-
LOGGER.log(level, "new success: {0}", outcome.getNormal());
337+
LOGGER.log(Level.FINE, "new success: {0}", outcome.getNormal());
350338
}
351339
if (whenOutcomeDelivered != null) {
352-
LOGGER.log(level, "previously delivered here", whenOutcomeDelivered);
340+
LOGGER.log(Level.FINE, "previously delivered here", whenOutcomeDelivered);
353341
}
354-
Throwable earlierFailure = outcome.getAbnormal();
355342
if (earlierFailure != null) {
356-
LOGGER.log(level, "earlier failure", earlierFailure);
343+
LOGGER.log(Level.FINE, "earlier failure", earlierFailure);
357344
} else {
358-
LOGGER.log(level, "earlier success: {0}", outcome.getNormal());
345+
LOGGER.log(Level.FINE, "earlier success: {0}", outcome.getNormal());
359346
}
360347
}
348+
if (failure != null && earlierFailure != null && !refersTo(failure, earlierFailure)) {
349+
earlierFailure.addSuppressed(failure);
350+
}
361351
}
362352
}
363353

354+
private static boolean refersTo(Throwable t1, Throwable t2) {
355+
return t1 == t2 || t1.getCause() != null && refersTo(t1.getCause(), t2) || Stream.of(t1.getSuppressed()).anyMatch(t3 -> refersTo(t3, t2));
356+
}
357+
364358
/**
365359
* When this step context has completed execution (successful or otherwise), plan the next action.
366360
*/

src/main/java/org/jenkinsci/plugins/workflow/cps/EnvActionImpl.java

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
package org.jenkinsci.plugins.workflow.cps;
2626

27+
import com.google.common.util.concurrent.Futures;
28+
import com.google.common.util.concurrent.ListenableFuture;
2729
import groovy.lang.GroovyObjectSupport;
2830
import hudson.EnvVars;
2931
import hudson.Extension;
@@ -32,28 +34,30 @@
3234
import hudson.model.TaskListener;
3335
import hudson.util.LogTaskListener;
3436
import java.io.IOException;
35-
import java.io.Serializable;
3637
import java.util.Collection;
3738
import java.util.Collections;
3839
import java.util.Map;
3940
import java.util.TreeMap;
4041
import java.util.logging.Level;
4142
import java.util.logging.Logger;
4243
import edu.umd.cs.findbugs.annotations.NonNull;
44+
import hudson.model.Queue;
4345
import jenkins.model.RunAction2;
4446
import org.jenkinsci.plugins.workflow.flow.FlowCopier;
4547
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
4648
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
4749
import org.jenkinsci.plugins.workflow.graph.FlowNode;
50+
import org.jenkinsci.plugins.workflow.pickles.Pickle;
4851
import org.jenkinsci.plugins.workflow.steps.EnvironmentExpander;
4952
import org.jenkinsci.plugins.workflow.support.actions.EnvironmentAction;
53+
import org.jenkinsci.plugins.workflow.support.pickles.SingleTypedPickleFactory;
5054
import org.kohsuke.accmod.Restricted;
5155
import org.kohsuke.accmod.restrictions.DoNotUse;
5256
import org.kohsuke.stapler.export.Exported;
5357
import org.kohsuke.stapler.export.ExportedBean;
5458

5559
@ExportedBean
56-
public class EnvActionImpl extends GroovyObjectSupport implements EnvironmentAction.IncludingOverrides, Serializable, RunAction2 {
60+
public class EnvActionImpl extends GroovyObjectSupport implements EnvironmentAction.IncludingOverrides, RunAction2 {
5761

5862
private static final Logger LOGGER = Logger.getLogger(EnvActionImpl.class.getName());
5963
private static final long serialVersionUID = 1;
@@ -199,4 +203,30 @@ public Collection<EnvironmentContributor> getEnvironmentContributors() {
199203

200204
}
201205

206+
@Extension public static class EnvActionImplPickleFactory extends SingleTypedPickleFactory<EnvActionImpl> {
207+
@Override
208+
protected Pickle pickle(EnvActionImpl object) {
209+
return new EnvActionImplPickle();
210+
}
211+
}
212+
213+
/**
214+
* Prevents multiple instances of {@link EnvActionImpl} from existing for a single Pipeline after a Jenkins restart
215+
* in case {@code env} is serialized into the program.
216+
*/
217+
private static class EnvActionImplPickle extends Pickle {
218+
@Override
219+
public ListenableFuture<?> rehydrate(FlowExecutionOwner owner) {
220+
try {
221+
Queue.Executable executable = owner.getExecutable();
222+
if (executable instanceof Run) {
223+
return Futures.immediateFuture(EnvActionImpl.forRun((Run)executable));
224+
} else {
225+
return Futures.immediateFailedFuture(new IllegalStateException("Invalid executable: " + executable));
226+
}
227+
} catch (IOException e) {
228+
return Futures.immediateFailedFuture(e);
229+
}
230+
}
231+
}
202232
}

src/test/java/org/jenkinsci/plugins/workflow/cps/CpsFlowExecutionTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep;
6464
import static org.hamcrest.MatcherAssert.assertThat;
6565
import static org.hamcrest.Matchers.containsString;
66+
import static org.hamcrest.Matchers.equalTo;
6667
import static org.hamcrest.Matchers.hasItem;
6768
import static org.junit.Assert.assertEquals;
6869
import static org.junit.Assert.assertFalse;
@@ -527,4 +528,24 @@ public boolean isAllowed(String groovyResourceUrl) {
527528
return groovyResourceUrl.endsWith("/trusted/foo.groovy");
528529
}
529530
}
531+
532+
@Issue("JENKINS-45327")
533+
@Test public void envActionImplPickle() throws Throwable {
534+
sessions.then(r -> {
535+
WorkflowJob p = r.createProject(WorkflowJob.class, "p");
536+
p.setDefinition(new CpsFlowDefinition(
537+
"def e = env\n" +
538+
"semaphore('wait')\n" + // An instance of EnvActionImpl is part of the program's state at this point.
539+
"e.foo = 'bar'\n", true)); // Without EnvActionImplPickle, this throws an NPE in EnvActionImpl.setProperty because owner is null.
540+
WorkflowRun b = p.scheduleBuild2(0).waitForStart();
541+
SemaphoreStep.waitForStart("wait/1", b);
542+
});
543+
sessions.then(r -> {
544+
WorkflowJob p = r.jenkins.getItemByFullName("p", WorkflowJob.class);
545+
WorkflowRun b = p.getLastBuild();
546+
SemaphoreStep.success("wait/1", null);
547+
r.assertBuildStatus(Result.SUCCESS, r.waitForCompletion(b));
548+
assertThat(EnvActionImpl.forRun(b).getEnvironment().get("foo"), equalTo("bar"));
549+
});
550+
}
530551
}

0 commit comments

Comments
 (0)