You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by ti...@apache.org on 2022/02/23 16:33:33 UTC

[maven-surefire] branch it-946 updated (7eaac29 -> d19f79f)

This is an automated email from the ASF dual-hosted git repository.

tibordigana pushed a change to branch it-946
in repository https://gitbox.apache.org/repos/asf/maven-surefire.git.


 discard 7eaac29  JIRA 946
     new d19f79f  [SUREFIRE-2023] The integration test Surefire946KillMainProcessInReusableForkIT hanged and timed out because SIGTERM happened before the first test has started. The plugin should be able to terminate itself whenever after SIGTERM.

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (7eaac29)
            \
             N -- N -- N   refs/heads/it-946 (d19f79f)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../org/apache/maven/plugin/surefire/AbstractSurefireMojo.java   | 2 +-
 .../apache/maven/plugin/surefire/InPluginVMSurefireStarter.java  | 9 +++++++--
 2 files changed, 8 insertions(+), 3 deletions(-)

[maven-surefire] 01/01: [SUREFIRE-2023] The integration test Surefire946KillMainProcessInReusableForkIT hanged and timed out because SIGTERM happened before the first test has started. The plugin should be able to terminate itself whenever after SIGTERM.

Posted by ti...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

tibordigana pushed a commit to branch it-946
in repository https://gitbox.apache.org/repos/asf/maven-surefire.git

commit d19f79ffb422011b225f2a44120c41d9a4a547ce
Author: tibor.digana <ti...@apache.org>
AuthorDate: Tue Feb 22 23:49:06 2022 +0100

    [SUREFIRE-2023] The integration test Surefire946KillMainProcessInReusableForkIT hanged and timed out because SIGTERM happened before the first test has started. The plugin should be able to terminate itself whenever after SIGTERM.
---
 .../plugin/surefire/AbstractSurefireMojo.java      |  50 +++--
 .../plugin/surefire/InPluginVMSurefireStarter.java |   9 +-
 .../plugin/surefire/booterclient/ForkStarter.java  | 201 +++++++++------------
 .../plugin/surefire/booterclient/Platform.java     |  17 ++
 .../surefire/booterclient/output/ForkClient.java   |  45 ++---
 .../booterclient/output/ForkClientTest.java        |  11 +-
 .../api/util/internal/ConcurrencyUtils.java        |  13 +-
 .../api/util/internal/ConcurrencyUtilsTest.java    |  41 +++--
 ...Surefire946KillMainProcessInReusableForkIT.java |   2 +-
 .../maven/surefire/common/junit4/Notifier.java     |   8 +-
 .../maven/surefire/testng/TestNGExecutor.java      |  18 +-
 11 files changed, 203 insertions(+), 212 deletions(-)

diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java
index a2d76c8..89f13aa 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java
@@ -154,6 +154,8 @@ import static org.apache.maven.surefire.shared.utils.StringUtils.isEmpty;
 import static org.apache.maven.surefire.shared.utils.StringUtils.isNotBlank;
 import static org.apache.maven.surefire.shared.utils.StringUtils.isNotEmpty;
 import static org.apache.maven.surefire.shared.utils.StringUtils.split;
+import static org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils.addShutDownHook;
+import static org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils.removeShutdownHook;
 
 /**
  * Abstract base class for running tests using Surefire.
@@ -922,29 +924,39 @@ public abstract class AbstractSurefireMojo
         // Stuff that should have been final
         setupStuff();
         Platform platform = PLATFORM.withJdkExecAttributesForTests( getEffectiveJvm() );
-
-        if ( verifyParameters() && !hasExecutedBefore() )
+        Thread shutdownThread = new Thread( platform::setShutdownState );
+        addShutDownHook( shutdownThread );
+        try
         {
-            DefaultScanResult scan = scanForTestClasses();
-            if ( !hasSuiteXmlFiles() && scan.isEmpty() )
+            if ( verifyParameters() && !hasExecutedBefore() )
             {
-                switch ( getEffectiveFailIfNoTests() )
+                DefaultScanResult scan = scanForTestClasses();
+                if ( !hasSuiteXmlFiles() && scan.isEmpty() )
                 {
-                    case COULD_NOT_RUN_DEFAULT_TESTS:
-                        throw new MojoFailureException(
-                            "No tests were executed!  (Set -DfailIfNoTests=false to ignore this error.)" );
-                    case COULD_NOT_RUN_SPECIFIED_TESTS:
-                        throw new MojoFailureException( "No tests matching pattern \""
-                            + getSpecificTests().toString()
-                            + "\" were executed! (Set "
-                            + "-D" + getPluginName() + ".failIfNoSpecifiedTests=false to ignore this error.)" );
-                    default:
-                        handleSummary( noTestsRun(), null );
-                        return;
+                    switch ( getEffectiveFailIfNoTests() )
+                    {
+                        case COULD_NOT_RUN_DEFAULT_TESTS:
+                            throw new MojoFailureException(
+                                "No tests were executed!  (Set -DfailIfNoTests=false to ignore this error.)" );
+                        case COULD_NOT_RUN_SPECIFIED_TESTS:
+                            throw new MojoFailureException( "No tests matching pattern \""
+                                + getSpecificTests().toString()
+                                + "\" were executed! (Set "
+                                + "-D" + getPluginName()
+                                + ".failIfNoSpecifiedTests=false to ignore this error.)" );
+                        default:
+                            handleSummary( noTestsRun(), null );
+                            return;
+                    }
                 }
+                logReportsDirectory();
+                executeAfterPreconditionsChecked( scan, platform );
             }
-            logReportsDirectory();
-            executeAfterPreconditionsChecked( scan, platform );
+        }
+        finally
+        {
+            platform.clearShutdownState();
+            removeShutdownHook( shutdownThread );
         }
     }
 
@@ -2446,7 +2458,7 @@ public abstract class AbstractSurefireMojo
         StartupReportConfiguration startupReportConfiguration = getStartupReportConfiguration( configChecksum, false );
         ProviderConfiguration providerConfiguration = createProviderConfiguration( runOrderParameters );
         return new InPluginVMSurefireStarter( startupConfiguration, providerConfiguration, startupReportConfiguration,
-                                              getConsoleLogger() );
+                                              getConsoleLogger(), platform );
     }
 
     // todo this is in separate method and can be better tested than whole method createForkConfiguration()
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/InPluginVMSurefireStarter.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/InPluginVMSurefireStarter.java
index 2e54e0d..78bc920 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/InPluginVMSurefireStarter.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/InPluginVMSurefireStarter.java
@@ -19,6 +19,7 @@ package org.apache.maven.plugin.surefire;
  * under the License.
  */
 
