|
52 | 52 | import hudson.model.Action; |
53 | 53 | import hudson.model.Result; |
54 | 54 | import hudson.util.Iterators; |
| 55 | +import io.jenkins.lib.versionnumber.JavaSpecificationVersion; |
55 | 56 | import jenkins.model.CauseOfInterruption; |
56 | 57 | import jenkins.model.Jenkins; |
57 | 58 | import org.jboss.marshalling.Unmarshaller; |
@@ -276,6 +277,13 @@ public class CpsFlowExecution extends FlowExecution implements BlockableResume { |
276 | 277 |
|
277 | 278 | boolean resumeBlocked = false; |
278 | 279 |
|
| 280 | + /** |
| 281 | + * Whether {@link CpsThreadGroup#isPaused} when loaded from disk. |
| 282 | + * @see #loadProgramAsync |
| 283 | + * @see #afterStepExecutionsResumed |
| 284 | + */ |
| 285 | + private transient boolean pausedWhenLoaded; |
| 286 | + |
279 | 287 | /** Subdirectory string where we store {@link FlowNode}s */ |
280 | 288 | private String storageDir = null; |
281 | 289 |
|
@@ -774,17 +782,8 @@ public void onSuccess(Unmarshaller u) { |
774 | 782 | try { |
775 | 783 | CpsThreadGroup g = (CpsThreadGroup) u.readObject(); |
776 | 784 | 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(); |
788 | 787 | } catch (Throwable t) { |
789 | 788 | onFailure(t); |
790 | 789 | } finally { |
@@ -870,6 +869,28 @@ void croak(Throwable t) { |
870 | 869 | } |
871 | 870 | } |
872 | 871 |
|
| 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 | + |
873 | 894 | /** |
874 | 895 | * Where we store {@link CpsThreadGroup}. |
875 | 896 | */ |
@@ -1298,6 +1319,7 @@ private static void cleanUpLoader(ClassLoader loader, Set<ClassLoader> encounter |
1298 | 1319 | if (encounteredClasses.add(clazz)) { |
1299 | 1320 | LOGGER.log(Level.FINER, "found {0}", clazz.getName()); |
1300 | 1321 | Introspector.flushFromCaches(clazz); |
| 1322 | + cleanUpClassInfoCache(clazz); |
1301 | 1323 | cleanUpGlobalClassSet(clazz); |
1302 | 1324 | cleanUpClassHelperCache(clazz); |
1303 | 1325 | cleanUpObjectStreamClassCaches(clazz); |
@@ -1358,6 +1380,44 @@ private static void cleanUpGlobalClassValue(@NonNull ClassLoader loader) throws |
1358 | 1380 | } |
1359 | 1381 | } |
1360 | 1382 |
|
| 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 | + |
1361 | 1421 | private static void cleanUpGlobalClassSet(@NonNull Class<?> clazz) throws Exception { |
1362 | 1422 | Class<?> classInfoC = Class.forName("org.codehaus.groovy.reflection.ClassInfo"); // or just ClassInfo.class, but unclear whether this will always be there |
1363 | 1423 | Field globalClassSetF = classInfoC.getDeclaredField("globalClassSet"); |
@@ -1405,13 +1465,16 @@ private static void cleanUpObjectStreamClassCaches(@NonNull Class<?> clazz) thro |
1405 | 1465 | for (String cacheFName : new String[] {"localDescs", "reflectors"}) { |
1406 | 1466 | Field cacheF = cachesC.getDeclaredField(cacheFName); |
1407 | 1467 | 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 | + } |
1415 | 1478 | } |
1416 | 1479 | } |
1417 | 1480 | } |
|
0 commit comments