6 Replies Latest reply on Aug 3, 2004 2:10 PM by john_anderson_ii

    OutOfMemoryError - Singleton PK Generator

    john_anderson_ii

      As a learning project, I'm writing an OSS J2EE application for help desk call tracking. The "tickets" use their ticket number as their primary key in the database, and the TICKETS table is access via a CMP bean. For several reasons I've decided to not use the auto-increment method for generating the PKs. Mainly I would like to remain as database independent as possible, and I would like to recyle the ticket numbers of tickets that are closed and pushed out of the database into an XML archive. To accomplish generating the Primary Keys (ticket numbers) I've written the following singleton bean following the wonderful example provided on these forums.*

      The following bean basically creates a TreeSet with all possible primary keys, then removes the primary keys that are already used. This bean does all this on first use, and remains in memory so that subsequent requests for new PKs are fast.

      /*
       * Created on Jun 28, 2004
       *
       * To change the template for this generated file go to
       * Window>Preferences>Java>Code Generation>Code and Comments
       */
      package org.jcallcenter.ejb.core;
      
      import java.rmi.RemoteException;
      
      import javax.ejb.EJBException;
      import javax.ejb.EntityBean;
      import javax.ejb.EntityContext;
      import javax.ejb.RemoveException;
      import javax.ejb.CreateException;
      import javax.ejb.ObjectNotFoundException;
      
      import javax.naming.Context;
      import javax.naming.InitialContext;
      
      import java.util.TreeSet;
      import java.util.Iterator;
      
      import javax.sql.DataSource;
      
      import java.sql.ResultSet;
      import java.sql.PreparedStatement;
      import java.sql.Connection;
      import java.sql.SQLException;
      
      import org.jcallcenter.supporting.core.EJBNameFactory;
      
      /**
       * @author janderson
       *
       * @ejb.bean
       * name = "PKTicket"
       * display-name = "PKTicket EJB"
       * view-type = "local"
       * jndi-name = "ejb/callcenter/core/PKTicket"
       * schema = "PKTICKET"
       * type = "BMP"
       * primkey-field = "MyPK"
       *
       * @jboss.persistence
       * create-table = "true"
       * table-name = "PKTICKET"
       *
       * @ejb.persistence
       * table-name = "PKTICKET"
       *
       * This class will act as a singleton for tracking Primary Keys. For all practical
       * purposes it will look like a normal entity bean attached to a database table.
       * However, it won't really be attached to any specific table.
       *
       * To change the template for this generated type comment go to
       * Window>Preferences>Java>Code Generation>Code and Comments
       */
      public class PKTicketBean implements EntityBean {
      
       /*
       * Get the name of the Datasource
       */
       private static final String DATASOURCE_NAME = EJBNameFactory.DATASOURCE_NAME;
      
       /*
       * Declare the maximum value for a Ticket Number (primary Key)
       */
      
       private static final Integer MAX_VALUE = new Integer(999999);
       /*
       * Declare the Entity Context
       */
      
       private EntityContext ctx;
      
       /*
       * The following string will be used as a fake primary key for the fake table
       * this bean belongs to. Since this is a singleton only one instance will actually
       * exist, but a primary key is required by the container.
       */
      
       public String defaultName;
      
       /*
       * An Tree to hold the Primary Keys from the ticket table.
       *
       */
      
       private TreeSet pks = new TreeSet();
      
       /*
       * An Integer to hold the PK of this object.
       */
      
       private Integer pk = new Integer(0);
      
       /*
       * This string will be used to set the name, think primary key, of this bean.
       */
      
       public static final String MY_NAME = "default";
       /*
       * A Tree to hold the Available Primary Keys.
       */
       private TreeSet availPks = new TreeSet();
      
       /*
       * Generic constructor method.
       */
       public PKTicketBean() {
       super();
      
       }
      
       /*
       * This method returns an Integer that contains the next primary key from
       * the ticket table. If the next primary key is out of range, this method
       * returns a -1.
       */
       /**
       * @ejb.interface-method
       */
       public Integer getPKs(){
       Integer I = (Integer) availPks.first();
       int comp = I.compareTo(MAX_VALUE);
       Integer rval = new Integer(-1);
       if(comp < 0){
       rval = I;
       availPks.remove(I);
       }
       return rval;
       }
       /*
       * The next two getters and setter will never be used. This is just to keep
       * the EJB Spec happy.
       */
       /**
       *
       */
       public String getMyPK(){
       return defaultName;
       }
       public void setMyPK(String name){
       this.defaultName = name;
       }
       /*
       * This method should never be used, but it can be used to reset the pk or
       * something maybe.
       */
       /**
       * @ejb.interface-method
       */
       public void setPK(Integer pk){
       this.pk = pk;
       }
      
       public void ejbActivate() throws EJBException, RemoteException {
      
      
       }
       /*
       * It is here that we will intitialize the TreeList pks. This should be done
       * once and only once upon deployment. It should be re-initialized whenever
       * records are purged off. It will have to be called implicity since it has
       * no means for detection.
       *
       */
      
       public void ejbLoad() throws EJBException, RemoteException {
       Connection c = null;
       DataSource dataSource;
       String sql = "SELECT ID FROM TICKET";
       PreparedStatement statement = null;
       Context context = null;
       ResultSet rs = null;
       //Replace me with a log4j directive.
       System.out.print("Refreshing the Ticket ID pool. This can take time...");
       try{
       context = new InitialContext();
       dataSource = (DataSource) context.lookup(DATASOURCE_NAME);
       c = dataSource.getConnection();
       statement = c.prepareStatement(sql);
       rs = statement.executeQuery();
       if(rs.next()){ //Rows were returned, pks exists.
       rs.beforeFirst();
       while(rs.next()){
       Integer I = new Integer(rs.getInt(1));
       //replace me with a log4j directive
       System.out.print(".");
       pks.add(new Integer(rs.getInt(1)));
      
       }
       }else{ //Rows were not returned, no PKs exists yet.
       pks.add(new Integer(0));
       }
       System.out.println(".Done!");
       }catch(Exception e){
       throw new EJBException("An Error was encountered while getting used Ticket ID's"+
       ". Message: " +e);
       }finally{
       try{
       c.close();
       }catch(SQLException e){
       throw new EJBException("An Error was encountered while getting used Ticket ID's"+
       ". Message: " +e);
       }
       }
       initializeAvailablePKs();
       }
      
       /*
       * This method will take the PKs pulled from the DB and compare it to a list
       * of all possible PKs. The PKs from the DB will be removed from the list of
       * available PKs.
       */
       private void initializeAvailablePKs(){
       int counter = MAX_VALUE.intValue();
       while(counter >= 0){
       availPks.add(new Integer(counter));
       counter--;
       }
       Iterator iterator = pks.iterator();
       while(iterator.hasNext()){
       Integer removal = (Integer) iterator.next();
       System.out.println("Removing Key " +removal.toString()+ " from Available PKs");
       boolean b = availPks.remove(removal);
       if(b){
       System.out.println("Removed!");
       }
       }
       }
       /*
       * This method should probably not ever need to be called unless records
       * have be purched off to be archived. It can be can be
       * used to refresh the data in this bean.
       */
      
       public String ejbCreate() throws CreateException, RemoteException{
       this.ejbLoad();
       return null;
       }
       /*EJB Happiness Method */
       public void ejbPostCreate(){
       }
       /*
       * Okay,this may seem a bit weird. We can't use localhome.create() to get
       * a reference to this bean because we would be refreshing the data every
       * single time we called it. Instead we want to return a reference to this
       * bean by Primary Key....er rather our fake primary key since we aren't really
       * persisting any data anyway.
       */
       public String ejbFindByPrimaryKey(String defaultName)throws ObjectNotFoundException,
       ObjectNotFoundException {
       if(!defaultName.equals(MY_NAME)){
       throw new ObjectNotFoundException();
       }else{
       this.defaultName = defaultName;
       }
       return defaultName;
       }
       /*Generic EJB Methods follow*/
       public void ejbPassivate() throws EJBException, RemoteException {
      
       }
      
       public void ejbRemove()
       throws RemoveException, EJBException, RemoteException {
      
       }
      
       public void ejbStore() throws EJBException, RemoteException {
      
       }
      
       public void setEntityContext(EntityContext arg0)
       throws EJBException, RemoteException {
       this.ctx = arg0;
       }
      
       public void unsetEntityContext() throws EJBException, RemoteException {
       ctx = null;
       }
      
      }
      


      The following code is from the CMP bean's facade that get's the next available primary key from PKTicket and passes it to Ticket's ejbCreate() method.

      /*
       * Created on Jun 22, 2004
       *
       * To change the template for this generated file go to
       * Window>Preferences>Java>Code Generation>Code and Comments
       */
      package org.jcallcenter.ejb.core;
      
      import java.rmi.RemoteException;
      
      import javax.ejb.EJBException;
      import javax.ejb.SessionBean;
      import javax.ejb.SessionContext;
      import javax.ejb.CreateException;
      import javax.ejb.ObjectNotFoundException;
      
      import javax.naming.Context;
      import javax.naming.InitialContext;
      import javax.naming.NamingException;
      
      
      import org.jcallcenter.interfaces.core.*;
      import org.jcallcenter.supporting.core.*;
      
      /**
       * @author janderson
       *
       * @ejb.bean
       * name = "TicketFacade"
       * display-name = "TicketFacade EJB"
       * description = "Accessor for TicketBean"
       * view-type = "remote"
       * type = "Stateless"
       * jndi-name = "ejb/CallCenter/Core/TicketFacade"
       * @ejb.ejb-ref
       * ejb-name = "Ticket"
       * ref-name = "ejb/CallCenter/Core/Ticket"
       * view-type = "local"
       * @jboss.ejb-local-ref
       * ref-name = "ejb/CallCenter/Core/Ticket"
       * jndi-name = "CallCenter/Core/Ticket"
       * @ejb.ejb-ref
       * ejb-name = "PKTicket"
       * ref-name = "ejb/CallCenter/Core/PKTicket"
       * view-type = "local"
       * @jboss.ejb-local-ref
       * ref-name = "ejb/CallCenter/Core/PKTicket"
       * jndi-name = "CallCenter/Core/PKTicket"
       *
       *
       * To change the template for this generated type comment go to
       * Window>Preferences>Java>Code Generation>Code and Comments
       */
      public class TicketFacadeBean implements SessionBean {
       TicketLocalHome ticketHome = null;
       PKTicketLocalHome pksHome = null;
      
       /**
       *
       */
       public TicketFacadeBean() {
       super();
       }
       /*
       * The next methods are for looking up local interfaces.
       */
       /**
       * @ejb.interface-method
       *
       */
      
       public void newTicket(){
       try{
       PKTicketLocal pks = pksHome.findByPrimaryKey(PKTicketBean.MY_NAME);
       Integer I = pks.getPKs();
       TicketLocal ticket = ticketHome.create(I,"This is a test ticket");
       }catch(CreateException e){
       throw new EJBException("There was a problem creating a new ticket. Message: " +e);
       }catch(ObjectNotFoundException e){
       throw new EJBException("The ticket number generator is lost. Message: " +e);
       }
       }
       /*
       * The next three methods are generic initialization and create methods.
       */
       private TicketLocalHome getTicketBeanLocalHome() throws NamingException{
       try{
       Context context = new InitialContext();
       return (TicketLocalHome) context.lookup(EJBNameFactory.TICKET_BEAN);
       }catch(NamingException e){
       throw new EJBException("There was an error looking up the JNDI name of Ticket EJB. Message: " +e);
       }
       }
       private PKTicketLocalHome getPKTicketLocalHome(){
       try{
       Context context = new InitialContext();
       return (PKTicketLocalHome) context.lookup(EJBNameFactory.PKTICKET_BEAN);
       }catch(NamingException e){
       throw new EJBException("There was an error looking up the JNDI name of PKTicket EJB. Message: " +e);
       }
       }
       /**
       * @ejb.create-method
       * @throws CreateException
       */
       public void ejbCreate() throws CreateException{
       try{
       ticketHome = getTicketBeanLocalHome();
       pksHome = getPKTicketLocalHome();
       }catch(NamingException e){
       throw new EJBException("There was an error looking up the JNDI name of Ticket EJB. Message: " +e);
       }
       }
       public void ejbActivate() throws EJBException, RemoteException {
       try{
       ticketHome = getTicketBeanLocalHome();
       }catch(NamingException e){
       throw new EJBException("There was an error looking up the JNDI name of Ticket EJB. Message: " +e);
       }
       }
       /*
       * Generic Stubs (non-Javadoc)
       * @see javax.ejb.SessionBean#ejbPassivate()
       */
       public void ejbPassivate() throws EJBException, RemoteException {
      
       }
       public void ejbRemove() throws EJBException, RemoteException {
      
      
       }
      
       public void setSessionContext(SessionContext arg0)
       throws EJBException, RemoteException {
      
       }
      
      }
      


      Finally, the next two code samples are the CMP EJB Ticket and the servlet that I'm using to test this setup.

      /*
       * Created on Jun 21, 2004
       *
       * To change the template for this generated file go to
       * Window>Preferences>Java>Code Generation>Code and Comments
       */
      package org.jcallcenter.ejb.core;
      
      import java.rmi.RemoteException;
      
      import javax.ejb.EJBException;
      import javax.ejb.EntityBean;
      import javax.ejb.EntityContext;
      import javax.ejb.RemoveException;
      import javax.ejb.CreateException;
      
      /**
       * @author janderson
       *
       * @ejb.bean
       * display-name = "Ticket EJB"
       * description = "Getters and setters for tickets"
       * name = "Ticket"
       * view-type = "local"
       * jndi-name = "ejb/CallCenter/Core/Ticket"
       * type = "CMP"
       * cmp-version = "2.x"
       * primkey-field = "ID"
       * schema = "TICKET"
       * @ejb.persistence
       * table-name = "TICKET"
       * @jboss.persistence
       * create-table = "true"
       * table-name = "TICKET"
       * post-table-create = "ALTER TABLE TICKET MODIFY ID INT UNSIGNED"
       * @ejb.finder
       * query = "Select Object(o) FROM TICKET o"
       * signature = "java.util.Collection findAll()"
       * unchecked = "true"
       *
       */
      
      public abstract class TicketBean implements EntityBean {
      
       /**
       * This class will hold generic TicketBean information.
       */
       public TicketBean() {
       super();
       }
       /**
       * @ejb.persistence
       * column-name = "ID"
       * jdbc-type = "INTEGER"
       * sql-type = "INTEGER"
       * @jboss.persistence
       * not-null = "true"
       * @jboss.jdbc-type
       * type = "INTEGER"
       * @jboss.sql-type
       * type = "INTEGER"
       *
       * @ejb.interface-method
       * @return
       */
       public abstract Integer getID();
      
      
       public abstract void setID(Integer ID);
       /**
       * @ejb.persistence
       * column-name = "DESCRIPTION"
       * jdbc-type = "VARCHAR"
       * sql-type = "VARCHAR(100)"
       * @jboss.persistence
       * not-null = "true"
       * @jboss.jdbc-type
       * type = "VARCHAR"
       * @jboss.sql-type
       * type = "VARCHAR(100)
       * "
       * @ejb.interface-method
       * @return
       */
       public abstract String getDesc();
       /**
       * @ejb.interface-method
       * @param Desc
       */
       public abstract void setDesc(String Desc);
      
       /**
       * @ejb.create-method
       * @param s
       * @throws CreateException
       */
      
       public Integer ejbCreate(Integer I, String s) throws CreateException{
       setDesc(s);
       setID(I);
       return null;
       }
      
       public void ejbPostCreate(Integer I, String s){}
      
       public void ejbActivate(String S) throws EJBException, RemoteException {
       }
       public void ejbLoad() throws EJBException, RemoteException {
       }
       public void ejbPassivate() throws EJBException, RemoteException {
       }
       public void ejbRemove()
       throws RemoveException, EJBException, RemoteException {
       }
       public void ejbStore() throws EJBException, RemoteException {
       }
       public void setEntityContext(EntityContext arg0)
       throws EJBException, RemoteException {
       }
       public void unsetEntityContext() throws EJBException, RemoteException {
       }
      
      }
      


      servlet...

      /*
       * Created on Jun 22, 2004
       *
       * To change the template for this generated file go to
       * Window>Preferences>Java>Code Generation>Code and Comments
       */
      package org.jcallcenter.web.core;
      
      import java.io.IOException;
      
      import javax.servlet.ServletException;
      import javax.servlet.http.HttpServlet;
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      import javax.naming.Context;
      import javax.naming.InitialContext;
      import javax.naming.NamingException;
      import javax.rmi.PortableRemoteObject;
      import java.io.PrintWriter;
      import org.jcallcenter.interfaces.core.TicketFacade;
      import org.jcallcenter.interfaces.core.TicketFacadeHome;
      /**
       * @author janderson
       *
       * *@web.servlet
       * name = "TicketTestServlet"
       * display-name = "Testing Auto-increment"
       * description = "None Available"
       *
       *@web.servlet-mapping
       * url-pattern = "/tickettest"
       *@web.ejb-ref
       * name = "ejb/TicketFacade"
       * type = "Session"
       * home = "TicketFacadeHome"
       * remote = "TicketFacade"
       * description = "The CMP Bean TicketFacade"
       *@jboss.ejb-ref-jndi
       * ref-name = "ejb/TicketFacade"
       * jndi-name = "ejb/CallCenter/Core/TicketFacade"
       *
       * To change the template for this generated type comment go to
       * Window>Preferences>Java>Code Generation>Code and Comments
       */
      public class TicketTestServlet extends HttpServlet {
       TicketFacadeHome ticketHome = null;
       /**
       *
       */
       public TicketTestServlet() {
       super();
       }
       protected void doPost(HttpServletRequest arg0, HttpServletResponse arg1)
       throws ServletException, IOException {
       arg1.setContentType("text/html");
       PrintWriter out = arg1.getWriter();
       out.println("<html><head><title>Ticket Test</title></head><body>");
       try{
       for(int i=0; i< 900000; i++){
       TicketFacade facade = ticketHome.create();
       facade.newTicket();
       }
       }catch(Exception e){
       System.out.println("didn't work" + e.getMessage());
       e.printStackTrace();
       }
       out.println("<h1>Success</h1>");
       out.println("</body></html>");
      
       }
       public void init() throws ServletException {
       try{
       Context ctx = new InitialContext();
       Object ref = ctx.lookup("java:/comp/env/ejb/TicketFacade");
       ticketHome = (TicketFacadeHome) PortableRemoteObject.narrow(ref,
       TicketFacadeHome.class);
       }catch(NamingException e){
       throw new ServletException("Could not resolve the JNDI name of TicketFacade. " +
       " Message: " +e);
       }
       }
      
      }
      


      Now, this setup works great. I create tickets, all have incremented Primary Keys, then I can purge off some tickets, re-initialse PKTicket and recyle the ticket numbers from the PKs I purged off.

      You'll notice the servlet loops to the maximum value I have set for available ticket numbers......well....that doesn't work so well.

      When I reach ticket number 12820 I get the following error.

      java.lang.OutOfMemoryError
      ; CausedByException is:
       Unexpected Error
      java.lang.OutOfMemoryError
      ; nested exception is:
       javax.ejb.EJBException: Unexpected Error
      java.lang.OutOfMemoryError
      ; - nested throwable: (javax.ejb.EJBException: Unexpected Error
      java.lang.OutOfMemoryError
      )
      15:09:55,622 ERROR [STDERR] org.jboss.tm.JBossTransactionRolledbackException: Unexpected Error
      java.lang.OutOfMemoryError
      ; CausedByException is:
       Unexpected Error
      java.lang.OutOfMemoryError
      ; nested exception is:
       javax.ejb.EJBException: Unexpected Error
      java.lang.OutOfMemoryError
      ; - nested throwable: (javax.ejb.EJBException: Unexpected Error
      java.lang.OutOfMemoryError
      )
      15:09:55,622 ERROR [STDERR] at org.jboss.ejb.plugins.LogInterceptor.handleException(LogInterceptor.java:262)
      15:09:55,622 ERROR [STDERR] at org.jboss.ejb.plugins.LogInterceptor.invoke(LogInterceptor.java:195)
      15:09:55,622 ERROR [STDERR] at org.jboss.ejb.plugins.ProxyFactoryFinderInterceptor.invoke(ProxyFactoryFinderInterceptor.java:122)
      15:09:55,622 ERROR [STDERR] at org.jboss.ejb.StatelessSessionContainer.internalInvoke(StatelessSessionContainer.java:331)
      15:09:55,622 ERROR [STDERR] at org.jboss.ejb.Container.invoke(Container.java:700)
      15:09:55,622 ERROR [STDERR] at sun.reflect.GeneratedMethodAccessor81.invoke(Unknown Source)
      15:09:55,632 ERROR [STDERR] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
      15:09:55,632 ERROR [STDERR] at java.lang.reflect.Method.invoke(Method.java:324)
      15:09:55,632 ERROR [STDERR] at org.jboss.mx.capability.ReflectedMBeanDispatcher.invoke(ReflectedMBeanDispatcher.java:284)
      15:09:55,632 ERROR [STDERR] at org.jboss.mx.server.MBeanServerImpl.invoke(MBeanServerImpl.java:546)
      15:09:55,632 ERROR [STDERR] at org.jboss.invocation.local.LocalInvoker.invoke(LocalInvoker.java:101)
      15:09:55,632 ERROR [STDERR] at org.jboss.invocation.InvokerInterceptor.invoke(InvokerInterceptor.java:90)
      15:09:55,632 ERROR [STDERR] at org.jboss.proxy.TransactionInterceptor.invoke(TransactionInterceptor.java:46)
      15:09:55,632 ERROR [STDERR] at org.jboss.proxy.SecurityInterceptor.invoke(SecurityInterceptor.java:45)
      15:09:55,632 ERROR [STDERR] at org.jboss.proxy.ejb.StatelessSessionInterceptor.invoke(StatelessSessionInterceptor.java:100)
      15:09:55,632 ERROR [STDERR] at org.jboss.proxy.ClientContainer.invoke(ClientContainer.java:85)
      15:09:55,632 ERROR [STDERR] at $Proxy106.newTicket(Unknown Source)
      15:09:55,632 ERROR [STDERR] at org.jcallcenter.web.core.TicketTestServlet.doPost(TicketTestServlet.java:61)
      15:09:55,632 ERROR [STDERR] at javax.servlet.http.HttpServlet.service(HttpServlet.java:760)
      15:09:55,632 ERROR [STDERR] at javax.servlet.http.HttpServlet.service(HttpServlet.java:853)
      15:09:55,632 ERROR [STDERR] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:247)
      15:09:55,632 ERROR [STDERR] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:193)
      15:09:55,632 ERROR [STDERR] at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:256)
      15:09:55,632 ERROR [STDERR] at org.apache.catalina.core.StandardPipeline$StandardPipelineValveContext.invokeNext(StandardPipeline.java:643)
      15:09:55,632 ERROR [STDERR] at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:480)
      15:09:55,632 ERROR [STDERR] at org.apache.catalina.core.ContainerBase.invoke(ContainerBase.java:995)
      15:09:55,632 ERROR [STDERR] at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:191)
      15:09:55,632 ERROR [STDERR] at org.apache.catalina.core.StandardPipeline$StandardPipelineValveContext.invokeNext(StandardPipeline.java:643)
      15:09:55,632 ERROR [STDERR] at org.jboss.web.tomcat.security.JBossSecurityMgrRealm.invoke(JBossSecurityMgrRealm.java:220)
      15:09:55,632 ERROR [STDERR] at org.apache.catalina.core.StandardPipeline$StandardPipelineValveContext.invokeNext(StandardPipeline.java:641)
      15:09:55,632 ERROR [STDERR] at org.apache.catalina.valves.CertificatesValve.invoke(CertificatesValve.java:246)
      15:09:55,632 ERROR [STDERR] at org.apache.catalina.core.StandardPipeline$StandardPipelineValveContext.invokeNext(StandardPipeline.java:641)
      15:09:55,632 ERROR [STDERR] at org.jboss.web.tomcat.tc4.statistics.ContainerStatsValve.invoke(ContainerStatsValve.java:76)
      15:09:55,632 ERROR [STDERR] at org.apache.catalina.core.StandardPipeline$StandardPipelineValveContext.invokeNext(StandardPipeline.java:641)
      15:09:55,632 ERROR [STDERR] at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:480)
      15:09:55,632 ERROR [STDERR] at org.apache.catalina.core.ContainerBase.invoke(ContainerBase.java:995)
      15:09:55,632 ERROR [STDERR] at org.apache.catalina.core.StandardContext.invoke(StandardContext.java:2417)
      15:09:55,632 ERROR [STDERR] at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:180)
      15:09:55,632 ERROR [STDERR] at org.apache.catalina.core.StandardPipeline$StandardPipelineValveContext.invokeNext(StandardPipeline.java:643)
      15:09:55,632 ERROR [STDERR] at org.apache.catalina.valves.ErrorDispatcherValve.invoke(ErrorDispatcherValve.java:171)
      15:09:56,202 ERROR [STDERR] at org.apache.catalina.core.StandardPipeline$StandardPipelineValveContext.invokeNext(StandardPipeline.java:641)
      15:09:56,202 ERROR [STDERR] at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:172)
      15:09:56,202 ERROR [STDERR] at org.apache.catalina.core.StandardPipeline$StandardPipelineValveContext.invokeNext(StandardPipeline.java:641)
      15:09:56,202 ERROR [STDERR] at org.jboss.web.tomcat.security.SecurityAssociationValve.invoke(SecurityAssociationValve.java:65)
      15:09:56,202 ERROR [STDERR] at org.apache.catalina.core.StandardPipeline$StandardPipelineValveContext.invokeNext(StandardPipeline.java:641)
      15:09:56,202 ERROR [STDERR] at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:577)
      15:09:56,202 ERROR [STDERR] at org.apache.catalina.core.StandardPipeline$StandardPipelineValveContext.invokeNext(StandardPipeline.java:641)
      15:09:56,202 ERROR [STDERR] at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:480)
      15:09:56,202 ERROR [STDERR] at org.apache.catalina.core.ContainerBase.invoke(ContainerBase.java:995)
      15:09:56,202 ERROR [STDERR] at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:174)
      15:09:56,202 ERROR [STDERR] at org.apache.catalina.core.StandardPipeline$StandardPipelineValveContext.invokeNext(StandardPipeline.java:643)
      15:09:56,202 ERROR [STDERR] at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:480)
      15:09:56,202 ERROR [STDERR] at org.apache.catalina.core.ContainerBase.invoke(ContainerBase.java:995)
      15:09:56,202 ERROR [STDERR] at org.apache.coyote.tomcat4.CoyoteAdapter.service(CoyoteAdapter.java:197)
      15:09:56,202 ERROR [STDERR] at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:781)
      15:09:56,202 ERROR [STDERR] at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.processConnection(Http11Protocol.java:549)
      15:09:56,202 ERROR [STDERR] at org.apache.tomcat.util.net.TcpWorkerThread.runIt(PoolTcpEndpoint.java:605)
      15:09:56,202 ERROR [STDERR] at org.apache.tomcat.util.threads.ThreadPool$ControlRunnable.run(ThreadPool.java:677)
      15:09:56,202 ERROR [STDERR] at java.lang.Thread.run(Thread.java:534)
      15:09:56,202 ERROR [STDERR] Caused by: javax.ejb.EJBException: Unexpected Error
      java.lang.OutOfMemoryError
      15:09:56,202 ERROR [STDERR] at org.jboss.ejb.plugins.AbstractTxInterceptor.invokeNext(AbstractTxInterceptor.java:182)
      15:09:56,202 ERROR [STDERR] at org.jboss.ejb.plugins.TxInterceptorCMT.runWithTransactions(TxInterceptorCMT.java:267)
      15:09:56,202 ERROR [STDERR] at org.jboss.ejb.plugins.TxInterceptorCMT.invokeHome(TxInterceptorCMT.java:98)
      15:09:56,202 ERROR [STDERR] at org.jboss.ejb.plugins.SecurityInterceptor.invokeHome(SecurityInterceptor.java:92)
      15:09:56,202 ERROR [STDERR] at org.jboss.ejb.plugins.LogInterceptor.invokeHome(LogInterceptor.java:120)
      15:09:56,202 ERROR [STDERR] at org.jboss.ejb.plugins.ProxyFactoryFinderInterceptor.invokeHome(ProxyFactoryFinderInterceptor.java:93)
      15:09:56,202 ERROR [STDERR] at org.jboss.ejb.EntityContainer.internalInvokeHome(EntityContainer.java:483)
      15:09:56,202 ERROR [STDERR] at org.jboss.ejb.Container.invoke(Container.java:720)
      15:09:56,202 ERROR [STDERR] at org.jboss.ejb.plugins.local.BaseLocalProxyFactory.invokeHome(BaseLocalProxyFactory.java:293)
      15:09:56,212 ERROR [STDERR] at org.jboss.ejb.plugins.local.LocalHomeProxy.invoke(LocalHomeProxy.java:110)
      15:09:56,212 ERROR [STDERR] at $Proxy102.create(Unknown Source)
      15:09:56,212 ERROR [STDERR] at org.jcallcenter.ejb.core.TicketFacadeBean.newTicket(TicketFacadeBean.java:76)
      15:09:56,212 ERROR [STDERR] at sun.reflect.GeneratedMethodAccessor83.invoke(Unknown Source)
      15:09:56,212 ERROR [STDERR] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
      15:09:56,212 ERROR [STDERR] at java.lang.reflect.Method.invoke(Method.java:324)
      15:09:56,212 ERROR [STDERR] at org.jboss.ejb.StatelessSessionContainer$ContainerInterceptor.invoke(StatelessSessionContainer.java:683)
      15:09:56,212 ERROR [STDERR] at org.jboss.resource.connectionmanager.CachedConnectionInterceptor.invoke(CachedConnectionInterceptor.java:185)
      15:09:56,212 ERROR [STDERR] at org.jboss.ejb.plugins.StatelessSessionInstanceInterceptor.invoke(StatelessSessionInstanceInterceptor.java:72)
      15:09:56,212 ERROR [STDERR] at org.jboss.ejb.plugins.AbstractTxInterceptor.invokeNext(AbstractTxInterceptor.java:84)
      15:09:56,212 ERROR [STDERR] at org.jboss.ejb.plugins.TxInterceptorCMT.runWithTransactions(TxInterceptorCMT.java:267)
      15:09:56,212 ERROR [STDERR] at org.jboss.ejb.plugins.TxInterceptorCMT.invoke(TxInterceptorCMT.java:128)
      15:09:56,212 ERROR [STDERR] at org.jboss.ejb.plugins.SecurityInterceptor.invoke(SecurityInterceptor.java:118)
      15:09:56,212 ERROR [STDERR] at org.jboss.ejb.plugins.LogInterceptor.invoke(LogInterceptor.java:191)
      15:09:56,212 ERROR [STDERR] ... 57 more
      


      Well, when I run the servlet again, PKTicket re-initializes itself, pulls all the PKs from the datastore, and sets up it's TreeSet again. Then, the servlet starts creating tickets starting from 12821. When it reaches 25640, it throws another OutOfMemoryError.

      Is this a limitation of TreeSet? Is there anyway to set the number of elements it should hold?

      I don't see this being a problem in production, because PKTicket will most likely be re-initialized before the 12820 mark, but I'm just curious if anyone has seen this before?[/url]

      *http://www.jboss.org/index.html?module=bb&op=viewtopic&t=50863

        • 1. Re: OutOfMemoryError - Singleton PK Generator
          jamesstrachan

          John,

          If I had known you were going to do this, I would have given you different advice !

          You are obviously running out of memory in the heap because of the number of integer ticket numbers stored and the overheads (hidden to you) of the objects in the tree set.

          You can increase the size of the heap - it's a command line iooption - but it would be better to design another way out of the problem.

          I suggest that you use an Entity Bean called NextNumberBean which has the responsibility for storing and issuing the next ticket number.

          The key code would be a getNextNumber() method coded as follows :-

           int getNextNumber() throws AllNumbersUsedException {
          
           int nextNumber = getNextNumber();
           int startNumber = nextNumber;
           boolean numberUsed = true;
           while ( numberUsed ) {
           numberUsed = checkIfUsed( nextNumber );
           if ( numberUsed ) {
           nextNumber = getNextNumber();
           if ( nextNumber == startNumber ) {
           throw new AllNumbersUsedException();
           }
           }
          
           return nextNumber;
           }
          
           private int getNextNumber() {
          
           lastIssuedNumber++;
           if ( lastIssuedNumber > MAXTICKETNUMBER ) {
           lastIssuedNumber = 1;
           }
          
           return storedLastNumber;
          
           }
          
          


          The private method checkIfUsed, for which I have not supplied code, would do a findByPrimaryKey on the Tickets Entity Bean and return true if the Ticket with that primary key exists.

          The integer lastIssuedNumber should normally be persisted using CMP.

          The functionality should be clear. The method increments For each number, the method checks whether the ticket still exists on the database, and goes for the next (and then the next) until an unused number is found. If all numbers are used - which you discover if you reach the start number without finding an unused slot - an exception is thrown.

          Performance will not be as fast as on your version, but you will probably be able to issue a ticket in about 50 milliseconds, depending on the speed of your box.


          James


          • 2. Re: OutOfMemoryError - Singleton PK Generator
            john_anderson_ii

            All in all, this looks like a much better method of doing things, and it will help me keep to my goal of 100% CMP. Plus, if it persisted it's last number used, as long as the next number didn't exist response time would still be fast, given this scenario:

            This code represents the ID column of the TICKET table after several closed tickets have been purged off:

            
            1
            2
            3
            5
            6
            7
            21
            22
            


            When first deployed, the first ticket would have a longer response time than the second because the bean would have to query by primary key for 1, 2, 3, and 4 before it found four open. On the next request for a number it would have to query 5, 6 and 7 before it found 8 open. The subsequent requests up to 20 would be fast because all those slots are open. Then once lastUsedNumber reaches 23, it's wide open for new ticket numbers.

            How would this design handle simultaneous requests? I tested the singleton design and I was unable to get duplicate keys so I'm reasonably sure the singleton bean blocked requests until it could complete them sequentially.

            • 3. Re: OutOfMemoryError - Singleton PK Generator
              jamesstrachan

              I forgot to mention that the next ticket number bean should also implement the "Singleton EJB" pattern as you should enforce a rule that only one component issues next numbers.

              I suggest that you persist the last number used. You should then get a uniformly fast response to each query, at the expense of an additional write for each ticket.

              An alternative is not to persist, but to get the initial number by a SQL statement :-

              select max(ID) from tickets
              


              This is slow for the first operation only - unless you can find a way to initialize as the server starts before the first user logs on.

              Tickets will normally be issued sequentially, and all the low numbers will be cleared before the maximum number is reached and the number rolls round.

              But the solution does cope with an unusually long lived record, where the table looks like :-

              5
              
              996
              997
              998
              999
              


              The EJB specification mandates that an EJB is locked by each method call. So a second user will wait until the first user completes the call. This is why you don't get duplicate numbers.

              Potentially, issuing the next number is a performance bottleneck.

              Assume that users take a minute on average to enter a ticket. 50 milliseconds is required to issue the next number. Then 400 concurrent users will require 50*400 milliseconds, or 20 seconds of each minute to issue numbers.

              So the next number EJB is busy for 33% of the time, and I would expect short queues of users to appear and disappear at that level of utilisation.

              When you consider that the server must also write out tickets, which will also take about 50 milliseconds per write, the server will be 70% utilised which will be close to the practical limit.



              • 4. Re: OutOfMemoryError - Singleton PK Generator
                john_anderson_ii

                Sorry for the very slow response. I was on vacation for a bit, then involved in other training, so I got a little out of the loop.

                Thank you so much for the good input on how to set this up.

                I want to run this idea past you.

                Suppose I make a singleton bean that perisists to the database it's own primary key (since there is only of these) the last used Primary key of the ticket, and a boolean. Since purging off the old closed out tickets requires a concious decision by the administrator, it wouldn't be much overhead to flip that boolean value whenever a purge was completed.

                Using this method the bean, under normal circumstances would need to check to see if the "purged" bit is flipped. If that bit is flipped, it would set it's last used value to 0, and start from the begining. If the bit is not flipped it will pull it's last used value, increment it, and findByPrimaryKey() that value. If that value is found to exist, it would repeat the findByPrimaryKey() process until it found a value that was open. Then it would write that value to the last used value slot.

                If the table looked like:

                
                1
                2
                3
                4
                5
                6
                .
                .
                .
                998
                999
                


                Then each access would take the same amount of time. If the table looked like this after a purge:

                
                1
                2
                3
                4
                6
                7
                .
                .
                999
                


                With only the number 5 record being purged off, than the first primary key would be a little slow, the second would be noticably slow, and normal operation would continue after that.

                However, it has been my experience that when records are purged out of a helpdesk system the resultant database looks like this:

                10
                227
                501
                550
                700
                720
                730
                740
                750
                760
                770
                780
                790
                800
                805
                810
                815
                .
                .
                990
                991
                992
                993
                


                The closer you get to the more recently created tickets the more tickets numbers there are being used.

                I almost wish I had spent more time going to school than in the Marines, because I would really like to know how to do the arithmetic behind calculating min, max, and mean speed in this situation.

                I was bouncing about a couple more solutions in my head. The first would be to use some sort of time/date/sourceIP or Username hash to generate a psudo-random primary key that will not be duplicated. I'm afraid that may be beyond my experience though. The second is to use a ticket number prefix that is incremented every time a group of tickets are purged off. The end result would be ticket series 1-x, then a purge, then ticket series 2-x, then a purge, and so on. On purge, the bean that controls the purging would check to see if any tickets in the 1- series exists, and if not, use that prefix for the next series, etc.

                What are your thoughts on these alternatives?


                • 5. Re: OutOfMemoryError - Singleton PK Generator
                  jamesstrachan

                  You are suggesting three alternatives :-

                  1. Restart from 1 after each purge;
                  2. Use a unique generated number - a GUID (Global Unique ID);
                  3. Use a two part number.

                  All three are workable, although (2) would probably meet serious resistance from users. They like to see understandable, sequential numbers.

                  But they are all more complex, require extra effort to code and have a risk of things going wrong.

                  The simplest solution is the best, providing that you make your rumber range sufficiently wide.

                  As an example, if you are expecting 100,000 help desk records a year, provide for 1,000,000 numbers before rolling round. The system will roll round to 1 after ten years, and it is highly unlikely that any low numbered records will still be around.

                  Use the mental energy to simplify the work to be done. They should have taught you that in the Marines - using different language !

                  James

                  • 6. Re: OutOfMemoryError - Singleton PK Generator
                    john_anderson_ii

                     

                    "jamesstrachan" wrote:

                    As an example, if you are expecting 100,000 help desk records a year, provide for 1,000,000 numbers before rolling round. The system will roll round to 1 after ten years, and it is highly unlikely that any low numbered records will still be around.


                    This, I think, is what will be done. Thank you for spending so much of your time on this!

                    John.