+import org.apache.maven.plugin.surefire.booterclient.Platform;
 import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
 import org.apache.maven.surefire.booter.ClasspathConfiguration;
 import org.apache.maven.surefire.booter.ProviderConfiguration;
@@ -52,16 +53,18 @@ public class InPluginVMSurefireStarter
     private final StartupReportConfiguration startupReportConfig;
     private final ProviderConfiguration providerConfig;
     private final ConsoleLogger consoleLogger;
+    private final Platform platform;
 
     public InPluginVMSurefireStarter( @Nonnull StartupConfiguration startupConfig,
                                       @Nonnull ProviderConfiguration providerConfig,
                                       @Nonnull StartupReportConfiguration startupReportConfig,
-                                      @Nonnull ConsoleLogger consoleLogger )
+                                      @Nonnull ConsoleLogger consoleLogger, @Nonnull Platform platform )
     {
         this.startupConfig = startupConfig;
         this.startupReportConfig = startupReportConfig;
         this.providerConfig = providerConfig;
         this.consoleLogger = consoleLogger;
+        this.platform = platform;
     }
 
     public RunResult runSuitesInProcess( @Nonnull DefaultScanResult scanResult )
@@ -84,7 +87,9 @@ public class InPluginVMSurefireStarter
 
         try
         {
-            return invokeProvider( null, testClassLoader, factory, providerConfig, false, startupConfig, true );
+            return platform.isShutdown()
+                ? new RunResult( 0, 0, 0, 0 )
+                : invokeProvider( null, testClassLoader, factory, providerConfig, false, startupConfig, true );
         }
         catch ( InvocationTargetException e )
         {
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java
index 2a2c820..17b0e54 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java
@@ -61,7 +61,6 @@ import javax.annotation.Nonnull;
 import java.io.Closeable;
 import java.io.File;
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Map;
 import java.util.Queue;
@@ -88,6 +87,8 @@ import static java.util.UUID.randomUUID;
 import static java.util.concurrent.Executors.newScheduledThreadPool;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.StreamSupport.stream;
 import static org.apache.maven.plugin.surefire.AbstractSurefireMojo.createCopyAndReplaceForkNumPlaceholder;
 import static org.apache.maven.plugin.surefire.SurefireHelper.DUMP_FILE_PREFIX;
 import static org.apache.maven.plugin.surefire.SurefireHelper.replaceForkThreadsInPath;
@@ -101,7 +102,7 @@ import static org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils.remov
 import static org.apache.maven.surefire.api.suite.RunResult.SUCCESS;
 import static org.apache.maven.surefire.api.suite.RunResult.failure;
 import static org.apache.maven.surefire.api.suite.RunResult.timeout;
-import static org.apache.maven.surefire.api.util.internal.ConcurrencyUtils.countDownToZero;
+import static org.apache.maven.surefire.api.util.internal.ConcurrencyUtils.runIfZeroCountDown;
 import static org.apache.maven.surefire.api.util.internal.DaemonThreadFactory.newDaemonThread;
 import static org.apache.maven.surefire.api.util.internal.DaemonThreadFactory.newDaemonThreadFactory;
 import static org.apache.maven.surefire.api.util.internal.StringUtils.NL;
@@ -288,15 +289,15 @@ public class ForkStarter
         }
     }
 
