# TxWrapper.java

    /*
    ** Module   : TxWrapper.java
    ** Abstract : management of the transaction-related state, where buffers are involved.
    **
    ** Copyright (c) 2022-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ----------------------------------------Description---------------------------------------
    ** 001 CA  20220906 Created initial version - extracted state which is transaction related and it involves
    **                  buffers, to reduce the complexity of BufferManager scope notification.
    **     CA  20221006 The 'nearestExternal' support needs to be in BufferManager, as it is used for computing
    **                  a scope in ChangeBroker - BufferManager is registered for external block scope.
    **     SVL 20230113 Improved performance by replacing some "for-each" loops with indexed "for" loops.
    ** 002 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 003 CA  20230607 TRANSACTION-MODE AUTO must preserve the tx in the root appserver block - this is required
    **                  because in modes other than State-free, requests can be executed on the same agent who had
    **                  TRANSACTION-MODE AUTO, and this must see as active the tx initially set.
    ** 004 DDF 20230627 Made FORCE_NO_UNDO_TEMP_TABLES non-final, renamed it.
    **     DDF 20230627 Made forceNoUndoTempTables accessible through a method.
    ** 005 AL2 20231013 Removed reclaiming of keys for _temp database. Reclaiming happens now inside FWD-H2.
    ** 006 CA  20231031 Added 'BlockDefinition' parameter to 'scopeStart'.
    ** 007 DDF 20231020 ErrorConditionException should be thrown when a ValidationException occurs, not create
    **                  a new IllegalStateException.
    ** 008 RAA 20231220 Stored recordBuffer.getCurrentRecord() call into a variable.
    ** 009 AL2 20240310 Added AggresiveFlushWrapper. This is a proxy for the TxWrapper which only validates.
    ** 010 CA  20240314 Improved AggresiveFlushWrapper - commitable registration is done only for blocks which
    **                  have a dirty buffer (directly or one of its inner blocks).
    ** 011 CA  20240315 Fixed regression in CA/010 - use tx level instead of dirtyBuffers size when cleaning the
    **                  aggressive flusher registration.
    ** 012 CA  20240423 Performance optimization, resolve the loadedBuffers dictionary only once in scopeFinished.
    ** 013 DDF 20240508 Added deregisterGlobalSavepointHooks() method.
    ** 014 CA  20240809 Avoid iterator usage, for performance improvement.
    **                  Populate the dirty and loaded buffers scopes only when data is added to them.
    ** 015 CA  20240827 Notify that a record has been flushed - for non-temp records, this will allow invalidation
    **                  of the FastFindCache for that DMO.
    ** 021 OM  20240909 Improved Database API.
    ** 022 OM  20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
    **                  based on [sharedDb] parameter.
    ** 023 OM  20250130 A bit of code cleanup.
    ** 024 LS  20250403 Updated begin(), commit() and rollback() to handle the PersistenceException using DBUtils.
    ** 025 ES  20250424 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;

    import java.util.*;
    import java.util.logging.*;

    import com.goldencode.p2j.persist.dirty.*;
    import com.goldencode.p2j.persist.lock.*;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.security.ContextLocal;
    import com.goldencode.p2j.security.WeightFactor;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.ErrorManager;
    import com.goldencode.p2j.util.logging.*;
    import com.goldencode.util.MutableInteger;

    /**
     * An object which manages the "master" {@code Commitable} and {@code Finalizable} events of an
     * application transaction. It creates a database transaction to correspond with the application
     * transaction, and as it receives events from the {@code TransactionManager}, it delegates related
     * responsibilities to various components of the persistence framework.
     */
    class TxWrapper
    implements Commitable,
               Finalizable
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(TxWrapper.class.getName());

       /** Stores context local instances of this class */
       private static final ContextLocal<WorkArea> context = new ContextLocal<WorkArea>()
       {
          @Override public WeightFactor getWeight() { return WeightFactor.LEVEL_2; }
          @Override protected WorkArea initialValue() { return (new WorkArea()); }
       };

       /** Backing database */
       private final Database database;

       /** Dirty share context */
       private final DirtyShareContext dirtyContext;

       /** Savepoint manager (can be {@code null} */
       private final SavepointManager savepointManager;

       /** Persistence context, for access to database session and transaction */
       private Persistence.Context persistenceCtx = null;

       /** Roll back the transaction when block finishes, if not previously acted upon */
       private boolean autoRollback = false;

       /** {@code true} for an initial transaction only until the first commit or rollback */
       private boolean initialTx = false;

       /** The {@link WorkArea} instance. */
       private final WorkArea wa;

       /**
        * The sole constructor.
        *
        * @param   wa
        *          The {@link WorkArea} instance.
        * @param   database
        *          The {@code Database} where the transaction takes place.
        */
       TxWrapper(WorkArea wa, Database database)
       {
          this.wa = wa;
          this.database = database;
          // Because the BufferManager manages all record buffers within the current user context, we cannot
          // separate the DIRTY-READ from the normal tables at this moment. Since this instance is only used
          // for final cleanup, we acquire the context for the DIRTY-READ database so that context will be
          // cleaned up correctly. The non-DIRTY-READ information will be ignored.
          this.dirtyContext = DirtyShareFactory.getContextInstance(database);
          this.savepointManager = (database.isTemporary() && DatabaseManager.isForceNoUndoTempTables())
                                ? null
                                : new SavepointManager();

          // register for master transaction commit/rollback
          wa.txHelper.registerTransactionCommit(this, true);

          // register for master transaction finish/iterate
          wa.txHelper.registerTransactionFinish(this, true);
       }

       /**
        * Get the helper to access the {@link Commitable} and {@link Resettable} state required to maintain
        * transaction-related state where buffers are involved.
        *
        * @param    bm
        *           The {@link BufferManager} instance.
        *
        * @return   The {@link TxWrapperHelper} instance.
        */
       public static TxWrapperHelper getHelper(BufferManager bm)
       {
          WorkArea wa = context.get();
          if (wa.bm == null)
          {
             wa.bm = bm;
          }
          return new TxWrapperHelper(wa);
       }

       /**
        * Implementation of {@code Finalizable} interface: provides a notification that the block whose scope
        * in which the object is registered is about to iterate and attempt another pass.  This provides an
        * opportunity to preserve state from the previous pass and make preparations for the coming pass.
        */
       @Override
       public void iterate()
       {
          if (!isActive())
          {
             return;
          }

          if (LOG.isLoggable(Level.FINE))
          {
             LOG.log(Level.FINE, message(persistenceCtx.getPersistence(), "master transaction iterate"));
          }

          end();
          begin();
       }

       /**
        * Implementation of {@code Finalizable} interface: provides a notification that the block whose scope
        * in which the object is registered is about to retry the same iteration (all loop control data is
        * unchanged). This provides an opportunity to clear or reset state from the previous pass.
        */
       @Override
       public void retry()
       {
          if (!isActive())
          {
             return;
          }

          if (LOG.isLoggable(Level.FINE))
          {
             LOG.log(Level.FINE, message(persistenceCtx.getPersistence(), "master transaction retry"));
          }

          end();
          begin();
       }

       /**
        * Implementation of {@code Finalizable} interface: Provides a notification that the scope in which the
        * object is registered is ending and the object's reference may or may not be lost after this method
        * is called. This provides a natural and standard mechanism to clean-up any resources that may need
        * to be closed explicitly such as operating system files or sockets.
        */
       @Override
       public void finished()
       {
          try
          {
             if (autoRollback)
             {
                if (isActive() && LOG.isLoggable(Level.FINE))
                {
                   LOG.log(Level.FINE, message(persistenceCtx.getPersistence(), "triggering auto-rollback"));
                }

                rollback(true);
             }
          }
          finally
          {
             if (isActive() && LOG.isLoggable(Level.FINE))
             {
                LOG.log(Level.FINE, message(persistenceCtx.getPersistence(), "master transaction finished"));
             }

             wa.txWrapperMap.remove(database);

             if (isActive())
             {
                end();
             }
             else
             {
                wa.inactiveTxWrappers.remove(database);
             }
          }
       }

       /**
        * Implementation of {@code Finalizable} interface: provides a notification that the external program
        * in which the object is registered is being deleted and the object's reference may be lost after this
        * method is called.
        */
       @Override
       public void deleted()
       {
          deregisterGlobalSavepointHooks();
       }

       /**
        * Implementation of {@code Commitable} interface: saves any temporary state to the persistent form or
        * storage that is associated with this class.
        *
        * @param    transaction
        *           {@code true} if this is a full transaction and {@code false} if this is only a
        *           sub-transaction (a nested scope with transaction support).
        */
       @Override
       public void commit(boolean transaction)
       {
          if (!isActive())
          {
             return;
          }

          if (LOG.isLoggable(Level.FINE))
          {
             LOG.log(Level.FINE, message(persistenceCtx.getPersistence(), "master transaction commit"));
          }

          try
          {
             initialTx = false;
             autoRollback = false;
             persistenceCtx.commit();
          }
          catch (PersistenceException exc)
          {
             if (LOG.isLoggable(Level.SEVERE))
             {
                LOG.log(Level.SEVERE, message(persistenceCtx.getPersistence(), "commit error"), exc);
             }
             DBUtils.handleException(database, exc);

             // No good way to recover from this.  Let it abend.
             throw new RuntimeException(exc);
          }
       }

       /**
        * Implementation of {@code Commitable} interface: notifies that a rollback has just occurred (all state
        * maintained through the {@link Undoable} interface has been already rolled back). This notification
        * can be used to implement cleanup of any custom state that needs to be maintained in this case. It can
        * also be used to implement a rollback that is completely independent of the {@code Undoable}
        * (externally driven) interface.
        *
        * @param    transaction
        *           {@code true} if this is a full transaction and {@code false} if this is only a
        *           sub-transaction (a nested scope with transaction support).
        */
       @Override
       public void rollback(boolean transaction)
       {
          if (!isActive())
          {
             return;
          }

          if (LOG.isLoggable(Level.FINE))
          {
             LOG.log(Level.FINE, message(persistenceCtx.getPersistence(), "master transaction rollback"));
          }

          if (database.isTemporary() && savepointManager == null)
          {
             // we force all temp-tables to be no-undo, so instead of rollback, do a
             // commit of the current transaction
             commit(transaction);

             return;
          }

          initialTx = false;
          autoRollback = false;
          try
          {
             persistenceCtx.rollback(false);
          }
          catch (PersistenceException exc)
          {
             if (LOG.isLoggable(Level.SEVERE))
             {
                LOG.log(Level.SEVERE, message(persistenceCtx.getPersistence(), "rollback error"), exc);
             }
             DBUtils.handleException(database, exc);

             // No good way to recover from this.  Let it abend.
             throw new RuntimeException(exc);
          }
       }

       /**
        * Implementation of {@code Commitable} interface: Notifies that a block is about to be normally exited
        * or normally iterated and that any deferred validation processing should be executed.
        * <p>
        * This particular implementation is a NOP.
        *
        * @param   transaction
        *          Ignored.
        * @param   aggressiveFlush
        *          Ignored.
        */
       @Override
       public void validate(boolean transaction, boolean aggressiveFlush)
       {
          // no-op
       }

       /**
        * Registers the commitable and finalizable for savepoint manager, if any.
        *
        * @param   fullTx
        *          {@code true} if this is a full-transaction.
        * @param    blockDepth
        *           zero-based depth of target block, starting from the outermost block. <code>0</code> indicates
        *           the global block.
        */
       void registerSavepointHooks(boolean fullTx, int blockDepth)
       {
          if (savepointManager != null)
          {
             savepointManager.registerBlockHooks(wa.txHelper, fullTx, blockDepth);
          }
       }

       /**
        * Deregisters the finalizable for savepoint manager from the global block, if any.
        */
       void deregisterGlobalSavepointHooks()
       {
          if (savepointManager != null)
          {
             savepointManager.deregisterGlobalBlockHooks(wa.txHelper);
          }
       }

       /**
        * Activates the transaction. This entails starting a database-level transaction and activating a
        * savepoint manager for that transaction.
        */
       void activate()
       {
          if (isActive())
          {
             return;
          }

          Persistence persistence = PersistenceFactory.getInstance(database);
          persistenceCtx = persistence.getContext(!database.isTenant());

          if (LOG.isLoggable(Level.FINE))
          {
             LOG.log(Level.FINE,
                     message(persistenceCtx.getPersistence(),
                             "registered for master transaction commit/finished"));

             persistenceCtx.enableTransactionTrace(true);
          }

          begin();
          initialTx = true;
       }

       /**
        * Checks whether this {@code TxWrapper} is active.
        *
        * @return  {@code true} only if {@code TxWrapper} was activated.
        */
       boolean isActive()
       {
          return persistenceCtx != null;
       }

       /**
        * Convert the current object to its string representation to be used during debugging.
        *
        * @return  A short string representation of this object to be used while debugging.
        */
       @Override
       public String toString()
       {
          return "TxWrapper{database=" + database + '}';
       }

       /**
        * Starts the actual transaction at persistence level.
        */
       private void begin()
       {
          try
          {
             if (!persistenceCtx.beginTransaction(savepointManager) && !initialTx)
             {
                // We are here because we tried to begin a database level transaction and one is
                // already open. This situation generally is invalid, except in an edge case
                // where a new transaction is opened while in TM's iterate worker. This can happen
                // if, for example, a new database is accessed during the execution of a trigger
                // fired during iterate processing. The only case we have seen of this so far is
                // due to a record being released during RB's iterate processing, which then fires
                // a write trigger, which accesses a buffer in a separate database (e.g., for
                // audit purposes) which did not previously have a transaction open. This in turn
                // invokes this object's c'tor, which registers for master finish notification,
                // but also opens a new transaction. We then get here a moment later during master
                // finish processing. Commit has not yet been called, since commitable processing
                // for that block already has occurred. The solution is to check the initialTx
                // flag and simply ignore the fact that a new transaction could not be created if
                // that flag is true. The flag will be set to false on the first commit or
                // rollback.
                Persistence persistence = persistenceCtx.getPersistence(); // same persistence
                if (LOG.isLoggable(Level.FINE))
                {
                   Throwable beginTxTrace = persistenceCtx.getBeginTxTrace();
                   if (beginTxTrace != null)
                   {
                      String msg = "Current transaction previously started at location...";
                      LOG.log(Level.FINE, persistence.message(msg), beginTxTrace);
                   }
                }

                throw new IllegalStateException(
                   message(persistence, "database transaction already active"));
             }

             autoRollback = true;
          }
          catch (PersistenceException exc)
          {
             DBUtils.handleException(database, exc);
             ErrorManager.handleStopException(new StopConditionException(exc));
          }
       }

       /**
        * Shuts down the transaction at persistence level and performs key management.
        */
       private void end()
       {
          // clear nursery
          persistenceCtx.getNursery().transactionEnded();

          // clean up dirty shares for this transaction
          if (dirtyContext != null)
          {
             dirtyContext.cleanup();
          }

          // release or downgrade record locks which may have been acquired during the transaction
          RecordLockContext lockCtx = persistenceCtx.getRecordLockContext();
          Set<Long> released = lockCtx.transactionEnded();

          // reclaim any primary keys for records deleted and committed or created and rolled back
          // during the transaction, provided those keys have been fully released (not just
          // downgraded) by the end of the transaction; note that this may "leak" reclaiming some
          // primary keys downgraded and still held as share locks by open buffers, for buffers
          // whose scope extends beyond the end of the transaction
          wa.bm.reclaimPendingKeys(persistenceCtx.getPersistence(), released);

          // Allow the connection manager to perform its end of transaction cleanup.  This must
          // occur after all transaction processing, because it may result in tearing down the
          // database's session factory.
          wa.connMgr.transactionEnded(database);
       }

       /**
        * Compose a message suitable for logging.  Reports open buffer scopes and
        * context information in addition to a custom message.
        *
        * @param   persistence
        *          Persistence object which will provide context info.
        * @param   text
        *          Custom message text.
        *
        * @return  Composed message text.
        */
       private String message(Persistence persistence, String text)
       {
          return persistence.message("scope " + wa.bm.getOpenBufferScopes() + ": " + text);
       }

       /**
        * Helper instance to access the {@link WorkArea} state without context-local lookups.
        */
       static class TxWrapperHelper
       {
          /** The {@link WorkArea} instance. */
          private final WorkArea wa;

          /**
           * Initialize this helper.
           *
           * @param    wa
           *           The {@link WorkArea} instance.
           */
          public TxWrapperHelper(WorkArea wa)
          {
             this.wa = wa;
          }

          /**
           * Register this buffer as 'dirty' in the current scope.
           * <p>
           * This registration doesn't mean that at the time of {@link #commit}, {@link #rollback}, or
           * {@link #validate} the buffer's record is still 'dirty' - this is used to limit the set of buffers
           * receiving such notifications, to avoid iterating over all {@link BufferManager#openBuffers}.
           *
           * @param    recordBuffer
           *           The record buffer.
           */
          public void registerDirtyBuffer(RecordBuffer recordBuffer)
          {
             wa.registerDirtyBuffer(recordBuffer);
          }

          /**
           * Notification that a record has been flushed.
           *
           * @param    buffer
           *           The buffer instance.
           * @param    dmoId
           *           The DMO id.
           */
          void recordFlushed(RecordBuffer buffer, int dmoId)
          {
             if (buffer.isTemporary())
             {
                return;
             }

             FastFindCache ffCache = buffer.getFastFindCache();
             Set<Long> dmoIds = wa.ffCacheChanges.computeIfAbsent(ffCache, (k) -> new HashSet<>());
             dmoIds.add(FastFindCache.combine(dmoId, null));
          }

          /**
           * Deregister the given, dynamic record buffer by removing it from the buffer manager's tracking.
           *
           * @param   buffer
           *          Dynamic record buffer to be removed.
           */
          void deregisterDynamicBuffer(RecordBuffer buffer)
          {
             wa.deregisterDynamicBuffer(buffer);
          }

          /**
           * Executes logic to explicitly begin a transaction.
           *
           * @param    inTx
           *           Flag indicating if we are in a transaction.
           * @param    fullTx
           *           Flag indicating if we are in a full transaction.
           * @param    blockDepth
           *           The current block depth (all blocks tracked by the {@code TransactionManager}).
           */
          public void beginTx(boolean inTx, boolean fullTx, int blockDepth)
          {
             wa.beginTx(inTx, fullTx, false, blockDepth);
          }

          /**
           * Executes logic to explicitly end a transaction.
           *
           * @param    inTx
           *           Flag indicating if we are in a transaction.
           * @param    fullTx
           *           Flag indicating if we are in a full transaction.
           */
          public void endTx(boolean inTx, boolean fullTx)
          {
             wa.endTx(inTx, fullTx);
          }

          /**
           * Executes logic required after a full transaction has ended.
           */
          public void endTxPost()
          {
             // if any DBs were used in this tx block and remained connected, disconnect now
             wa.endTxPost();
          }

          /**
           * Check whether the current block is within an application-level transaction.
           *
           * @return  {@code true} if we are in a transaction, else {@code false}.
           */
          public boolean isTransaction()
          {
             return wa.isTransaction();
          }

          /**
           * Get the number of block scope transitions made. This is meant to be a semaphore of sorts, to allow
           * a caller to determine whether this value has changed since some previous point when it was last
           * retrieved. Since it is only meant to detect that the current block has changed over time, we do not
           * protect against overflow.
           *
           * @return  The current unique block ID.
           */
          public long getScopeTransitions()
          {
             return wa.getScopeTransitions();
          }

          /**
           * Get the current counter value of application-leve transactions which have been opened.
           *
           * @return  Transaction count.
           */
          public long getTransactionCount()
          {
             return wa.getTransactionCount();
          }

          /**
           * Track the buffer's database.  This will increment the {@link WorkArea#activeDatabases} counter when
           * a new buffer is activated, and decrement it when it exists its scope.
           *
           * @param    buffer
           *           The buffer to track.
           * @param    add
           *           Flag indicating if the buffer is opening or closing the scope.
           *
           * @throws   IllegalStateException
           *           If we are removing an active buffer and there is no entry for it in
           *           {@link WorkArea#activeDatabases}.
           */
          public void trackDatabase(RecordBuffer buffer, boolean add)
          {
             wa.trackDatabase(buffer, add);
          }

          /**
           * Given a database which has one or more buffers with an open scope operating within an application
           * level transaction, possibly activate the transaction wrapper associated with that database.
           * Activation entails starting a database-level transaction and activating a savepoint manager
           * for that transaction.
           * <p>
           * If the transaction wrapper for the given database already is active, do nothing.
           *
           * @param   database
           *          Database with at least one buffer with an open scope.
           */
          public void maybeActivateTxWrapper(Database database)
          {
             wa.maybeActivateTxWrapper(database);
          }

          /**
           * Indicate whether the current block is &quot;important&quot; from the standpoint of transitioning into
           * or out of its scope. A block is considered &quot;important&quot; in this sense if it is either
           * completely outside of an application transaction, or if it is within a transaction AND has transaction
           * block properties (i.e., is not a NO_TRANSACTION block).
           * <p>
           * This notion of importance is used to determine whether the various scoped data structures managed by
           * this class need to add or delete a scope when transitioning into or out of the block, respectively,
           * and whether we need to perform processing (e.g., copying/moving data between scopes) associated with
           * such transitions.
           *
           * @return  {@code true} if the block is considered &quot;important&quot; by the above criteria, else
           *          {@code false}.
           */
          public boolean isImportantBlockTransition()
          {
             return wa.isImportantBlockTransition();
          }

          /**
           * Register the {@link WorkArea} scopeable as pending, to be added to the next block.
           */
          public void registerPendingScopeable()
          {
             wa.txHelper.registerPendingScopeable(wa);
          }

          /**
           * Notify the buffer manager that a new record was loaded (or the old record was reloaded)
           * into the specified record buffer.
           *
           * @param   recordBuffer
           *          The record buffer in which a record was loaded (or the old record was reloaded).
           */
          public void notifyRecordWasLoaded(RecordBuffer recordBuffer)
          {
             wa.notifyRecordWasLoaded(recordBuffer);
          }

          /**
           * Remove all the buffers from the dirty buffers scope.
           *
           * @param    allBuffers
           *           The buffers to remove.
           */
          public void cleanDirtyBuffers(Map<String, RecordBuffer> allBuffers)
          {
             for (RecordBuffer buf : allBuffers.values())
             {
                // remove it from the dirty buffers
                wa.dirtyBuffers.removeEntryThroughScope(buf, wa.dirtyBuffers.size());
             }
          }
       }

       /**
        * Manages transaction-related state where buffers are involved.  State and methods are extracted from
        * {@link BufferManager}, and these require scope notification and management for each
        * {@link #isImportantBlockTransition() important block}.
        */
       private static class WorkArea
       implements Commitable,
                  Resettable,
                  Scopeable
       {
          /** Database to transaction wrapper instance map */
          private final Map<Database, TxWrapper> txWrapperMap = new HashMap<>();

          /** Transaction helper */
          private final TransactionManager.TransactionHelper txHelper;

          /** Helper to use the ProcedureManager without any context local lookups. */
          private final ProcedureManager.ProcedureHelper pm = ProcedureManager.getProcedureHelper();

          /** The buffer manager instance. */
          private BufferManager bm = null;

          /** Connection manager */
          private final ConnectionManager connMgr;

          /** Transaction wrappers not yet activated */
          private final Map<Database, TxWrapper> inactiveTxWrappers = new HashMap<>();

          /** Transaction finalizable to manage transaction state locally (avoid context-local calls) */
          private final Finalizable xactFin = new Finalizable()
          {
             @Override public void finished() { transactionDepth = -1; invalidateFastFindCache(); }
             @Override public void deleted()  { invalidateFastFindCache(); }
             @Override public void iterate()  { invalidateFastFindCache(); }
             @Override public void retry()    { invalidateFastFindCache(); }
          };

          /** Context-local access to unique constraint tracker */
          private final UniqueTracker.Context uniqueTrackerCtx = UniqueTracker.getContext();

          /**
           * The real scope depth counter, as {@link #loadedBuffers} and {@link #dirtyBuffers} push scopes only
           * when they are used.
           */
          private int scopeDepth = 0;

          /** Bitset with a flag indicating which scope was not yet loaded, for {@link #loadedBuffers}. */
          private final BitSet emptyLoadedScopes = new BitSet();

          /** Bitset with a flag indicating which scope was not yet loaded, for {@link #dirtyBuffers}. */
          private final BitSet emptyDirtyScopes = new BitSet();

          /** Counter of the number of scope transitions (push or pop) */
          private long scopeTransitions = 0L;

          /** Block depth at which full application transaction began, or -1 if not in a transaction */
          private int transactionDepth = -1;

          /** Counter of the number of application-level transactions */
          private long transactionCount = 0L;

          /** Counter of in-use buffers for each database. */
          private final Map<Database, MutableInteger> activeDatabases = new LinkedHashMap<>();

          /**
           * Scoped dictionary which key set contains all buffers into which a new record was loaded (or
           * the old record was reloaded) into corresponding scopes. Value set doesn't contain any useful
           * information and is used as a temporary data storage during copying from one scope level to
           * another.
           */
          private final ScopedDictionary<RecordBuffer, Boolean> loadedBuffers = new ScopedDictionary<>();

          /**
           * The buffers touched via a Create, Update, Copy or Delete operation, in each scope. If the full transaction
           * block is not {@link #commit committed} or {@link #rollback rolled back}, then the buffers will be merged
           * in the previous scope.
           */
          private final TailScopedDictionary<RecordBuffer, Boolean> dirtyBuffers = new TailScopedDictionary<>();

          /** The scopes which require an {@link AggresiveFlushWrapper} to be registered as commitable. */
          private final BitSet aggressiveFlushScopes = new BitSet();

          /** The singleton of {@link AggresiveFlushWrapper} for this instance. */
          private final AggresiveFlushWrapper aggressiveFlusher = new AggresiveFlushWrapper(this);

          /** {@link FastFindCache} instances which need to have the specified DMOs invalidated.  */
          private final Map<FastFindCache, Set<Long>> ffCacheChanges = new IdentityHashMap<>();

          /**
           * Initialize this instance.
           */
          public WorkArea()
          {
             txHelper = TransactionManager.getTransactionHelper();
             connMgr = ConnectionManager.get();

             loadedBuffers.setIdentityKeys(true);
             dirtyBuffers.setIdentityKeys(true);

             txHelper.registerResettable(this);
          }

          /**
           * A full transaction or sub-transaction is committing. Notify all dirty buffers in the current scope.
           *
           * @param   transaction
           *          {@code true} if full transaction; {@code false} if sub-transaction.
           */
          @Override
          public void commit(boolean transaction)
          {
             boolean hasDirty = !emptyDirtyScopes.get(scopeDepth);
             Map<RecordBuffer, Boolean> buffers = hasDirty ? dirtyBuffers.getDictionaryAtScope(0, false) : null;
             if (buffers != null && !buffers.isEmpty())
             {
                // avoid capturing lambdas
                if (transaction)
                {
                   buffers.forEach((buf, b) -> buf.commit(true));
                   buffers.clear();
                }
                else
                {
                   buffers.forEach((buf, b) -> buf.commit(false));
                }
             }
          }

          /**
           * A full transaction or sub-transaction is rolling back. Notify all dirty buffers in the current scope.
           *
           * @param   transaction
           *          {@code true} if full transaction; {@code false} if sub-transaction.
           */
          @Override
          public void rollback(boolean transaction)
          {
             boolean hasDirty = !emptyDirtyScopes.get(scopeDepth);
             Map<RecordBuffer, Boolean> buffers = hasDirty ? dirtyBuffers.getDictionaryAtScope(0, false) : null;
             if (buffers != null && !buffers.isEmpty())
             {
                // avoid capturing lambdas
                if (transaction)
                {
                   buffers.forEach((buf, b) -> buf.rollback(true));
                   buffers.clear();
                }
                else
                {
                   buffers.forEach((buf, b) -> buf.rollback(false));
                }
             }
          }

          /**
           * A block is exiting or iterating normally within a transaction. Notify all dirty buffers in the current
           * scope.
           *
           * @param   transaction
           *          {@code true} if full transaction; {@code false} if sub-transaction.
           * @param   aggressiveFlush
           *          {@code true} if transaction manager is in aggressive subtransaction
           *          flush mode, indicating that any transient buffers should be validated and
           *          flushed, regardless of other state.
           */
          @Override
          public void validate(boolean transaction, boolean aggressiveFlush)
          {
             boolean hasDirty = !emptyDirtyScopes.get(scopeDepth);
             TailMap<RecordBuffer, Boolean> buffers = hasDirty ? dirtyBuffers.getDictionaryAtScope(0, false) : null;
             if (buffers != null && !buffers.isEmpty())
             {
                ArrayList<Map<RecordBuffer, Boolean>> validatedAddedDirtyBuffers = null;
                try (AutoCloseable itersafe = buffers.flagIterating())
                {
                   for (RecordBuffer buf : buffers.keySet())
                   {
                      buf.validate(transaction, aggressiveFlush);

                      // The next block is rarely entered, only in edge cases.
                      // Details:
                      // The invocation of buf.validate can (for example) result in
                      // an ABL publish + subscribe (from an OEDB write trigger),
                      // resulting in more buffers that need to be validated.
                      // Since we are iterating, the extra buffers are added to
                      // a standalone 'tail' collection. We validate them immediately,
                      // and add them to dirtyBuffers after iterating has completed.
                      Map<RecordBuffer, Boolean> addedDirtyBuffers = buffers.pop();
                      while (addedDirtyBuffers != null)
                      {
                         // Validate all additions; avoid iterator usage
                         addedDirtyBuffers.forEach((v, b) -> v.validate(transaction, aggressiveFlush));

                         // Store the validated additions to add them afterwards
                         // A list is used for clarity and to facilitate possible
                         // removal logic in the future (which can't happen at the moment).
                         if (validatedAddedDirtyBuffers == null)
                         {
                            validatedAddedDirtyBuffers = new ArrayList<>();
                         }
                         validatedAddedDirtyBuffers.add(addedDirtyBuffers);

                         // Fetch possible new additions of the added validate invocations.
                         // We take control of this collection, at the TailMap it
                         // is forgotten.
                         addedDirtyBuffers = buffers.pop();
                      }
                   }
                }
                catch (ErrorConditionException e)
                {
                   throw e;
                }
                catch (Exception e)
                {
                   throw new IllegalStateException("Exception at dirtyBuffers.flagIterating block", e);
                }

                // The next block is rarely entered, only in edge cases, see above.
                if (validatedAddedDirtyBuffers != null)
                {
                   for (int i = 0; i < validatedAddedDirtyBuffers.size(); i++)
                   {
                      Map<RecordBuffer, Boolean> bufmap = validatedAddedDirtyBuffers.get(i);
                      // Note: addEntry(false, ...) because:
                      //       We do not need the global scope.
                      //       At the top of this method, scope 0 is used, not scope -1 (global).
                      bufmap.forEach((k, v) -> dirtyBuffers.addEntry(false, k, v));
                   }
                }

                // do not clear the buffers here, only commit/rollback can clear it
             }
          }

          /**
           * Register this buffer as 'dirty' in the current scope.
           * <p>
           * This registration doesn't mean that at the time of {@link #commit}, {@link #rollback}, or
           * {@link #validate} the buffer's record is still 'dirty' - this is used to limit the set of buffers
           * receiving such notifications, to avoid iterating over all {@link BufferManager#openBuffers}.
           *
           * @param    recordBuffer
           *           The record buffer.
           */
          public void registerDirtyBuffer(RecordBuffer recordBuffer)
          {
             if (!aggressiveFlushScopes.isEmpty())
             {
                for (int blockDepth = aggressiveFlushScopes.nextSetBit(0);
                         blockDepth >= 0;
                         blockDepth = aggressiveFlushScopes.nextSetBit(blockDepth + 1))
                {
                   txHelper.registerCommitAt(blockDepth, aggressiveFlusher);
                }

                // registration is done only once for all scopes
                aggressiveFlushScopes.clear();
             }

             checkDirtyScopes();
             dirtyBuffers.addEntry(false, recordBuffer, false);
          }

          /**
           * Deregister the given, dynamic record buffer by removing it from the buffer manager's tracking.
           *
           * @param   buffer
           *          Dynamic record buffer to be removed.
           */
          public void deregisterDynamicBuffer(RecordBuffer buffer)
          {
             int[] loadedBuffersScopes = buffer.getLoadedBuffersScope();
             int loadedScopes = loadedBuffers.size();
             for (int scope : loadedBuffersScopes)
             {
                int depth = loadedScopes - scope;
                Map<RecordBuffer, Boolean> loaded = loadedBuffers.getDictionaryAtScope(depth, false);
                if (loaded != null)
                {
                   loaded.remove(buffer);
                }
             }
             // remove from global scope always
             Map<RecordBuffer, Boolean> loaded = loadedBuffers.getDictionaryAtScope(-1, false);
             if (loaded != null)
             {
                loaded.remove(buffer);
             }
          }

          /**
           * Notify the buffer manager that a new record was loaded (or the old record was reloaded)
           * into the specified record buffer.
           *
           * @param   recordBuffer
           *          The record buffer in which a record was loaded (or the old record was reloaded).
           */
          public void notifyRecordWasLoaded(RecordBuffer recordBuffer)
          {
             Record currentRecord = recordBuffer.getCurrentRecord();
             if (currentRecord != null && !recordBuffer.isDynamic())
             {
                pm.notifyStaticRecordWasLoaded(recordBuffer);
             }

             if (currentRecord == null || recordBuffer.isTemporary())
             {
                return;
             }

             checkLoadedScopes();
             loadedBuffers.addEntry(false, recordBuffer, false);
             recordBuffer.addLoadedBuffersScope(loadedBuffers.size());
          }

          /**
           * Get the number of block scope transitions made. This is meant to be a semaphore of sorts, to allow
           * a caller to determine whether this value has changed since some previous point when it was last
           * retrieved. Since it is only meant to detect that the current block has changed over time, we do not
           * protect against overflow.
           *
           * @return  The current unique block ID.
           */
          public long getScopeTransitions()
          {
             return scopeTransitions;
          }

          /**
           * Track the buffer's database.  This will increment the {@link #activeDatabases} counter when a new buffer
           * is activated, and decrement it when it exists its scope.
           *
           * @param    buffer
           *           The buffer to track.
           * @param    add
           *           Flag indicating if the buffer is opening or closing the scope.
           *
           * @throws   IllegalStateException
           *           If we are removing an active buffer and there is no entry for it in {@link #activeDatabases}.
           */
          public void trackDatabase(RecordBuffer buffer, boolean add)
          {
             if (add)
             {
                Database database = buffer.getDatabase();
                MutableInteger count = activeDatabases.get(database);
                if (count == null)
                {
                   activeDatabases.put(database, count = new MutableInteger(0));
                }
                count.set(count.get() + 1);
             }
             else if (buffer.isActive() && buffer.getOpenScopeCount() > 0)
             {
                Database database = buffer.getDatabase();
                MutableInteger count = activeDatabases.get(database);
                if (count == null)
                {
                   throw new IllegalStateException("Tracking for " + database + " is unbalanced!");
                }

                if (count.get() == 1)
                {
                   activeDatabases.remove(database);
                }
                else
                {
                   count.set(count.get() - 1);
                }
             }
          }

          /**
           * Given a database which has one or more buffers with an open scope operating within an application
           * level transaction, possibly activate the transaction wrapper associated with that database.
           * Activation entails starting a database-level transaction and activating a savepoint manager
           * for that transaction.
           * <p>
           * If the transaction wrapper for the given database already is active, do nothing.
           *
           * @param   database
           *          Database with at least one buffer with an open scope.
           */
          public void maybeActivateTxWrapper(Database database)
          {
             TxWrapper txw = inactiveTxWrappers.remove(database);
             if (txw != null)
             {
                txw.activate();
             }
          }

          /**
           * Check whether the current block is within an application-level transaction.
           *
           * @return  {@code true} if we are in a transaction, else {@code false}.
           */
          public boolean isTransaction()
          {
             return transactionDepth >= 0;
          }

          /**
           * Get the current counter value of application-leve transactions which have been opened.
           *
           * @return  Transaction count.
           */
          public long getTransactionCount()
          {
             return transactionCount;
          }

          /**
           * Create an inactive transaction wrapper for the given database. It will be activated the
           * first time an open buffer scope is detected or when a new buffer scope is opened, at which
           * time a database-level transaction will be opened.
           *
           * @param   database
           *          Database for which this transaction wrapper is created.
           *
           * @return  Transaction wrapper object.
           */
          private TxWrapper createTxWrapper(Database database)
          {
             TxWrapper txw = new TxWrapper(this, database);
             txWrapperMap.put(database, txw);
             inactiveTxWrappers.put(database, txw);

             return txw;
          }

          /**
           * Executes logic to explicitly begin a transaction.
           *
           * @param    inTx
           *           Flag indicating if we are in a transaction.
           * @param    fullTx
           *           Flag indicating if we are in a full transaction.
           * @param    aggresiveFlush
           *           Flag indicating if the block isn't a transaction but it still needs to aggressively flush.
           * @param    blockDepth
           *           The current block depth (all blocks tracked by the {@code TransactionManager}).
           */
          public void beginTx(boolean inTx, boolean fullTx, boolean aggresiveFlush, int blockDepth)
          {
             if (inTx)
             {
                if (fullTx)
                {
                   // create a database xaction wrapper for each connected database, but do not
                   // activate it yet
                   txWrapperMap.clear();         // should be empty already
                   inactiveTxWrappers.clear();   // should be empty already
                   ArrayList<Database> databases = connMgr.getActiveDatabases();
                   for (int i = 0; i < databases.size(); i++)
                   {
                      Database database = databases.get(i);
                      createTxWrapper(database);
                   }

                   transactionDepth = blockDepth - 1;  // match TM transaction level
                   transactionCount++;

                   // register for master transaction finish/iterate to clear transaction depth
                   txHelper.registerTransactionFinish(xactFin, true);
                }

                // register various services for callbacks if current block has transaction properties
                if (txHelper.currentTransactionLevel() != TransactionManager.NO_TRANSACTION)
                {
                   // register savepoint manager for callbacks at full and sub-transaction blocks
                   for (TxWrapper txw : txWrapperMap.values())
                   {
                      txw.registerSavepointHooks(fullTx, blockDepth - 1);
                   }

                   // register to get Commitable notifications for open buffers
                   txHelper.registerCommitAt(blockDepth - 1, this);

                   // register unique tracker context to get Commitable/Finalizable notifications
                   uniqueTrackerCtx.register(txHelper, blockDepth - 1);
                }
             }
             else if (aggresiveFlush)
             {
                aggressiveFlushScopes.set(blockDepth - 1);
             }
             else
             {
                aggressiveFlushScopes.clear(blockDepth - 1);
             }

             scopeTransitions++;

             if (fullTx)
             {
                // activate a pending TxWrapper for any database with a buffer that already is open
                // avoid iterator usage
                activeDatabases.forEach((db, c) -> maybeActivateTxWrapper(db));
             }
          }

          /**
           * Executes logic to explicitly end a transaction.
           *
           * @param    inTx
           *           Flag indicating if we are in a transaction.
           * @param    fullTx
           *           Flag indicating if we are in a full transaction.
           */
          public void endTx(boolean inTx, boolean fullTx)
          {
             scopeTransitions++;

             if (!inTx)
             {
                aggressiveFlushScopes.clear(txHelper.getNestingLevel() - 1);
             }
          }

          /**
           * Executes logic required after a full transaction has ended.
           */
          public void endTxPost()
          {
             // if any DBs were used in this tx block and remained connected, disconnect now
             connMgr.transactionEnded();
          }

          /**
           * Indicate whether the current block is &quot;important&quot; from the standpoint of transitioning into
           * or out of its scope. A block is considered &quot;important&quot; in this sense if it is either
           * completely outside of an application transaction, or if it is within a transaction AND has transaction
           * block properties (i.e., is not a NO_TRANSACTION block).
           * <p>
           * This notion of importance is used to determine whether the various scoped data structures managed by
           * this class need to add or delete a scope when transitioning into or out of the block, respectively,
           * and whether we need to perform processing (e.g., copying/moving data between scopes) associated with
           * such transitions.
           *
           * @return  {@code true} if the block is considered &quot;important&quot; by the above criteria, else
           *          {@code false}.
           */
          public boolean isImportantBlockTransition()
          {
             return txHelper.isGlobalBlock()  ||
                    !txHelper.isTransaction() ||
                    txHelper.currentTransactionLevel() != TransactionManager.NO_TRANSACTION;
          }

          /**
           * Releases the buffers into which a new record was loaded (or the old
           * record was reloaded) into the current or deeper scopes, or only clears
           * the list of buffers pending to be released.
           *
           * @param clearOnly
           *        If <code>true</code> then clear the list of buffers pending to be
           *        released, otherwise perform release of these buffers and clear
           *        the list after that.
           */
          @Override
          public void resetState(boolean clearOnly)
          {
             boolean hasLoaded = !emptyLoadedScopes.get(scopeDepth);
             Map<RecordBuffer, Boolean> buffers = hasLoaded ? loadedBuffers.getDictionaryAtScope(0, false) : null;
             if (buffers == null || buffers.isEmpty())
             {
                return;
             }

             if (!clearOnly)
             {
                // avoid iterators
                buffers.forEach((buf, b) -> buf.release(false, false));
             }

             buffers.clear();
          }

          /**
           * Provides a notification that a new scope is about to be entered.
           *
           * @param    block
           *           The explicit block definition which required this notification.
           */
          @Override
          public void scopeStart(BlockDefinition block)
          {
             scopeDepth++;
             // both scopes are empty
             emptyLoadedScopes.set(scopeDepth, true);
             emptyDirtyScopes.set(scopeDepth, true);

             boolean inTx = txHelper.isTransaction();
             boolean fullTx = inTx && txHelper.isFullTransaction();
             int blockDepth = txHelper.getNestingLevel();
             boolean aggresiveFlush = block.forFlushing;

             beginTx(inTx, fullTx, aggresiveFlush, blockDepth);
          }

          /**
           * Provides a notification that a scope is about to be exited.
           */
          @Override
          public void scopeFinished()
          {
             // here we will be out of sync with the TM on a transaction boundary, because our
             // transaction depth is cleared with a transaction finalizable, which is called before this
             // scope finished notification, so we rely on the TM.isTransaction() instead of our own
             boolean inTx = txHelper.isTransaction();
             boolean fullTx = txHelper.isFullTransaction();

             endTx(inTx, fullTx);

             if (!emptyDirtyScopes.get(scopeDepth))
             {
                Map<RecordBuffer, Boolean> dirtyBufs = dirtyBuffers.getDictionaryAtScope(0, false);
                dirtyBuffers.deleteScope();

                // ensure we are populated properly
                try
                {
                   scopeDepth--;
                   checkDirtyScopes();
                }
                finally
                {
                   scopeDepth++;
                }

                if (dirtyBufs != null && !dirtyBufs.isEmpty() && dirtyBuffers.size() > 1)
                {
                   // do not merge into the global block, as all changes should have been already committed or rolled
                   // back.  this is required as temporary buffers can be changed without an active transaction present
                   // on the stack: in this case, they will be pushed to the previous scope until the global block is
                   // reached, when they will be discarded.

                   // non-capturing lambda
                   dirtyBufs.forEach((buffer, b) ->
                   {
                      // merge into previous scope, but only buffers with records which were changed, as the record
                      // associated (at initial dirty registration of the buffer) may have been flushed to the database,
                      // and the current record may not be 'dirty'
                      if (buffer.getCurrentRecord() != null)
                      {
                         Record record = buffer.getCurrentRecord();
                         if (buffer.isTouched() ||
                             record.checkState(DmoState.NEW)      ||
                             record.checkState(DmoState.STALE)    ||
                             record.checkState(DmoState.DELETING) ||
                             record.checkState(DmoState.DELETED)  ||
                             record.checkState(DmoState.INVALID)  ||
                             record.checkState(DmoState.CHANGED)  ||
                             record.checkState(DmoState.TRACKED))
                         {
                            dirtyBuffers.addEntry(false, buffer, false);
                         }
                      }
                   });
                }
             }

             // Copy the set of buffers contained in the current scope of the
             // loadedBuffers (which is about to be removed) to the enclosing scope.
             // Only buffers which are open at the enclosing or a higher scope
             // should be copied.
             if (loadedBuffers.size() > 1 && !emptyLoadedScopes.get(scopeDepth))
             {
                Map<RecordBuffer, Boolean> currentLoadedBuffers = loadedBuffers.getDictionaryAtScope(0, false);
                if (!currentLoadedBuffers.isEmpty())
                {
                   Collection<RecordBuffer> currentOpenBuffers = bm.getOpenBuffers();

                   Map<RecordBuffer, Boolean> loadedBufs = loadedBuffers.getDictionaryAtScope(1, true);
                   int loadedBufsize = loadedBuffers.size();
                   for (RecordBuffer buffer : currentLoadedBuffers.keySet())
                   {
                      if (buffer.getOpenScopeCount() > 1 || !currentOpenBuffers.contains(buffer))
                      {
                         // if this buffer is missing at the current scope of open
                         // buffers or it presents there but has open scope count > 1,
                         // that means that it presents at a higher level and it should
                         // be copied to the enclosing scope
                         loadedBufs.put(buffer, Boolean.FALSE);
                         buffer.addLoadedBuffersScope(loadedBufsize - 1);
                      }
                   }
                }

                loadedBuffers.deleteScope();
             }

             if (fullTx)
             {
                endTxPost();
             }

             // don't care about reseting the scope's bit
             scopeDepth--;
          }

          /**
           * Provides a notification that an external scope is about to be deleted.
           */
          @Override
          public void scopeDeleted()
          {
             // no-op
          }

          /**
           * Get the {@link ScopeId} for the instance.
           *
           * @return   The scope ID, or <code>null</code> for cases when the scope notification is not done via
           *           {@link BlockDefinition#scopeList}.
           */
          @Override
          public ScopeId getScopeId()
          {
             return ScopeId.TX_WRAPPER;
          }

          /**
           * Check the {@link #emptyLoadedScopes} and ensure that the entire {@link #loadedBuffers} scope stack
           * is populated.
           */
          private void checkLoadedScopes()
          {
             int depth = scopeDepth;
             while (depth >= 0 && emptyLoadedScopes.get(depth))
             {
                loadedBuffers.addScope(null);
                emptyLoadedScopes.set(depth, false);
                depth = depth - 1;
             }
          }

          /**
           * Check the {@link #emptyDirtyScopes} and ensure that the entire {@link #dirtyBuffers} scope stack
           * is populated.
           */
          private void checkDirtyScopes()
          {
             int depth = scopeDepth;
             while (depth >= 0 && emptyDirtyScopes.get(depth))
             {
                dirtyBuffers.addScope(null);
                emptyDirtyScopes.set(depth, false);
                depth = depth - 1;
             }
          }

          /**
           * Invalidate all DMOs with {@link #ffCacheChanges}.
           */
          private void invalidateFastFindCache()
          {
             if (ffCacheChanges.isEmpty())
             {
                return;
             }

             ffCacheChanges.forEach(FastFindCache::invalidate);
             ffCacheChanges.clear();
          }
       }

       /**
        * A proxy for a {@link Commitable} that suppressed the commits and rollbacks. This should
        * be used in aggressive flushing contexts that are not registered as transactions. This is usually
        * the case with {@code forEach} constructs that should aggressively validate and flush records after
        * each iteration, even if not inside a transaction.
        */
       static class AggresiveFlushWrapper
       implements Commitable
       {
          /** The proxied transaction wrapper which has its commit and rollback suppressed */
          private final Commitable commitable;

          /**
           * Basic constructor.
           *
           * @param   commitable
           *          The Commitable to be proxied
           */
          public AggresiveFlushWrapper(Commitable commitable)
          {
             this.commitable = commitable;
          }

          /**
           * {@inheritDoc}
           */
          @Override
          public void commit(boolean transaction)
          {
             // no-op
          }

          /**
           * {@inheritDoc}
           */
          @Override
          public void rollback(boolean transaction)
          {
             // no-op
          }

          /**
           * {@inheritDoc}
           */
          @Override
          public void validate(boolean transaction, boolean aggressiveFlush)
          throws ErrorConditionException
          {
             commitable.validate(transaction, aggressiveFlush);
          }

       }
    }