/* * Copyright 2007-2010 Sun Microsystems, Inc. * * This file is part of Project Darkstar Server. * * Project Darkstar Server is free software: you can redistribute it * and/or modify it under the terms of the GNU General Public License * version 2 as published by the Free Software Foundation and * distributed hereunder to you. * * Project Darkstar Server 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 com.sun.sgs.impl.util; import com.sun.sgs.app.ExceptionRetryStatus; import com.sun.sgs.app.ManagedObject; import com.sun.sgs.app.NameNotBoundException; import com.sun.sgs.app.TransactionException; import com.sun.sgs.auth.Identity; import com.sun.sgs.impl.kernel.StandardProperties; import com.sun.sgs.impl.sharedutil.LoggerWrapper; import com.sun.sgs.impl.sharedutil.PropertiesWrapper; import com.sun.sgs.kernel.ComponentRegistry; import com.sun.sgs.kernel.TaskQueue; import com.sun.sgs.kernel.TaskScheduler; import com.sun.sgs.kernel.TransactionScheduler; import com.sun.sgs.service.DataService; import com.sun.sgs.service.Node; import com.sun.sgs.service.Service; import com.sun.sgs.service.TransactionProxy; import com.sun.sgs.service.WatchdogService; import java.io.IOException; import java.io.Serializable; import java.util.Properties; import java.util.logging.Level; /** * An abstract implementation of a service. It manages state * transitions (i.e., initialized, ready, shutting down, shutdown), in * progress call tracking for services with embedded remote servers, * and shutdown support. * * <p>The {@link #getName getName} method invokes the instance's {@code * toString} method, so a concrete subclass of {@code AbstractService} * should provide an implementation of the {@code toString} method. * * An {@link #AbstractService} supports the following properties: <p> * * Property: com.sun.sgs.io.retries * Default: 5 retries * Specifies how many times an IO task should be retried before performing * failure procedures. * * <dl style="margin-left: 1em"> * * <dt> <i>Property:</i> <code><b> * com.sun.sgs.impl.util.io.task.max.retries * </b></code><br> * <i>Default:</i> 5 retries <br> * Specifies how many times an {@link IoRunnable IoRunnable} task should * be retried before performing failure procedures. The value * must be greater than or equal to {@code 0}.<p> * * <dt> <i>Property:</i> <code><b> * com.sun.sgs.impl.util.io.task.wait.time * </b></code><br> * <i>Default:</i> 100 milliseconds <br> * Specifies the wait time between {@link IoRunnable IoRunnable} task * retries. The value must be greater than or equal to {@code 0}. * * </dl> <p> * */ public abstract class AbstractService implements Service { /** Service state. */ protected static enum State { /** The service is initialized. */ INITIALIZED, /** The service is ready. */ READY, /** The service is shutting down. */ SHUTTING_DOWN, /** The service is shut down. */ SHUTDOWN } /** The transaction proxy, or null if configure has not been called. */ protected static volatile TransactionProxy txnProxy = null; /** The application name. */ protected final String appName; /** The logger for the subclass. */ protected final LoggerWrapper logger; /** The data service. */ protected final DataService dataService; /** The task scheduler. */ protected final TaskScheduler taskScheduler; /** The transaction scheduler. */ protected final TransactionScheduler transactionScheduler; /** The task owner. */ protected final Identity taskOwner; /** The lock for {@code state} and {@code callsInProgress} fields. */ private final Object lock = new Object(); /** The server state. */ private State state; /** The count of calls in progress. */ private int callsInProgress = 0; /** Thread for shutting down the server. */ private volatile Thread shutdownThread; /** Prefix for io task related properties. */ public static final String IO_TASK_PROPERTY_PREFIX = "com.sun.sgs.impl.util.io.task"; /** * An optional property that specifies the maximum number of retries for * IO tasks in services. */ public static final String IO_TASK_RETRIES_PROPERTY = IO_TASK_PROPERTY_PREFIX + ".max.retries"; /** * An optional property that specifies the wait time between successive * IO task retries. */ public static final String IO_TASK_WAIT_TIME_PROPERTY = IO_TASK_PROPERTY_PREFIX + ".wait.time"; /** The default number of IO task retries **/ private static final int DEFAULT_MAX_IO_ATTEMPTS = 5; /** The default time interval to wait between IO task retries **/ private static final int DEFAULT_RETRY_WAIT_TIME = 100; /** The time (in milliseconds) to wait between retries for IO * operations. */ protected final int retryWaitTime; /** The maximum number of retry attempts for IO operations. */ protected final int maxIoAttempts; /** * Constructs an instance with the specified {@code properties}, {@code * systemRegistry}, {@code txnProxy}, and {@code logger}. It initializes * the {@code appName} field to the value of the {@code * com.sun.sgs.app.name} property and sets this service's state to {@code * INITIALIZED}. * * @param properties service properties * @param systemRegistry system registry * @param txnProxy transaction proxy * @param logger the service's logger * * @throws IllegalArgumentException if the {@code com.sun.sgs.app.name} * property is not defined in {@code properties} */ protected AbstractService(Properties properties, ComponentRegistry systemRegistry, TransactionProxy txnProxy, LoggerWrapper logger) { if (properties == null) { throw new NullPointerException("null properties"); } else if (systemRegistry == null) { throw new NullPointerException("null systemRegistry"); } else if (txnProxy == null) { throw new NullPointerException("null txnProxy"); } else if (logger == null) { throw new NullPointerException("null logger"); } synchronized (AbstractService.class) { if (AbstractService.txnProxy == null) { AbstractService.txnProxy = txnProxy; } else { assert AbstractService.txnProxy == txnProxy; } } appName = properties.getProperty(StandardProperties.APP_NAME); if (appName == null) { throw new IllegalArgumentException( "The " + StandardProperties.APP_NAME + " property must be specified"); } PropertiesWrapper wrappedProps = new PropertiesWrapper(properties); retryWaitTime = wrappedProps.getIntProperty( IO_TASK_WAIT_TIME_PROPERTY, DEFAULT_RETRY_WAIT_TIME, 0, Integer.MAX_VALUE); maxIoAttempts = wrappedProps.getIntProperty( IO_TASK_RETRIES_PROPERTY, DEFAULT_MAX_IO_ATTEMPTS, 0, Integer.MAX_VALUE); this.logger = logger; this.taskScheduler = systemRegistry.getComponent(TaskScheduler.class); this.transactionScheduler = systemRegistry.getComponent(TransactionScheduler.class); this.dataService = txnProxy.getService(DataService.class); this.taskOwner = txnProxy.getCurrentOwner(); setState(State.INITIALIZED); } /** {@inheritDoc} */ public String getName() { return toString(); } /** * {@inheritDoc} * * <p>If this service is in the {@code INITIALIZED} state, this * method sets the state to {@code READY} and invokes the {@link * #doReady doReady} method. If this service is already in the * {@code READY} state, this method performs no actions. If this * service is shutting down, or is already shut down, this method * throws {@code IllegalStateException}. * * @throws Exception if a problem occurs * @throws IllegalStateException if this service is shutting down * or is already shut down */ public void ready() throws Exception { logger.log(Level.FINEST, "ready"); synchronized (lock) { switch (state) { case INITIALIZED: setState(State.READY); break; case READY: return; case SHUTTING_DOWN: case SHUTDOWN: throw new IllegalStateException("service shutting down"); default: throw new AssertionError(); } } doReady(); } /** * Performs ready operations. This method is invoked by the * {@link #ready ready} method only once so that the subclass can * perform any operations necessary during the "ready" phase. * * @throws Exception if a problem occurs */ protected abstract void doReady() throws Exception; /** * {@inheritDoc} * * <p>If this service is in the {@code INITIALIZED} state or * {@code READY} state, this method sets the state to * {@code SHUTTING_DOWN}, waits for all calls in progress to * complete, starts a thread to invoke the {@link #doShutdown * doShutdown} method, waits for that thread to complete, and * returns. If this service is in the {@code SHUTTING_DOWN} * state, this method will block until the shutdown is complete. If * this service is in the {@code SHUTDOWN} state, then it will * return immediately. Any retries or interruption handling should be * done in the service's implementation of the * {@link #doShutdown() doShutdown} method. * */ public void shutdown() { logger.log(Level.FINEST, "shutdown"); synchronized (lock) { switch (state) { case INITIALIZED: case READY: logger.log(Level.FINEST, "initiating shutdown"); setState(State.SHUTTING_DOWN); while (callsInProgress > 0) { try { lock.wait(); } catch (InterruptedException e) { return; } } shutdownThread = new ShutdownThread(); shutdownThread.start(); break; case SHUTTING_DOWN: break; case SHUTDOWN: return; default: throw new AssertionError(); } } try { shutdownThread.join(); } catch (InterruptedException e) { return; } } /** * Performs shutdown operations. This method is invoked by the * {@link #shutdown shutdown} method only once so that the * subclass can perform any operations necessary to shutdown the * service. */ protected abstract void doShutdown(); /** * Checks the service version. If a version is not associated with the * given {@code versionKey}, then a new {@link Version} object * (constructed with the specified {@code majorVersion} and {@code * minorVersion}) is bound in the data service with the specified key. * * <p>If an old version is bound to the specified key and that old * version is not equal to the current version (as specified by {@code * majorVersion}/{@code minorVersion}), then the {@link * #handleServiceVersionMismatch handleServiceVersionMismatch} method is * invoked to convert the old version to the new version. If the {@code * handleVersionMismatch} method returns normally, the old version is * removed and the current version is bound to the specified key. * * <p>This method must be called within a transaction. * * @param versionKey a key for the version * @param majorVersion a major version * @param minorVersion a minor version * @throws TransactionException if there is a problem with the * current transaction * @throws IllegalStateException if {@code handleVersionMismatch} is * invoked and throws a {@code RuntimeException} */ protected final void checkServiceVersion( String versionKey, int majorVersion, int minorVersion) { if (versionKey == null) { throw new NullPointerException("null versionKey"); } Version currentVersion = new Version(majorVersion, minorVersion); try { Version oldVersion = (Version) dataService.getServiceBinding(versionKey); if (!currentVersion.equals(oldVersion)) { try { handleServiceVersionMismatch(oldVersion, currentVersion); dataService.removeObject(oldVersion); dataService.setServiceBinding(versionKey, currentVersion); } catch (IllegalStateException e) { throw e; } catch (RuntimeException e) { throw new IllegalStateException( "exception occurred while upgrading from version: " + oldVersion + ", to: " + currentVersion, e); } } } catch (NameNotBoundException e) { // No version exists yet; store first version in data service. dataService.setServiceBinding(versionKey, currentVersion); } } /** * Handles conversion from the {@code oldVersion} to the {@code * currentVersion}. This method is invoked by {@link #checkServiceVersion * checkServiceVersion} if a version mismatch is detected and is invoked * from within a transaction. * * @param oldVersion the old version * @param currentVersion the current version * @throws IllegalStateException if the old version cannot be upgraded * to the current version */ protected abstract void handleServiceVersionMismatch( Version oldVersion, Version currentVersion); /** * Returns this service's state. * * @return this service's state */ protected State getState() { synchronized (lock) { return state; } } /** * Increments the number of calls in progress. This method should * be invoked by remote methods to both increment in progress call * count and to check the state of this server. When the call has * completed processing, the remote method should invoke {@link * #callFinished callFinished} before returning. * * @throws IllegalStateException if this service is shutting down */ protected void callStarted() { synchronized (lock) { if (shuttingDown()) { throw new IllegalStateException("service is shutting down"); } callsInProgress++; } } /** * Decrements the in progress call count, and if this server is * shutting down and the count reaches 0, then notifies the waiting * shutdown thread that it is safe to continue. A remote method * should invoke this method when it has completed processing. */ protected void callFinished() { synchronized (lock) { callsInProgress--; if (state == State.SHUTTING_DOWN && callsInProgress == 0) { lock.notifyAll(); } } } /** * Returns {@code true} if this service is shutting down. * * @return {@code true} if this service is shutting down */ public boolean shuttingDown() { synchronized (lock) { return state == State.SHUTTING_DOWN || state == State.SHUTDOWN; } } /** * Returns {@code true} if this service is in the initialized state * but is not yet ready to run. * * @return {@code true} if this service is in the initialized state */ protected boolean isInInitializedState() { synchronized (lock) { return state == State.INITIALIZED; } } /** * Runs a transactional task to query the status of the node with the * specified {@code nodeId} and returns {@code true} if the node is alive * and {@code false} otherwise. * * <p>This method must be called from outside a transaction or {@code * IllegalStateException} will be thrown. * * @param nodeId a node ID * @return {@code true} if the node with the associated ID is * considered alive, otherwise returns {@code false} * @throws IllegalStateException if this method is invoked inside a * transactional context */ public boolean isAlive(long nodeId) { checkNonTransactionalContext(); try { CheckNodeStatusTask nodeStatus = new CheckNodeStatusTask(nodeId); transactionScheduler.runTask(nodeStatus, taskOwner); return nodeStatus.isAlive; } catch (IllegalStateException ignore) { // Ignore because the service is shutting down. } catch (Exception e) { // This shouldn't happen, so log. if (logger.isLoggable(Level.WARNING)) { logger.logThrow( Level.WARNING, e, "running CheckNodeStatusTask throws"); } } // TBD: is this the correct value to return? We can't really tell // what the status of a non-local node is if the local node is // shutting down. return false; } /** * Creates a {@code TaskQueue} for dependent, transactional tasks. * * @return the task queue */ public TaskQueue createTaskQueue() { return transactionScheduler.createTaskQueue(); } /** * Executes the specified {@code ioTask} by invoking its {@link * IoRunnable#run run} method. If the specified task throws an * {@code IOException}, this method will retry the task for a fixed * number of times. The method will stop retrying if the node with * the given {@code nodeId} is no longer alive. The number of retries * and the wait time between retries are configurable properties. * * <p> * This method must be called from outside a transaction or {@code * IllegalStateException} will be thrown. * * @param ioTask a task with IO-related operations * @param nodeId the node that is the target of the IO operations * @return {@code true} if the task was successfully executed and * {@code false} if the specified node is no longer alive * @throws IllegalStateException if this method is invoked within a * transactional context */ public boolean runIoTask(IoRunnable ioTask, long nodeId) { int maxAttempts = maxIoAttempts; checkNonTransactionalContext(); do { try { ioTask.run(); return true; } catch (IOException e) { if (logger.isLoggable(Level.FINEST)) { logger.logThrow(Level.FINEST, e, "IoRunnable {0} throws", ioTask); } if (maxAttempts-- == 0) { logger.logThrow(Level.WARNING, e, "A communication error occured while running an " + "IO task. Reporting node {0} as failed.", nodeId); // Report failure of remote node since are // having trouble contacting it txnProxy.getService(WatchdogService.class). reportFailure(nodeId, this.getClass().toString()); break; } try { // TBD: what back-off policy do we want here? Thread.sleep(retryWaitTime); } catch (InterruptedException ie) { } } } while (isAlive(nodeId)); return false; } /** * Returns the data service. * * @return the data service */ public DataService getDataService() { return dataService; } /** * Returns {@code true} if the specified exception is retryable, and * {@code false} otherwise. A retryable exception is one that * implements {@link ExceptionRetryStatus} and invoking its {@link * ExceptionRetryStatus#shouldRetry shouldRetry} method returns {@code * true}. * * @param e an exception * @return {@code true} if the specified exception is retryable, and * {@code false} otherwise */ public static boolean isRetryableException(Exception e) { return (e instanceof ExceptionRetryStatus) && ((ExceptionRetryStatus) e).shouldRetry(); } /** * An immutable class to hold the current version of the keys * and data persisted by a service. */ public static class Version implements ManagedObject, Serializable { /** Serialization version. */ private static final long serialVersionUID = 1L; private final int majorVersion; private final int minorVersion; /** * Constructs an instance with the specified {@code major} and * {@code minor} version numbers. * * @param major a major version number * @param minor a minor version number */ public Version(int major, int minor) { majorVersion = major; minorVersion = minor; } /** * Returns the major version number. * @return the major version number */ public int getMajorVersion() { return majorVersion; } /** * Returns the minor version number. * @return the minor version number */ public int getMinorVersion() { return minorVersion; } /** {@inheritDoc} */ @Override public String toString() { return "Version[major:" + majorVersion + ", minor:" + minorVersion + "]"; } /** {@inheritDoc} */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } else if (obj == null) { return false; } else if (obj.getClass() == this.getClass()) { Version other = (Version) obj; return majorVersion == other.majorVersion && minorVersion == other.minorVersion; } return false; } /** {@inheritDoc} */ @Override public int hashCode() { int result = 17; result = 37 * result + majorVersion; result = 37 * result + minorVersion; return result; } } /* -- Private methods and classes -- */ /** * Sets this service's state to {@code newState}. * * @param newState a new state. */ private void setState(State newState) { synchronized (lock) { state = newState; } } /** * Checks that the current thread is not in a transactional context * and throws {@code IllegalStateException} if the thread is in a * transactional context. */ public void checkNonTransactionalContext() { if (txnProxy.inTransaction()) { throw new IllegalStateException( "operation not allowed from a transactional context"); } } /** * A task to obtain the status of a given node. */ private static class CheckNodeStatusTask extends AbstractKernelRunnable { private final long nodeId; volatile boolean isAlive = false; /** Constructs an instance with the specified {@code nodeId}. */ CheckNodeStatusTask(long nodeId) { super(null); this.nodeId = nodeId; } /** {@inheritDoc} */ public void run() { WatchdogService watchdogService = txnProxy.getService(WatchdogService.class); Node node = watchdogService.getNode(nodeId); isAlive = node != null && node.isAlive(); } } /** * Thread for shutting down service/server. */ private final class ShutdownThread extends Thread { /** Constructs an instance of this class as a daemon thread. */ ShutdownThread() { super(ShutdownThread.class.getName()); setDaemon(true); } /** {@inheritDoc} */ public void run() { try { doShutdown(); } catch (RuntimeException e) { logger.logThrow( Level.WARNING, e, "shutting down service throws"); // swallow exception } setState(AbstractService.State.SHUTDOWN); } } }