-    private RunResult run( SurefireProperties effectiveSystemProperties, Map<String, String> providerProperties )
+    private RunResult run( SurefireProperties effectiveSystemProps, Map<String, String> providerProperties )
             throws SurefireBooterForkException
     {
         TestLessInputStreamBuilder builder = new TestLessInputStreamBuilder();
-        PropertiesWrapper props = new PropertiesWrapper( providerProperties );
         TestLessInputStream stream = builder.build();
         Thread shutdown = createImmediateShutdownHookThread( builder, providerConfiguration.getShutdown() );
         ScheduledFuture<?> ping = triggerPingTimerForShutdown( builder );
         int forkNumber = drawNumber();
+        PropertiesWrapper props = new PropertiesWrapper( providerProperties );
         try
         {
             addShutDownHook( shutdown );
@@ -304,8 +305,10 @@ public class ForkStarter
                     new DefaultReporterFactory( startupReportConfiguration, log, forkNumber );
             defaultReporterFactories.add( forkedReporterFactory );
             ForkClient forkClient = new ForkClient( forkedReporterFactory, stream, forkNumber );
-            return fork( null, props, forkClient, effectiveSystemProperties, forkNumber, stream,
-                    forkConfiguration.getForkNodeFactory(), false );
+            ForkNodeFactory node = forkConfiguration.getForkNodeFactory();
+            return forkConfiguration.getPluginPlatform().isShutdown()
+                ? new RunResult( 0, 0, 0, 0 )
+                : fork( null, props, forkClient, effectiveSystemProps, forkNumber, stream, node, false );
         }
         finally
         {
@@ -336,81 +339,68 @@ public class ForkStarter
     }
 
     @SuppressWarnings( "checkstyle:magicnumber" )
-    private RunResult runSuitesForkOnceMultiple( final SurefireProperties effectiveSystemProperties, int forkCount )
+    private RunResult runSuitesForkOnceMultiple( SurefireProperties effectiveSystemProps, int forkCount )
         throws SurefireBooterForkException
     {
-        ThreadPoolExecutor executorService = new ThreadPoolExecutor( forkCount, forkCount, 60, SECONDS,
-                                                                  new ArrayBlockingQueue<Runnable>( forkCount ) );
-        executorService.setThreadFactory( FORKED_JVM_DAEMON_THREAD_FACTORY );
+        ThreadPoolExecutor executor
+            = new ThreadPoolExecutor( forkCount, forkCount, 60L, SECONDS, new ArrayBlockingQueue<>( forkCount ) );
+        executor.setThreadFactory( FORKED_JVM_DAEMON_THREAD_FACTORY );
 
-        final Queue<String> tests = new ConcurrentLinkedQueue<>();
+        Queue<String> tests = new ConcurrentLinkedQueue<>();
 
         for ( Class<?> clazz : getSuitesIterator() )
         {
             tests.add( clazz.getName() );
         }
 
-        final Queue<TestProvidingInputStream> testStreams = new ConcurrentLinkedQueue<>();
+        Queue<TestProvidingInputStream> forks = new ConcurrentLinkedQueue<>();
 
         for ( int forkNum = 0, total = min( forkCount, tests.size() ); forkNum < total; forkNum++ )
         {
-            testStreams.add( new TestProvidingInputStream( tests ) );
+            forks.add( new TestProvidingInputStream( tests ) );
         }
 
-        ScheduledFuture<?> ping = triggerPingTimerForShutdown( testStreams );
-        Thread shutdown = createShutdownHookThread( testStreams, providerConfiguration.getShutdown() );
+        Thread shutdown = createShutdownHookThread( forks, providerConfiguration.getShutdown() );
+        addShutDownHook( shutdown );
+        ScheduledFuture<?> ping = triggerPingTimerForShutdown( forks );
 
         try
         {
-            addShutDownHook( shutdown );
             int failFastCount = providerConfiguration.getSkipAfterFailureCount();
-            final AtomicInteger notifyStreamsToSkipTestsJustNow = new AtomicInteger( failFastCount );
-            final Collection<Future<RunResult>> results = new ArrayList<>( forkCount );
-            for ( final TestProvidingInputStream testProvidingInputStream : testStreams )
-            {
-                Callable<RunResult> pf = new Callable<RunResult>()
+            AtomicInteger notifyForksToSkipTestsNow = new AtomicInteger( failFastCount );
+            Collection<Future<RunResult>> results =
+                forks.stream()
+                .filter( fork -> !forkConfiguration.getPluginPlatform().isShutdown() )
+                .map( fork -> (Callable<RunResult>) () ->
                 {
-                    @Override
-                    public RunResult call()
-                        throws Exception
+                    int forkNumber = drawNumber();
+                    DefaultReporterFactory reporter =
+                        new DefaultReporterFactory( startupReportConfiguration, log, forkNumber );
+                    defaultReporterFactories.add( reporter );
+                    ForkClient client = new ForkClient( reporter, fork, forkNumber );
+                    client.setStopOnNextTestListener( () ->
+                        runIfZeroCountDown( () -> notifyStreamsToSkipTests( forks ), notifyForksToSkipTestsNow ) );
+                    Map<String, String> providerProperties = providerConfiguration.getProviderProperties();
+                    PropertiesWrapper keyValues = new PropertiesWrapper( providerProperties );
+                    ForkNodeFactory node = forkConfiguration.getForkNodeFactory();
+                    try
                     {
-                        int forkNumber = drawNumber();
-                        DefaultReporterFactory reporter =
-                                new DefaultReporterFactory( startupReportConfiguration, log, forkNumber );
-                        defaultReporterFactories.add( reporter );
-                        ForkClient forkClient = new ForkClient( reporter, testProvidingInputStream, forkNumber )
-                        {
-                            @Override
-                            protected void stopOnNextTest()
-                            {
-                                if ( countDownToZero( notifyStreamsToSkipTestsJustNow ) )
-                                {
-                                    notifyStreamsToSkipTests( testStreams );
-                                }
-                            }
-                        };
-                        Map<String, String> providerProperties = providerConfiguration.getProviderProperties();
-                        try
-                        {
-                            return fork( null, new PropertiesWrapper( providerProperties ), forkClient,
-                                    effectiveSystemProperties, forkNumber, testProvidingInputStream,
-                                    forkConfiguration.getForkNodeFactory(), true );
-                        }
-                        finally
-                        {
-                            returnNumber( forkNumber );
-                        }
+                        return fork( null, keyValues, client, effectiveSystemProps, forkNumber, fork, node, true );
                     }
-                };
-                results.add( executorService.submit( pf ) );
-            }
-            return awaitResultsDone( results, executorService );
+                    finally
+                    {
+                        returnNumber( forkNumber );
+                    }
+                } ).map( executor::submit )
+                    .collect( toList() );
+
+            return awaitResultsDone( results, executor );
         }
         finally
         {
             removeShutdownHook( shutdown );
             ping.cancel( true );
-            closeExecutor( executorService );
+            closeExecutor( executor );
         }
     }
 
