Package com.sun.ejb.containers

Source Code of com.sun.ejb.containers.EJBTimerService$TimerCache

/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License").  You
* may not use this file except in compliance with the License. You can obtain
* a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
* or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
* Sun designates this particular file as subject to the "Classpath" exception
* as provided by Sun in the GPL Version 2 section of the License file that
* accompanied this code.  If applicable, add the following below the License
* Header, with the fields enclosed by brackets [] replaced by your own
* identifying information: "Portions Copyrighted [year]
* [name of copyright owner]"
*
* Contributor(s):
*
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license."  If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above.  However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package com.sun.ejb.containers;

import java.util.Date;
import java.util.Collection;
import java.util.Set;
import java.util.HashSet;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TimerTask;
import java.util.Properties;
import java.util.logging.Logger;
import java.util.logging.Level;

import java.text.DateFormat;
import java.text.SimpleDateFormat;

import java.io.File;
import java.io.FileWriter;
import java.io.FileReader;
import java.io.BufferedWriter;
import java.io.BufferedReader;
import java.io.PrintWriter;
import java.io.File;
import java.io.Serializable;

import com.sun.logging.LogDomains;
import com.sun.ejb.ContainerFactory;

import javax.ejb.Timer;
import javax.ejb.EJBException;
import javax.ejb.FinderException;
import javax.ejb.CreateException;
import javax.ejb.RemoveException;

import com.sun.enterprise.admin.monitor.callflow.Agent;
import com.sun.enterprise.admin.monitor.callflow.RequestType;
import com.sun.enterprise.Switch;
import com.sun.enterprise.deployment.*;

import com.sun.enterprise.server.ApplicationServer;
import com.sun.enterprise.instance.InstanceEnvironment;
import com.sun.enterprise.instance.AppsManager;
import com.sun.enterprise.instance.ServerManager;

import com.sun.enterprise.config.serverbeans.ServerBeansFactory;
import com.sun.enterprise.server.ServerContext;
import com.sun.enterprise.config.serverbeans.EjbContainer;
import com.sun.enterprise.config.serverbeans.EjbTimerService;

import javax.transaction.TransactionManager;

