DirtyShareMultiplexerImpl.java

/*
** Module   : DirtyShareMultiplexerImpl.java
** Abstract : Handles server-to-server requests to manage the information 
**            about uncommitted modifications to database records.
**
** Copyright (c) 2004-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- --------------------------------Description-------------------------------------
** 001 SVL 20080605   @38559 Created initial version. Handles server-to-
**                           server requests to manage the information 
**                           about uncommitted modifications.
** 002 ECF 20080612   @38727 Replaced isEntityDirty() with
**                           getEntityDirtyState(). New method returns
**                           state information (is entity dirty with
**                           changes and/or deletes), rather than a simple
**                           boolean.
** 003 CA  20080716   @39116 Renamed method lockUniqueIndexes to 
**                           lockAllIndexes - this will lock all the 
**                           indexes for a certain entity. Modified the
**                           delete(...) and rollbackInsert(...) methods
**                           so the index locking can be disabled.
** 004 ECF 20080815   @39547 Added support for global change events. These
**                           are events that represent index-changing
**                           inserts, updates, or deletes, which are
**                           processed by sessions other than the session
**                           in which the event originated.
** 005 ECF 20090513   @42170 Modified variant of update() method. Added lock
**                           parameter to make index locking optional.
** 006 ECF 20090602   @42575 Added support for Progress isolation leak quirk.
**                           Added updateSnapshots() method.
** 007 ECF 20090609   @42646 Modified cleanupChanges(). The list of entities
**                           which have had their uncommitted changes leaked
**                           is now passed in, allowing proper cleanup.
** 008 ECF 20090720   @43311 Changed delete() method signature. Now it throws
**                           PersistenceException.
** 009 ECF 20131028          Import change.
** 010 ECF 20140825          Track unvalidated inserts.
** 011 OM  20160307          Added support for EXTENTs in update() methods. Updated signature of
**                           getDirtyInfo().
** 012 ECF 20200901          New ORM implementation.
** 013 SVL 20230516          registerForGlobalEvents can return null now.
** 014 AL2 20230822          Removed readOnly parameter.
** 015 TJD 20240123          Java 17 compatibility updates
** 016 TJD 20230509          Extend interface to allow dirty DMO leaks and to update shared snapshots
** 017 AL2 20240903          Added integration with DirtyData.
*/
/*
** 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.remote;

import java.util.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.Record;
import com.goldencode.p2j.persist.dirty.*;
import com.goldencode.p2j.persist.event.*;
import com.goldencode.p2j.persist.lock.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.net.RemoteObject;

/**
 * A singleton instance of this class services server-to-server requests to
 * manage the information about uncommitted modifications to database records.
 * This instance is registered as a remote object and serves dirty share
 * management requests for one or more databases which the server
 * authoritatively manages.
 * <p>
 * Service requests may originate from more than one external server, so this
 * implementation is threadsafe.
 *
 * @author SVL
 */