@@ -423,68 +413,57 @@ public class ForkStarter
     }
 
     @SuppressWarnings( "checkstyle:magicnumber" )
-    private RunResult runSuitesForkPerTestSet( final SurefireProperties effectiveSystemProperties, int forkCount )
+    private RunResult runSuitesForkPerTestSet( SurefireProperties effectiveSystemProps, int forkCount )
         throws SurefireBooterForkException
     {
-        ArrayList<Future<RunResult>> results = new ArrayList<>( 500 );
-        ThreadPoolExecutor executorService =
-            new ThreadPoolExecutor( forkCount, forkCount, 60, SECONDS, new LinkedBlockingQueue<Runnable>() );
-        executorService.setThreadFactory( FORKED_JVM_DAEMON_THREAD_FACTORY );
-        final TestLessInputStreamBuilder builder = new TestLessInputStreamBuilder();
-        ScheduledFuture<?> ping = triggerPingTimerForShutdown( builder );
+        TestLessInputStreamBuilder builder = new TestLessInputStreamBuilder();
         Thread shutdown = createCachableShutdownHookThread( builder, providerConfiguration.getShutdown() );
+        ThreadPoolExecutor executor =
+            new ThreadPoolExecutor( forkCount, forkCount, 60, SECONDS, new LinkedBlockingQueue<>() );
+        executor.setThreadFactory( FORKED_JVM_DAEMON_THREAD_FACTORY );
+        ScheduledFuture<?> ping = triggerPingTimerForShutdown( builder );
         try
         {
             addShutDownHook( shutdown );
             int failFastCount = providerConfiguration.getSkipAfterFailureCount();
-            final AtomicInteger notifyStreamsToSkipTestsJustNow = new AtomicInteger( failFastCount );
-            for ( final Object testSet : getSuitesIterator() )
+            AtomicInteger notifyForksToSkipTestsNow = new AtomicInteger( failFastCount );
+            Collection<Future<RunResult>> results =
+                stream( ( (Iterable<?>) getSuitesIterator() ).spliterator(), false )
+                    .filter( fork -> !forkConfiguration.getPluginPlatform().isShutdown() )
+                    .map( testSet -> (Callable<RunResult>) () ->
             {
-                Callable<RunResult> pf = new Callable<RunResult>()
+                int forkNumber = drawNumber();
+                DefaultReporterFactory forkedReporterFactory =
+                    new DefaultReporterFactory( startupReportConfiguration, log, forkNumber );
+                defaultReporterFactories.add( forkedReporterFactory );
+                TestLessInputStream stream = builder.build();
+                ForkClient forkClient = new ForkClient( forkedReporterFactory, stream, forkNumber );
+                NotifiableTestStream notifiable = builder.getCachableCommands();
+                forkClient.setStopOnNextTestListener( () ->
+                    runIfZeroCountDown( notifiable::skipSinceNextTest, notifyForksToSkipTestsNow ) );
+                Map<String, String> providerProperties = providerConfiguration.getProviderProperties();
+                PropertiesWrapper keyValues = new PropertiesWrapper( providerProperties );
+                ForkNodeFactory node = forkConfiguration.getForkNodeFactory();
+                try
                 {
-                    @Override
-                    public RunResult call()
-                        throws Exception
-                    {
-                        int forkNumber = drawNumber();
-                        DefaultReporterFactory forkedReporterFactory =
-                            new DefaultReporterFactory( startupReportConfiguration, log, forkNumber );
-                        defaultReporterFactories.add( forkedReporterFactory );
-                        TestLessInputStream stream = builder.build();
-                        ForkClient forkClient = new ForkClient( forkedReporterFactory, stream, forkNumber )
-                        {
-                            @Override
-                            protected void stopOnNextTest()
-                            {
-                                if ( countDownToZero( notifyStreamsToSkipTestsJustNow ) )
-                                {
-                                    builder.getCachableCommands().skipSinceNextTest();
-                                }
-                            }
-                        };
-                        try
-                        {
-                            return fork( testSet,
-                                         new PropertiesWrapper( providerConfiguration.getProviderProperties() ),
-                                         forkClient, effectiveSystemProperties, forkNumber, stream,
-                                         forkConfiguration.getForkNodeFactory(), false );
-                        }
-                        finally
-                        {
-                            returnNumber( forkNumber );
-                            builder.removeStream( stream );
-                        }
-                    }
-                };
-                results.add( executorService.submit( pf ) );
-            }
-            return awaitResultsDone( results, executorService );
+                    return fork( testSet, keyValues, forkClient, effectiveSystemProps, forkNumber, stream, node,
+                        false );
+                }
+                finally
+                {
+                    returnNumber( forkNumber );
+                    builder.removeStream( stream );
+                }
+            } ).map( executor::submit )
+                .collect( toList() );
+
+            return awaitResultsDone( results, executor );
         }
         finally
         {
             removeShutdownHook( shutdown );
             ping.cancel( true );
-            closeExecutor( executorService );
+            closeExecutor( executor );
         }
     }
 
