#2770: Handle posix signals SIGTERM, SIGINT, SIGHUP gracefully

- Move working contents of Bungeecord.stop() to a separate function
named independentThreadStop() intended to be called from a separate thread.
- Added a new generic shutdown hook to call independentThreadStop when
the JVM begins shutting down.
This commit is contained in:
Byron Marohn 2020-03-08 19:22:22 +11:00 committed by md_5
parent 6ad26cc8fa
commit 697f0875e6

View File

@ -40,6 +40,7 @@ import java.util.TimerTask;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Handler; import java.util.logging.Handler;
import java.util.logging.Level; import java.util.logging.Level;
@ -134,6 +135,11 @@ public class BungeeCord extends ProxyServer
private final Map<UUID, UserConnection> connectionsByOfflineUUID = new HashMap<>(); private final Map<UUID, UserConnection> connectionsByOfflineUUID = new HashMap<>();
private final Map<UUID, UserConnection> connectionsByUUID = new HashMap<>(); private final Map<UUID, UserConnection> connectionsByUUID = new HashMap<>();
private final ReadWriteLock connectionLock = new ReentrantReadWriteLock(); private final ReadWriteLock connectionLock = new ReentrantReadWriteLock();
/**
* Lock to protect the shutdown process from being triggered simultaneously
* from multiple sources.
*/
private final ReentrantLock shutdownLock = new ReentrantLock();
/** /**
* Plugin manager. * Plugin manager.
*/ */
@ -297,6 +303,15 @@ public class BungeeCord extends ProxyServer
} }
}, 0, TimeUnit.MINUTES.toMillis( 5 ) ); }, 0, TimeUnit.MINUTES.toMillis( 5 ) );
metricsThread.scheduleAtFixedRate( new Metrics(), 0, TimeUnit.MINUTES.toMillis( Metrics.PING_INTERVAL ) ); metricsThread.scheduleAtFixedRate( new Metrics(), 0, TimeUnit.MINUTES.toMillis( Metrics.PING_INTERVAL ) );
Runtime.getRuntime().addShutdownHook( new Thread()
{
@Override
public void run()
{
independentThreadStop( getTranslation( "restart" ), false );
}
} );
} }
public void startListeners() public void startListeners()
@ -385,90 +400,110 @@ public class BungeeCord extends ProxyServer
} }
@Override @Override
public synchronized void stop(final String reason) public void stop(final String reason)
{ {
new Thread( "Shutdown Thread" )
{
@Override
public void run()
{
independentThreadStop( reason, true );
}
}.start();
}
// This must be run on a separate thread to avoid deadlock!
@SuppressFBWarnings("DM_EXIT")
@SuppressWarnings("TooBroadCatch")
private void independentThreadStop(final String reason, boolean callSystemExit)
{
// Acquire the shutdown lock
// This needs to actually block here, otherwise running 'end' and then ctrl+c will cause the thread to terminate prematurely
shutdownLock.lock();
// Acquired the shutdown lock
if ( !isRunning ) if ( !isRunning )
{ {
// Server is already shutting down - nothing to do
shutdownLock.unlock();
return; return;
} }
isRunning = false; isRunning = false;
new Thread( "Shutdown Thread" ) stopListeners();
getLogger().info( "Closing pending connections" );
connectionLock.readLock().lock();
try
{ {
@Override getLogger().log( Level.INFO, "Disconnecting {0} connections", connections.size() );
@SuppressFBWarnings("DM_EXIT") for ( UserConnection user : connections.values() )
@SuppressWarnings("TooBroadCatch")
public void run()
{ {
stopListeners(); user.disconnect( reason );
getLogger().info( "Closing pending connections" ); }
} finally
{
connectionLock.readLock().unlock();
}
connectionLock.readLock().lock(); try
try {
{ Thread.sleep( 500 );
getLogger().log( Level.INFO, "Disconnecting {0} connections", connections.size() ); } catch ( InterruptedException ex )
for ( UserConnection user : connections.values() ) {
{ }
user.disconnect( reason );
}
} finally
{
connectionLock.readLock().unlock();
}
try if ( reconnectHandler != null )
{ {
Thread.sleep( 500 ); getLogger().info( "Saving reconnect locations" );
} catch ( InterruptedException ex ) reconnectHandler.save();
{ reconnectHandler.close();
} }
saveThread.cancel();
metricsThread.cancel();
if ( reconnectHandler != null ) getLogger().info( "Disabling plugins" );
{ for ( Plugin plugin : Lists.reverse( new ArrayList<>( pluginManager.getPlugins() ) ) )
getLogger().info( "Saving reconnect locations" ); {
reconnectHandler.save(); try
reconnectHandler.close(); {
} plugin.onDisable();
saveThread.cancel(); for ( Handler handler : plugin.getLogger().getHandlers() )
metricsThread.cancel();
// TODO: Fix this shit
getLogger().info( "Disabling plugins" );
for ( Plugin plugin : Lists.reverse( new ArrayList<>( pluginManager.getPlugins() ) ) )
{
try
{
plugin.onDisable();
for ( Handler handler : plugin.getLogger().getHandlers() )
{
handler.close();
}
} catch ( Throwable t )
{
getLogger().log( Level.SEVERE, "Exception disabling plugin " + plugin.getDescription().getName(), t );
}
getScheduler().cancel( plugin );
plugin.getExecutorService().shutdownNow();
}
getLogger().info( "Closing IO threads" );
eventLoops.shutdownGracefully();
try
{
eventLoops.awaitTermination( Long.MAX_VALUE, TimeUnit.NANOSECONDS );
} catch ( InterruptedException ex )
{
}
getLogger().info( "Thank you and goodbye" );
// Need to close loggers after last message!
for ( Handler handler : getLogger().getHandlers() )
{ {
handler.close(); handler.close();
} }
System.exit( 0 ); } catch ( Throwable t )
{
getLogger().log( Level.SEVERE, "Exception disabling plugin " + plugin.getDescription().getName(), t );
} }
}.start(); getScheduler().cancel( plugin );
plugin.getExecutorService().shutdownNow();
}
getLogger().info( "Closing IO threads" );
eventLoops.shutdownGracefully();
try
{
eventLoops.awaitTermination( Long.MAX_VALUE, TimeUnit.NANOSECONDS );
} catch ( InterruptedException ex )
{
}
getLogger().info( "Thank you and goodbye" );
// Need to close loggers after last message!
for ( Handler handler : getLogger().getHandlers() )
{
handler.close();
}
// Unlock the thread before optionally calling system exit, which might invoke this function again.
// If that happens, the system will obtain the lock, and then see that isRunning == false and return without doing anything.
shutdownLock.unlock();
if ( callSystemExit )
{
System.exit( 0 );
}
} }
/** /**