# SessionFactory.java

    /*
    ** Module   : SessionFactory.java
    ** Abstract : Allows closed sessions to be reclaimed.
    **
    ** Copyright (c) 2023-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 DDF 20230512 Created initial version.
    **     DDF 20230517 Manage session closing across all SessionFactory instances using a Daemon Thread.
    **     DDF 20230518 Deregister SessionFactory instances. Added a functional interface that can throw
    **                  PersistenceException.
    **     DDF 20230523 Made changes to synchronization. Added logs.
    **     DDF 20230704 Made lifespan value configurable, allow lifespan to be 0 in which case the
    **                  managing thread won't run.
    ** 002 DDF 20230807 Disable session reclaiming when lifespan is negative.
    ** 003 RAA 20240424 Added the tenant id as part of the create function.
    **     RAA 20240513 Added skipSessionReclaim parameter to the create function.
    **     RAA 20240613 A new session is now created through the constructor that takes both the database
    **                  configuration and the tenant id.
    **     RAA 20240627 Made the sessions to be reclaimed tenant-aware.
    **     RAA 20240627 Fixed reclaimable comparisons to match the tenant changes.
    **     RAA 20240702 Reverted the tenant-aware changes.
    **     RAA 20240710 Added null check in expire.
    **     RAA 20240726 Updated parameters of the getDatabaseCredentials() call.
    ** 004 OM  20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
    **                  based on [sharedDb] parameter.
    ** 005 OM  20250211 New parameter for TenantManager.getTenant().
    ** 006 AP  20250417 Call the hook when a Session is reclaimed.
    */

    /*
    ** 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.orm;

    import java.util.*;
    import java.util.concurrent.ConcurrentLinkedQueue;

    import com.goldencode.p2j.directory.DirectoryService;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.persist.orm.TenantManager.Tenant;
    import com.goldencode.p2j.util.Utils;
    import com.goldencode.p2j.util.logging.CentralLogger;

    /**
     * Allows a "closed" session to be reclaimed. Each Persistence context has a
     * session which can be reclaimed after it is closed, allowing for better
     * caching of active buffers.
     */
    public class SessionFactory
    {
       /** Logger. */
       private static final CentralLogger LOG = CentralLogger.get(SessionFactory.class);

       /** If the session reclaiming is enabled/disabled. */
       private static boolean isReclaimable = true;

       /** The session that can be reclaimed. */
       private Session reclaimable;

       /** Timestamp representing when each of the reclaimable sessions was added. */
       private Long timestamp;

       /**
        * Constructor that registers the SessionFactory in a Daemon Thread
        * that manages session closing.
        */
       public SessionFactory()
       {
          if (isReclaimable)
          {
             SessionCloseThread.register(this);
          }

          reclaimable = null;
          timestamp = null;
       }

       /**
        * Return a boolean value that tells if the session reclaiming is enabled/disabled.
        *
        * @return  {@code true} if session reclaiming is enabled, {@code false} otherwise.
        */
       public boolean isEnabled()
       {
          return isReclaimable;
       }

       /**
        * Reclaims a session if one is available or create a new one with the given parameters.
        *
        * @param   database
        *          The database used for creating the session
        * @param   dmoVersion
        *          The DMO version tracker
        * @param   initialize
        *          Initialization lambda used to associate records with a new session when it is created and not
        *          reclaimed.
        * @param   tenantName
        *          The name of the tenant for which the session is created.
        *
        * @return  If a session can be reclaimed, the reclaimable Session object is returned. Otherwise, a new
        *          session is created using the Database and DmoVersioning object provided.
        *
        * @throws  PersistenceException
        *          If there is an error getting a JDBC connection from the data source.
        */
       public Session create(Database database,
                             DmoVersioning dmoVersion,
                             PersistenceConsumer<Session> initialize,
                             String tenantName)
       throws PersistenceException
       {
          Session session;
          if (isReclaimable)
          {
             synchronized (this)
             {
                if (reclaimable != null)
                {
                   session = reclaimable;
                   reclaimable = null;
                   timestamp = null;
                   session.claim();
                   return session;
                }
             }
          }

          DatabaseConfig config = null;
          if (tenantName != null && !tenantName.equalsIgnoreCase(TenantManager.DEFAULT_TENANT_NAME))
          {
             Tenant tenant = TenantManager.getTenant(tenantName, null);
             if (tenant != null)
             {
                config = DatabaseManager.createSettings(tenant,
                                                        database,
                                                        tenant.getDatabaseCredentials(database.getName(), true));
             }
          }

          session = new Session(database, config, dmoVersion, tenantName);
          initialize.accept(session);
          return session;
       }

       /**
        * Mark a session as being reclaimable.
        *
        * @param   session
        *          The session that is marked as reclaimable.
        */
       public synchronized void markReclaimable(Session session)
       {
          if (reclaimable != null)
          {
             LOG.severe("There is already an existing reclaimable session. Closing it...");
             expire();
          }

          reclaimable = session;
          timestamp = System.currentTimeMillis();
       }

       /**
        * Method used to close a session that can't be reclaimed.
        * A session can't be reclaimed when it's lifespan expires.
        */
       public synchronized void expire()
       {
          try
          {
             if (reclaimable != null)
             {
                reclaimable.close();
             }
          }
          catch (PersistenceException exc)
          {
             LOG.severe("Expired session could not be closed.");
          }
          finally
          {
             reclaimable = null;
             timestamp = null;
          }
       }

       /**
        * A thread used to manage SessionFactory reclaimable sessions and
        * close any sessions that have exceeded their lifespan.
        */
       public static class SessionCloseThread
       extends Thread
       {
          /** Lifespan of a thread (ms). When it is exceeded, the session is closed. */
          private static int lifespan = 1000;

          /** A thread-safe queue that stores all SessionFactory instances created. */
          private static Queue<SessionFactory> factories;

          /** Value used to check if the thread is initialized. */
          private static boolean initialized = false;

          /**
           * Constructor that initializes the Queue and initializes
           * the current thread as a Daemon thread.
           */
          private SessionCloseThread()
          {
             super("Idle Sessions Close Thread");
             setDaemon(true);
          }

          /**
           * Creates an instance of the Thread and starts it.
           */
          public synchronized static void initialize()
          {
             if (!initialized)
             {
                DirectoryService ds = DirectoryService.getInstance();
                if (!ds.bind())
                {
                   throw new RuntimeException("Directory bind failed");
                }

                try
                {
                   String path = "persistence/session-lifespan";
                   lifespan = Utils.getDirectoryNodeInt(ds, path, 1000, false);
                   if (lifespan < 0)
                   {
                      isReclaimable = false;
                      LOG.info("Session lifespan is negative, session reclaiming will be disabled.");
                   }
                }
                finally
                {
                   ds.unbind();
                }

                initialized = true;
                if (isReclaimable)
                {
                   factories = new ConcurrentLinkedQueue<>();
                }

                if (lifespan > 0)
                {
                   new SessionCloseThread().start();
                }
                else
                {
                   LOG.info("Managing thread for reclaimable sessions will not be created.");
                }
             }
          }

          /**
           * Add a SessionFactory instance to the queue managed
           * by the thread.
           *
           * @param   factory
           *          A SessionFactory instance.
           */
          private static void register(SessionFactory factory)
          {
             factories.add(factory);
          }

          /**
           * Removes a SessionFactory instance from the queue.
           * If the factory has a session that it's still active,
           * it closes it.
           *
           * @param   factory
           *          A SessionFactory instance.
           */
          public static void deregister(SessionFactory factory)
          {
             if (!isReclaimable)
             {
                return;
             }

             factories.remove(factory);
             synchronized (factory)
             {
                if (factory.reclaimable != null)
                {
                   factory.expire();
                }
             }
          }

          /**
           * The thread sleeps for the duration of a lifespan,
           * then it calls the method for removing expired sessions.
           */
          public void run()
          {
             try
             {
                while (true)
                {
                   Thread.sleep(lifespan);
                   removeIdleSessions();
                }
             }
             catch (Exception e)
             {
                LOG.severe("Exception in SessionFactory Manager Thread.");
             }
          }

          /**
           * Removes any sessions that have exceeded their lifespan from
           * all existent factories.
           */
          private void removeIdleSessions()
          {
             // To avoid a concurrent modification exception, create a copy of the Queue.
             List<SessionFactory> factoriesCopy = new ArrayList<>(factories);
             for (SessionFactory factory : factoriesCopy)
             {
                synchronized (factory)
                {
                   if (factory.reclaimable == null)
                   {
                      continue;
                   }

                   if (System.currentTimeMillis() - factory.timestamp > lifespan)
                   {
                      factory.expire();
                   }
                }
             }
          };
       }

       /**
        * Functional Interface that is able to throw PersistenceException.
        * It is similar to Consumer, it accepts a single input argument and
        * returns no result.
        *
        * @param   <T>
        *          The type of the parameter value.
        */
       @FunctionalInterface
       public interface PersistenceConsumer<T>
       {
          /**
           * Performs an operation on the input argument.
           *
           * @param   t
           *          Accepted value.
           *
           * @throws  PersistenceException
           */
          void accept(T t) throws PersistenceException;
       }
    }