@@ -662,7 +641,10 @@ public class ForkStarter
             log.error( "Closing the streams after (InterruptedException) '" + e.getLocalizedMessage() + "'" );
             // maybe implement it in the Future.cancel() of the extension or similar
             forkChannel.disable();
-            err.disable();
+            if ( err != null )
+            {
+                err.disable();
+            }
         }
         catch ( Exception e )
         {
@@ -678,16 +660,7 @@ public class ForkStarter
             log.debug( "Closing the fork " + forkNumber + " after "
                 + ( forkClient.isSaidGoodBye() ? "saying GoodBye." : "not saying Good Bye." ) );
             currentForkClients.remove( forkClient );
-            try
-            {
-                Closeable c = forkClient.isSaidGoodBye() ? closer : commandReader;
-                c.close();
-            }
-            catch ( IOException e )
-            {
-                InPluginProcessDumpSingleton.getSingleton()
-                    .dumpException( e, e.getLocalizedMessage(), dumpLogDir, forkNumber );
-            }
+            closer.close();
 
             if ( runResult == null )
             {
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/Platform.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/Platform.java
index 97a3121..852b274 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/Platform.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/Platform.java
@@ -40,6 +40,8 @@ public final class Platform
 
     private final JdkAttributes jdk;
 
+    private volatile boolean shutdown;
+
     public Platform()
     {
         // the job may take 50 or 80 ms
@@ -53,6 +55,21 @@ public final class Platform
         this.jdk = jdk;
     }
 
+    public boolean isShutdown()
+    {
+        return shutdown;
+    }
+
+    public void setShutdownState()
+    {
+        this.shutdown = true;
+    }
+
+    public void clearShutdownState()
+    {
+        this.shutdown = false;
+    }
+
     public Long getPluginPid()
     {
         try
diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkClient.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkClient.java
index c8eb672..37e50f5 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkClient.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/output/ForkClient.java
@@ -56,7 +56,7 @@ import static org.apache.maven.surefire.api.report.CategorizedReportEntry.report
  *
  * @author Kristian Rosenvold
  */
-public class ForkClient
+public final class ForkClient
     implements EventHandler<Event>
 {
     private static final long START_TIME_ZERO = 0L;
@@ -89,7 +89,8 @@ public class ForkClient
 
     private volatile StackTraceWriter errorInFork;
 
-    public ForkClient( DefaultReporterFactory defaultReporterFactory, NotifiableTestStream notifiableTestStream,
+    public ForkClient( DefaultReporterFactory defaultReporterFactory,
+                       NotifiableTestStream notifiableTestStream,
                        int forkNumber )
     {
         this.defaultReporterFactory = defaultReporterFactory;
@@ -110,12 +111,16 @@ public class ForkClient
         notifier.setAcquireNextTestListener( new AcquireNextTestListener() );
         notifier.setConsoleErrorListener( new ErrorListener() );
         notifier.setByeListener( new ByeListener() );
-        notifier.setStopOnNextTestListener( new StopOnNextTestListener() );
         notifier.setConsoleDebugListener( new DebugListener() );
         notifier.setConsoleWarningListener( new WarningListener() );
         notifier.setExitErrorEventListener( new ExitErrorEventListener() );
     }
 
+    public void setStopOnNextTestListener( ForkedProcessEventListener listener )
+    {
+        notifier.setStopOnNextTestListener( listener );
+    }
+
     private final class TestSetStartingListener
             implements ForkedProcessReportEventListener<TestSetReportEntry>
     {
@@ -277,15 +282,6 @@ public class ForkClient
         }
     }
 
-    private final class StopOnNextTestListener implements ForkedProcessEventListener
-    {
-        @Override
-        public void handle()
-        {
-            stopOnNextTest();
-        }
-    }
-
     private final class DebugListener implements ForkedProcessStringEventListener
     {
         @Override
@@ -316,13 +312,6 @@ public class ForkClient
         }
     }
 
-    /**
-     * Overridden by a subclass, see {@link org.apache.maven.plugin.surefire.booterclient.ForkStarter}.
-     */
-    protected void stopOnNextTest()
-    {
-    }
-
     public void kill()
     {
         if ( !saidGoodBye )
@@ -338,7 +327,7 @@ public class ForkClient
      * @param currentTimeMillis    current time in millis seconds
      * @param forkedProcessTimeoutInSeconds timeout in seconds given by MOJO
      */
-    public final void tryToTimeout( long currentTimeMillis, int forkedProcessTimeoutInSeconds )
+    public void tryToTimeout( long currentTimeMillis, int forkedProcessTimeoutInSeconds )
     {
         if ( forkedProcessTimeoutInSeconds > 0 )
         {
@@ -352,13 +341,13 @@ public class ForkClient
         }
     }
 
-    public final DefaultReporterFactory getDefaultReporterFactory()
+    public DefaultReporterFactory getDefaultReporterFactory()
     {
         return defaultReporterFactory;
     }
 
     @Override
-    public final void handleEvent( @Nonnull Event event )
+    public void handleEvent( @Nonnull Event event )
     {
         notifier.notifyEvent( event );
     }
@@ -373,7 +362,7 @@ public class ForkClient
         }
     }
 
-    public final boolean hadTimeout()
+    public boolean hadTimeout()
     {
         return testSetStartedAt.get() == START_TIME_NEGATIVE_TIMEOUT;
     }
@@ -409,7 +398,7 @@ public class ForkClient
                 .writeTestOutput( new TestOutputReportEntry( output, isStdout, newLine, /*todo*/ null, null ) );
     }
 
-    public final Map<String, String> getTestVmSystemProperties()
+    public Map<String, String> getTestVmSystemProperties()
     {
         return unmodifiableMap( testVmSystemProperties );
     }
@@ -420,7 +409,7 @@ public class ForkClient
      *
      * @return A mock provider reporter
      */
-    public final RunListener getReporter()
+    public RunListener getReporter()
     {
         return getTestSetReporter();
     }
@@ -440,17 +429,17 @@ public class ForkClient
         // no op
     }
 