/*
* EJBTimerService is the central controller of the EJB timer service. 
* There is one instance of EJBTimerService per VM. All operations and
* state transitions on timers pass through EJB Timer Service.  This
* reduces the overall complexity by encapsulating the timer logic
* within this class and keeping the supporting classes simple.
*
* @author Kenneth Saks
*/
public class EJBTimerService
implements com.sun.ejb.spi.distributed.DistributedEJBTimerService
{
    private long nextTimerIdMillis_ = 0;
    private long nextTimerIdCounter_ = 0;
    private String serverName_;
    private String domainName_;

    // @@@ Double-check that the individual server id, domain name,
    // and cluster name cannot contain the TIMER_ID_SEP
    // characters.     

    // separator between components that make up timer id and owner id
    private static final String TIMER_ID_SEP = "@@";

    // Owner id of the server instance in which we are currently running.
    private String ownerIdOfThisServer_;       

    // A cache of timer info for all timers *owned* by this server instance.
    private TimerCache timerCache_;

    private TimerLocalHome timerLocalHome_;
    private TimerMigrationLocalHome timerMigrationLocalHome_;
    private boolean shutdown_;

    // Total number of ejb components initialized as timed objects between the
    // start and end of a single server instance.  This value is used during
    // restoreTimers() as an optimization to avoid initialization overhead in
    // the common case that there are no applications with timed objects. 
    // It is NOT intended to be a consistent count of the current number of
    // timed objects, so there is no need to decrement the number when a
    // container is undeployed.
    private long totalTimedObjectsInitialized_ = 0;

    private static final Logger logger =
        LogDomains.getLogger(LogDomains.EJB_LOGGER);

    // Defaults for configurable timer service properties.

    private static final long MINIMUM_DELIVERY_INTERVAL = 7000;
    private static final int MAX_REDELIVERIES = 1;
    private static final long REDELIVERY_INTERVAL = 5000;

    private String appID;

    // minimum amount of time between either a timer creation and its first
    // expiration or between subsequent timer expirations. 
    private long minimumDeliveryInterval_ = MINIMUM_DELIVERY_INTERVAL;

    // maximum number of times the container will attempt to retry a
    // timer delivery before giving up.
    private long maxRedeliveries_         = MAX_REDELIVERIES;

    // amount of time the container waits between timer redelivery attempts.
    private long redeliveryInterval_      = REDELIVERY_INTERVAL;

    private static final String TIMER_SERVICE_FILE =
        "__timer_service_shutdown__.dat";
    private static final String TIMER_SERVICE_DOWNTIME_FORMAT =
        "yyyy/MM/dd HH:mm:ss";

    // This boolean value would be set in PE to be a default value of false.
    // In case of EE the default value would be true. When set to true the
    // timer service would have maximim consistency with performance degration.
    private boolean performDBReadBeforeTimeout = false;
   
    private static final String strDBReadBeforeTimeout =
        "com.sun.ejb.timer.ReadDBBeforeTimeout";
    private boolean foundSysPropDBReadBeforeTimeout = false;

    public EJBTimerService(String appID, TimerLocalHome timerLocalHome,
                           TimerMigrationLocalHome timerMigrationLocalHome)
    {
        timerLocalHome_ = timerLocalHome;
        timerMigrationLocalHome_ = timerMigrationLocalHome;
        timerCache_     = new TimerCache();
        shutdown_       = false;
        this.appID = appID;

        domainName_ = ServerManager.instance().getDomainName();
        InstanceEnvironment server =
            ApplicationServer.getServerContext().getInstanceEnvironment();
        serverName_ = server.getName();

        initProperties();
    }

    private void initProperties() {

        try {
           
            // Check for property settings from domain.xml
            ServerContext sc = ApplicationServer.getServerContext();
            EjbContainer ejbc = ServerBeansFactory.
                getConfigBean(sc.getConfigContext()).getEjbContainer();
            EjbTimerService ejbt = ejbc.getEjbTimerService();

            if( ejbt != null ) {

                String valString = ejbt.getMinimumDeliveryIntervalInMillis();
                long val = (valString != null) ?
                    Long.parseLong(valString) : -1;
                   
                if( val > 0 ) {
                    minimumDeliveryInterval_ = val;
                }

                valString = ejbt.getMaxRedeliveries();
                val = (valString != null) ? Long.parseLong(valString) : -1;
                // EJB 2.1 specification minimum is 1
                if( val > 0 ) {
                    maxRedeliveries_ = val;
                }

                valString = ejbt.getRedeliveryIntervalInternalInMillis();
                val = (valString != null) ? Long.parseLong(valString) : -1;
                if( val > 0 ) {
                    redeliveryInterval_ = val;
                }

                // If the system property com.sun.ejb.timer.ReadDBBeforeTimeout
                // is defined by the user use that the value of the flag
                // performDBReadBeforeTimeout
                foundSysPropDBReadBeforeTimeout =
                    getDBReadBeforeTimeoutProperty();

                // The default value for ReadDBBeforeTimeout in case of PE
                // is false. For SE/EE the correct default would set when the
                // EJBLifecyleImpl gets created as part of the EE lifecycle module
                setPerformDBReadBeforeTimeout( false );
            }

            // Compose owner id for all timers created with this
            // server instance. 
            InstanceEnvironment server = sc.getInstanceEnvironment();
            String serverName = server.getName();
            ownerIdOfThisServer_ = serverName;

        } catch(Exception e) {
            logger.log(Level.FINE, "Exception converting timer service " +
               "domain.xml properties.  Defaults will be used instead.", e);
        }

        logger.log(Level.FINE, "EJB Timer Service properties : " +
                   "min delivery interval = " + minimumDeliveryInterval_ +
                   "\nmax redeliveries = " + maxRedeliveries_ +
                   "\nredelivery interval = " + redeliveryInterval_);
    }

    synchronized void timedObjectCount() {
        totalTimedObjectsInitialized_++;
    }

    /**
     * Return the ownerId of the server instance in
     * which we are running.
     */
    String getOwnerIdOfThisServer() {
        return ownerIdOfThisServer_;
    }

    /**
     *--------------------------------------------------------------
     * Methods to be implemented for DistributedEJBTimerService
     *--------------------------------------------------------------
     */

    /**
     * Create EJBException using the exception that is passed in
     */
    private EJBException createEJBException( Exception ex ) {
        EJBException ejbEx = new EJBException();
        ejbEx.initCause(ex);
        return ejbEx;
    }

    /**
     * Provide a count of timers owned by each server
     */
    public String[] listTimers( String[] serverIds ) {
        String[] totalTimers = new String[ serverIds.length ];
        try {
            for ( int i = 0; i < serverIds.length; i++ ) {
                totalTimers[i] = new String(
                    new Integer(
                        timerLocalHome_.selectCountAllTimersOwnedBy(
                                serverIds[i] )).toString());
            }
        } catch( Exception ex ) {
            logger.log( Level.SEVERE, "Exception in listTimers() : " , ex );

            //Propogate any exceptions caught
            EJBException ejbEx = createEJBException( ex );
            throw ejbEx;
        }
        return totalTimers;
    }

    /**
     * Take ownership of another server's timers. 
     */
    public int migrateTimers(String fromOwnerId) {

        String ownerIdOfThisServer = getOwnerIdOfThisServer();

        if( fromOwnerId.equals(ownerIdOfThisServer) ) {
            /// Error. The server from which timers are being
            // migrated should never be up and running OR receive this
            // notification.
            logger.log(Level.WARNING, "Attempt to migrate timers from " +
                        "an active server instance " + ownerIdOfThisServer);
            throw new IllegalStateException("Attempt to migrate timers from " +
                                            " an active server instance " +
                                            ownerIdOfThisServer);
        }

        logger.log(Level.INFO, "Beginning timer migration process from " +
                   "owner " + fromOwnerId + " to " + ownerIdOfThisServer);

        TransactionManager tm = Switch.getSwitch().getTransactionManager();

        Set toRestore = new HashSet();

        try {
                             
            tm.begin();

            // The timer objects we'll use to set the new owner id come
            // from TimerMigrationBean
            Set toMigrate = timerMigrationLocalHome_.
                selectAllTimersOwnedBy(fromOwnerId);

            // The timer objects we'll use for restoring come from TimerBean
            // The same EJB QL query is used for selectAllTimersOwnedBy(owner)
            toRestore = timerLocalHome_.
                selectAllTimersOwnedBy(fromOwnerId);

            for(Iterator iter = toMigrate.iterator(); iter.hasNext();) {
                TimerMigrationLocal next  = (TimerMigrationLocal) iter.next();
                next.setOwnerId(ownerIdOfThisServer);
            }

            tm.commit();

        } catch(Exception e) {
            // Don't attempt to restore any timers since an error has
            // occurred.  This could be the expected result in the case that
            // multiple server instances attempted the migration at the same
            // time. 
            //FindBugs [Deadstore]: toRestore = new HashSet();

            logger.log(Level.FINE, "timer migration error", e);

            try {
                tm.rollback();
            } catch(Exception re) {
                logger.log(Level.FINE, "timer migration rollback error", re);
            }

            //Propagate the exception caught
            EJBException ejbEx = createEJBException( e );
            throw ejbEx;
        }

  int totalTimersMigrated = toRestore.size();

        if( toRestore.size() > 0 ) {

            boolean success = false;
            try {
               
                logger.log(Level.INFO, "Timer migration phase 1 complete. " +
                           "Changed ownership of " + toRestore.size() +
                           " timers.  Now reactivating timers...");

                tm.begin();
               
                _restoreTimers(toRestore);   
                success = true;
               
            } catch(Exception e) {

                logger.log(Level.FINE, "timer restoration error", e);

                //Propogate any exceptions caught as part of the transaction
                EJBException ejbEx = createEJBException( e );
                throw ejbEx;

            } finally {
                // We're not modifying any state in this tx so no harm in
                // always committing.
                try {
                    tm.commit();
                } catch(Exception re) {
                    logger.log(Level.FINE, "timer migration error", re);  

                    if( success ) {
                        //Propogate any exceptions caught when trying to commit
                        //the transaction
                        EJBException ejbEx = createEJBException( re );
                        throw ejbEx;
                    }
                }
            }
        } else {
            logger.log(Level.INFO, fromOwnerId + " has 0 timers in need " +
                       "of migration");                   
        }
       
        return totalTimersMigrated;

    } //migrateTimers()

    public void setPerformDBReadBeforeTimeout( boolean defaultDBReadValue ) {

        // If the system property com.sun.ejb.timer.ReadDBBeforeTimeout
        // has been defined by the user then use that value else use the default
        if ( !foundSysPropDBReadBeforeTimeout ) {
            performDBReadBeforeTimeout = defaultDBReadValue;

            if( logger.isLoggable(Level.FINE) ) {
                logger.log(Level.FINE, "EJB Timer Service property : " +
                           "\nread DB before timeout delivery = "
                           performDBReadBeforeTimeout);
            }

        }
    }

    /**
     * Check to see if the user has defined a System property to specify if
     * we need to check the timer table in the database and confirm that the
     * timer is valid before delivering the ejbTimeout() for that timer.
     *
     * In case of PE - the default value is false
     * and for SE/EE - the default value is true
     *
     * But in all cases (PE/SE/EE) the user can set the System property
     * "READ_DB_BEFORE_EJBTIMEOUT" to change the behaviour
     */
    private boolean getDBReadBeforeTimeoutProperty() {

        boolean result = false;
        try{
            Properties props = System.getProperties();
            String str=props.getProperty( strDBReadBeforeTimeout );
            if( null != str) {
    str = str.toLowerCase();
                performDBReadBeforeTimeout = Boolean.valueOf(str).booleanValue();

                if( logger.isLoggable(Level.FINE) ) {
                    logger.log(Level.FINE, "EJB Timer Service property : " +
                               "\nread DB before timeout delivery = "
                               performDBReadBeforeTimeout);
                }

                result = true;
            }
        } catch(Exception e) {
            logger.log(Level.INFO,
                "ContainerFactoryImpl.getDebugMonitoringDetails(), " +
                " Exception when trying to " +
                "get the System properties - ", e);
        }
        return result;
    }

    /**
     * Called at server startup *after* user apps have been re-activated
     * to restart any active EJB timers. 
     */
    void restoreTimers() throws Exception {

        // Optimization.  Skip timer restoration if there aren't any
        // applications with timed objects deployed. 
        if( totalTimedObjectsInitialized_ == 0 ) {
            return;
        }

        TransactionManager tm = Switch.getSwitch().getTransactionManager();
        Set allActiveTimers = new HashSet();
        try {
            // create a tx in which to do database access for all timers
            // needing restoration.  This gives us better performance that
            // doing individual transactions per timer.           
            tm.begin();

            // This operation can take a while, since in some configurations
            // this will be the first time the connection to the database
            // is initialized.  In addition, there's an initialization
            // cost to generating the SQL for the underlying
            // ejbql queries the first time any TimerBean query is called.
            allActiveTimers =
                timerLocalHome_.selectAllActiveTimersOwnedByThisServer();

            _restoreTimers(allActiveTimers);
        } catch(Exception e) {

            // Problem accessing timer service so disable it.
            ContainerFactoryImpl cf = (ContainerFactoryImpl)
                Switch.getSwitch().getContainerFactory();
            cf.setEJBTimerService(null);

            logger.log(Level.WARNING, "ejb.timer_service_init_error", e);

            // No need to propagate exception.  EJB Timer Service is disabled
            // but that won't affect the rest of the EJB container services.
            return;

        } finally {
            // try to commit regardless of success or failure.
            try {
                tm.commit();
            } catch(Exception e) {
                logger.log(Level.WARNING, "ejb.timer_service_init_error", e);
            }
        }
    }

    /**
     * The portion of timer restoration that deals with registering the
     * JDK timer tasks and checking for missed expirations.
     */
    private void _restoreTimers(Set timersEligibleForRestoration) {
                     
        // Do timer restoration in two passes.  The first pass updates
        // the timer cache with each timer.  The second pass schedules
        // the JDK timer tasks. 
       
        Map timersToRestore = new HashMap();

        for(Iterator iter = timersEligibleForRestoration.iterator();
            iter.hasNext();) {

            TimerLocal next  = (TimerLocal) iter.next();
            long containerId = next.getContainerId();

            // Timer might refer to an obsolete container.
            BaseContainer container = getContainer(containerId);
            if( container != null ) {
               
                TimerPrimaryKey timerId = (TimerPrimaryKey)next.getPrimaryKey();
                Date initialExpiration = next.getInitialExpiration();
              
                // Create an instance of RuntimeTimerState.

                // Only access timedObjectPrimaryKey if timed object is
                // an entity bean.  That allows us to lazily load the underlying
                // blob for stateless session and message-driven bean timers.
                Object timedObjectPrimaryKey = null;
                if( container instanceof EntityContainer ) {
                    timedObjectPrimaryKey = next.getTimedObjectPrimaryKey();
                }

                RuntimeTimerState timerState = new RuntimeTimerState
                    (timerId, initialExpiration,
                     next.getIntervalDuration(), container,
                     timedObjectPrimaryKey);                  

                timerCache_.addTimer(timerId, timerState);
               
                // If a single-action timer is still in the database it never
                // successfully delivered, so always reschedule a timer task
                // for it.  For periodic timers, we use the last known
                // expiration time to decide whether we need to fire one
                // ejbTimeout to make up for any missed ones.
                Date expirationTime = initialExpiration;
                Date now = new Date();

                if( timerState.isPeriodic() ) {
                    // lastExpiration time, or null if we either aren't
                    // tracking last expiration or an expiration hasn't
                    // occurred yet for this timer.
                    Date lastExpiration = next.getLastExpiration();

                    // @@@ need to handle case where last expiration time
                    // is not stored in database.  This will be the case
                    // when we add configuration for update-db-on-delivery.
                    // However, for now assume we do update the db on each
                    // ejbTimeout.  Therefore, if (lastExpirationTime == null),
                    // it means the timer didn't successfully complete any
                    // timer expirations.                 
                                                    
                    if( (lastExpiration == null) &&
                        now.after(initialExpiration) ) {
                       
                        // This timer didn't even expire one time.
                        logger.log(Level.INFO,
                                   "Rescheduling missed expiration for " +
                                   "periodic timer " +
                                   timerState + ". Timer expirations should " +
                                   " have been delivered starting at " +
                                   initialExpiration);

                        // keep expiration time at initialExpiration.  That
                        // will force an ejbTimeout almost immediately. After
                        // that the timer will return to fixed rate expiration.
                                               
                    } else if ( (lastExpiration != null) &&
                                ( (now.getTime() - lastExpiration.getTime()
                                   > next.getIntervalDuration()) ) ) {
                       
                        logger.log(Level.INFO,
                                   "Rescheduling missed expiration for " +
                                   "periodic timer " +
                                   timerState + ".  Last timer expiration " +
                                   "occurred at " + lastExpiration);
                       
                        // Timer expired at least once and at least one
                        // missed expiration has occurred.

                        // keep expiration time at initialExpiration.  That
                        // will force an ejbTimeout almost immediately. After
                        // that the timer will return to fixed rate expiration.
                       
                    } else {

                        // In this case, at least one expiration has occurred
                        // but that was less than one period ago so there were
                        // no missed expirations.                    
                        expirationTime =
                            calcNextFixedRateExpiration(timerState);
                    }
                   
                } else // single-action timer

                    if( now.after(initialExpiration) ) {
                        logger.log(Level.INFO,
                                   "Rescheduling missed expiration for " +
                                   "single-action timer " +
                                   timerState + ". Timer expiration should " +
                                   " have been delivered at " +
                                   initialExpiration);
                    }
                }

                timersToRestore.put(timerState, expirationTime);

            } else {
                // Timed object's container no longer exists.
                try {
                    next.remove();
                } catch(RemoveException e) {
                    logger.log(Level.FINE,
                        "Removing timer " + next.getPrimaryKey() +
                               " for unknown container " + containerId, e);
                }
            }
        } // End -- for each active timer

        for(Iterator entries = timersToRestore.entrySet().iterator();
            entries.hasNext(); ) {
            Map.Entry next  = (Map.Entry) entries.next();
            RuntimeTimerState nextTimer = (RuntimeTimerState) next.getKey();
            TimerPrimaryKey timerId    = nextTimer.getTimerId();
            Date expiration = (Date) next.getValue();
            scheduleTask(timerId, expiration);
            logger.log(Level.FINE,
                       "EJBTimerService.restoreTimers(), scheduling timer " +
                       nextTimer);
        }

        logger.log(Level.FINE, "DONE EJBTimerService.restoreTimers()");
    }

    void shutdown() {
        // Set flag to prevent any new timer expirations.
        shutdown_ = true;
    }

    /**
     * Cancel all timers associated with a particular entity bean
     * identity.  This is typically called when an entity bean is
     * removed.  Note that this action falls under the normal EJB
     * Timer removal semantics, which means it can be rolled back
     * if the transaction rolls back.
     */
    void cancelEntityBeanTimers(long containerId, Object primaryKey) {
        try {
            // Get *all* timers for this entity bean identity.  This includes
            // even timers *not* owned by this server instance, but that
            // are associated with the same entity bean and primary key.
            Collection timers = getTimers(containerId, primaryKey);
            if( logger.isLoggable(Level.FINE) ) {
                if( timers.isEmpty() ) {
                    logger.log(Level.FINE, "0 cancelEntityBeanTimers for " +
                               containerId + ", " + primaryKey);
                }
            }
            for(Iterator iter = timers.iterator(); iter.hasNext();) {
                TimerLocal next = (TimerLocal) iter.next();
                try {
                    cancelTimer(next);
                } catch(Exception e) {
                    logger.log(Level.WARNING, "ejb.cancel_entity_timer",
                               new Object[] { next.getPrimaryKey() });
                    logger.log(Level.WARNING, "", e);
                }
            }
        } catch(Exception e) {
            logger.log(Level.WARNING, "ejb.cancel_entity_timers",
                       new Object[] { new Long(containerId), primaryKey });
            logger.log(Level.WARNING, "", e);
        }
    }

    /**
     * Destroy all timers associated with a particular ejb container
     * and owned by this server instance. 
     * This is typically called when an ejb is undeployed.  It expunges
     * all timers whose timed object matches the given container.  In
     * the case of an entity bean container, all timers associated with
     * any of that container's entity bean identities will be destroyed.
     * This action *can not* be rolled back. 
     */
    void destroyTimers(long containerId) {
        Set timers = null;

        TransactionManager tm = Switch.getSwitch().getTransactionManager();

        try {
           
            // create a tx in which to do database access for all timers
            // that need to be deleted.  This gives us better performance that
            // doing individual transactions per timer.           
            tm.begin();
           
            // Get *all* timers for this ejb. Since the app is being undeployed
            // any server instance can delete all the timers for the same ejb.
            // Whichever one gets there first will actually do the delete.
            timers = timerLocalHome_.selectTimersByContainer(containerId);

            for(Iterator iter = timers.iterator(); iter.hasNext();) {
                TimerLocal next = (TimerLocal) iter.next();
                TimerPrimaryKey nextTimerId = null;
                RuntimeTimerState nextTimerState = null;
                try {
                    nextTimerId = (TimerPrimaryKey) next.getPrimaryKey();
                    nextTimerState = getTimerState(nextTimerId);
                    if( nextTimerState != null ) {
                        synchronized(nextTimerState) {
                            if( nextTimerState.isScheduled() ) {
                                EJBTimerTask timerTask =
                                    nextTimerState.getCurrentTimerTask();
                                timerTask.cancel();
                            }
                        }
                    }
                    next.remove();
                } catch(Exception e) {
                    logger.log(Level.WARNING, "ejb.destroy_timer_error",
                               new Object[] { nextTimerId });
                    logger.log(Level.WARNING, "", e);
                } finally {
                    if( nextTimerState != null ) {
                        timerCache_.removeTimer(nextTimerId);
                    }
                }
            }

        } catch(Exception ex) {
            logger.log(Level.WARNING, "destroy_timers_error",
                       new Object[] { new Long(containerId) });
            logger.log(Level.WARNING, "", ex);
            return;
        } finally {
            try {
                tm.commit();
            } catch(Exception e) {
                // Most likely caused by two or more server instances trying
                // to delete the timers for the same ejb at the same time.
                logger.log(Level.WARNING, "destroy_timers_error",
                           new Object[] { new Long(containerId) });
                logger.log(Level.WARNING, "", e);
            }
        }

        return;
    }

    void rescheduleTask(TimerPrimaryKey timerId, Date expiration) {
        scheduleTask(timerId, expiration, true);
    }

    void scheduleTask(TimerPrimaryKey timerId, Date expiration) {
        scheduleTask(timerId, expiration, false);
    }

    void scheduleTask(TimerPrimaryKey timerId, Date expiration,
                      boolean rescheduled) {
   
        RuntimeTimerState timerState = getTimerState(timerId);

        if( timerState != null ) {
            synchronized(timerState) {

                Date timerExpiration = expiration;

                if( !rescheduled ) {
                    // Guard against very small timer intervals. The EJB Timer
                    // service is defined in units of milliseconds, but it is
                    // intended for coarse-grained events.  Very small timer
                    // intervals (e.g. 1 millisecond) are likely to overload
                    // the server, so compensate by adjusting to a configurable
                    // minimum interval.
                    Date cutoff = new Date(new Date().getTime() +
                                           getMinimumDeliveryInterval());
                    if( expiration.before(cutoff) ) {
                        timerExpiration = cutoff;
                    }
                }

                EJBTimerTask timerTask =
                    new EJBTimerTask(timerExpiration, timerId, this);
                if( logger.isLoggable(Level.FINE) ) {
                    logger.log(Level.FINE, (rescheduled ? "RE-" : "") +
                               "Scheduling " + timerState +
                               " for timeout at " + timerExpiration);
                }
                if( rescheduled ) {
                    timerState.rescheduled(timerTask);
                } else {
                    timerState.scheduled(timerTask);
                }

                java.util.Timer jdkTimer = Switch.getSwitch().getTimer();
                jdkTimer.schedule(timerTask, timerExpiration);           
            }
        } else {
           
            logger.log(Level.FINE, "No timer state found for " +
                       (rescheduled ? "RE-schedule" : "schedule") +
                       " request of " + timerId +
                       " for timeout at " + expiration);           
        }
    }


    /**
     * Called from TimerBean to cancel the next scheduled expiration
     * for a timer.
     * @return (initialExpiration time) if state is CREATED or
     *         (time that expiration would have occurred) if state=SCHEDULED or
     *         (null) if state is BEING_DELIVERED or timer id not found
     */
    Date cancelTask(TimerPrimaryKey timerId) {
      
        Date timeout = null;

        RuntimeTimerState timerState = getTimerState(timerId);
        if( timerState != null ) {
            synchronized(timerState) {

                if( timerState.isCreated() ) {
                    timeout = timerState.getInitialExpiration();
                } else if( timerState.isScheduled() ) {
                    EJBTimerTask timerTask = timerState.getCurrentTimerTask();
                    timeout = timerTask.getTimeout();
                    timerTask.cancel();
                }
                timerState.cancelled();

            }
        } else {
            logger.log(Level.FINE, "No timer state found for " +
                       "cancelTask request of " + timerId);                  
        }

        return timeout;
    }

    /**
     * Called from TimerBean in case where Timer is cancelled from within
     * its own ejbTimeout method and then rolled back.
     */
    void restoreTaskToDelivered(TimerPrimaryKey timerId) {
       
        RuntimeTimerState timerState = getTimerState(timerId);
        if( timerState != null ) {
            synchronized(timerState) {
                timerState.restoredToDelivered();
            }
            if( logger.isLoggable(Level.FINE) ) {
                logger.log(Level.FINE, "Restoring " + timerId +
                   " to delivered state after it was cancelled and " +
                   " rolled back from within its own ejbTimeout method");
            }
        } else {
            logger.log(Level.FINE, "No timer state found for " +
                       "restoreTaskToDelivered request of " + timerId);      
        }
    }

    void expungeTimer(TimerPrimaryKey timerId) {
        // Expunge timer from timer cache without removing timer associated
        // timer bean.
        expungeTimer(timerId, false);
    }


    private Date calcInitialFixedRateExpiration(long timerServiceWentDownAt,
            RuntimeTimerState timerState)
    {
        if (!timerState.isPeriodic()) {
            throw new IllegalStateException();
        }
        Date now = new Date();

        long nowMillis = now.getTime();
        long initialExpiration = timerState.getInitialExpiration().getTime();

        long now2initialDiff = nowMillis - initialExpiration;
        long count = now2initialDiff / timerState.getIntervalDuration();
        long previousExpiration =
            initialExpiration  + (count * timerState.getIntervalDuration());

        if ((previousExpiration >= timerServiceWentDownAt)
                && (previousExpiration <= nowMillis))
        {
            //We certainly missed this one while the server was down
            logger.log(Level.INFO, "ejb.deliver_missed_timer",
                       new Object[] { timerState.getTimerId(),
                                      new Date(previousExpiration) });
            return now;
        } else {
            //Calculate the new expiration time
            return calcNextFixedRateExpiration(timerState);
        }

    }

    private Date calcNextFixedRateExpiration(RuntimeTimerState timerState) {

        if( !timerState.isPeriodic() ) {
            throw new IllegalStateException("Timer " + timerState + " is " +
                                            "not a periodic timer");
        }

        Date initialExpiration = timerState.getInitialExpiration();
        long intervalDuration  = timerState.getIntervalDuration();

        return calcNextFixedRateExpiration(initialExpiration, intervalDuration);
    }
   
    private Date calcNextFixedRateExpiration(Date initialExpiration,
                                             long intervalDuration) {      

        Date now = new Date();
        long nowMillis = now.getTime();

        // In simplest case, initial expiration hasn't even occurred yet.
        Date nextExpirationTime = initialExpiration;
       
        if( now.after(initialExpiration) ) {
            long timeSinceInitialExpire =
                (nowMillis - initialExpiration.getTime());
                               
            // number of time intervals since initial expiration.
            // intervalDuration is guaranteed to be >0 since this is a
            // periodic timer.
            long numIntervals =
                (timeSinceInitialExpire / intervalDuration);
           
            // Increment the number of intervals and multiply by the interval
            // duration to calculate the next fixed-rate boundary.
            nextExpirationTime = new Date(initialExpiration.getTime() +
                ((numIntervals + 1) * intervalDuration));
        }

        return nextExpirationTime;
    }

    /**
     * Remove all traces of a timer.  This should be written defensively
     * so that if expunge is called multiple times for the same timer id,
     * the second, third, fourth, etc. calls will not cause exceptions.
     */
    private void expungeTimer(TimerPrimaryKey timerId,
                              boolean removeTimerBean) {
        // First remove timer bean.  Don't update cache until
        // afterwards, since accessing of timer bean might require
        // access to timer state(e.g. timer application classloader)
        if( removeTimerBean ) {
            removeTimerBean(timerId);
        }
        timerCache_.removeTimer(timerId);
    }

    /**
     * @param primaryKey can be null if timed object is not an entity bean.
     * @return Primary key of newly created timer
     */
    TimerPrimaryKey createTimer(long containerId, Object timedObjectPrimaryKey,
                                long initialDuration, long intervalDuration,
                                Serializable info) throws CreateException {

        Date now = new Date();

        Date initialExpiration = new Date(now.getTime() + initialDuration);

        return createTimer(containerId, timedObjectPrimaryKey,
                           initialExpiration, intervalDuration, info);
    }

    /**
     * @param primaryKey can be null if timed object is not an entity bean.
     * @return Primary key of newly created timer
     */
    TimerPrimaryKey createTimer(long containerId, Object timedObjectPrimaryKey,
                                Date initialExpiration, long intervalDuration,
                                Serializable info) throws CreateException {

        BaseContainer container = getContainer(containerId);
        if( container == null ) {
            throw new CreateException("invalid container id " + containerId +
                                      " in createTimer request");
        }
       
        Class ejbClass = container.getEJBClass();
        if( !container.isTimedObject() ) {
            throw new CreateException
                ("Attempt to create an EJB Timer from a bean that is " +
                 "not a Timed Object.  EJB class " + ejbClass +
                 " must implement javax.ejb.TimedObject or " +
                 " annotation a timeout method with @Timeout");
        }

        TimerPrimaryKey timerId = new TimerPrimaryKey(getNextTimerId());

        RuntimeTimerState timerState =
            new RuntimeTimerState(timerId, initialExpiration,
                                  intervalDuration, container,
                                  timedObjectPrimaryKey);

        synchronized(timerState) {
            // Add timer entry before calling TimerBean.create, since
            // create() actions might call back on EJBTimerService and
            // need access to timer cache.
            timerCache_.addTimer(timerId, timerState);
            try {
                timerLocalHome_.create(timerId.getTimerId(), containerId,
                                       ownerIdOfThisServer_,
                                       timedObjectPrimaryKey,
                                       initialExpiration, intervalDuration,
                                       info);
            } catch(Exception e) {
                logger.log(Level.SEVERE, "ejb.create_timer_failure",
                           new Object[] { new Long(containerId),
                                          timedObjectPrimaryKey,
                                          info });
                logger.log(Level.SEVERE, "", e);
                // Since timer was never created, remove it from cache.
                timerCache_.removeTimer(timerId);
                if( e instanceof CreateException ) {
                    throw ((CreateException)e);
                } else {
                    EJBException ejbEx = new EJBException();
                    ejbEx.initCause(e);
                    throw ejbEx;
                }
            }
        }

        return timerId;
    }

    /**
     * Use database query to retrieve all active timers.  Results must
     * be transactionally consistent. E.g.,  a client calling
     * getTimers within a transaction where a timer has been
     * created but not committed "sees" the timer but a client
     * in a different transaction doesn't.
     *
     * @param primaryKey can be null if not entity bean
     *
     * @return Collection of TimerLocal objects.
     */
    private Collection getTimers(long containerId,
                                 Object timedObjectPrimaryKey)
        throws FinderException {

        // The results should include all timers for the given ejb
        // and/or primary key, including timers owned by other server instances.

        // @@@ Might want to consider cases where we can use
        // timer cache to avoid some database access in PE/SE, or
        // even in EE with the appropriate consistency tradeoff.
       
        Collection activeTimers =
            timerLocalHome_.selectActiveTimersByContainer(containerId);
       
        Collection timersForTimedObject = activeTimers;

        if( timedObjectPrimaryKey != null ) {
                                 
            // Database query itself can't do equality check on primary
            // key of timed object so perform check ourselves.
          
            timersForTimedObject = new HashSet();
           
            for(Iterator iter = activeTimers.iterator(); iter.hasNext();) {
                TimerLocal next = (TimerLocal) iter.next();
              
                Object nextTimedObjectPrimaryKey =
                    next.getTimedObjectPrimaryKey();
                if( nextTimedObjectPrimaryKey.equals(timedObjectPrimaryKey) ) {
                    timersForTimedObject.add(next);
                }
            }
        }

        return timersForTimedObject;
    }

    /**
     * Use database query to retrieve the timer ids of all active
     * timers.  Results must be transactionally consistent. E.g., 
     * a client calling getTimerIds within a transaction where a
     * timer has been created but not committed "sees" the timer
     * but a client in a different transaction doesn't. Called by
     * EJBTimerServiceWrapper when caller calls getTimers.
     *
     * @param primaryKey can be null if not entity bean
     * @return Collection of Timer Ids.
     */
    Collection getTimerIds(long containerId, Object timedObjectPrimaryKey)
        throws FinderException {

        // The results should include all timers for the given ejb
        // and/or primary key, including timers owned by other server instances.

        // @@@ Might want to consider cases where we can use
        // timer cache to avoid some database access in PE/SE, or
        // even in EE with the appropriate consistency tradeoff.             
       
        Collection timerIdsForTimedObject = new HashSet();

        if( timedObjectPrimaryKey == null ) {

            timerIdsForTimedObject =
                timerLocalHome_.selectActiveTimerIdsByContainer(containerId);

        } else {
                                 
            // Database query itself can't do equality check on primary
            // key of timed object so perform check ourselves.
          
            Collection timersForTimedObject = getTimers(containerId,
                                                        timedObjectPrimaryKey);

            timerIdsForTimedObject = new HashSet();
           
            for(Iterator iter = timersForTimedObject.iterator();
                iter.hasNext();) {
                TimerLocal next = (TimerLocal) iter.next();
                timerIdsForTimedObject.add(next.getPrimaryKey());
            }
        }

        return timerIdsForTimedObject;
    }
   
    /**
     * Get the application class loader for the timed object
     * that created a given timer.
     */
    ClassLoader getTimerClassLoader(long containerId) {      
        BaseContainer container = getContainer(containerId);       
        return (container != null) ? container.getClassLoader() : null;
    }

    TimerLocalHome getTimerBeanHome() {
        return timerLocalHome_;
    }

    private RuntimeTimerState getTimerState(TimerPrimaryKey timerId) {
        return timerCache_.getTimerState(timerId);
    }

    private TimerLocal findTimer(TimerPrimaryKey timerId)
        throws FinderException {
        return timerLocalHome_.findByPrimaryKey(timerId);
    }      

    //
    // Logic used by TimerWrapper for javax.ejb.Timer methods.
    //

    void cancelTimer(TimerPrimaryKey timerId)
        throws FinderException, Exception {

        // @@@ We can't assume this server instance owns the timer
        // so always ask the database.  Investigate possible use of
        // timer cache for optimization.

        // Look up timer bean from database.  Throws FinderException if
        // timer no longer exists.
        TimerLocal timerBean = findTimer(timerId);
        cancelTimer(timerBean);           
       
    }

    private void cancelTimer(TimerLocal timerBean) throws Exception {
        if( timerBean.isCancelled() ) {
            // Already cancelled within this tx.  Nothing more to do.
        } else {
            timerBean.cancel();
            timerBean.remove();
        }
    }

    /**
     * Return next planned timeout for this timer.  We have a fair amount
     * of leeway regarding the consistency of this information.  We should
     * strive to detect the case where the timer no longer exists.  However,
     * since the current timer instance may not even own this timer,
     * it's difficult to know the exact time of delivery in another server
     * instance.  In the case of single-action timers, we return the
     * expiration time that was provided upon timer creation.  For
     * periodic timers, we can derive the next scheduled fixed rate
     * expiration based on the initial expiration and the interval. 
     */
    Date getNextTimeout(TimerPrimaryKey timerId) throws FinderException {

        // @@@ We can't assume this server instance owns the timer
        // so always ask the database.  Investigate possible use of
        // timer cache for optimization.

        TimerLocal timerBean = findTimer(timerId);
        if( timerBean.isCancelled() ) {
            // The timer has been cancelled within this tx.
            throw new FinderException("timer " + timerId + " does not exist");
        }

        Date initialExpiration = timerBean.getInitialExpiration();

        Date nextTimeout = timerBean.repeats() ?
            calcNextFixedRateExpiration(initialExpiration,
                                        timerBean.getIntervalDuration()) :
            initialExpiration;

        return nextTimeout;
    }

    Serializable getInfo(TimerPrimaryKey timerId) throws FinderException {
       
        // @@@ We can't assume this server instance owns the timer
        // so always ask the database.  Investigate possible use of
        // timer cache for optimization.

        TimerLocal timerBean = findTimer(timerId);
        if( timerBean.isCancelled() ) {
            // The timer has been cancelled within this tx.
            throw new FinderException("timer " + timerId + " does not exist");
        }

        return timerBean.getInfo();
    }
   
    boolean timerExists(TimerPrimaryKey timerId) {
        boolean exists = false;

        // @@@ We can't assume this server instance owns the timer
        // so always ask the database.  Investigate possible use of
        // timer cache for optimization.
       
        try {
            TimerLocal timerBean = findTimer(timerId);
            // Make sure timer hasn't been cancelled within the current tx.
            exists = timerBean.isActive();
        } catch(FinderException fe) {
            exists = false;
        }
       
        return exists;
    }

    private void removeTimerBean(TimerPrimaryKey timerId) {
        try {
            TimerLocal timerBean = findTimer(timerId);
            timerBean.remove();
        } catch(Throwable t) {
            logger.log(Level.WARNING, "ejb.remove_timer_failure",
                       new Object[] { timerId });
            logger.log(Level.WARNING, "", t);
        }
    }

    private BaseContainer getContainer(long containerId) {
        ContainerFactory cf = Switch.getSwitch().getContainerFactory();
        return (BaseContainer) cf.getContainer(containerId);
    }

    /**
     * Called from timer thread.  Used to deliver ejb timeout.
     */
    private void deliverTimeout(TimerPrimaryKey timerId) {

        if( logger.isLoggable(Level.FINE) ) {
            logger.log(Level.FINE, "EJBTimerService.deliverTimeout(): work "
                       +
                       "thread is processing work for timerId = " + timerId);
        }

        if( shutdown_ ) {
            if( logger.isLoggable(Level.FINE) ) {
                logger.log(Level.FINE, "Cancelling timeout for " + timerId +
                           " due to server shutdown.  Expiration " +
                           " will occur when server is restarted.");
            }
            return;
        }
   
        RuntimeTimerState timerState = getTimerState(timerId);

        //
        // Make some defensive state checks.  It's possible that the
        // timer state changed between the time that the JDK timer task expired
        // and we got called on this thread.
        //

        if( timerState == null ) {
            logger.log(Level.FINE, "Timer state is NULL for timer " + timerId +
                       " in deliverTimeout");
            return;
        }

        BaseContainer container = getContainer(timerState.getContainerId());

        synchronized(timerState) {
            if( container == null ) {
                logger.log(Level.FINE, "Unknown container for timer " +
                           timerId + " in deliverTimeout.  Expunging timer.");
                expungeTimer(timerId, true);
                return;
            } else if ( !timerState.isBeingDelivered() ) {
                logger.log(Level.FINE, "Timer state = " +
                           timerState.stateToString() +
                           "for timer " + timerId + " before callEJBTimeout");
                return;
            } else {
                if( logger.isLoggable(Level.FINE) ) {
                    logger.log(Level.FINE, "Calling ejbTimeout for timer " +
                               timerState);
                }
            }
        }
        
        try {
                   
            Switch.getSwitch().getCallFlowAgent().
                    requestStart(RequestType.TIMER_EJB);
            container.onEnteringContainer();
            // Need to address the case that another server instance
            // cancelled this timer.  For maximum consistency, we will need
            // to do a database read before each delivery.  This can have
            // significant performance implications, so investigate possible
            // reduced consistency tradeoffs. 
            if( performDBReadBeforeTimeout) {

                if( logger.isLoggable(Level.FINE) ) {
                    logger.log(Level.FINE, "For Timer :" + timerId +
                    ": check the database to ensure that the timer is still " +
                    " valid, before delivering the ejbTimeout call" );
                }

                if( ! checkForTimerValidity(timerId) ) {
                    // The timer for which a ejbTimeout is about to be delivered
                    // is not present in the database. This could happen in the
                    // SE/EE case as other server instances (other than the owner)
                    // could call a cancel on the timer - deleting the timer from
                    // the database.
                    // Also it is possible that the timer is now owned by some other
                    // server instance
                    return;
                }
            }

            ///
            // Call container to invoke ejbTimeout on the bean instance.
            // The remaining actions are divided up into two categories :
            //
            // 1) Actions that should be done within the same transaction
            //    context as the ejbTimeout call itself.  These are
            //    handled by having the ejb container call back on the
            //    postEjbTimeout method after it has invoked bean.ejbTimeout
            //    but *before* it has called postInvoke.  That way any
            //    transactional operations like setting the last update time
            //    for periodic timers or removing a successfully delivered
            //    single-action timer can be done within the same tx as
            //    the ejbTimeout itself.  Note that there is no requirement
            //    that the ejbTimeout will be configured for CMT/RequiresNew.
            //    If there isn't a container-managed transaction,
            //    postEjbTimeout will still be called, and the database
            //    operations will be done in their own transaction.  While
            //    partitioning the actions like this adds some complexity,
            //    it's preferable to pushing this detailed timer semantics
            //    into the container's callEJBTimeout logic.
            //
            // 2) Post-processing for setting up next timer delivery and
            //    other redelivery conditions. 
            //
            boolean redeliver = container.callEJBTimeout(timerState, this);

            if( shutdown_ ) {
                // Server is shutting down so we can't finish processing
                // the timer expiration. 
                if( logger.isLoggable(Level.FINE) ) {
                    logger.log(Level.FINE, "Cancelling timeout for " + timerId
                               +
                               " due to server shutdown. Expiration will " +
                               " occur on server restart");
                }
                return;
            }

            // Resynchronize on timer state since a state change could have
            // happened either within ejbTimeout or somewhere else            

            timerState = getTimerState(timerId);

            if( timerState == null ) {
                // This isn't an error case.  The most likely reason is that
                // the ejbTimeout method itself cancelled the timer.
                logger.log(Level.FINE, "Timer no longer exists for " +
                           timerId + " after callEJBTimeout");
                return;
            }


            synchronized(timerState) {
                Date now = new Date();
                if( timerState.isCancelled() ) {
                    // nothing more to do.
                } else if( redeliver ) {
                    if( timerState.getNumFailedDeliveries() <
                        getMaxRedeliveries() ) {
                        Date redeliveryTimeout = new Date
                            (now.getTime() + getRedeliveryInterval());
                        if( logger.isLoggable(Level.FINE) ) {
                            logger.log(Level.FINE,"Redelivering " + timerState);
                        }
                        rescheduleTask(timerId, redeliveryTimeout);
                    } else {
                        int numDeliv = timerState.getNumFailedDeliveries() + 1;
                        logger.log(Level.INFO,
                           "ejb.timer_exceeded_max_deliveries",
                           new Object[] { timerState.toString(),
                                              new Integer(numDeliv)});
                        expungeTimer(timerId, true);
                    }
                } else if( timerState.isPeriodic() ) {

                    // Any necessary transactional operations would have
                    // been handled in postEjbTimeout callback.  Here, we
                    // just schedule the JDK timer task for the next ejbTimeout
                   
                    Date expiration = calcNextFixedRateExpiration(timerState);
                    scheduleTask(timerId, expiration);
                } else {
                  
                    // Any necessary transactional operations would have
                    // been handled in postEjbTimeout callback.  Nothing
                    // more to do for this single-action timer that was
                    // successfully delivered.                
                }
            }

        } catch(Exception e) {
            logger.log(Level.FINE, "callEJBTimeout threw exception " +
                       "for timer id " + timerId , e);
            expungeTimer(timerId, true);
        } finally {
            container.onLeavingContainer();
            Switch.getSwitch().getCallFlowAgent().requestEnd();
        }
    }

    /**
     *  Called from BaseContainer during callEJBTimeout after bean.ejbTimeout
     *  but before postInvoke.  NOTE that this method is called whether or not
     *  the ejbTimeout method is configured for container-managed transactions.
     *  This method is *NOT* called if the container has already determined
     *  that a redelivery is necessary.
     *
     *  @return true if successful , false otherwise. 
     */
    boolean postEjbTimeout(TimerPrimaryKey timerId) {   

        boolean success = true;

        if( shutdown_ ) {
            // Server is shutting down so we can't finish processing
            // the timer expiration. 
            return success;
        }

        // Resynchronize on timer state since a state change could have
        // happened either within ejbTimeout or somewhere else            

        RuntimeTimerState timerState = getTimerState(timerId);

        if( timerState != null ) {
       
            // Since the ejbTimeout was called successfully increment the
            // delivery count
            BaseContainer container = getContainer(timerState.getContainerId());
            container.incrementDeliveredTimedObject();
                                 
            synchronized(timerState) {
               
                if( timerState.isCancelled() ) {
                    // nothing more to do.
                } else {
                   
                    try {
                        TimerLocal timerBean = getValidTimerFromDB( timerId );
                        if( null == timerBean ) {
                            return false;
                        }

                        if( timerState.isPeriodic() ) {
                            Date now = new Date();
                            timerBean.setLastExpiration(now);  
                           
                            // Since timer was successfully delivered, update
                            // last delivery time in database if that option is
                            // enabled.
                            // @@@ add configuration for update-db-on-delivery
                            if( logger.isLoggable(Level.FINE) ) {
                                logger.log(Level.FINE,
                                           "Setting last expiration " +
                                           " for periodic timer " + timerState +
                                           " to " + now);
                            }                           

                        } else {
                                                       
                            if( logger.isLoggable(Level.FINE) ) {
                                logger.log(Level.FINE, "Single-action timer " +
                                   timerState + " was successfully delivered. "
                                   + " Removing...");
                            }

                            // Timer has expired sucessfully, so remove it.
                            cancelTimer(timerBean);
                        }
                    } catch(Exception e) {
                       
                        // @@@ i18N
                        logger.log(Level.WARNING, "Error in post-ejbTimeout " +
                                   "timer processing for " + timerState, e);
                        success = false;
                    }                                      
                }
            }
        }

        return success;
    }

    /**
     * This method is called to check if the timer is still valid.
     * In the SE/EE case the timer might be cancelled by any other
     * server instance (other than the owner server instance)
     * that is part of the same cluster. Until we have a messaging
     * system in place we would have to do a database query to
     * check if the timer is still valid.
     * Also check that the timer is owned by the current server instance
     *
     *  @return false if the timer record is not found in the database,
     *          true  if the timer is still valid.
     */
    private boolean checkForTimerValidity(TimerPrimaryKey timerId) {

        boolean result = true;

        TimerLocal timerBean = getValidTimerFromDB( timerId );
        if( null == timerBean) {
            result = false;
        }

        return result;
    }

    private TimerLocal getValidTimerFromDB(TimerPrimaryKey timerId) {

        boolean result       = true;
        TimerLocal timerBean = null;

        try {

            timerBean = findTimer(timerId);

            // There is a possibility that the same timer might be
            // migrated across to a different server. Hence check
            // that the ownerId of the timer record is the same as
            // the current server
            if( ! ( timerBean.getOwnerId().equals(
                ownerIdOfThisServer_) ) ) {
                logger.log(Level.WARNING,
                    "The timer (" + timerId + ") is not owned by " +
                    "server (" + ownerIdOfThisServer_ + ") that " +
                    "initiated the ejbTimeout. This timer is now " +
                    "owned by (" + timerBean.getOwnerId() + "). \n" +
                    "Hence delete the timer from " +
                    ownerIdOfThisServer_ + "'s cache.");

                result = false;
            }

        } catch( FinderException fex ) {
            // The timer does not exist in the database
            if( logger.isLoggable(Level.FINE) ) {
                logger.log(Level.FINE, "Timer :" + timerId +
                    ": has been cancelled by another server instance. " +
                    "Expunging the timer from " + ownerIdOfThisServer_ +
                    "'s cache.");
            }

            result = false;

        } finally {
            if( !result ) {
                // The timer is either not present in the database or it is now
                // owned by some other server instance, hence remove the cache
                //entry for the timer from the current server
                expungeTimer(timerId, false);
                timerBean = null;
            } else {
                if( logger.isLoggable(Level.FINE) ) {
                    logger.log(Level.FINE,
                        "The Timer :" + timerId +
                        ": is a valid timer for the server (" +
                        ownerIdOfThisServer_ + ")");
                }
            }
        }

        return timerBean;
    }

    /**
     * This method is called back from the EJBTimerTask object
     * on the JDK Timer Thread.  Work performed in this callback
     * should be short-lived, so do a little bookkeeping and then
     * launch a separate thread to invoke ejbTimeout, etc.
     */

    void taskExpired(TimerPrimaryKey timerId) {
        RuntimeTimerState timerState = getTimerState(timerId);

        if( timerState != null ) {
            synchronized(timerState) {
                if( timerState.isScheduled() ) {
                    timerState.delivered();

                    if( logger.isLoggable(Level.FINE) ) {
                        logger.log(Level.FINE,
                           "Adding work pool task for timer " + timerId);
                    }

                    TaskExpiredWork work = new TaskExpiredWork(this, timerId);
                    com.sun.ejb.containers.util.ContainerWorkPool.addLast(work);
                } else {
                    logger.log(Level.FINE, "Timer " + timerId +
                               " is not in scheduled state.  Current state = "
                               + timerState.stateToString());
                }
            }
        } else {
            logger.log(Level.FINE, "null timer state for timer id " + timerId);
        }

        return;
    }

    /**
     * Generate a unique key for the persistent timer object.
     * Key must be unique across server shutdown and startup, and
     * within all server instances sharing the same timer table.
     */
    private synchronized String getNextTimerId() {

        if( nextTimerIdCounter_ <= 0 ) {
            nextTimerIdMillis_  = System.currentTimeMillis();
            nextTimerIdCounter_ = 1;
        } else {
            nextTimerIdCounter_++;
        }

        // @@@ Add cluster ID

        return new String(nextTimerIdCounter_ +
                          TIMER_ID_SEP + nextTimerIdMillis_ +
                          TIMER_ID_SEP + serverName_ +
                          TIMER_ID_SEP + domainName_);
    }

    //
    // Accessors for timer service properties.
    //
    private long getMinimumDeliveryInterval() {
        return minimumDeliveryInterval_;
    }

    private long getMaxRedeliveries() {
        return maxRedeliveries_;
    }

    private long getRedeliveryInterval() {
        return redeliveryInterval_;
    }

    //
    // This is a global cache of timer data *only for timers owned by
    // this server instance*.  It is not transactionally
    // consistent.  Operations requiring those semantics should query
    // the database for TimerBean info.  Any timer for which there is an
    // active JDK timer task must be contained within this cache.
    //
    // Note : this class supports concurrent access.
    //
    private class TimerCache {

        // Maps timer id to timer state.
        private Map timers_;

        // Map of timer information per container.
        //
        // For stateless session beans and message-driven beans,
        // container id is mapped to a Long value representing the
        // number of timers.
        //
        //
        // For entity beans, container id is mapped to a list of
        // primary keys.  NOTE : This list can contain duplicate primary keys
        // in the case where the same entity bean identity has more
        // than one associated timer.
       
        private Map containerTimers_;

        public TimerCache() {
            // Create unsynchronized collections.  TimerCache will
            // provide concurrency control.
            timers_ = new HashMap();
            containerTimers_ = new HashMap();
        }

        public synchronized void addTimer(TimerPrimaryKey timerId,
                                          RuntimeTimerState timerState) {
            if( logger.isLoggable(Level.FINE) ) {
                logger.log(Level.FINE, "Adding timer " + timerState);
            }

            timers_.put(timerId, timerState);

            Long containerId = new Long(timerState.getContainerId());

            Object containerInfo = containerTimers_.get(containerId);

            if( timerState.timedObjectIsEntity() ) {
                Collection entityBeans;
                if( containerInfo == null ) {
                    // NOTE : This list *can* contain duplicates, since
                    // the same entity bean can be the timed object for
                    // multiple timers.
                    entityBeans = new ArrayList();
                    containerTimers_.put(containerId, entityBeans);
                } else {
                    entityBeans = (Collection) containerInfo;
                }
                entityBeans.add(timerState.getTimedObjectPrimaryKey());
            } else {
                Long timerCount = (containerInfo == null) ? new Long(1) :
                    new Long(((Long) containerInfo).longValue() + 1);
                containerTimers_.put(containerId, timerCount);
            }

        }

        /**
         * Remove a timer from the cache. This should be coded
         * defensively since it's possible it will be called multiple
         * times for the same timer.
         */
        public synchronized void removeTimer(TimerPrimaryKey timerId) {
            if( logger.isLoggable(Level.FINE) ) {
                logger.log(Level.FINE, "Removing timer " + timerId);
            }

            RuntimeTimerState timerState = (RuntimeTimerState)
                timers_.remove(timerId);

            if( timerState == null) {
                return;
            }

            Long containerId = new Long(timerState.getContainerId());
            Object containerInfo = containerTimers_.get(containerId);
               
            if( containerInfo != null ) {
                if( timerState.timedObjectIsEntity() ) {
                    Collection entityBeans = (Collection) containerInfo;
                    if( entityBeans.size() == 1 ) {
                        // Only one left -- blow away the container.
                        containerTimers_.remove(containerId);
                    } else {
                        // Remove a single instance of this primary key
                        // from the list.  There could still be other
                        // instances of the same primary key.
                        entityBeans.remove
                            (timerState.getTimedObjectPrimaryKey());
                    }
                } else {
                    long timerCount = ((Long) containerInfo).longValue();
                    if( timerCount == 1 ) {
                        // Only one left -- blow away the container
                        containerTimers_.remove(containerId);
                    } else {
                        Long newCount = new Long(timerCount - 1);
                        containerTimers_.put(containerId, newCount);
                    }                        
                }
            }
        }

        public synchronized RuntimeTimerState getTimerState(TimerPrimaryKey
                                                            timerId) {
            return (RuntimeTimerState) timers_.get(timerId);
        }

        // True if the given entity bean has any timers and false otherwise.
        public synchronized boolean entityBeanHasTimers(long containerId,
                                                        Object pkey) {
            Object containerInfo = containerTimers_.get(new Long(containerId));
            return (containerInfo != null) ?
                ((Collection) containerInfo).contains(pkey) : false;
        }

        // True if the ejb represented by this container id has any timers
        // and false otherwise. 
        public synchronized boolean containerHasTimers(long containerId) {
            return containerTimers_.containsKey(new Long(containerId));
        }

        // Placeholder for logic to ensure timer cache consistency.
        public synchronized void validate() {
        }

    } //TimerCache{}

    private File getTimerServiceShutdownFile()
        throws Exception
    {
        File timerServiceShutdownDirectory;
        File timerServiceShutdownFile;

        InstanceEnvironment env = ApplicationServer.getServerContext().
                                    getInstanceEnvironment();
        AppsManager appsManager = new AppsManager(env, false);

        String j2eeAppPath = appsManager.getLocation(appID);
        timerServiceShutdownDirectory = new File(j2eeAppPath + File.separator);
        timerServiceShutdownDirectory.mkdirs();
        timerServiceShutdownFile = new File(j2eeAppPath + File.separator
                + TIMER_SERVICE_FILE);

        return timerServiceShutdownFile;
    }

    private long getTimerServiceDownAt() {
        long timerServiceWentDownAt = -1;
        try {
            File timerServiceShutdownFile  = getTimerServiceShutdownFile();

            if (timerServiceShutdownFile.exists()) {
                DateFormat dateFormat = 
                    new SimpleDateFormat(TIMER_SERVICE_DOWNTIME_FORMAT);
       
                FileReader fr = new FileReader(timerServiceShutdownFile);
                BufferedReader br = new BufferedReader(fr, 128);
                String line = br.readLine();

                Date myDate = dateFormat.parse(line);
                timerServiceWentDownAt = myDate.getTime();
                logger.log(Level.INFO, "ejb.timer_service_last_shutdown",
                           new Object[] { line });
            } else {
                logger.log(Level.WARNING, "ejb.timer_service_shutdown_unknown",
                           new Object[] { timerServiceShutdownFile });
            }
        } catch (Throwable th) {
            logger.log(Level.WARNING, "ejb.timer_service_shutdown_unknown",
                       new Object[] { "" });
            logger.log(Level.WARNING, "", th);
        }
        return timerServiceWentDownAt;
    }


    /**
     * Called from TimerBeanContainer
     */
    public void onShutdown() {
        try {
            DateFormat dateFormat = 
                new SimpleDateFormat(TIMER_SERVICE_DOWNTIME_FORMAT);
            String downTimeStr = dateFormat.format(new Date());

            File timerServiceShutdownFile  = getTimerServiceShutdownFile();
            FileWriter fw = new FileWriter(timerServiceShutdownFile);
            PrintWriter pw = new PrintWriter(fw);

            pw.println(downTimeStr);

            pw.flush();
            pw.close();
            fw.close();
            logger.log(Level.INFO, "ejb.timer_service_shutdown_msg",
                       new Object[] { downTimeStr });
        } catch (Throwable th) {
            logger.log(Level.WARNING, "ejb.timer_service_shutdown_unknown",
                       new Object[] { TIMER_SERVICE_FILE });
            logger.log(Level.WARNING, "", th);
        }
    }


    /**
     * This class gets a callback on a worker thread where the actual
     * ejbTimeout invocation will be made. 
     */
    private static class TaskExpiredWork
        implements com.sun.enterprise.util.threadpool.Servicable
    {
        private EJBTimerService timerService_;
        private TimerPrimaryKey timerId_;

        public TaskExpiredWork(EJBTimerService timerService,
                               TimerPrimaryKey timerId) {
            timerService_ = timerService;
            timerId_ = timerId;
        }

        public void prolog() { }
       
        public void epilog() { }
       
        public void service() { run(); }
       
        public void run() {
            // Delegate to Timer Service.
            timerService_.deliverTimeout(timerId_);
        }

    } // TaskExpiredWork

}
TOP

Related Classes of com.sun.ejb.containers.EJBTimerService$TimerCache

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.