public final class DirtyShareMultiplexerImpl
extends DatabaseMultiplexer<DirtyShareManager>
implements DirtyShareMultiplexer
{
   /** Singleton instance of this class */
   private static DirtyShareMultiplexer instance = null;
   
   /**
    * Initialize the dirty share multiplexer for this server instance. This
    * method is to be called during server bootstrap, but not before the local
    * database manager has been initialized.
    * <p>
    * This method enforces the singleton pattern.
    *
    * @throws  IllegalStateException
    *          if this method is invoked more than once.
    */
   public static synchronized void initialize()
   {
      if (instance != null)
      {
         throw new IllegalStateException(
            "Dirty share multiplexer already initialized for this process");
      }
      
      instance = new DirtyShareMultiplexerImpl();
      RemoteObject.registerNetworkServer(DirtyShareMultiplexer.class, instance);
   }
   
   /**
    * Default constructor which initializes the resources needed for this
    * multiplexer to service dirty share manager requests.
    */
   private DirtyShareMultiplexerImpl()
   {
      super();
   }
   
   /**
    * Register with a global event clearinghouse an interest in insert,
    * delete, and update events which affect the given DMO entity.
    * 
    * @param   database
    *          Value that uniquely identifies the target database on the
    *          remote server.
    * @param   entity
    *          DMO entity for which the caller is interested in tracking
    *          change.
    * 
    * @return  A unique identifier for the registrant, assigned by the event
    *          clearinghouse, used later to collect events and deregister or
    *          <code>null</code> if global events are disabled.
    * 
    * @see     #getGlobalEvents(int, long, long)
    * @see     #deregisterForGlobalEvents(int, long)
    */
   public Long registerForGlobalEvents(int database, String entity)
   {
      return lookupWorker(database).registerForGlobalEvents(entity);
   }
   
   /**
    * Deregister with a global event clearinghouse an interest in insert,
    * delete, and update events which affect a particular DMO type.
    * 
    * @param   database
    *          Value that uniquely identifies the target database on the
    *          remote server.
    * @param   registerID
    *          A unique identifier for the registrant being deregistered.
    * 
    * @see     #getGlobalEvents(int, long, long)
    * @see     #registerForGlobalEvents(int, String)
    */
   public void deregisterForGlobalEvents(int database, long registerID)
   {
      lookupWorker(database).deregisterForGlobalEvents(registerID);
   }
   
   /**
    * Retrieve any new events that have been collected by a global event
    * clearinghouse, representing inserts, deletes, and updates made to
    * record types for which the caller previously has registered interest.
    * 
    * @param   database
    *          Value that uniquely identifies the target database on the
    *          remote server.
    * @param   registerID
    *          A unique identifier for an object which has registered interest
    *          in DMO changing events.
    * @param   lastID
    *          A unique identifier representing the last event retrieved by
    *          the caller.  If less than 0, it is assumed no events have yet
    *          been retrieved since the caller registered interest in these
    *          events.
    * 
    * @return  Array of applicable, new events, or <code>null</code> if no
    *          such events have been collected.
    * 
    * @throws  EventRegistrationException
    *          if <code>registerID</code> is invalid or has expired.
    */
   public GlobalChangeEvent[] getGlobalEvents(int database, long registerID, long lastID)
   throws EventRegistrationException
   {
      return lookupWorker(database).getGlobalEvents(registerID, lastID);
   }
   
   /**
    * Lock or unlock all indexes associated with the given DMO entity.
    * 
    * @param   database
    *          Value that uniquely identifies the target database on the
    *          remote server.
    * @param   entity
    *          Name of an entity, which is the name of the DMO implementation
    *          class associated with a table being tracked for uncommitted
    *          changes.
    * @param   lockType
    *          Type of lock to acquire, or <code>NONE</code> to release
    *          existing locks.  <code>NO_WAIT</code> variants will be treated
    *          as regular, blocking variants.
    * 
    * @throws  PersistenceException
    *          if there is an error querying JDBC metadata while retrieving
    *          index information.
    */
   public void lockAllIndexes(int database, String entity, int lockType)
   throws PersistenceException
   {
      LockType lt = LockType.fromInt(lockType);
      lookupWorker(database).lockAllIndexes(entity, lt);
   }
   
   /**
    * Begin tracking a newly inserted record in the dirty database.
    * <p>
    * This record is removed when the current transaction is committed or rolled back.
    * 
    * @param   database
    *          Value that uniquely identifies the target database on the remote server.
    * @param   entity
    *          Name of an entity, which is the name of the DMO implementation class associated with a table
    *          being tracked for uncommitted changes.
    * @param   copy
    *          Record to be tracked.
    * @param   indexState
    *          The index update state of the record at the time of insertion.
    * @param   lock
    *          {@code true} to acquire write locks to insert the record; {@code false} to assume write locks
    *          are acquired by calling code.
    * @param   dirtyData
    *          The data that was changed and caused this update. This is explicitly sent
    *          because sometimes we can't infer the dirty props from the DMO anymore.
    * 
    * @throws  PersistenceException
    *          if there is any database error.
    */
   @Override
   public void insert(int database, String entity, Record copy, IndexState indexState, boolean lock, DirtyData dirtyData)
   throws PersistenceException
   {
      lookupWorker(database).insert(entity, copy, indexState, lock, dirtyData);
   }
   
   /**
    * Rollback the insert of a record previously introduced with the {@link
    * #insert(int, String, Record, IndexState, boolean, DirtyData)} method.
    * 
    * @param   database
    *          Value that uniquely identifies the target database on the
    *          remote server.
    * @param   ident
    *          Identifier for the target record, which encapsulates its entity
    *          name and primary key.
    * @param   lock
    *          <code>true</code> to acquire write locks;
    *          <code>false</code> to assume write locks are acquired by
    *          calling code.
    * 
    * @throws  PersistenceException
    *          if there is any database error.
    */
   public void rollbackInsert(int database, RecordIdentifier<String> ident, boolean lock)
   throws PersistenceException
   {
      lookupWorker(database).rollbackInsert(ident, lock);
   }
   
   /**
    * Indicate whether the record specified by the given entity and primary
    * key is being tracked for changes within an uncommitted transaction.
    * This includes records which have been added to the dirty database due to
    * an insert or an update, not due to a delete.
    * 
    * @param   database
    *          Value that uniquely identifies the target database on the
    *          remote server.
    * @param   entity
    *          Name of an entity, which is the name of the DMO implementation
    *          class associated with a table being tracked for uncommitted
    *          changes.
    * @param   id
    *          Primary key of the record to be checked.
    * 
    * @return  <code>true</code> to indicate the target record is being
    *          tracked for changes, else <code>false</code>.
    */
   public boolean isTracked(int database, String entity, Long id)
   {
      return lookupWorker(database).isTracked(entity, id);
   }
   
   /**
    * Begin tracking a DMO for changes, based on the updates indicated by the
    * {@code properties}, {@code extIndexes} and {@code values} arguments.
    * <p>
    * Make a copy of the given record, apply the specified updates to that
    * copy, and store the copy in the dirty database.
    * <p>
    * This record is removed when the adding context's current transaction is
    * committed or rolled back.
    * 
    * @param   database
    *          Value that uniquely identifies the target database on the remote server.
    * @param   entity
    *          Name of an entity, which is the name of the DMO implementation class associated with a table
    *          being tracked for uncommitted changes.
    * @param   dmo
    *          Record to be tracked.
    * @param   lock
    *          {@code true} to acquire write locks to update the record;
    *          {@code false} to assume write locks are acquired by calling code.
    * @param   dirtyData
    *          The data that was changed and caused this update. This is explicitly sent
    *          because sometimes we can't infer the dirty props from the DMO anymore.
    * 
    * @throws  PersistenceException
    *          if there is any database error.
    */
   public void update(int database,
                      String entity,
                      Record dmo,
                      boolean lock,
                      DirtyData dirtyData)
   throws PersistenceException
   {
      lookupWorker(database).update(entity, dmo, lock, dirtyData);
   }
   
   /**
    * Retrieve a DMO which is being tracked in the dirty database, and apply the specified updates
    * to it, as provided by the {@code properties}, {@code extIndexes} and {@code values}
    * arguments. It is assumed the DMO already exists in the database, from a previous call to
    * {@link #update(int, String, Record, boolean, DirtyData)}.
    * 
    * @param   database
    *          Value that uniquely identifies the target database on the remote server.
    * @param   entity
    *          Name of an entity, which is the name of the DMO implementation
    *          class associated with a table being tracked for uncommitted changes.
    * @param   id
    *          Primary key id of the record to be retrieved and updated.
    * @param   lock
    *          <code>true</code> to acquire write locks to update the record;
    *          <code>false</code> to assume write locks are acquired by calling code.
    * @param   dirtyData
    *          The data that was changed and caused this update. This is explicitly sent
    *          because sometimes we can't infer the dirty props from the DMO anymore.
    * 
    * @throws  PersistenceException
    *          if there is any database error.
    */
   public void update(int database,
                      String entity,
                      Long id,
                      boolean lock, 
                      DirtyData dirtyData)
   throws PersistenceException
   {
      lookupWorker(database).update(entity, id, false, lock, dirtyData);
   }
   
   /**
    * Indicate whether any uncommitted modification is being tracked for the
    * database table associated with the specified entity.  A modification is
    * the insertion, deletion, or update of a record.  Insertions and updates
    * considered the same for purposes of this method.
    * 
    * @param   database
    *          Value that uniquely identifies the target database on the
    *          remote server.
    * @param   entity
    *          Name of an entity, which is the name of the DMO implementation
    *          class associated with a table being tracked for uncommitted
    *          changes.
    * 
    * @return  Bitfield indicating dirty status of entity, using constants
    *          from {@link DirtyInfo}.  Zero return indicates entity is not
    *          dirty.  Only <code>CHANGE</code> and <code>DELETE</code> are
    *          detected, not <code>INSERT</code>.
    */
   public int getEntityDirtyState(int database, String entity)
   {
      return lookupWorker(database).getEntityDirtyState(entity);
   }
   
   /**
    * Indicate whether the record specified by the given entity and primary
    * key has been deleted within an uncommitted transaction in <i>any</i>
    * context.
    * 
    * @param   database
    *          Value that uniquely identifies the target database on the
    *          remote server.
    * @param   entity
    *          Name of an entity, which is the name of the DMO implementation
    *          class associated with a table being tracked for uncommitted
    *          changes.
    * @param   id
    *          Primary key of the record to be checked.
    * 
    * @return  <code>true</code> if the specified record has been deleted,
    *          else <code>false</code>.
    */
   public boolean isDeleted(int database, String entity, Long id)
   {
      return lookupWorker(database).isDeleted(entity, id);
   }
   
   /**
    * Mark a record as having been deleted in an uncommitted transaction, or
    * unmark a record which previously was so marked.
    * <p>
    * A record is unmarked when the adding context's current transaction is
    * committed or rolled back.
    * 
    * @param   database
    *          Value that uniquely identifies the target database on the
    *          remote server.
    * @param   entity
    *          Name of an entity, which is the name of the DMO implementation
    *          class associated with a table being tracked for uncommitted
    *          changes.
    * @param   id
    *          Primary key of deleted record.
    * @param   rollback
    *          If <code>false</code>, the target record is marked as deleted;
    *          if <code>true</code>, it is unmarked.
    * @param   lock
    *          <code>true</code> to acquire write locks to delete the record;
    *          <code>false</code> to assume write locks are acquired by
    *          calling code.
    * 
    * @throws  PersistenceException
    *          if there was an error deleting the record from the database.
    */
   public void delete(int database, String entity, Long id, boolean rollback, boolean lock)
   throws PersistenceException
   {
      lookupWorker(database).delete(entity, id, rollback, lock);
   }
   
   
   /**
    * Retrieve from the dirty database the record specified by the given
    * entity and primary key, if it exists.
    * 
    * @param   database
    *          Value that uniquely identifies the target database on the
    *          remote server.
    * @param   entity
    *          Name of an entity, which is the name of the DMO implementation
    *          class associated with a table being tracked for uncommitted
    *          changes.
    * @param   id
    *          Primary key of the record to be retrieved.
    * 
    * @return  Target record if it exists in the dirty database, else
    *          <code>null</code>.
    * 
    * @throws  PersistenceException
    *          if there is any database error.
    */
   public Record getDirtyDMO(int database, String entity, Long id)
   throws PersistenceException
   {
      return lookupWorker(database).getDirtyDMO(entity, id);
   }
   
   /**
    * Execute an HQL query and return the results as a list. If the query returns multiple results
    * per row, each element in the list will be an array of {@code Object}s.
    * <p>
    * No locking is attempted.
    *
    * @param   database
    *          Value that uniquely identifies the target database on the remote server.
    * @param   hql
    *          HQL query statement passed to Hibernate.  This method makes no
    *          assumptions as to the types of object(s) returned.
    * @param   params
    *          Substitution values for the query.  If none, this should be an empty array.
    * @param   maxResults
    *          The maximum number of elements to be returned in the list.
    *          If this value is non-positive, no upper limit is applied.
    * @param   startOffset
    *          The 0-based offset of the first record to retrieve.  If this
    *          value is non-positive, an offset of 0 is used by default.
    *
    * @return  A list of results, or {@code null} if no result was found.
    *
    * @throws  PersistenceException
    *          if there was an error executing the query.
    */
   @Override
   public <T> List<T> list(int database,
                           String hql,
                           Object[] params,
                           int maxResults,
                           int startOffset)
   throws PersistenceException
   {
      return lookupWorker(database).list(hql, params, maxResults, startOffset);
   }
   
   /**
    * Given certain search criteria and possibly the primary key of a potential DMO match in the
    * primary database, report any significant information currently being tracked for uncommitted
    * transactions, which might override data (or the lack thereof) found in the primary database.
    * <p>
    * If a {@link DirtyInfo} object is returned by this method, it will contain overriding
    * information, such as a dirty DMO which matched the search criteria, whether that record was
    * newly inserted, or the fact that the candidate record found in the primary search has been
    * deleted or modified in an uncommitted transaction.  A {@code null} return indicates no
    * overriding information was available.
    * 
    * @param   database
    *          Value that uniquely identifies the target database on the remote server.
    * @param   entity
    *          Name of an entity, which is the name of the DMO implementation class associated
    *          with a table being tracked for uncommitted changes.
    * @param   candidateID
    *          Primary key ID of DMO, if any, found in the primary database, which matches the
    *          current query criteria.
    * @param   index
    *          UID of index which governs the ordering of data in the current query.
    * @param   statements
    *          One or more HQL statements to be executed against the dirty database to find a
    *          record which matches the current query criteria.  The list is in order of most
    *          specific to least specific search criteria.
    * @param   params
    *          Query substitution parameters which match placeholders in the most specific HQL
    *          statement.
    * @param   rowidLookup
    *          The predicate of the query does a lookup for ROWID/RECID of the table. In case of
    *          transient records they are accessible without the need of having a full matched
    *          index.
    * 
    * @return  An object which summarizes the findings, or {@code null} if no uncommitted change
    *          information was available for the given arguments.
    *
    * @throws  PersistenceException
    *          if there was any database error.
    */
   @Override
   public DirtyInfo getDirtyInfo(int database,
                                 String entity,
                                 Long candidateID,
                                 int index,
                                 List<String> statements,
                                 Object[] params,
                                 boolean rowidLookup)
   throws PersistenceException
   {
      return lookupWorker(database).getDirtyInfo(entity, candidateID, index, statements, params, rowidLookup);
   }
   
   /**
    * Stop tracking uncommitted changes to the the subset of records
    * represented by <code>subChanges</code>, and remove from the master set
    * of uncommitted, inserted records the subset of records represented by
    * <code>subInserts</code>.
    * 
    * @param   database
    *          Value that uniquely identifies the target database on the
    *          remote server.
    * @param   subChanges
    *          Sets of primary keys representing those records to be removed
    *          from uncommitted change tracking, mapped by entity name.
    * @param   subInserts
    *          A set of identifiers of those records to be removed from the
    *          master set of uncommitted inserts.
    * 
    * @throws  PersistenceException
    *          if there is any database error.
    */
   public void cleanupChanges(int database,
                              Map<String, Set<Long>> subChanges,
                              Set<RecordIdentifier<String>> subInserts)
   throws PersistenceException
   {
      lookupWorker(database).cleanupChanges(subChanges, subInserts);
   }
   
   /**
    * Remove from the master set of uncommitted, deleted records the subset of
    * records represented by <code>subDeletes</code>.
    * 
    * @param   database
    *          Value that uniquely identifies the target database on the
    *          remote server.
    * @param   subDeletes
    *          Sets of primary keys representing those records to be removed
    *          from uncommitted delete tracking, mapped by entity name.
    */
   public void cleanupDeletes(int database, Map<String, Set<Long>> subDeletes)
   {
      lookupWorker(database).cleanupDeletes(subDeletes);
   }
   
   /**
    * Get the dirty share manager which is associated with the given database.
    *
    * @param   database
    *          Physical database with which the returned worker is associated.
    *
    * @return  Worker object to which service requests associated with the
    *          given database must be delegated.
    */
   protected DirtyShareManager getWorker(Database database)
   {
      return DirtyShareFactory.getManagerInstance(database);
   }
   
   /**
    * Update DMO snapshot to be leaked to other sessions
    * 
    * @param   database
    *          Value that uniquely identifies the target database on the
    *          remote server.
    * @param   entity
    *          Name of an entity, which is the name of the DMO implementation
    *          class associated with the table being created.
    * @param   dmos
    *          DMOs to be leaked, not NULL 
    * @throws  PersistenceException
    *          if there is any database error.
    */
   @Override
   public void updateSnaphots(int database, String entity, Set<Record> dmos)
   throws PersistenceException
   {
      lookupWorker(database).updateSnapshots(entity, dmos);
   }
}