-    public final boolean isSaidGoodBye()
+    public boolean isSaidGoodBye()
     {
         return saidGoodBye;
     }
 
-    public final StackTraceWriter getErrorInFork()
+    public StackTraceWriter getErrorInFork()
     {
         return errorInFork;
     }
 
-    public final boolean isErrorInFork()
+    public boolean isErrorInFork()
     {
         return errorInFork != null;
     }
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/output/ForkClientTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/output/ForkClientTest.java
index 7822300..c440114 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/output/ForkClientTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/output/ForkClientTest.java
@@ -438,15 +438,8 @@ public class ForkClientTest
                 .thenReturn( new File( target, "surefire-reports" ) );
         NotifiableTestStream notifiableTestStream = mock( NotifiableTestStream.class );
         final boolean[] verified = {false};
-        ForkClient client = new ForkClient( factory, notifiableTestStream, 0 )
-        {
-            @Override
-            protected void stopOnNextTest()
-            {
-                super.stopOnNextTest();
-                verified[0] = true;
-            }
-        };
+        ForkClient client = new ForkClient( factory, notifiableTestStream, 0 );
+        client.setStopOnNextTestListener( () -> verified[0] = true );
         client.handleEvent( new ControlStopOnNextTestEvent() );
         verifyZeroInteractions( notifiableTestStream );
         verifyZeroInteractions( factory );
diff --git a/surefire-api/src/main/java/org/apache/maven/surefire/api/util/internal/ConcurrencyUtils.java b/surefire-api/src/main/java/org/apache/maven/surefire/api/util/internal/ConcurrencyUtils.java
index a43d8f3..fd2c189 100644
--- a/surefire-api/src/main/java/org/apache/maven/surefire/api/util/internal/ConcurrencyUtils.java
+++ b/surefire-api/src/main/java/org/apache/maven/surefire/api/util/internal/ConcurrencyUtils.java
@@ -38,11 +38,11 @@ public final class ConcurrencyUtils
      * Decreases {@code counter} to zero, or does not change the counter if negative.
      * This method pretends been atomic. Only one thread can succeed setting the counter to zero.
      *
+     * @param runner run if this Thread has concurrently decremented the counter down to zero
      * @param counter atomic counter
-     * @return {@code true} if this Thread modified concurrent counter from any positive number down to zero.
      */
     @SuppressWarnings( "checkstyle:emptyforiteratorpad" )
