DirtyShareContext.java
/*
** Module : DirtyShareContext.java
** Abstract : The implementation of DirtyShareContext
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20080420 @38626 Created initial version. Client context's
** primary bridge to the DirtyShareManager.
** Manages dirty state specific to the current
** context.
** 002 ECF 20080610 @38682 Added safety logic to prevent NPEs.
** 003 ECF 20080612 @38719 Fixed getDirtyInfo(). If an uncommitted
** delete was made before the backing table in
** the dirty database was created, we could
** attempt a query nonetheless and bomb.
** 004 ECF 20080627 @39073 Minor optimization. Avoid collection empty
** check is some cases.
** 005 CA 20080716 @39122 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.
** 006 ECF 20080815 @39542 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.
** 007 ECF 20081211 @40869 Fixed delete()/rollbackInsert() defect. The
** deletion of a record which was inserted in
** the same transaction rolls back the insert,
** but does not track the record as deleted.
** This caused a problem when the insert itself
** was later rolled back. Such records are now
** tracked via the deadInserts set.
** 008 ECF 20090428 @42129 Added isLocalInsert(). Reports whether a record
** is being tracked as newly inserted by this
** context.
** 009 ECF 20090513 @42173 Modified update() method. Need to pass new lock
** parameter to modified DirtyShareManager update()
** method.
** 010 ECF 20090601 @42569 Added support for Progress isolation leak quirk.
** Added new methods.
** 011 ECF 20090610 @42639 Modified updateSnapshots() method. Now returns
** list of DMOs affected and tracks entities whose
** uncommitted changes have been leaked. The list of
** such entities is now used during cleanup.
** 012 ECF 20090611 @42661 Optimized updateSnapshots(). In a long-running
** transaction with many updates, it was doing much
** unnecessary work. We now track which DMOs need to
** have their snapshots updated and refresh only
** these.
** 013 CA 20090721 @43285 Fixed rollback of dirty records, when a record is
** in dead insert mode - the changes map should not
** be checked in this case.
** 014 ECF 20090721 @43307 Modified delete(). If the record being deleted is
** already being tracked for changes, it must be
** removed from the changes and dirtySnapshots
** collections.
** 015 ECF 20090819 @43692 Added new update() variant. Used when it is known
** that the target DMO already is being tracked by
** the dirty share manager.
** 016 ECF 20110405 Fixed dirty change leak. Remove record identifier
** from deadInserts set upon insert to avoid error
** during rollbackInsert() for same record later.
** Otherwise, a record is leaked in the dirty share
** manager for the life of the server process. Also
** fixed (separate) minor bug in rollbackInsert()
** which was not removing dirtySnapshot identifiers
** when their corresponding records were removed
** from dirty share tracking.
** 017 ECF 20131028 Import change.
** 018 ECF 20140821 Changed insert API to allow insertion of unvalidated, newly created
** records.
** 019 OM 20160307 Added support for EXTENTs in update() methods. Updated signature of
** getDirtyInfo().
** 020 ECF 20160608 Removed unnecessary intern calls.
** 021 ECF 20160917 Added ignoreLocal parameter to isDirtyDelete.
** 022 ECF 20171217 Prevent error trying to merge null DMOs in updateSnapshots.
** 20171219 Avoid using incorrect DMO in update during new record creation.
** 023 ECF 20180719 Fixed defect in optimization in getDirtyInfo.
** 024 ECF 20200901 New ORM implementation.
** 025 OM 20200929 Reset the temporary NEW flag after record is saved to dirty database.
** OM 20210419 After a record is successfully inserted in database table, the buffer storing it
** is removed from early inserts list of dirty context.
** OM 20221103 New class names for FQLPreprocessor, FQLExpression, FQLBundle, and FQLCache.
** 026 AL2 20230822 Removed readOnly parameter.
** 027 AL2 20240319 Extended isEntityDirty to allow checking only the current context.
** 028 TJD 20240123 Java 17 compatibility updates
** 029 TJD 20230421 Allow reuse of DELETED and then UPDATED dirty share records
** TJD 20230508 Updates needed to support dirty records leaks to other sessions on FIND
** TJD 20230724 Migrate earlyPublishEntities from List to Set to improve performance
** TJD 20240109 Fixes and updates to cross section DirtyShare
** TJD 20240308 DirtyShare optimizations and simplifications
** TJD 20240318 Support for cases with released records
** TJD 20240604 Support for cases with records modified after initial flushing
** TJD 20240619 Fix intra session earlyInserts leaks
** AL2 20240903 Added dirtyData as a marker on the data that was actually changed.
** 030 AL2 20241021 Adapt getDirtyInfo for CAN-FIND cases in which the buffer is not loaded with the
** actual DMO as a result of the query execution.
** 031 OM 20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
** based on [sharedDb] parameter.
** 032 AL2 20250110 Use entity and DMO instead of buffer when updating.
** 033 ES 20250426 Handle StopConditionException through ErrorManager.
*/
/*
** 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.dirty;
import java.util.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.Record;
import com.goldencode.p2j.persist.event.*;
import com.goldencode.p2j.persist.lock.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
/**
* Default implementation of a client context's primary interface to store
* uncommitted index updates for sharing with other contexts, and to read
* uncommitted index updates made by other contexts. This is intended to
* support compatibility with Progress' peculiar transaction isolation
* behavior of immediately sharing updates to indexed fields across user
* sessions, but deferring the sharing of updates to non-indexed fields until
* the enclosing transaction is committed.
* <p>
* This implementation maintains state specific to the current context, in
* order both to filter information appropriately, and to be able to clean up
* shared data when the current context's transactions are completed.
*
* @see DirtyShareManager
* @see DefaultDirtyShareManager
*/
public final class DirtyShareContext
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(DirtyShareContext.class.getName());
/** Object which manages dirty reads for the database */
private final DirtyShareManager manager;
/** Identifiers of records inserted by this context */
private Set<RecordIdentifier<String>> inserts = null;
/** Identifiers of records leaked by this context */
private Set<RecordIdentifier<String>> leaks = null;
/** Identifiers of records inserted by this context, which deletes have rolled back */
private Set<RecordIdentifier<String>> deadInserts = null;
/** Buffers holding newly created records, which have not yet had all their indices updated */
private Map<RecordIdentifier<String>, RecordBuffer> earlyInserts = null;
/** Map of entity names to sets of IDs inserted/updated by this context */
private Map<String, Set<Long>> changes = null;
/** Map of entity names to sets of IDs deleted by this context */
private Map<String, Set<Long>> deletes = null;
/** Set of IDs of DMOs changed since last updateSnapshots() call */
private Set<RecordIdentifier<String>> dirtySnapshots = null;
/**
* Constructor which stores a reference to the dirty share manager for the
* associated database.
*
* @param manager
* Dirty share manager for the associated database.
*/
DirtyShareContext(DirtyShareManager manager)
{
this.manager = manager;
}
/**
* Register with a global event clearinghouse an interest in insert,
* delete, and update events which affect the given DMO entity.
*
* @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.
*
* @see #getGlobalEvents(long, long)
* @see #deregisterForGlobalEvents(long)
*/
public Long registerForGlobalEvents(String entity)
{
return manager.registerForGlobalEvents(entity);
}
/**
* Deregister with a global event clearinghouse an interest in insert,
* delete, and update events which affect a particular DMO type.
*
* @param registerID
* A unique identifier for the registrant being deregistered.
*
* @see #getGlobalEvents(long, long)
* @see #registerForGlobalEvents(String)
*/
public void deregisterForGlobalEvents(long registerID)
{
manager.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 registerID
* A unique identifier for an object which has registered interest
* in record-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(long registerID, long lastID)
throws EventRegistrationException
{
return manager.getGlobalEvents(registerID, lastID);
}
/**
* Lock or unlock all indexes associated with the given DMO entity.
*
* @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(String entity, LockType lockType)
throws PersistenceException
{
manager.lockAllIndexes(entity, lockType);
}
/**
* Begin tracking a newly inserted record in the dirty database.
* <p>
* This record is removed when the adding context's current transaction is committed or rolled back.
*
* @param buffer
* Record buffer responsible for the insert.
* @param dmo
* Record to be tracked.
* @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.
*/
public void insert(RecordBuffer buffer, Record dmo, boolean lock, DirtyData dirtyData)
throws PersistenceException
{
if (dmo == null)
{
// no current buffer, no-op
return;
}
String entity = buffer.getEntityName();
Long id = dmo.primaryKey();
RecordIdentifier<String> ident = new RecordIdentifier<>(entity, id);
// make a copy of the DMO to share with other contexts
Record copy = dmo.snapshot();
// if record comes from record nursery and needs validation mark it as such
if (dmo.needsValidation())
{
if (earlyInserts == null)
{
earlyInserts = new HashMap<>();
}
earlyInserts.putIfAbsent(ident, buffer);
}
if (deadInserts != null)
{
// Remove this record from the dead inserts set if we were tracking
// it as such; this will prevent any problems if we later have to
// roll back this insert.
deadInserts.remove(ident);
}
// store in database
manager.insert(entity, copy, dmo.getIndexState(), lock, dirtyData);
// Track insert in local context.
if (inserts == null)
{
inserts = new HashSet<>();
}
inserts.add(ident);
trackChange(entity, id);
// the NEW flag was added above to instruct the [Persister] that this is a new record for this database
// and it needs to be inserted, not updated, but it was not cleared because the second parameter of
// [session.save(copy, false);] prevented the flag to be unset. Doing this now:
copy.updateState(null, DmoState.NEW, false);
}
/**
* Indicate whether the specified record is being tracked as newly inserted
* by this context.
*
* @param ident
* Record identifier of the record to be checked.
*
* @return <code>true</code> if this record is newly inserted by this
* context, else <code>false</code>.
*/
public boolean isTracked(RecordIdentifier<String> ident)
{
if (changes != null && changes.containsKey(ident.getTable()))
{
if (changes.get(ident.getTable()).contains(ident.getRecordID()))
{
return true;
}
}
return manager.isTracked(ident.getTable(), ident.getRecordID());
}
/**
* If the specified record is an unvalidated insert created by a buffer local to this context,
* get the buffer which created it.
*
* @param ident
* Record identifier of the record to be checked.
*
* @return Creating buffer, or <code>null</code> if the record is not a locally created,
* unvalidated insert.
*/
public RecordBuffer getCreatingBuffer(RecordIdentifier<String> ident)
{
return earlyInserts != null ? earlyInserts.get(ident) : null;
}
/**
* Rollback the insert of a record previously introduced with the {@link
* #insert(RecordBuffer, Record, boolean, DirtyData)} method.
*
* @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 IllegalStateException
* if the record was not inserted in this context during the
* current transaction
* @throws PersistenceException
* if there is any database error.
*/
public void rollbackInsert(RecordIdentifier<String> ident, boolean lock)
throws PersistenceException
{
// if the target record was already deleted from the dirty context, then
// the changes map check is not relevant (it may be empty if only one
// record is in the dirty context and this is a dead record)
if ((deadInserts == null || !deadInserts.contains(ident)) &&
(changes == null || changes.isEmpty() ||
inserts == null || !inserts.contains(ident)))
{
throw new IllegalStateException(
"No dirty entry for '" + ident + "'; cannot roll back insert");
}
try
{
if (deadInserts != null && deadInserts.remove(ident))
{
// This insert occurred within the existing transaction, and was
// rolled back by a subsequent delete within the transaction, so
// it already has been removed from the dirty share manager. We
// cannot remove it again as this will cause an error.
if (deadInserts.isEmpty())
{
deadInserts = null;
}
return;
}
// Remove from local context insert tracking.
inserts.remove(ident);
if (earlyInserts != null)
{
earlyInserts.remove(ident);
}
// Remove from database.
manager.rollbackInsert(ident, lock);
}
finally
{
// Remove from local context change tracking.
String entity = ident.getTable();
Long id = ident.getRecordID();
Set<Long> ids = changes.get(entity);
if (ids != null && ids.remove(id))
{
if (ids.isEmpty())
{
changes.remove(entity);
}
dirtySnapshots.remove(ident);
}
}
}
/**
* Notify this object that a record was inserted into database and should not be tracked any more in
* {@code earlyInserts}.
*
* @param id
* The identifier of the record.
*/
public void recordInserted(RecordIdentifier<String> id)
{
if (earlyInserts != null && !earlyInserts.isEmpty())
{
earlyInserts.remove(id);
}
}
/**
* Track an update to one or more indexed, DMO properties, as indicated by the
* {@code properties}, {@code extIndexes} and {@code values} arguments.
* <p>
* Any record added to the dirty database as a result of this method is removed when the adding
* context's current transaction is committed or rolled back.
* <p>
* Assumes appropriate indexes have been locked by the caller.
*
* @param entity
* The entity table for which the DMO is provided
* @param dmo
* The DMO that should be registered as updated to the dirty-share mechanism.
* @param transientIndexValidated
* {@code true} if the update was caused by an index validation of a transient record.
* @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(String entity,
Record dmo,
boolean transientIndexValidated,
DirtyData dirtyData)
throws PersistenceException
{
if (dmo == null)
{
// no-op
return;
}
Long id = dmo.primaryKey();
// determine whether the dirty share manager is already tracking changes to this record from
// a previous insert or update. If not, begin tracking the record, then update it. This set
// of calls is assumed to be "atomic", since we must have an exclusive lock on the record.
if (manager.isTracked(entity, id))
{
manager.update(entity,
id,
transientIndexValidated,
false,
dirtyData);
}
else
{
// get a snapshot of a current record if not yet tracked and start tracking it
// not sure if this ever will be called as most cases are handled by insert now.
Record copy = dmo.snapshot();
copy.updateState(null, DmoState.NEW, true); // the record is new relative to DIRTY database
manager.update(entity,
copy,
false,
dirtyData);
copy.updateState(null, DmoState.NEW, false);
}
// Track update in local context.
trackChange(entity, id);
}
/**
* Mark a record as having been deleted in an uncommitted transaction, or
* unmark a record which previously was so marked.
* <p>
* If marking a record as deleted, that was earlier inserted by the current
* context, rollback the insert, but do not mark the record as deleted.
* <p>
* A record is unmarked when the adding context's current transaction is
* committed or rolled back.
*
* @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 a database error occurs rolling back an earlier insert.
*/
public void delete(String entity, Long id, boolean rollback, boolean lock)
throws PersistenceException
{
RecordIdentifier<String> ident = new RecordIdentifier<>(entity, id);
if (!rollback)
{
// Special case 1: if we are deleting a record we previously
// inserted in the same transaction, remove the record, but do not
// update the shared deleted records state.
if (inserts != null && inserts.contains(ident))
{
rollbackInsert(ident, lock);
if (deadInserts == null)
{
deadInserts = new HashSet<>(8);
}
deadInserts.add(ident);
return;
}
// Special case 2: if we are deleting a record we previously
// updated in the same transaction, remove the record, but still
// update the shared deleted records state.
if (changes != null)
{
Set<Long> changed = changes.get(entity);
if (changed != null && changed.remove(id))
{
if (changed.isEmpty())
{
changes.remove(entity);
}
}
}
if (dirtySnapshots != null)
{
dirtySnapshots.remove(ident);
}
}
// Update shared state.
try
{
manager.delete(entity, id, rollback, lock);
}
finally
{
// Track state locally, so we can clean up at end of transaction.
if (rollback)
{
if (deletes != null)
{
Set<Long> ids = deletes.get(entity);
if (ids != null)
{
if (ids.remove(id) && ids.isEmpty())
{
deletes.remove(entity);
}
}
}
}
else
{
Set<Long> ids = null;
if (deletes == null)
{
deletes = new HashMap<>();
}
else
{
ids = deletes.get(entity);
}
if (ids == null)
{
ids = new HashSet<>();
deletes.put(entity, ids);
}
ids.add(id);
}
}
}
/**
* Indicate whether there are currently any uncommitted changes (inserts,
* updates, or deletes) in the table represented by the given DMO entity
* name.
*
* @param entity
* Fully qualified name of the DMO entity to be checked.
*
* @return <code>true</code> if there any pending, uncommitted changes in
* the backing table, else <code>false</code>.
*/
public boolean isEntityDirty(String entity)
{
return isEntityDirty(entity, false);
}
/**
* Indicate whether there are currently any uncommitted changes (inserts,
* updates, or deletes) in the table represented by the given DMO entity
* name.
*
* @param entity
* Fully qualified name of the DMO entity to be checked.
* @param localOnly
* Check for the dirty status only in the current context.
*
* @return <code>true</code> if there any pending, uncommitted changes in
* the backing table, else <code>false</code>.
*/
public boolean isEntityDirty(String entity, boolean localOnly)
{
if (localOnly)
{
if (changes != null && changes.containsKey(entity))
{
return true;
}
else if (deletes != null && deletes.containsKey(entity))
{
return true;
}
return false;
}
else
{
int dirtyState = manager.getEntityDirtyState(entity);
return (dirtyState != DirtyInfo.NONE);
}
}
/**
* Indicate whether the record specified by the given entity and primary key has
* been deleted within an uncommitted transaction within a context, optionally
* ignoring deletes in the current context.
*
* @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.
* @param ignoreLocal
* Ignore a delete if it occurred in the current context and only report
* whether a delete of the given record occurred in a different context.
*
* @return {@code true} if the specified record has been deleted, else {@code false}.
*/
public boolean isDirtyDelete(String entity, Long id, boolean ignoreLocal)
{
if (!manager.isDeleted(entity, id))
{
return false;
}
if (ignoreLocal)
{
// It is marked deleted in the shared space, but if this context is the
// one that deleted it, return false.
Set<Long> deleted = null;
if (deletes != null)
{
deleted = deletes.get(entity);
}
return (deleted == null || !deleted.contains(id));
}
return true;
}
/**
* Retrieve from the dirty database the record specified by the given
* entity and primary key, if it exists, and if it was not put there by the
* current context.
*
* @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, and was not
* put there by the current context, else <code>null</code>.
*
* @throws PersistenceException
* if there is any database error.
*/
public Record getDirtyDMO(String entity, Long id)
throws PersistenceException
{
// If we're the context making the change, we don't need the dirty
// copy, since it's the same as the one we have in our current session.
if (changes != null)
{
Set<Long> changed = changes.get(entity);
if (changed != null && changed.contains(id))
{
return null;
}
}
return manager.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 entity
* Name of an entity, which is the name of the DMO implementation class associated
* with a table being tracked for uncommitted changes. Should be {@code null}
* for a multi-table query.
* @param fql
* FQL query statement. 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.
*/
public <T> List<T> list(String entity,
String fql,
Object[] params,
int maxResults,
int startOffset)
throws PersistenceException
{
// if a specific entity is provided, check its dirty state to provide a quick out for the
// common case
if (entity != null)
{
int dirtyState = manager.getEntityDirtyState(entity);
if ((dirtyState & DirtyInfo.CHANGE) == DirtyInfo.NONE)
{
return null;
}
}
// query the dirty share manager
return manager.list(fql, params, maxResults, startOffset);
}
/**
* Given certain search criteria and possibly 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 buffer
* Record buffer associated with the current query.
* @param index
* UID of index which governs the ordering of data in the current query.
* @param queryType
* Type of query.
* @param bundle
* An object containing one or more FQL 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 FQL
* statement.
* @param found
* DMO, if any, found in the primary database, which matches the current query criteria.
* @param byRowid
* 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.
* @param bufferLoadedByQuery
* The buffer associated with the current query is going to be loaded by the query.
* This doesn't happen for CAN-FIND, that uses the buffer only to reference the entity, not
* for actual loading.
*
* @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 is any database error.
*/
public DirtyInfo getDirtyInfo(RecordBuffer buffer,
int index,
int queryType,
FQLBundle bundle,
Object[] params,
Record found,
boolean byRowid,
boolean bufferLoadedByQuery)
throws PersistenceException
{
String entity = buffer.getEntityName();
int dirtyState = manager.getEntityDirtyState(entity);
// quick out (hopefully the common case): if there are no changes pending for the target
// entity, we can bypass the check altogether
if (dirtyState == DirtyInfo.NONE)
{
return null;
}
Long foundID = (found != null ? found.primaryKey() : null);
// if we found a record in the primary database with a unique find, and we already are
// tracking that record for changes/insert in this context (and it is validated), the
// primary database in this session already reflects reality, and we don't need to check
// the dirty database
if (foundID != null && queryType == QueryConstants.UNIQUE && changes != null)
{
Set<Long> ids = changes.get(entity);
if (ids != null && ids.contains(foundID) &&
(earlyInserts == null || !earlyInserts.containsKey(new RecordIdentifier<>(entity, foundID))))
{
return null;
}
}
// has the record found in primary database been deleted?
// if record was deleted and changed afterwards we should continue here
if ((dirtyState & DirtyInfo.DELETE) == DirtyInfo.DELETE &&
((dirtyState & DirtyInfo.CHANGE) != DirtyInfo.CHANGE) &&
foundID != null &&
isDirtyDelete(entity, foundID, false))
{
DirtyInfo info = new DirtyInfo();
info.setDeleted();
return info;
}
DirtyInfo info = null;
if ((dirtyState & DirtyInfo.CHANGE) == DirtyInfo.CHANGE)
{
List<String> statements = bundle.getDirtyStatements();
info = manager.getDirtyInfo(entity, foundID, index, statements, params, byRowid);
if (info == null)
{
return null;
}
Record dirtyDMO = info.getDirtyDMO();
if (dirtyDMO != null && changes != null)
{
// ignore validated changes that are driven by this context; they are already
// reflected in the primary database
Long id = dirtyDMO.primaryKey();
RecordIdentifier<String> ident = new RecordIdentifier<>(entity, id);
Set<Long> changed = changes.get(entity);
// if dmo is changed in the current context
if (changed != null &&
changed.contains(id))
{
// first check whether the dirty DMO represents an unvalidated record being managed by this
// context; if so, replace the copy generated by the dirty database with the actual
// record being managed for the primary database.
if (earlyInserts != null && earlyInserts.containsKey(ident))
{
RecordBuffer creatingBuffer = earlyInserts.get(ident);
// if DMO was created by a different buffer replace DirtyInfo with local version
if ((!bufferLoadedByQuery || creatingBuffer != buffer) && creatingBuffer.isTransient())
{
Record localDMO = creatingBuffer.getCurrentRecord();
info.setInserted();
info.setLocal();
info.setDirtyDMOs(localDMO, localDMO);
}
else
{
// ignore DirtyInfo result in other case
return null;
}
}
// otherwise if we have a candidate and it exists in DirtyShare database ignore DirtyInfo,
// our candidate shall prevail
else if (foundID != null && info.isModified())
{
return null;
}
}
}
}
return info;
}
/**
* Clean up all information contributed to the dirty share manager by this context for the current
* transaction. This method is called when a full database transaction has been committed or rolled back
* against the primary database. It is also invoked when the client context ends. It removes all records
* it added to the dirty database for new inserts, updates, leaks and deletes.
*
* @throws StopConditionException
* if a critical database error occurs that would leave the dirty database in an improper state.
*/
public void cleanup()
{
try
{
if (changes != null)
{
if (leaks != null && inserts != null)
{
inserts.addAll(leaks);
}
manager.cleanupChanges(changes, inserts);
}
if (deletes != null)
{
manager.cleanupDeletes(deletes);
}
}
catch (PersistenceException exc)
{
ErrorManager.handleStopException(new StopConditionException(exc));
}
finally
{
leaks = null;
changes = null;
inserts = null;
deadInserts = null;
earlyInserts = null;
deletes = null;
dirtySnapshots = null;
}
}
/**
* Remember that the record represented by the given entity and primary key
* ID has been modified in the current context. This is necessary for
* proper cleanup at the end of the current transaction.
*
* @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 modified record.
*/
private void trackChange(String entity, Long id)
{
if (changes == null)
{
changes = new HashMap<>();
}
if (dirtySnapshots == null)
{
dirtySnapshots = new HashSet<>();
}
Set<Long> ids = changes.computeIfAbsent(entity, k -> new HashSet<>());
ids.add(id);
RecordIdentifier<String> ident = new RecordIdentifier<String>(entity, id);
dirtySnapshots.add(ident);
}
/**
* Overwrite the state of the records in the dirty database which match the
* IDs of all DMOs for the given entities, which have been modified by the
* current context.
* <p>
* This is necessary to support a quirk/bug in Progress which compromises
* transaction isolation by leaking uncommitted changes (even non-indexed
* data) under certain conditions.
*
* @param entities
* List of entities, which are the names of the DMO implementation
* classes associated with tables being tracked for uncommitted
* changes. Only those records which match these entities will
* have their current state leaked.
* @param persistence
* Persistence service object to be used to retrieved the latest
* version of the target records in the current context.
*
* @return List of all DMOs whose snapshots were updated with the dirty
* share manager. The list may be empty, but will not be
* <code>null</code>.
*
* @throws PersistenceException
* if there is any database error.
*/
public List<Record> updateSnapshots(Set<String> entities, Persistence persistence)
throws PersistenceException
{
List<Record> allDMOs = null;
if (changes == null || dirtySnapshots == null)
{
// Nothing to update.
return Collections.emptyList();
}
for (String entity : entities)
{
Set<Long> changed = changes.get(entity);
if (changed == null || changed.isEmpty())
{
continue;
}
if (allDMOs == null)
{
allDMOs = new ArrayList<>();
}
Set<Record> entityDMOs = new HashSet<>();
Iterator<Long> iter = changed.iterator();
while(iter.hasNext())
{
Long id = iter.next();
RecordIdentifier<String> ident = new RecordIdentifier<>(entity, id);
Record dmo = null;
if ((earlyInserts == null || !earlyInserts.containsKey(ident)) &&
dirtySnapshots.remove(ident))
{
// get record from local cache if stored or made visible
dmo = persistence.quickLoad(ident, Persistence.SHARED_CTX);
}
else if (dirtySnapshots.remove(ident))
{
// get record from earlyInserts
RecordBuffer buffer = earlyInserts.get(ident);
dmo = buffer.getCurrentRecord();
if (dmo == null)
{
// early inserts record is released and cant do anything about it, try next one
continue;
}
IndexState is = dmo.getIndexState();
// but if given record do not have all indices complete do not dirty leak it
if (!is.isEmpty())
{
dmo = null;
}
}
if (dmo != null)
{
if (leaks == null)
{
leaks = new HashSet<>();
}
// leak that dirty DMO
leaks.add(ident);
entityDMOs.add(dmo);
// continue updating list of DMOs to be potentially evicted
allDMOs.add(dmo);
}
}
if (!entityDMOs.isEmpty())
{
manager.updateSnapshots(entity, entityDMOs);
}
}
if (allDMOs == null)
{
allDMOs = Collections.emptyList();
}
return allDMOs;
}
}