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);
}
}