-    public static boolean countDownToZero( AtomicInteger counter )
+    public static void runIfZeroCountDown( Runnable runner, AtomicInteger counter )
     {
         for (;;)
         {
@@ -52,12 +52,17 @@ public final class ConcurrencyUtils
                 int newCounter = c - 1;
                 if ( counter.compareAndSet( c, newCounter ) )
                 {
-                    return newCounter == 0;
+                    boolean isZero = newCounter == 0;
+                    if ( isZero )
+                    {
+                        runner.run();
+                    }
+                    break;
                 }
             }
             else
             {
-                return false;
+                break;
             }
         }
     }
diff --git a/surefire-api/src/test/java/org/apache/maven/surefire/api/util/internal/ConcurrencyUtilsTest.java b/surefire-api/src/test/java/org/apache/maven/surefire/api/util/internal/ConcurrencyUtilsTest.java
index a2181c1..10c03e3 100644
--- a/surefire-api/src/test/java/org/apache/maven/surefire/api/util/internal/ConcurrencyUtilsTest.java
+++ b/surefire-api/src/test/java/org/apache/maven/surefire/api/util/internal/ConcurrencyUtilsTest.java
@@ -21,12 +21,12 @@ package org.apache.maven.surefire.api.util.internal;
 
 import org.junit.Test;
 
-import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.FutureTask;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
-import static org.apache.maven.surefire.api.util.internal.ConcurrencyUtils.countDownToZero;
+import static org.apache.maven.surefire.api.util.internal.ConcurrencyUtils.runIfZeroCountDown;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
 import static org.junit.Assert.assertTrue;
@@ -45,7 +45,9 @@ public class ConcurrencyUtilsTest
     public void countDownShouldBeUnchangedAsZeroNegativeTest()
     {
         AtomicInteger atomicCounter = new AtomicInteger( 0 );
-        assertFalse( countDownToZero( atomicCounter ) );
+        AtomicBoolean runner = new AtomicBoolean();
+        runIfZeroCountDown( () -> runner.set( true ), atomicCounter );
+        assertFalse( runner.get() );
         assertThat( atomicCounter.get(), is( 0 ) );
     }
 
@@ -53,7 +55,9 @@ public class ConcurrencyUtilsTest
     public void countDownShouldBeUnchangedAsNegativeNegativeTest()
     {
         AtomicInteger atomicCounter = new AtomicInteger( -1 );
-        assertFalse( countDownToZero( atomicCounter ) );
+        AtomicBoolean runner = new AtomicBoolean();
+        runIfZeroCountDown( () -> runner.set( true ), atomicCounter );
+        assertFalse( runner.get() );
         assertThat( atomicCounter.get(), is( -1 ) );
     }
 
@@ -61,7 +65,9 @@ public class ConcurrencyUtilsTest
     public void countDownShouldBeDecreasedByOneThreadModification()
     {
         AtomicInteger atomicCounter = new AtomicInteger( 10 );
-        assertFalse( countDownToZero( atomicCounter ) );
+        AtomicBoolean runner = new AtomicBoolean();
+        runIfZeroCountDown( () -> runner.set( true ), atomicCounter );
+        assertFalse( runner.get() );
         assertThat( atomicCounter.get(), is( 9 ) );
     }
 
@@ -69,7 +75,9 @@ public class ConcurrencyUtilsTest
     public void countDownToZeroShouldBeDecreasedByOneThreadModification()
     {
         AtomicInteger atomicCounter = new AtomicInteger( 1 );
-        assertTrue( countDownToZero( atomicCounter ) );
+        AtomicBoolean runner = new AtomicBoolean();
+        runIfZeroCountDown( () -> runner.set( true ), atomicCounter );
+        assertTrue( runner.get() );
         assertThat( atomicCounter.get(), is( 0 ) );
     }
 
@@ -77,27 +85,28 @@ public class ConcurrencyUtilsTest
     public void countDownShouldBeDecreasedByTwoThreadsModification()
         throws ExecutionException, InterruptedException
     {
-        final AtomicInteger atomicCounter = new AtomicInteger( 3 );
+        AtomicInteger atomicCounter = new AtomicInteger( 3 );
 
-        FutureTask<Boolean> task = new FutureTask<Boolean>( new Callable<Boolean>()
+        FutureTask<Boolean> task = new FutureTask<>( () ->
         {
-            @Override
-            public Boolean call()
-                throws Exception
-            {
-                return countDownToZero( atomicCounter );
-            }
+            AtomicBoolean runner = new AtomicBoolean();
+            runIfZeroCountDown( () -> runner.set( true ), atomicCounter );
+            return runner.get();
         } );
         Thread t = new Thread( task );
         t.start();
 
-        assertFalse( countDownToZero( atomicCounter ) );
+        AtomicBoolean runner = new AtomicBoolean();
+        runIfZeroCountDown( () -> runner.set( true ), atomicCounter );
+        assertFalse( runner.get() );
 
         assertFalse( task.get() );
 
         assertThat( atomicCounter.get(), is( 1 ) );
 
-        assertTrue( countDownToZero( atomicCounter ) );
+        runner.set( false );
+        runIfZeroCountDown( () -> runner.set( true ), atomicCounter );
+        assertTrue( runner.get() );
 
         assertThat( atomicCounter.get(), is( 0 ) );
     }
diff --git a/surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire946KillMainProcessInReusableForkIT.java b/surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire946KillMainProcessInReusableForkIT.java
index 730786d..cdd3236 100644
--- a/surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire946KillMainProcessInReusableForkIT.java
+++ b/surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire946KillMainProcessInReusableForkIT.java
@@ -95,7 +95,7 @@ public class Surefire946KillMainProcessInReusableForkIT
                 "-" + shutdownMavenMethod + "-" + shutdownSurefireMethod )
                 .sysProp( "distinct.classifier", classifierOfDummyDependency )
                 .sysProp( "surefire.shutdown", shutdownSurefireMethod )
