/*
* Copyright (C) 2011 Laurent Caillette
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation, either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.novelang.outfit.shell;
import java.io.IOException;
import java.lang.management.RuntimeMXBean;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import javax.management.ObjectName;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import org.novelang.logger.Logger;
import org.novelang.logger.LoggerFactory;
import org.novelang.outfit.shell.insider.Insider;
/**
* Starts and shuts down a JVM with an {@link org.novelang.outfit.shell.insider.InsiderAgent}
* that installs an {@link org.novelang.outfit.shell.insider.Insider} for various tasks.
* This class takes great care of JVM shutdown.
* <ul>
* <li>
* With a {@link org.novelang.outfit.shell.ShutdownStyle#GENTLE} shutdown, it calls
* {@link org.novelang.outfit.shell.insider.Insider#shutdown()} that attempts to perform
* a nice {@code System.exit()} that calls shutdown hooks.
* </li> <li>
* The JVM starts with a special system property
* {@value org.novelang.outfit.shell.ShutdownTools#SHUTDOWN_TATTOO_PROPERTYNAME} used by
* {@link org.novelang.outfit.shell.ShutdownTools#shutdownAllTattooedVirtualMachines()} to recognize JVMs to shut down.
* </li> <li>
* The {@link org.novelang.outfit.shell.insider.Insider} in the JVM halts when
* {@link org.novelang.outfit.shell.insider.Insider#keepAlive()} wasn't called for a few seconds.
* The {@link JavaShell} runs a thread doing this, so if its JVM gets down, the JVM it started
* soon goes down, too.
* </li>
* </ul>
*
* @author Laurent Caillette
*/
public class JavaShell extends ProcessShell {
private static final Logger LOGGER = LoggerFactory.getLogger( JavaShell.class ) ;
private final BootstrappingJmxKit jmxKit ;
private final Integer heartbeatPeriodMilliseconds ;
/**
* A semaphore that is slightly redundant with the one inside {@link org.novelang.outfit.shell.ProcessShell}
* but this helps preserving encapsulation.
*/
private final Semaphore ownStartupSensorSemaphore ;
/**
* The count of permits that {@link #ownStartupSensorSemaphore} will try to acquire.
* It depends upon the number of sensors inside {@link org.novelang.outfit.shell.TieredStartupSensor}.
*/
private final int startupSensorSemaphorePermitCount ;
private final long startupTimeoutDuration ;
private final TimeUnit startupTimeoutTimeUnit ;
/**
* We use this object even when using always the same {@link JmxKit} because it does useful things.
*/
private final JmxBeanPool jmxBeanPool ;
public JavaShell( final JavaShellParameters parameters ) {
super(
parameters.getWorkingDirectory(),
parameters.getNickname(),
JavaShellTools.createProcessArguments(
parameters.getJvmArguments(),
parameters.getJmxKit(),
parameters.getJmxPortConfiguredAtJvmStartup(),
parameters.getJavaClasses(),
parameters.getProgramArguments(),
calculateHeartbeatFatalDelay( parameters )
),
createTieredStartupSensor(
LOCAL_INSIDER_STARTED,
parameters.getStartupSensor()
)
) ;
this.jmxKit = parameters.getJmxKit() ;
if( this.jmxKit != null ) {
// Host is always localhost since we create process only on the local machine.
jmxBeanPool = new JmxBeanPool( "localhost", checkNotNull( parameters.getJmxPortConfiguredAtJvmStartup() ) ) ;
} else {
jmxBeanPool = null ;
}
{
final Long timeoutDuration = parameters.getStartupTimeoutDuration() ;
if( timeoutDuration == null ) {
this.startupTimeoutDuration = STARTUP_TIMEOUT_DURATION ;
} else {
checkArgument( timeoutDuration > 0 ) ;
this.startupTimeoutDuration = timeoutDuration ;
}
}
{
final TimeUnit timeUnit = parameters.getStartupTimeoutTimeUnit() ;
if( timeUnit == null ) {
this.startupTimeoutTimeUnit = STARTUP_TIMEOUT_UNIT ;
} else {
this.startupTimeoutTimeUnit = timeUnit ;
}
}
this.heartbeatPeriodMilliseconds = parameters.getHeartbeatPeriodMilliseconds() ;
this.ownStartupSensorSemaphore = THREADLOCAL_SEMAPHORE.get() ;
this.startupSensorSemaphorePermitCount = THREADLOCAL_PERMITCOUNT.get() ;
THREADLOCAL_SEMAPHORE.set( null ) ;
THREADLOCAL_PERMITCOUNT.set( null ) ;
// Code checker happy: avoid access in both synchronized and unsynchronized context.
synchronized( stateLock ) {
heartbeatSender = null ;
insider = null ;
}
}
private static final String HEARTBEAT_FATAL_DELAY_PROPERTYNAME = "org.novelang.outfit.shell.heartbeatfataldelay" ;
private static Integer calculateHeartbeatFatalDelay( final JavaShellParameters parameters ) {
final String systemProperty = System.getProperty( HEARTBEAT_FATAL_DELAY_PROPERTYNAME ) ;
if( systemProperty == null ) {
return parameters.getHeartbeatFatalDelayMilliseconds() ;
} else {
return Integer.parseInt( systemProperty ) ;
}
}
private static final Predicate< String > LOCAL_INSIDER_STARTED = new Predicate< String >() {
@Override
public boolean apply( final String input ) {
// Seems printed by the JVM itself.
return input.contains( "Loaded org.novelang.outfit.shell.insider.InsiderAgent." ) ;
}
} ;
private Insider insider;
private HeartbeatSender heartbeatSender;
/**
* We need a dedicated lock for {@link #processIdentifier} otherwise there is a deadlock
* when watcher threads try to access it while {@link #connect()} executes.
*/
private final Object processIdentifierLock = new Object() ;
private int processIdentifier = JavaShellTools.UNDEFINED_PROCESS_ID ;
@Override
public String getNickname() {
final String defaultNickname = super.getNickname() ;
final int currentIdentifier ;
synchronized( processIdentifierLock ) {
currentIdentifier = processIdentifier ;
}
return defaultNickname + (
currentIdentifier == JavaShellTools.UNDEFINED_PROCESS_ID ? "" : "#" + currentIdentifier ) ;
}
// =======
// Startup
// =======
public void start()
throws
IOException,
InterruptedException,
ProcessCreationException,
ProcessInitializationException
{
// There are two steps to ensure JVM is ready.
// First, launch the process and wait for the magic message in the console
// claiming InsiderAgent loaded.
// Second, obtain the JMX connection.
// Following the principle of the least surprise, we keep the overall timeout
// as defined. For each step, we wait half of the overall timeout.
final long adjustedTimeoutDurationMilliseconds =
startupTimeoutTimeUnit.toMillis( startupTimeoutDuration ) / 2L ;
try {
synchronized( stateLock ) {
super.start( adjustedTimeoutDurationMilliseconds, TimeUnit.MILLISECONDS ) ;
connect() ;
if( insider != null ) {
startHeartbeatSender() ;
insider.startWatchingKeepalive() ;
// insider.keepAlive() ; // Ensure JMX working.
}
}
if( hasDefaultJmxKit() ) {
synchronized( processIdentifierLock ) {
final RuntimeMXBean runtimeMXBean = getManagedBean(
RuntimeMXBean.class, JavaShellTools.RUNTIME_MX_BEAN_OBJECTNAME ) ;
processIdentifier = JavaShellTools.extractProcessId( runtimeMXBean.getName() ) ;
}
}
ownStartupSensorSemaphore.tryAcquire(
startupSensorSemaphorePermitCount,
adjustedTimeoutDurationMilliseconds,
TimeUnit.MILLISECONDS
) ;
LOGGER.info( "Started ", getNickname(), "." ) ;
} catch( Exception e ) {
LOGGER.error( "Couldn't start ", getNickname(), ". Cleaning up..." ) ;
shutdownProcessQuiet() ;
cleanup() ;
if( e instanceof ProcessCreationException ) {
throw ( ProcessCreationException ) e ;
}
throw new ProcessInitializationException( "Couldn't initialize " + getNickname(), e ) ;
}
}
private void startHeartbeatSender() {
if( heartbeatPeriodMilliseconds == null ) {
heartbeatSender = new HeartbeatSender( insider, heartbeatSenderNotifiee, getNickname() ) ;
} else {
heartbeatSender = new HeartbeatSender(
insider,
heartbeatSenderNotifiee,
getNickname(),
heartbeatPeriodMilliseconds
) ;
}
}
// ===================
// Process termination
// ===================
/**
* Requests the underlying process to shutdown. When requested to shut down in a
* {@link org.novelang.outfit.shell.ShutdownStyle#GENTLE} or
* {@link org.novelang.outfit.shell.ShutdownStyle#WAIT} style, the method waits until the
* process gently terminates.
*
* @param shutdownStyle a non-null object.
* @throws InterruptedException should not happen.
* @throws java.io.IOException too bad.
*/
public Integer shutdown( final ShutdownStyle shutdownStyle )
throws InterruptedException, IOException
{
LOGGER.info( "Shutdown (", shutdownStyle, ") requested for ", getNickname(), "..." ) ;
// Code checker happy: avoid access to static member in synchronized context.
final Logger logger = LOGGER ;
Integer exitStatus = null ;
synchronized( stateLock ) {
try {
if( insider == null ) {
logger.info( "Not started or already shut down." ) ;
} else {
switch( shutdownStyle ) {
case GENTLE :
try {
if( hasDefaultJmxKit() ) {
insider.shutdown() ;
}
} catch( Exception e ) {
logger.info( "Shutdown request failed: ", e.getMessage(), ", forcing..." ) ;
exitStatus = shutdownProcess( true ) ;
break ;
}
// ... After asking for shutdown, we wait for natural process end. TODO: add timeout?
case WAIT :
exitStatus = shutdownProcess( false ) ;
break ;
case FORCED :
shutdownProcess( true ) ;
break ;
default :
throw new IllegalArgumentException( "Unsupported: " + shutdownStyle ) ;
}
}
} finally {
// Do this all the time because there can be a running process and a null insider
// in the case of no default jmxKit.
shutdownProcessQuiet() ;
cleanup();
}
}
LOGGER.info( "Shutdown (", shutdownStyle, ") complete for ", getNickname(),
" with exit status code of ", exitStatus, "." ) ;
return exitStatus ;
}
private void cleanup() {
insider = null ;
try {
if( heartbeatSender != null ) {
heartbeatSender.stop() ;
heartbeatSender = null ;
}
if( jmxBeanPool != null ) {
jmxBeanPool.disconnectAll() ;
}
synchronized( processIdentifierLock ) {
processIdentifier = JavaShellTools.UNDEFINED_PROCESS_ID ;
}
} catch( Exception e ) {
LOGGER.error( e, "Something went wrong during cleanup." ) ;
}
LOGGER.info( "Cleanup done for ", getNickname(), "." ) ;
}
private final HeartbeatSender.Notifiee heartbeatSenderNotifiee = new HeartbeatSender.Notifiee() {
@Override
public void onUnreachableProcess() {
handleUnreachableProcess() ;
}
} ;
private void handleUnreachableProcess() {
final Logger logger = LOGGER ;
synchronized( stateLock ) {
if( insider != null ) {
logger.info( "Couldn't send heartbeat to ", getNickname(), ", cleaning up..." );
shutdownProcessQuiet() ;
cleanup() ;
}
}
}
private void shutdownProcessQuiet() {
try {
shutdownProcess( true ) ;
} catch( InterruptedException e ) {
LOGGER.error( e, "Not supposed to happen during a forced shutdown" ) ;
}
}
// ===
// JMX
// ===
public final boolean hasDefaultJmxKit() {
return jmxKit != null ;
}
/**
* Synchronization on {@link #stateLock} left to caller.
*/
private void connect() throws IOException, InterruptedException {
if( jmxKit != null ) {
insider = getManagedBean( Insider.class, Insider.NAME ) ;
}
}
/**
* Returns a proxy on a JMX bean in the launched JVM, using default {@link JmxKit}.
*
* @param beanClass a non-null object.
* @param beanName a non-null object.
* @return a non-null object.
*
* @throws IllegalStateException if {@link #hasDefaultJmxKit()} returns {@code false}.
*/
public < BEAN > BEAN getManagedBean( final Class< BEAN > beanClass, final ObjectName beanName )
throws IOException, InterruptedException
{
Preconditions.checkState( hasDefaultJmxKit() ) ;
return jmxBeanPool.getManagedBean( beanClass, beanName, jmxKit ) ;
}
// ==============
// Default values
// ==============
private static final long STARTUP_TIMEOUT_DURATION = 20L ;
private static final TimeUnit STARTUP_TIMEOUT_UNIT = TimeUnit.SECONDS ;
// =======================================================================
// Boring stuff to access member variable before superclass initialization
// =======================================================================
private static final ThreadLocal< Semaphore > THREADLOCAL_SEMAPHORE =
new ThreadLocal< Semaphore >() ;
private static final ThreadLocal< Integer > THREADLOCAL_PERMITCOUNT =
new ThreadLocal< Integer >() ;
private static TieredStartupSensor createTieredStartupSensor(
final Predicate< String >... startupSensors
) {
final Semaphore semaphore = new Semaphore( 0, true ) ;
final TieredStartupSensor tieredStartupSensor =
new TieredStartupSensor( semaphore, startupSensors ) ;
THREADLOCAL_SEMAPHORE.set( semaphore ) ;
THREADLOCAL_PERMITCOUNT.set( tieredStartupSensor.getInitialPredicateCount() ) ;
return tieredStartupSensor ;
}
}