RecordNursery.java
/*
** Module : RecordNursery.java
** Abstract : A holding site for newly created records which are not yet flushed to the database.
**
** Copyright (c) 2020-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --------------------------------------Description----------------------------------------
** 001 ECF 20200820 Created initial version.
** 002 ECF 20200924 Create session if needed.
** CA 20201003 Use an identity map where possible.
** OM 20220614 Be sure the WRITE trigger is invoked before flushing a record to SQL table, but avoid
** indirect recursive call at the same time.
** OM 20221012 The TriggerTracker are bound to DMO, not to the buffer they are contained into.
** CA 20221021 Always allow the records to be made visible, just block a possible trigger recursion.
** OM 20230111 The dirty context must be notified when a record was flushed to persistence.
** 003 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 004 RAA 20230525 Disabled TriggerTracker for TemporaryBuffer.
** 005 TJD 20221125 Update DirtyShareContext on every index update
** TJD 20240110 Fixes for DirtyShare cross session.
** AL2 20240627 Fixed indexUpdated not identifying the primary index as 0.
** TJD 20240710 Leak intro-session changes to database, write trigger started only for the first leak
** TJD 20240902 Allow checking if RecordNursery holds any DMO related to given RecordBuffer
** TJD 20240912 DirtyShareSupport flags logic dependency update
** 006 SP 20241029 Updated containsRecordBufferDmo to also check if the indexedRecord map is empty.
** 007 EAB 20241008 Created a routine to check for other buffers referencing the same DMO.
** AL2 20241023 Fixed conditional to check if DMO is still in use by another buffer.
** AL2 20241105 Check isReferenced instead of isInUse, as we are not interested in TRACKED records.
** 008 AL2 20240829 Major refactor of the RecordNursery to also manage updated records on a non-index basis.
** AL2 20240830 Used update instead of insert when making the records visible through the updates stash.
** AL2 20240903 Continued refactoring. Properly integrated dirty data construct to better reflect updates.
** AL2 20241025 Added extra checks for cross/intra session dirty share.
** AL2 20241106 Properly invalidate ffCache no matter the configuration for the index updated.
** AL2 20241112 Get buffer from remove caller instead of inferring from the stored DMOs.
** 009 AL2 20241127 Notify the removal attempt of a record from the stash only if it was part of it.
** 010 AL2 20241203 Always notify the removal attempt when cross-session dirty-share is active.
** 011 AL2 20240110 Don't force dirty-share insert on buffer flush if the record is not already tracked.
*/
/*
** 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;
import java.util.*;
import com.goldencode.p2j.persist.dirty.*;
import com.goldencode.p2j.persist.lock.LockType;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.persist.trigger.*;
import com.goldencode.p2j.util.BaseDataType;
import com.goldencode.p2j.util.logging.*;
/**
* A holding site for new or updated DMOs which are not yet flushed to the primary database. When changes to
* a new record's data fully update an index, the record is stored here until it is persisted to the database
* (or such persistence fails). For updated records, any update will register that records in the nursery to
* delay its inserting in the dirty database as much as possible.
*
* When a query is about to be executed on the same index for new records or the same table for updated
* records, any pending DMOs stored are made visible, either by persisting them to their primary
* database, or inserting them into their dirty database.
*/
public final class RecordNursery
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(RecordNursery.class);
/** Stash of records that were registered to the nursery as new */
private RecordsStash<IndexedKey> newRecords;
/** Stash of records that were registered to the nursery as updated, but are not transient. */
private RecordsStash<Key> updatedRecords;
/** Fast-find cache, which must be invalidated when an index is updated, to prevent stale results */
private final FastFindCache ffCache;
/** {@code true} if the cross-session dirty share functionality is enabled */
private final boolean crossSessionEnabled;
/** {@code true} if the intra-session dirty share functionality is enabled */
private final boolean intraSessionEnabled;
/**
* Convenient method to retrieve a {@link DirtyData} object for an existing set of dirty props.
* If the dirty props is not provided, the one from the dmo will be inferred. This is decided
* due to the cases where dmo was freshly flushed and there are no dirty props anymore. In that case
* the dirty props are passed manually on the last parameter. Based on this dirty data, the dirty
* database will do the update.
*
* @param buffer
* The buffer that was holding the dmo when the flushing was triggered.
* @param dmo
* The dmo whose dirty data should be gathered.
* @param dirtyProps
* The properties that were known to be dirty before flush, or {@code null} if this was not
* triggered by a flush and the dirty props can be retrieved from the dmo.
*
* @return A dirty data instance used for inserting/updating DMOs in the dirty database.
*/
static DirtyData getDirtyData(RecordBuffer buffer, BaseRecord dmo, BitSet dirtyProps)
{
if (dirtyProps == null)
{
dirtyProps = dmo.getDirtyProps();
}
if (dirtyProps == null || dirtyProps.size() == 0)
{
return null;
}
DmoMeta dmoMeta = buffer.getDmoInfo();
RecordMeta metadata = dmoMeta.getRecordMeta();
PropertyMeta[] propertyMeta = metadata.getPropertyMeta(false);
// recalculate the size, taking into considerations the arrays
int newSize = dirtyProps.cardinality();
String[] dup = new String[newSize]; // property name
int[] dui = new int[newSize]; // extent offset (-1 for scalars)
BaseDataType[] duv = new BaseDataType[newSize]; // the values
for (int i = 0, j = dirtyProps.nextSetBit(0); j >= 0; j = dirtyProps.nextSetBit(j + 1), i++)
{
PropertyMeta pm = propertyMeta[j];
dup[i] = pm.getName();
dui[i] = pm.getExtent() == 0 ? -1 : j - pm.getOffset();
duv[i] = pm.getDataHandler().getField((Record) dmo, j);
}
return new DirtyData(dup, dui, duv);
}
/**
* Default constructor.
*
* @param ffCache
* The fast-find cache that should be sometimes invalidated by this nursery.
*/
public RecordNursery(FastFindCache ffCache)
{
this.ffCache = ffCache;
this.newRecords = new TransientRecordsStash(this, ffCache);
this.updatedRecords = new UpdatedRecordsStash(this, ffCache);
this.crossSessionEnabled = DirtyShareSupport.isEnabledCrossSession();
this.intraSessionEnabled = DirtyShareSupport.isEnabledIntraSession();
}
/**
* A record was just updated, but its state is not flushed yet to avoid any unwanted write triggers.
* Store the corresponding DMO in the nursery until it is ready to be flushed into the database.
*
* @param buffer
* Record buffer containing the record.
* @param dmo
* Record to be stored.
* @throws PersistenceException
* if there is any error persisting the record to its primary or dirty database.
*/
public void indexUpdated(RecordBuffer buffer, BaseRecord dmo)
throws PersistenceException
{
int dmoUid = buffer.getDmoInfo().getId();
Integer mpxID = buffer.getMultiplexID();
if (this.shouldForceDirty(buffer))
{
DirtyShareContext dctx = buffer.getDirtyContext();
// track the record into the dirty share context
dctx.insert(buffer, (Record) dmo, true, null);
}
else if (this.shouldRetainInNursery(buffer))
{
Key key = new Key(dmoUid, mpxID);
updatedRecords.recordChanges(buffer, dmo, key);
}
ffCache.invalidate(dmoUid, mpxID);
}
/**
* A set of indexes were updated by changes to a newly created, as yet unvalidated record. Store
* the corresponding DMO in the nursery until it is ready to be inserted into the database.
* Make change visible via DirtyShareContext if needed - call DirtyShare only once for all indexes
*
* @param buffer
* Record buffer containing the record.
* @param indices
* Bit set representing those indices that were updated. If <b>null</b> update only primary index.
* @param unique
* {@code true} if {@code indices} refers to unique indices; else {@code false}.
* @param dmo
* Record to be stored.
* @throws PersistenceException
* if there is any error persisting the record to its primary or dirty database.
*/
public void indexUpdated(RecordBuffer buffer, BitSet indices, boolean unique, BaseRecord dmo)
throws PersistenceException
{
int dmoUid = buffer.getDmoInfo().getId();
Integer mpxID = buffer.getMultiplexID();
if (this.shouldForceDirty(buffer))
{
DirtyShareContext dctx = buffer.getDirtyContext();
// track the record into the dirty share context
dctx.insert(buffer, (Record) dmo, true, null);
}
else if (this.shouldRetainInNursery(buffer))
{
if (indices == null)
{
IndexedKey key = new IndexedKey(dmoUid, mpxID, 0);
newRecords.recordChanges(buffer, dmo, key);
}
else
{
for (int i = indices.nextSetBit(0); i >= 0; i = indices.nextSetBit(i + 1))
{
int idxUid = (i + 1) * (unique ? 1 : -1);
// indexUpdated can be called for non-transient records, so check the buffer state first
IndexedKey key = new IndexedKey(dmoUid, mpxID, idxUid);
newRecords.recordChanges(buffer, dmo, key);
}
}
}
if (indices == null)
{
ffCache.invalidate(dmoUid, mpxID, 0);
}
else
{
for (int i = indices.nextSetBit(0); i >= 0; i = indices.nextSetBit(i + 1))
{
int idxUid = (i + 1) * (unique ? 1 : -1);
ffCache.invalidate(dmoUid, mpxID, idxUid);
}
}
}
/**
* The current transaction ended; clean up data structures.
*/
public void transactionEnded()
{
newRecords.clear();
updatedRecords.clear();
}
/**
* The given record was flushed to the database or determined to be invalid; remove it from the nursery.
*
* @param buffer
* The buffer that determined the remove
* @param dmo
* The record to be removed.
* @throws PersistenceException
* the removal may flush some information to the dirty manager, so it may cause persistence
* issues.
*/
public void remove(RecordBuffer buffer, BaseRecord dmo)
throws PersistenceException
{
remove(buffer, dmo, null);
}
/**
* The given record was flushed to the database or determined to be invalid; remove it from the nursery.
*
* @param buffer
* The buffer that determined the remove
* @param dmo
* The record to be removed.
* @param dirtyProps
* Dirty properties before the dmo was actually validated and flushed. This is explicitly sent
* because we can't infer the dirty props from the DMO anymore.
*
* @throws PersistenceException
* the removal may flush some information to the dirty manager, so it may cause persistence
* issues.
*/
public void remove(RecordBuffer buffer, BaseRecord dmo, BitSet dirtyProps)
throws PersistenceException
{
newRecords.remove(buffer, dmo, dirtyProps);
updatedRecords.remove(buffer, dmo, dirtyProps);
}
/**
* Check if RecordNursery holds any DMO related to given RecordBuffer
*
* @param buffer
* Record buffer containing the record.
*
* @return {@code true} if there is at least one DMO related to given RecordBuffer
*/
public boolean containsRecordBufferDmo(RecordBuffer buffer)
{
int dmoUid = buffer.getDmoInfo().getId();
Integer multiplex = buffer.getMultiplexID();
int nonUniqueIndexesCount = buffer.getDmoInfo().getIndexCount(false);
int uniqueIndexesCount = buffer.getDmoInfo().getIndexCount(true);
Key key = new Key(dmoUid, multiplex);
if (updatedRecords.containsKeyNotEmpty(key))
{
return true;
}
for (int idxUid = -nonUniqueIndexesCount; idxUid <= uniqueIndexesCount; idxUid++)
{
IndexedKey indexedKey = new IndexedKey(dmoUid, multiplex, idxUid);
if (newRecords.containsKeyNotEmpty(indexedKey))
{
return true;
}
}
return false;
}
/**
* Make all records which have been stored in the nursery on a particular index visible
* (searchable). If the record is a temp-table record (whether valid or not) or a persistent record
* which is determined to be valid, persist it to its database. For persistent records which have not
* yet been fully validated, insert it into the dirty share manager.
*
* @param dmoUid
* Unique DMO type/table identifier.
* @param multiplex
* Optional multiplex ID (temp-tables only); otherwise {@code null}.
* @param idxUid
* Index identifier (unique within DMO type/table).
*
* @throws PersistenceException
* if there is any error persisting the record to its primary or dirty database.
*/
public void makeVisible(int dmoUid, Integer multiplex, int idxUid)
throws PersistenceException
{
boolean invalidateCache1 = newRecords.makeVisible(new IndexedKey(dmoUid, multiplex, idxUid));
boolean invalidateCache2 = updatedRecords.makeVisible(new Key(dmoUid, multiplex));
if (invalidateCache1 || invalidateCache2)
{
ffCache.invalidate(dmoUid, multiplex);
}
}
/**
* Check if the record nursery registration should be skipped for this buffer according to the
* cross/intra session dirty configuration. Temporary records should never go into the cross-dirty
* share manager, while the persistent records need to be forced to go there only if the
* cross-dirty is enabled.
* <p>
* Such strategy is needed to avoid any delay of reporting changes to the cross-dirty share manager.
* An intra-session scenario has the luxury to cache the changes here and report them only when
* needed.
*
* @param buffer
* The buffer for which the check should be done.
*
* @return {@code true} if the buffer changes should bypass the record nursery caching and be
* made visible into the dirty share manager right away.
*/
private boolean shouldForceDirty(RecordBuffer buffer)
{
if (buffer.isTemporary())
{
return false;
}
return this.crossSessionEnabled && buffer.getDirtyContext() != null;
}
/**
* Check if the record nursery registration is required for this buffer according to the
* cross/intra session dirty configuration. Temporary records should go in the record
* nursery only if the intra-session configuration is enabled. For persistent records,
* we shall register only if intra-session dirty share is enabled, but the cross one
* is disabled. If the cross-session dirty share is enabled, then the changes are
* already eagerly reflected in the dirty database - no need for caching.
*
* @param buffer
* The buffer for which the check should be done.
*
* @return {@code true} if the buffer changes should be retained the record nursery caching
* and be made visible into the dirty share manager at the right time..
*/
private boolean shouldRetainInNursery(RecordBuffer buffer)
{
if (buffer.isTemporary())
{
return this.intraSessionEnabled;
}
else
{
return this.intraSessionEnabled && !this.crossSessionEnabled;
}
}
/**
* A collection of records that were not physically flushed to the primary database yet to avoid
* unwanted unique index violations or write trigger firing. These should theoretically reside in the
* dirty database, but are stored here to delay the dirty database work as much as possible for the
* sake of performance.
*
* @param <K>
* The type of key that will group the records
*/
private static abstract class RecordsStash<K extends Key>
{
/** The nursery associated with this stash. */
final RecordNursery nursery;
/** Maps of records to buffers, organized by a specific key. */
final Map<K, Map<BaseRecord, RecordBuffer>> records = new HashMap<>();
/** Map of record to keys in {@link #records} map, to allow faster cleanup of the latter */
final Map<BaseRecord, Set<K>> keysByDmo = new IdentityHashMap<>();
/** The fast find cache that may be subject to changes with work from this stash. */
final FastFindCache ffCache;
/**
* Basic constructor.
*
* @param nursery
* The record nursery associated with this stash.
* @param ffCache
* The ff cache that may be subject to invalidation after working with the stash.
*/
public RecordsStash(RecordNursery nursery, FastFindCache ffCache)
{
this.nursery = nursery;
this.ffCache = ffCache;
}
/**
* Check if this stash contains a certain key.
* @param key
* The key to be checked.
* @return {@code true} if the provided key is managed by this stash.
*/
public boolean containsKey(K key)
{
return records.containsKey(key);
}
/**
* Check if this stash contains a certain key and is not empty.
* @param key
* The key to be checked.
* @return {@code true} if the provided key is managed by this stash.
*/
public boolean containsKeyNotEmpty(K key)
{
Map<BaseRecord, RecordBuffer> record = records.get(key);
return record != null && !record.isEmpty();
}
/**
* A record was just updated, but its state is not flushed yet to avoid any unwanted behavior.
* Register the change to the stash until it is ready to be flushed into the database.
*
* @param buffer
* Record buffer containing the record.
* @param dmo
* Record to be stored.
* @param key
* The key that defines the group in which the record belongs.
*/
public void recordChanges(RecordBuffer buffer, BaseRecord dmo, K key)
{
Map<BaseRecord, RecordBuffer> dmos = records.computeIfAbsent(key, k -> new IdentityHashMap<>());
dmos.put(dmo, buffer);
Set<K> keys = keysByDmo.computeIfAbsent(dmo, k -> new HashSet<>());
keys.add(key);
}
/**
* Make all records stored in this stash visible (searchable).
* <ul>
* <li>If the record is a temp-table record (whether valid or not) or a persistent record
* which is determined to be valid, persist it to its database.</li>
* <li>For persistent records which have not yet been fully validated,
* insert it into the dirty share manager.</li>
* </ul>
*
* @param key
* The key that defines the group in which the record belongs.
*
* @return {@code true} if the ff cache should be invalidated.
*
* @throws PersistenceException
* if there is any error persisting the record to its primary or dirty database.
*/
public abstract boolean makeVisible(K key)
throws PersistenceException;
/**
* This is called when a dmo is removed from the record nursery. This can happen either because the
* record was flush into the primary database or it was actually deleted. If it was deleted, for sure
* it should be removed from the nursery to avoid phantom records. If it was flushed, then don't insist
* on making it available through the dirty share manager if not already tracked.
*
* @param buffer
* The buffer that caused the create/update in the first place, registering the dmo in
* this nursery.
* @param dmo
* The record that was removed from the nursery.
* @param dirtyProps
* Dirty properties before the dmo was actually validated and flushed. This is explicitly sent
* because we can't infer the dirty props from the DMO anymore.
*
* @throws PersistenceException
* the removal may flush some information to the dirty manager, so it may cause persistence
* issues.
*/
public abstract void notifyRemoved(RecordBuffer buffer, BaseRecord dmo, BitSet dirtyProps)
throws PersistenceException;
/**
* The given record was flushed to the database or determined to be invalid; remove it from the nursery.
*
* @param buffer
* The buffer that determined the remove
* @param dmo
* The record to be removed.
* @param dirtyProps
* Dirty properties before the dmo was actually validated and flushed. This is explicitly sent
* because we can't infer the dirty props from the DMO anymore.
*
* @throws PersistenceException
* the removal may flush some information to the dirty manager, so it may cause persistence
* issues.
*/
public void remove(RecordBuffer buffer, BaseRecord dmo, BitSet dirtyProps)
throws PersistenceException
{
boolean forceNotify = false;
if (nursery.shouldRetainInNursery(buffer))
{
Set<K> keys = keysByDmo.remove(dmo);
if (keys != null && !keys.isEmpty())
{
for (K key : keys)
{
Map<BaseRecord, RecordBuffer> map = records.get(key);
if (map != null)
{
map.remove(dmo);
}
}
forceNotify = true;
}
}
if (dirtyProps != null && (forceNotify || nursery.shouldForceDirty(buffer)))
{
notifyRemoved(buffer, dmo, dirtyProps);
}
}
/**
* The current transaction ended; clean up data structures.
*/
public void clear()
{
records.clear();
keysByDmo.clear();
}
/**
* Useful method that saved a dmo to the primary database. This should be carefully used by makeVisible
* methods to flush a record directly to the primary database if possible.
*
* @param buffer
* The buffer that holds the dmo which should be flushed.
* @param dmo
* The dmo that should be flushed
* @param wasNew
* {@code true} if this was called to flush a newly created record for the first time.
*
* @return {@code true} if the fast find cache should be invalidated.
*
* @throws PersistenceException
* The method is flushing to the database; this can pop up.
*/
boolean saveDmo(RecordBuffer buffer, BaseRecord dmo, boolean wasNew)
throws PersistenceException
{
boolean invalidateCache = false;
// then save the (possibly invalid) record to the database
Session session = buffer.getSession(true);
boolean needTx = !session.inTransaction();
try
{
if (needTx)
{
session.beginTransaction();
}
session.save(dmo, true);
if (needTx)
{
session.commit();
}
invalidateCache = true;
if (wasNew)
{
buffer.recordInserted();
buffer.markPersisted();
}
buffer.reportChange((Record) dmo, wasNew, false);
}
catch (PersistenceException exc)
{
LOG.warning("", exc);
if (needTx)
{
session.rollback();
}
}
return invalidateCache;
}
/**
* Gateway to the dirty share manager. This should be used when the record should be propagated to the
* underlying dirty database: either due to a need to make visible or because it was requested to be
* removed from the nursery. TODO: move to updated records stash
*
* @param buffer
* The buffer that caused the updated dmo to be updated.
* @param dmo
* The dmo that was updated.
* @param dirtyProps
* Dirty properties before the dmo was actually validated and flushed. This is explicitly sent
* because we can't infer the dirty props from the DMO anymore.
*
*
* @throws PersistenceException
* the removal may flush some information to the dirty manager, so it may cause persistence
* issues.
*/
void updateDirty(RecordBuffer buffer, BaseRecord dmo, BitSet dirtyProps)
throws PersistenceException
{
String entity = buffer.getEntityName();
DirtyShareContext dirtyContext = buffer.getDirtyContext();
if (dirtyContext == null)
{
return;
}
try
{
// lock all indexes while sharing (and validating, if needed)
// TODO: optimization: lock only dirty indexes unless we're inserting a new record. As part of this
// optimization, we need to consider the remote server case, such that we minimize the data
// sent across (an array or collections of index name strings may not be efficient).
dirtyContext.lockAllIndexes(entity, LockType.EXCLUSIVE);
DirtyData dirtyData = getDirtyData(buffer, dmo, dirtyProps);
if (dirtyData != null)
{
dirtyContext.update(buffer.getEntityName(), (Record) dmo, false, dirtyData);
DmoMeta dmoMeta = buffer.getDmoInfo();
ffCache.invalidate(dmoMeta.getId(), buffer.getMultiplexID());
}
}
finally
{
// Unlock unique indexes. See TODO above.
dirtyContext.lockAllIndexes(entity, LockType.NONE);
}
}
}
/**
* Stash dedicated for transient records that were registered with the nursery. These are special in the
* sense that they are made visible "per-index", not entirely. So the key is honoring the index on which
* the record should be visible.
*/
private static class TransientRecordsStash
extends RecordsStash<IndexedKey>
{
/**
* Basic constructor.
*
* @param nursery
* The record nursery associated with this stash.
* @param ffCache
* The ff cache that may be subject to invalidation after working with the stash.
*/
TransientRecordsStash(RecordNursery nursery, FastFindCache ffCache)
{
super(nursery, ffCache);
}
/**
* Make all records stored in this stash visible (searchable).
* <ul>
* <li>If the record is a temp-table record (whether valid or not) or a persistent record
* which is determined to be valid, persist it to its database.</li>
* <li>For persistent records which have not yet been fully validated,
* insert it into the dirty share manager.</li>
* </ul>
*
* @param key
* The key that defines the group in which the record belongs.
*
* @return {@code true} if the ff cache should be invalidated.
*
* @throws PersistenceException
* if there is any error persisting the record to its primary or dirty database.
*/
@Override
public boolean makeVisible(IndexedKey key)
throws PersistenceException
{
Map<BaseRecord, RecordBuffer> dmos = records.get(key);
if (dmos == null || dmos.isEmpty())
{
return false;
}
boolean invalidateCache = false;
try
{
for (Map.Entry<BaseRecord, RecordBuffer> entry : dmos.entrySet())
{
BaseRecord dmo = entry.getKey();
RecordBuffer buffer = entry.getValue();
if (buffer.getCurrentRecord() == null)
{
if (dmo.isReferenced())
{
buffer = buffer.getBufferManager()
.getRecordBuffersByDMOBufClass(buffer.getDMOBufInterface().getName(),
buffer.getMultiplexID(),
dmo.primaryKey());
if (buffer == null)
{
continue;
}
}
else
{
continue;
}
}
Set<IndexedKey> keys = keysByDmo.get(dmo);
if (keys != null)
{
keys.remove(key);
}
boolean temp = buffer.isTemporary();
BitSet dirtyProps = null;
if (temp || !dmo.needsValidation())
{
if ((buffer.getTriggerTracker() == null ||
!buffer.getTriggerTracker()
.isExecuting(DatabaseEventType.WRITE, buffer.getCurrentRecord().primaryKey())))
{
// avoid indirect recursion. This buffer is already about to be flushed up the stack
// invoke the Write trigger, if necessary, but only for the first time record is flushed
// consider other flushes as local cross-buffers leaks
buffer.maybeFireWriteTrigger();
}
// temp doesn't use dirty share; don't snapshot the dirty props
dirtyProps = temp ? null : (BitSet) dmo.getDirtyProps().clone();
invalidateCache = saveDmo(buffer, dmo, true);
}
// in case dmo needs validation store it in the dirty db to allow correct querying via dirty DB
else
{
DirtyShareContext dctx = buffer.getDirtyContext();
if (dctx != null)
{
// insert the record into the dirty share context
dctx.insert(buffer, (Record) dmo, true, null);
invalidateCache = true;
}
}
remove(buffer, dmo, dirtyProps);
}
}
finally
{
dmos.clear();
}
return invalidateCache;
}
/**
* This is called when a dmo is removed from the record nursery. This can happen either because the
* record was flush into the primary database or it was actually deleted. If it was deleted, for sure
* it should be removed from the nursery to avoid phantom records. If it was flushed, then don't insist
* on making it available through the dirty share manager if not already tracked.
*
* @param buffer
* The buffer that caused the create/update in the first place, registering the dmo in
* this nursery.
* @param dmo
* The record that was removed from the nursery.
* @param dirtyProps
* Dirty properties before the dmo was actually validated and flushed. This is explicitly sent
* because we can't infer the dirty props from the DMO anymore.
*
* @throws PersistenceException
* the removal may flush some information to the dirty manager, so it may cause persistence
* issues.
*/
@Override
public void notifyRemoved(RecordBuffer buffer, BaseRecord dmo, BitSet dirtyProps)
throws PersistenceException
{
DirtyShareContext dctx = buffer.getDirtyContext();
if (dirtyProps != null && dirtyProps.cardinality() > 0 && dctx != null)
{
boolean isTracked = dctx.isTracked(new RecordIdentifier<>(buffer.getEntityName(),
dmo.primaryKey()));
if (!isTracked && !nursery.shouldForceDirty(buffer))
{
// if the record is not tracked, don't start tracking it for nothing
return;
}
DirtyData dirtyData = getDirtyData(buffer, dmo, dirtyProps);
// insert the record anyway as it should be visible downstream (with or without dirty changes)
dctx.insert(buffer, (Record) dmo, true, dirtyData);
}
}
}
/**
* Stash dedicated for updated records that were registered with the nursery. These are shashed here because
* we couldn't afford flushing them to the database yet, mostly due to write triggers. Delay the introduction
* to the dirty database as much as possible.
*/
private static class UpdatedRecordsStash
extends RecordsStash<Key>
{
/**
* Basic constructor.
*
* @param nursery
* The record nursery associated with this stash.
* @param ffCache
* The ff cache that may be subject to invalidation after working with the stash.
*/
UpdatedRecordsStash(RecordNursery nursery, FastFindCache ffCache)
{
super(nursery, ffCache);
}
/**
* Make all records stored in this stash visible (searchable).
* <ul>
* <li>If the record is a temp-table record (whether valid or not) or a persistent record
* which is determined to be valid, persist it to its database.</li>
* <li>For persistent records which have not yet been fully validated,
* insert it into the dirty share manager.</li>
* </ul>
*
* @param key
* The key that defines the group in which the record belongs.
*
* @return {@code true} if the ff cache should be invalidated.
*
* @throws PersistenceException
* if there is any error persisting the record to its primary or dirty database.
*/
@Override
public boolean makeVisible(Key key)
throws PersistenceException
{
Map<BaseRecord, RecordBuffer> dmos = records.get(key);
if (dmos == null || dmos.isEmpty())
{
return false;
}
boolean invalidateCache = false;
try
{
for (Map.Entry<BaseRecord, RecordBuffer> entry : dmos.entrySet())
{
BaseRecord dmo = entry.getKey();
RecordBuffer buffer = entry.getValue();
if (buffer.getCurrentRecord() == null)
{
if (dmo.isReferenced())
{
buffer = buffer.getBufferManager()
.getRecordBuffersByDMOBufClass(buffer.getDMOBufInterface().getName(),
buffer.getMultiplexID(),
dmo.primaryKey());
if (buffer == null)
{
continue;
}
}
else
{
continue;
}
}
Set<Key> keys = keysByDmo.get(dmo);
if (keys != null)
{
keys.remove(key);
}
boolean temp = buffer.isTemporary();
if (temp)
{
invalidateCache = saveDmo(buffer, dmo, false);
}
else
{
// in case dmo needs validation store it in the dirty db to allow correct querying via dirty DB
updateDirty(buffer, dmo, null);
}
// the DMO is removed from keysByDmo, so we should forcefully update it in the dirty-share
remove(buffer, dmo, null);
}
}
finally
{
dmos.clear();
}
return invalidateCache;
}
/**
* The given record was flushed to the database or determined to be invalid; remove it from the nursery.
*
* @param dmo
* The record to be removed.
* @param dirtyProps
* Dirty properties before the dmo was actually validated and flushed. This is explicitly sent
* because we can't infer the dirty props from the DMO anymore.
*
* @throws PersistenceException
* the removal may flush some information to the dirty manager, so it may cause persistence
* issues.
*/
@Override
public void notifyRemoved(RecordBuffer buffer, BaseRecord dmo, BitSet dirtyProps)
throws PersistenceException
{
DirtyShareContext dctx = buffer.getDirtyContext();
if (dctx != null &&
(dirtyProps != null && dirtyProps.cardinality() != 0) &&
dctx.isTracked(new RecordIdentifier<>(buffer.getEntityName(), dmo.primaryKey())))
{
updateDirty(buffer, dmo, dirtyProps);
}
}
}
/**
* The key that groups the records in the stash, based on the DMO id and eventually the multiplex.
* This is used for updated records as they are visible on all indexes at any time.
*/
private static class Key
{
/** Unique DMO type identifier */
final long dmoUid;
/** Cached hash code */
int hash;
/**
* Basic constructor.
*
* @param duid
* The id of the DMO
* @param multiplex
* The multiplex (if any) of the DMO.
*/
Key(int duid, Integer multiplex)
{
this.dmoUid = multiplex == null ? duid : ((((long) multiplex) << 32) | duid);
}
/**
* Simple method that computes the hash and caches it
*
* @return The hash of this key.
*/
int computeHash()
{
return this.hash = Long.hashCode(dmoUid);
}
/**
* Basic equals implementation
*/
@Override
public boolean equals(Object o)
{
Key that = (Key) o;
return this.dmoUid == that.dmoUid;
}
/**
* Basic hashCode implementation
*/
@Override
public int hashCode()
{
return hash != 0 ? hash : computeHash();
}
/**
* Basic toString implementation
*/
@Override
public String toString()
{
return "Key [dmoUid=" + dmoUid + "," + ", hash=" + hash + "]";
}
}
/**
* The key that groups the records in the stash, based on the DMO id, eventually the multiplex and
* the id of an index. This is used for new records as they are visible only on fully updated indexes.
* Thus, it is important to ensure that only records on specific indexes are made visible.
*/
private static class IndexedKey
extends Key
{
/** Unique index identifier */
private final int idxUid;
/**
* Basic constructor.
*
* @param duid
* The id of the DMO
* @param multiplex
* The multiplex (if any) of the DMO.
* @param iuid
* The id of the index used for this key.
*/
IndexedKey(int duid, Integer multiplex, int iuid)
{
super(duid, multiplex);
this.idxUid = iuid;
}
/**
* Simple method that computes the hash and caches it
*
* @return The hash of this key.
*/
int computeHash()
{
return this.hash = (int) (31 * super.computeHash() + idxUid);
}
/**
* Basic equals implementation
*/
@Override
public boolean equals(Object o)
{
IndexedKey that = (IndexedKey) o;
return super.equals(o) && this.idxUid == that.idxUid;
}
/**
* Basic toString implementation
*/
@Override
public String toString()
{
return "IndexedKey [dmoUid=" + dmoUid + ", idxUid=" + idxUid + ", hash=" + hash + "]";
}
}
}