-                .sysProp( "selfdestruct.timeoutInMillis", "10000" )
+                .sysProp( "selfdestruct.timeoutInMillis", "20000" )
                 .sysProp( "selfdestruct.method", shutdownMavenMethod )
                 .sysProp( "testSleepTime", String.valueOf( TEST_SLEEP_TIME ) )
                 .addGoal( "org.apache.maven.plugins.surefire:maven-selfdestruct-plugin:selfdestruct" )
diff --git a/surefire-providers/common-junit4/src/main/java/org/apache/maven/surefire/common/junit4/Notifier.java b/surefire-providers/common-junit4/src/main/java/org/apache/maven/surefire/common/junit4/Notifier.java
index ab70395..927df00 100644
--- a/surefire-providers/common-junit4/src/main/java/org/apache/maven/surefire/common/junit4/Notifier.java
+++ b/surefire-providers/common-junit4/src/main/java/org/apache/maven/surefire/common/junit4/Notifier.java
@@ -33,7 +33,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import static org.apache.maven.surefire.common.junit4.JUnit4ProviderUtil.toClassMethod;
-import static org.apache.maven.surefire.api.util.internal.ConcurrencyUtils.countDownToZero;
+import static org.apache.maven.surefire.api.util.internal.ConcurrencyUtils.runIfZeroCountDown;
 
 /**
  * Extends {@link RunNotifier JUnit notifier},
@@ -172,11 +172,7 @@ public class Notifier
      */
     private void fireStopEvent()
     {
-        if ( countDownToZero( skipAfterFailureCount ) )
-        {
-            pleaseStop();
-        }
-
+        runIfZeroCountDown( this::pleaseStop, skipAfterFailureCount );
         reporter.testExecutionSkippedByUser();
     }
 }
diff --git a/surefire-providers/surefire-testng/src/main/java/org/apache/maven/surefire/testng/TestNGExecutor.java b/surefire-providers/surefire-testng/src/main/java/org/apache/maven/surefire/testng/TestNGExecutor.java
index c0799f7..54fea3d 100644
--- a/surefire-providers/surefire-testng/src/main/java/org/apache/maven/surefire/testng/TestNGExecutor.java
+++ b/surefire-providers/surefire-testng/src/main/java/org/apache/maven/surefire/testng/TestNGExecutor.java
@@ -57,7 +57,7 @@ import static org.apache.maven.surefire.api.util.ReflectionUtils.newInstance;
 import static org.apache.maven.surefire.api.util.ReflectionUtils.tryGetConstructor;
 import static org.apache.maven.surefire.api.util.ReflectionUtils.tryGetMethod;
 import static org.apache.maven.surefire.api.util.ReflectionUtils.tryLoadClass;
-import static org.apache.maven.surefire.api.util.internal.ConcurrencyUtils.countDownToZero;
+import static org.apache.maven.surefire.api.util.internal.ConcurrencyUtils.runIfZeroCountDown;
 
 /**
  * Contains utility methods for executing TestNG.
@@ -137,7 +137,7 @@ final class TestNGExecutor
                 xmlTest.setName( metadata.testName );
                 addSelector( xmlTest, groupMatchingSelector );
                 addSelector( xmlTest, methodNameFilteringSelector );
-                xmlTest.setXmlClasses( new ArrayList<XmlClass>() );
+                xmlTest.setXmlClasses( new ArrayList<>() );
 
                 suiteAndNamedTests.testNameToTest.put( metadata.testName, xmlTest );
             }
@@ -366,18 +366,10 @@ final class TestNGExecutor
     {
         final AtomicInteger currentFaultCount = new AtomicInteger( skipAfterFailureCount );
 
-        return new Stoppable()
+        return () ->
         {
-            @Override
-            public void fireStopEvent()
-            {
-                if ( countDownToZero( currentFaultCount ) )
-                {
-                    FailFastEventsSingleton.getInstance().setSkipOnNextTest();
-                }
-
-                reportManager.testExecutionSkippedByUser();
-            }
+            runIfZeroCountDown( () -> FailFastEventsSingleton.getInstance().setSkipOnNextTest(), currentFaultCount );
+            reportManager.testExecutionSkippedByUser();
         };
     }