# SequenceIdentityManager.java

    /*
    ** Module   : SequenceIdentityManager.java
    ** Abstract : Identity manager implementation which uses a global sequence to generate new IDs
    **
    ** Copyright (c) 2004-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- --------------------------Description-------------------------------
    ** 001 ECF 20080805   @39306 Created initial version. Implementation of
    **                           IdentityManager interface which retrieves new
    **                           IDs from a global sequence.
    ** 002 CA  20080815   @39446 Support API change in DBUtils.
    ** 003 SVL 20080901   @39647 Added primary key reclamation. Implements
    **                           IdentityPoolManager instead of IdentityManager.
    ** 004 ECF 20090107   @41011 Fixed server shutdown. Timer needed to be a
    **                           daemon, it was holding up a clean shutdown of
    **                           the server.
    ** 005 ECF 20090527   @42474 Fixed Context.getStatement(). It was possible to
    **                           return a closed statement.
    ** 006 SVL 20090720   @43238 ProtectedTimer is used instead of Timer.
    ** 007 SVL 20090930   @44056 If any error has raised into
    **                           IdentityPool.nextPrimaryKey() then stop the
    **                           identity pool and use only sequence method for ID
    **                           generation.
    ** 008 SVL 20130331          Upgraded to Hibernate 4.
    ** 009 ECF 20130510          Fixed sequence access.
    ** 010 ECF 20141222          Lazily initialize SessionFactory to avoid NPE for certain dialects.
    **                           Replaced Apache commons logging with J2SE logging.
    ** 011 OM  20151208          Optimized string operations. Fixed typos in javadoc.
    ** 012 ECF 20160824          Greatly simplified sequence access. Do not start identity pool key
    **                           scanning if it is configured as disabled.
    ** 013 ECF 20160907          Implemented key prefetch algorithm, which caches ascending keys
    **                           prefetched from the id generator sequence, to avoid the overhead of
    **                           a database round trip for every new key.
    ** 014 ECF 20200419          Removed Hibernate dependencies.
    ** 015 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
    ** 016 OM  20241128          Multi-tenant runtime support: selected the proper persistence context, eventually
    **                           based on [sharedDb] parameter.
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.persist.id;

    import java.math.*;
    import java.util.*;
    import java.util.logging.*;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.persist.dialect.*;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.util.logging.*;

    /**
     * Implementation of the {@link IdentityManager} interface which uses a sequence (global to a
     * specific database) to produce 64-bit integer IDs which are unique across all records in that
     * database.
     * <p>
     * Current limitations:
     * <ul>
     *   <li>Wrapping to a negative number after we increment beyond {@code Long.MAX_VALUE} is not
     *       handled at this time.
     *   <li>Does not detect/repair fragmentation of the primary key address space for a table;
     *       existing gaps in primary keys are never plugged. Note that a solution for this item may
     *       preclude having to address the previous item.
     * </ul>
     */
    public final class SequenceIdentityManager
    implements IdentityPoolManager
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(IdentityManager.class.getName());

       /** Range arguments passed to prefetch SQL statement */  // TODO: make configurable
       private static final Integer[] PREFETCH_ARGS =  new Integer[] { 1, 1000 };

       /** Queue of prefetched results */
       private Queue<Long> prefetched = null;

       /** Identity pool used by this identity manager. */
       private IdentityPool identityPool = null;

       /** Persistence service object associated with this identity manager */
       private Persistence persistence = null;

       /** SQL statement used to prefetch multiple sequence values at once */
       private String prefetchKeySQL = null;

       /** SQL statement used to get next sequence value */
       private String nextKeySQL = null;

       /**
        * Default constructor.
        */
       public SequenceIdentityManager()
       {
       }

       /**
        * Specify the database for the identity manager.
        * <code>DefaultIdentityManager</code> is initialized here - current
        * maximum IDs for each table are retrieved.
        *
        * @param   persistence
        *          Persistence service object associated with identity manager.
        */
       public void setPersistence(Persistence persistence)
       {
          this.persistence = persistence;

          Dialect dialect = persistence.getDialect();
          prefetchKeySQL = dialect.getSequencePrefetchString(Persistence.ID_GEN_SEQUENCE);

          // initialize prefetch queue if feature is supported by dialect
          if (prefetchKeySQL != null)
          {
             prefetched = new LinkedList<>();
          }
          else
          {
             nextKeySQL = dialect.getSequenceNextValString(Persistence.ID_GEN_SEQUENCE);
          }
       }

       /**
        * Return the next primary key for the specified table.  The pool of
        * reclaimed IDs is checked first.  If no reclaimed ID is available, a new
        * ID is generated from the sequence.
        *
        * @param   table
        *          Table for which the next primary key will be returned.
        *
        * @return  The next primary key for the given table.
        *
        * @throws  PersistenceException
        *          if there is an error determining the next primary key ID.
        */
       public Long nextPrimaryKey(String table)
       throws PersistenceException
       {
          // try to get the key from identity pool first
          if (identityPool != null)
          {
             try
             {
                Long key = identityPool.nextPrimaryKey();
                if (key != null)
                {
                   return key;
                }
             }
             catch (Throwable thr)
             {
                if (LOG.isLoggable(Level.SEVERE))
                {
                   LOG.log(Level.SEVERE, "Identity pool has been stopped because of an error.", thr);
                }
                identityPool.stop();
                identityPool = null;
             }
          }

          // try to get key from queue of prefetched keys, filling queue if necessary
          if (prefetched != null)
          {
             Long key = null;

             synchronized (prefetched)
             {
                if (!prefetched.isEmpty())
                {
                   key = prefetched.remove();
                }
                else
                {
                   boolean tx = persistence.beginTransaction(Persistence.SHARED_CTX);
                   ScrollableResults<BigInteger> results = null;

                   try
                   {
                      // all tables from same logical database use the same id sequence from the shared
                      // physical database
                      results = persistence.executeSQLQuery(prefetchKeySQL, Persistence.SHARED_CTX, PREFETCH_ARGS);
                      if (results.next())
                      {
                         // first result will be returned
                         key = results.get(0, BigInteger.class).longValue();
                      }

                      while (results.next())
                      {
                         // remaining results are cached in the queue for future requests
                         prefetched.add(results.get(0, BigInteger.class).longValue());
                      }
                   }
                   finally
                   {
                      if (results != null)
                      {
                         try
                         {
                            results.close();
                         }
                         catch (PersistenceException exc)
                         {
                            if (LOG.isLoggable(Level.WARNING))
                            {
                               LOG.log(Level.WARNING,
                                       "Error closing results [ " + prefetchKeySQL + " ]",
                                       exc);
                            }
                         }
                      }
                      if (tx)
                      {
                         persistence.commit(Persistence.SHARED_CTX);
                      }
                   }
                }
             }

             return key;
          }

          return persistence.getSingleSQLResult(nextKeySQL, Persistence.SHARED_CTX, null);
       }

       /**
        * Inform the identity manager that the given primary keys can be reused.
        *
        * @param   keys
        *          Keys for reuse.
        */
       public void reclaimKeys(Long[] keys)
       {
          if (identityPool != null)
          {
             identityPool.reclaimKeys(keys);
          }
       }

       /**
        * Sets identity pool which can be used by this identity manager.
        *
        * @param   identityPool
        *          Identity pool for use.
        */
       public void setIdentityPool(IdentityPool identityPool)
       {
          if (identityPool == null)
          {
             return;
          }

          this.identityPool = identityPool;

          if (!identityPool.isKeyScanningEnabled())
          {
             return;
          }

          try
          {
             Long sequenceValue = persistence.getSingleSQLResult(nextKeySQL, Persistence.SHARED_CTX, null);

             // Set the next sequence value as a start key for the
             // identity pool and add "wasted" sequence value to the pool.
             identityPool.setScanStartKey(sequenceValue - 1);
             identityPool.reclaimKeys(new Long[] {sequenceValue});
          }
          catch (PersistenceException exc)
          {
             if (LOG.isLoggable(Level.SEVERE))
             {
                LOG.log(Level.SEVERE,
                        "Cannot get the current sequence value. Identity pool cannot be started.",
                        exc);
             }
          }
       }
    }