# LockTableUpdater.java

    /*
    ** Module   : LockTableUpdater.java
    ** Abstract : Manages metadata lock records for a particular, primary database
    **
    ** Copyright (c) 2013-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 ECF 20131101 Created initial version.
    ** 002 ECF 20140116 Use session ID as user number (id) for lock events.
    ** 003 ECF 20140213 Change query strings to use JPA-style query substitution placeholders.
    ** 004 ECF 20140509 Fix HQL_FILENUM query string to use new computed column prefix.
    ** 005 ECF 20140509 Encoded more maintainable version of previous fix 004 at OM's suggestion.
    ** 006 ECF 20140918 Only roll back in process event if in a transaction.
    ** 007 ECF 20151203 Fixed HQL_FILENUM query, which was searching on historical table name, while
    **                  the substitution parameter provided by the record lock event is the converted
    **                  table name.
    ** 008 ECF 20160228 Name the update worker thread.
    ** 009 ECF 20160421 Replaced expensive, sequence-based primary key request with simple counter.
    ** 010 ECF 20160514 Replaced expensive HQL queries with more efficient SQL queries.
    ** 011 ECF 20160609 SQL performance improvement.
    ** 012 ECF 20160822 Fixed failing lock metadata updates.
    ** 013 OM  20190111 Added support for LockChain field.
    ** 014 ECF 20200906 New ORM implementation.
    ** 015 CA  20200924 Replaced Method.invoke with ReflectASM.
    **     OM  20201012 Force use locally cached meta information instead of map lookup.
    **     IAS 20220324 Performance optimization in the UserTableStatUpdater style.
    **     ECF 20220630 Fixed MinimalLock getter/setter names to match conversion of historical names in _Lock
    **                  table. Code cleanup.
    **     TJD 20220504 Upgrade do Java 11 minor changes
    ** 016 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 017 GBB 20230825 SecurityManager session methods calls updated.
    ** 018 HC  20240222 Enabled JMX on FWD Client.
    ** 019 TJD 20240208 Java 17 dependencies updates
    ** 020 RAA 20240423 Modified constructor to also include the database configuration.
    ** 021 OM  20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
    **                  based on [sharedDb] parameter.
    ** 022 OM  20250131 A bit of code maintenance.
    */

    /*
    ** 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.meta;

    import java.lang.reflect.*;
    import java.util.*;
    import java.util.concurrent.*;
    import java.util.concurrent.atomic.*;
    import java.util.concurrent.locks.*;
    import java.util.logging.*;
    import com.goldencode.p2j.jmx.*;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.persist.Record;
    import com.goldencode.p2j.persist.lock.*;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.security.SecurityManager;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.logging.*;
    import com.goldencode.proxy.*;

    /**
     * A {@link com.goldencode.p2j.persist.lock.LockListener lock listener} implementation which
     * receives record lock events and updates the metadata lock in-memory map accordingly.
     * When application executes query against the _Lock table the information about active locks is
     * persisted into the meta database before executing the query.
     * Persisted locks are deleted from the meta table in a separate thread when they are released.
     */
    public final class LockTableUpdater
    implements LockListener
    {
       /** Logger */
       private static final CentralLogger log = CentralLogger.get(LockTableUpdater.class.getName());

       /** Worker which processes record lock events in a dedicated server thread */
       private final UpdateWorker worker;

       /** Numeric ID of the user in the current session */
       private final ContextLocal<integer> sessionID = new ContextLocal<integer>()
       {
          /** Security manager */
          private final SecurityManager securityManager;

          /** List of reclaimed user numbers */
          private final List<integer> reclaimedSessionIDs;

          /** Highest allocated session ID to date (may no longer be active) */
          private int highSessionID = 0;

          // Constructor for this anonymous inner class
          {
             securityManager = SecurityManager.getInstance();
             reclaimedSessionIDs = (securityManager == null ? new LinkedList<>() : null);
          }

          @Override
          protected integer initialValue()
          {
             integer sessionID;

             if (securityManager != null)
             {
                sessionID = new integer(securityManager.sessionSm.getSessionId());
             }
             else
             {
                // fallback method, though we should always have a security manager in server mode
                synchronized (this)
                {
                   if (!reclaimedSessionIDs.isEmpty())
                   {
                      sessionID = reclaimedSessionIDs.remove(0);
                   }
                   else
                   {
                      sessionID = new integer(++highSessionID);
                   }
                }
             }

             return sessionID;
          }

          protected void cleanup(integer value)
          {
             if (securityManager == null)
             {
                synchronized (this)
                {
                   reclaimedSessionIDs.add(value);
                }
             }
          }
       };

       /**
        * Constructor.
        * @param   primary
        *          Primary database
        * @param   metaDatabase
        *          Metadata database.
        * @param   cfg
        *          The database configuration.
        */
       public LockTableUpdater(Database primary, Database metaDatabase, DatabaseConfig cfg)
       {
          this.worker = new UpdateWorker(PersistenceFactory.getInstance(metaDatabase, cfg), primary);
       }

       /**
        * Persist locks in the meta database
        */
       public void persist()
       {
          worker.persist();
       }
       /**
        * Method which is called by a {@link LockManager} in response to a lock event. Does some
        * preliminary processing and sanity checking, then queues the event for further processing on
        * a dedicated thread.
        *
        * @param   event
        *          Object which describes the lock event.
        */
       public void lockChanged(RecordLockEvent event)
       {
          LockType oldType = event.getOldType();
          LockType newType = event.getNewType();

          // if old and new lock types are both NONE, nothing to do, but log a warning, because this
          // suggests a problem
          // TODO: does this work with NO-WAIT flag, too?
          if (LockType.NONE.equals(oldType) && LockType.NONE.equals(newType))
          {
             if (log.isLoggable(Level.WARNING))
             {
                log.log(Level.WARNING, "Unexpected lock event:  " + event);

                return;
             }
          }

          event.setUserid(sessionID.get());
          worker.addEvent(event);
       }

       /**
        * A worker containing a queue to which record lock events are written from multiple user
        * threads/contexts, and from which those events are read in a dedicated, daemon thread. The
        * idea is to offload the potentially more time-consuming work of querying and updating the
        * metadata database from a dedicated thread, to minimize any performance penalty of this work
        * on user sessions. Since this naturally introduces the possibility of the lock metadata to
        * get out of sync with the actual lock states in the lock manager, a synchronization mechanism
        * is used to ensure any pending updates are flushed to the database before any query against
        * this table is executed. This way, only those user sessions actually querying the lock
        * metadata are affected by any lag in updating the metadata from record lock events.
        * <p>
        * Note that some analysis of the lock event has to occur in the user session in which the
        * lock event occurred, but as much of the work as possible is deferred until the separate
        * thread processes the events.
        */
       private static class UpdateWorker
       implements LockTableUpdaterStatMBean
       {
          /** Metadata lock table name */
          private static final String LOCK_TABLE = "meta_lock";

          /** SQL query to find a lock record by PK */
          private static final String SQL_LOCK =
             "select m." + Session.PK + " from " + LOCK_TABLE + " m where m." + Session.PK + " = ?1";

          /** The offset between a table and its surrogate chain id. */
          private static final int CHAIN_OFFSET = 1_000_000;

          /** MetaLock DMO implementation class */
          private static final Class<? extends Record> dmoClass;

          /** Map of <code>MinimalLock</code> methods to <code>MetaLockImpl</code> methods */
          private static final Map<Method, Method> methodMap = new HashMap<>();

          /*
           * Before this static block is executed the MetadataManager.initialize() is processed. At
           * that time, if the _Lock meta table is present configured in configuration file, the
           * specialized DMO interface - dmoName is already registered with DmoMetadataManager.
           */
          static
          {
             Class<? extends Record> implementingClass;
             String dmoName = DmoMetadataManager.getDmoBasePackage() + "._meta.MetaLock";
             try
             {
                DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(Class.forName(dmoName));
                implementingClass = dmoInfo.getImplementationClass();

                // map MinimalLock methods to corresponding dmoClass methods; if we try to use
                // MinimalLock methods with a MetaLockImpl object, we will raise
                // IllegalArgumentException during Method.invoke(), because our object will not be an
                // instance of MinimalLock
                for (Method ifcMeth : MinimalLock.class.getMethods())
                {
                   Method dmoMeth = implementingClass.getMethod(ifcMeth.getName(), ifcMeth.getParameterTypes());
                   methodMap.put(ifcMeth, dmoMeth);
                }
             }
             catch (ClassNotFoundException | IllegalArgumentException exc)
             {
                // not a fatal error; application does not use _Lock table
                implementingClass = null;
             }
             catch (NoSuchMethodException exc)
             {
                // our MinimalLock does not match the full _Lock DMO from conversion time
                throw new RuntimeException("Error preparing MetaLock implementation class", exc);
             }
             dmoClass = implementingClass;
          }

          /** Number of processed lock events. */
          private static final AtomicLong lockEvents = new AtomicLong(0);

          /** Number of records in VST. */
          private static final AtomicLong persistedLocks = new AtomicLong(0);

          /** Temporary holder of the number of records in VST - before commit/rollback. */
          private static final AtomicLong persistedLocks_ = new AtomicLong(0);

          /** Number of VST records deleted not in batch. */
          private static final AtomicLong deletesFromVST = new AtomicLong(0);

          /** Number of 'persist' calls */
          private static final AtomicLong persistCalls = new AtomicLong(0);

          /** Executor for operations with VST */
          private final Executor executor = Executors.newSingleThreadExecutor(
             new ThreadFactory()
             {
                @Override
                public Thread newThread(Runnable r)
                {
                   Thread workThread = new AssociatedThread(r);
                   workThread.setDaemon(true);
                   workThread.setName("Lock metadata update thread");
                   return workThread;
                }
             }
          );

          /** Locks' map */
          private final ConcurrentMap<LockRecId, MinimalLockImpl> locks = new ConcurrentSkipListMap<>();

          /** Map version */
          private final AtomicLong mapVersion = new AtomicLong();

          /** Meta table vesrion */
          private final AtomicLong databaseVersion = new AtomicLong(0);

          /** Locks' map guard */
          private final ReentrantReadWriteLock guard = new ReentrantReadWriteLock();

          /** database name */
          private final Database primary;

          /** Persistence helper object for metadata database */
          private final Persistence persistence;

          /** Proxy object for the current metadata lock DMO */
          private final MinimalLock proxy;

          /** Invocation handler which maps proxy method calls to underlying DMO methods */
          private final Handler handler = new Handler();

          /** Next available primary key for a new lock metadata record */
          private final AtomicLong nextPrimaryKey = new AtomicLong(1L);

          /** Dummy empty class to be used as the super class for proxy. */
          public static class Empty {}

          /**
           * Constructor.
           *
           * @param   persistence
           *          Persistence helper object for metadata database.
           */
          UpdateWorker(Persistence persistence, Database primary)
          {
             this.persistence = persistence;
             this.primary = primary;
             this.proxy = ProxyFactory.getProxy(Empty.class, new Class<?>[] { MinimalLock.class }, handler);
             FwdServerJMX.register(new LockTableUpdaterStat(this), primary.getName() + ".locks");
             executor.execute(() -> {}); // start executor
          }

          /**
           * Submit persisting locks task to the meta database and wait for end
           */
          public void persist()
          {
             CountDownLatch latch = new CountDownLatch(1);
             executor.execute(() -> {
                try
                {
                   doPersist();
                   latch.countDown();
                }
                catch(Throwable t)
                {
                   log.log(Level.WARNING, "Failed to persist lock table data", t);
                }
             });
             try
             {
                latch.await();
             }
             catch (InterruptedException ignore)
             {
             }
          }

          /**
           * Persist locks to the meta database
           */
          public void doPersist()
          {
             persistCalls.incrementAndGet();
             persistedLocks_.set(persistedLocks.get());
             guard.writeLock().lock();
             boolean commit = false;
             try
             {
                if (databaseVersion.get() >= mapVersion.get())
                {
                   return;
                }
                commit = persistence.beginTransaction(Persistence.META_CTX);
                for(Iterator<Map.Entry<LockRecId, MinimalLockImpl>> it = locks.entrySet().iterator();
                    it.hasNext(); )
                {
                   Map.Entry<LockRecId, MinimalLockImpl> e = it.next();
                   e.getValue().persist();
                   if (e.getValue().deleted)
                   {
                      it.remove();
                   }
                }
                if (commit)
                {
                   commit = false;   // to avoid double-rollback in catch block if commit fails
                   persistence.commit(Persistence.META_CTX);
                }
                for (Iterator<Map.Entry<LockRecId, MinimalLockImpl>> it = locks.entrySet().iterator();
                     it.hasNext(); )
                {
                   Map.Entry<LockRecId, MinimalLockImpl> e = it.next();
                   e.getValue().commit();
                   if (e.getValue().deleted)
                   {
                      it.remove();
                   }
                }
                persistedLocks.set(persistedLocks_.get());
                databaseVersion.set(mapVersion.get());
             }
             catch (PersistenceException e1)
             {
                log.log(Level.WARNING, "Failed to persist lock data", e1);
                locks.values().forEach(MinimalLockImpl::rollback);
                if (commit)
                {
                   try
                   {
                      persistence.rollback(Persistence.META_CTX);
                   }
                   catch (PersistenceException ignore)
                   {
                   }
                }
             }
             finally
             {
                guard.writeLock().unlock();
             }
          }

          /**
           * Add a record lock event to the queue, locking out other write and read operations until
           * the queue is updated. Notify a the worker thread when the new event has been added.
           *
           * @param   event
           *          Record lock event to be added.
           */
          void addEvent(RecordLockEvent event)
          {
             lockEvents.incrementAndGet();
             guard.readLock().lock();
             try
             {
                mapVersion.incrementAndGet();
                LockRecId lrId = new LockRecId(event);
                if (event.getNewType() == LockType.NONE )
                {
                   MinimalLockImpl ml = locks.get(lrId);
                   if (ml != null && ml.persisted.get() >= 0)
                   {
                      // lock was already persisted, VST cleanup is required
                      ml.guardedUpdate(event);
                      executor.execute(() -> {
                         try
                         {
                            if (ml.persist())
                            {
                               locks.remove(lrId);
                               deletesFromVST.incrementAndGet();
                               persistedLocks.decrementAndGet();
                            }
                         }
                         catch(Throwable t)
                         {
                           log.log(Level.WARNING, "Cannot delete lock data for " + ml.eventString, t);
                         }
                      });
                   }
                   else
                   {
                      locks.remove(lrId);
                   }
                   return;
                }
                MinimalLockImpl ml = locks.computeIfAbsent(lrId, k -> new MinimalLockImpl(event));
                if (!ml.version.compareAndSet(-1, 0)) // not a new lock, should be updated
                {
                   ml.guardedUpdate(event);
                }
             }
             finally
             {
                guard.readLock().unlock();
             }
          }

          /**
           * Extract an integral lock record ID from the lock event. This will be a positive value
           * for records and a negative value for tables.
           *
           * @param   event
           *          A record lock event.
           *
           * @return  See above.
           */
          public long getLockRecordID(RecordLockEvent event)
          {
             Long recordID = event.getIdentifier().getRecordID();
             long recID = (recordID != null) ? (Long) recordID : -getFileNum(event);

             return recID;
          }

          /**
           * Get the file number associated with a lock event.
           *
           * @param   event
           *          Record lock event.
           *
           * @return  File number for the table associated with the event.
           */
          private Integer getFileNum(RecordLockEvent event)
          {
             RecordIdentifier<String> ident = event.getIdentifier();
             String table = ident.getTable();
             Integer fileNum;
             if (MetadataManager.inUse(MetadataManager._FILE))
             {
                // converted table name is stored in MetaFile.userMisc
                fileNum = MetadataManager.getFileNum(primary, table);

                if (fileNum == null)
                {
                   String msg = "Table '" + table + "' not found in metadata [" + event + "]";
                   throw new IllegalStateException(msg);
                }
             }
             else
             {
                // the _File metadata is not available
                fileNum = 0;
             }
             return fileNum;
          }

          /**
           * Invocation handler which delegates calls to the {@link MinimalLock} proxy methods to the
           * backing DMO instance.
           */
          private static class Handler
          implements InvocationHandler
          {
             /** DMO delegate whose methods ultimately are invoked */
             private Object delegate = null;

             /**
              * Process method invocations on the {@link MinimalLock} proxy by delegating them to the
              * backing DMO instance.
              *
              * @param   proxy
              *          Proxy on which the enclosing class invokes methods.
              * @param   method
              *          Method which was invoked.
              * @param   args
              *          Method arguments.
              */
             @Override
             public Object invoke(Object proxy, Method method, Object[] args)
             throws Throwable
             {
                Method dmoMethod = methodMap.get(method);

                return Utils.invoke(dmoMethod, delegate, args);
             }

             /**
              * Set the backing DMO delegate instance.
              *
              * @param   delegate
              *          DMO whose methods will be invoked.
              */
             void setDelegate(Object delegate)
             {
                this.delegate = delegate;
             }
          }

          /** In-memory holder for the _Lock VST data*/
          private class MinimalLockImpl
          {
             /** Lock guard */
             public final ReentrantReadWriteLock guard = new ReentrantReadWriteLock();

             /** Lock version */
             public final AtomicLong version = new AtomicLong(-1);

             /** Lock version last persisted */
             public final AtomicLong persisted = new AtomicLong(-1);

             /** Flag indicating that the corresponding record was already deleted in VST */
             public volatile boolean deleted = false;

             /** Temporary holder for the 'deleted' flag -used until commit/rollback */
             public volatile boolean deleted_ = false;

             /** Primary key */
             public final long key;

             /** Lock Id */
             public final int64 lockId;

             /** User */
             public final integer lockUsr;

             /** Lock name */
             public final character lockName;

             /** Table fileNum */
             public final integer lockTable;

             /** Record Id */
             public final int64 lockRecId;

             /** Chain */
             public final integer lockChain;

             /** Old lock type */
             private LockType oldType;

             /** New lock type */
             private LockType newType;

             /** Lock type string */
             private character lockType;

             /** Lock flags */
             private character lockFlags;

             /** Event string representation (for logging) */
             private String eventString;

             /**
              * Constructor
              *
              * @param event
              *        Lock event
              */
             public MinimalLockImpl(RecordLockEvent event)
             {
                key = nextPrimaryKey.incrementAndGet();

                // set user name and user id
                String subject = event.getOsUserName();
                if (subject == null)
                {
                   subject = event.getLocker().getSubject();
                }
                lockName = new character(subject);
                lockUsr = event.getUserid();

                // get file number for table
                Integer fileNum = getFileNum(event);

                // assign other properties that will not change with new events
                lockId = new int64(key);    // re-use primary key
                lockTable = new integer(fileNum);
                lockRecId = new int64(getLockRecordID(event));

                // In ProgressOE, the locking table is divided into chains anchored in a hash table.
                // These chains are provided for fast lookup of record locks by Rec-id.
                // FWD OTOH, relies on underlying database for recid/rowid lookup so the chain is at
                // least irrelevant. In order to have a substitute and keep things simple, we will
                // assume that each table has a single chain and its value is computed as an fixed
                // offset from the table ID.
                lockChain = new integer(fileNum + CHAIN_OFFSET);

                update(event);
             }

             /**
              * Guarded update holder with event data
              *
              * @param   event
              *          lock event
              */
             public void guardedUpdate(RecordLockEvent event)
             {
                guard.writeLock().lock();
                try
                {
                   update(event);
                   version.incrementAndGet();
                }
                finally
                {
                   guard.writeLock().unlock();
                }
             }

             /**
              * Update holder with event data
              *
              * @param   event
              *          lock event
              */
             public void update(RecordLockEvent event)
             {
                this.oldType = event.getOldType();
                this.newType = event.getNewType();
                String code = newType.isExclusive() ? "X" : "S"; // TODO:  get this right
                if (oldType.isShare())
                {
                   code += " U"; // an upgrade
                }
                // From ABL online manual: values of flags field specify:
                //    S: a share lock,
                //    X: an exclusive lock,
                //    U: a lock upgraded from share to exclusive,
                //    L: a lock in limbo,
                //    Q: a queued lock,
                //    K: a lock kept across transaction end boundary,
                //    J: a lock is part of a JTA transaction,
                //    C: a lock is in create mode for JTA,
                //    E: a lock wait timeout has expired on this queued lock.
                this.lockFlags = new character(code);

                boolean isTable = (event.getIdentifier() instanceof TableIdentifier);
                this.lockType = new character(isTable ? "TAB" : "REC");
                this.eventString = String.format("%s: %d[%d, %d]", event,
                                                 key, lockRecId.longValue(), lockUsr.intValue());
             }

             /**
              * Create record in the _Lock VST
              */
             private Record createRecord() throws PersistenceException
             {
                Record dmo = null;

                try
                {
                   // create new DMO instance and back the proxy with it
                   dmo = dmoClass.getDeclaredConstructor().newInstance();
                   dmo.initialize(null, true);
                   handler.setDelegate(dmo);

                   // assign primary key
                   dmo.primaryKey(key);
                   // lock meta record for writing
                   RecordIdentifier<String> metaIdent = new RecordIdentifier<>(dmoClass.getName(),
                                                                               dmo.primaryKey());
                   persistence.lock(LockType.EXCLUSIVE, metaIdent, true, Persistence.META_CTX);
                   proxy.setLockName(lockName);
                   proxy.setLockUsr(lockUsr);
                   proxy.setLockId(lockId); // re-use primary key
                   proxy.setLockTable(lockTable);
                   proxy.setLockRecId(lockRecId);
                   proxy.setLockChain(lockChain);
                }
                catch (ReflectiveOperationException exc)
                {
                   String msg = String.format("Error creating meta lock record for event %s", eventString);

                   throw new PersistenceException(msg, exc);
                }
                return dmo;
             }

             /** Find record in the _Lock VST */
             private Record findRecord() throws PersistenceException
             {
                Long id = persistence.getSingleSQLResult(SQL_LOCK, Persistence.META_CTX, new Object[] { key });
                Record dmo = null;

                if (id != null)
                {
                   RecordIdentifier<String> metaIdent = new RecordIdentifier<>(dmoClass.getName(), id);
                   persistence.lock(LockType.EXCLUSIVE, metaIdent, true, Persistence.META_CTX);
                   dmo = persistence.quickLoad(metaIdent, Persistence.META_CTX);
                }

                if (dmo == null)
                {
                   throw new PersistenceException("No lock record found for lock event:  " + eventString);
                }

                handler.setDelegate(dmo);

                return dmo;
             }

             /**
              * Guarded persist lock data in the _Lock VST.
              *
              * @return  <code>true</code> if success
              * @throws  PersistenceException
              *          on database error.
              */
             public boolean persist()
             throws PersistenceException
             {
                guard.writeLock().lock();
                try
                {
                   return doPersist();
                }
                finally
                {
                   guard.writeLock().unlock();
                }
             }

             /**
              * Commit the changes of the lock data state
              */
             private void commit()
             {
                persisted.set(version.get());
                deleted = deleted_;
                deleted_ = false;
             }

             /**
              * Roll back the changes of the lock data state
              */
             private void rollback()
             {
                deleted_ = false;
             }

             /**
              * Persist lock data in the _Lock VST.
              *
              * @return  {@code true} on success.
              *
              * @throws  PersistenceException
              *          On database error.
              */
             private boolean doPersist()
             throws PersistenceException
             {
                if (persisted.get() >= version.get() || deleted)
                {
                   return false;
                }
                Record dmo = null;
                boolean commit = false;

                try
                {
                   commit = persistence.beginTransaction(Persistence.META_CTX);

                   if (persisted.get() < 0)
                   {
                      dmo = createRecord();
                      if (log.isLoggable(Level.FINE))
                      {
                         log.fine("created lock record [" + eventString + "]");
                      }
                      persistedLocks_.incrementAndGet();
                   }
                   else
                   {
                      try
                      {
                         dmo = findRecord();
                      }
                      catch (Exception e)
                      {
                         log.log(Level.WARNING, "No record found", e);
                         try
                         {
                            if (commit)
                            {
                               this.rollback();
                               persistence.rollback(Persistence.META_CTX);
                            }
                         }
                         catch (PersistenceException ignore)
                         {
                         }
                         return false;
                      }
                      if (LockType.NONE.equals(newType))
                      {
                         persistence.delete(dmo);
                         persistedLocks_.decrementAndGet();
                         deleted_ = true;
                         if (log.isLoggable(Level.FINE))
                         {
                            log.fine("deleted lock record [" + eventString + "]");
                         }
                      }
                      else
                      {
                         proxy.setLockFlags(lockFlags);
                         proxy.setLockType(lockType);
                         if (log.isLoggable(Level.FINE))
                         {
                            log.fine("updated lock record [" + eventString + "]");
                         }
                      }
                   }
                   persistence.save(dmo, dmo.primaryKey());
                   persistence.flush(Persistence.META_CTX);

                   if (commit)
                   {
                      commit = false;   // to avoid double-rollback in catch block if commit fails
                      persistence.commit(Persistence.META_CTX);
                      this.commit();
                   }
                   return true;
                }
                catch (PersistenceException exc)
                {
                   if (log.isLoggable(Level.SEVERE))
                   {
                      String msg = String.format("Error updating _lock table for %s (%s)",
                                                 persistence.getDatabase(Persistence.META_CTX).toString(),
                                                 eventString);
                      log.log(Level.SEVERE, msg, exc);
                   }
                   if (commit)
                   {
                      try
                      {
                         this.rollback();
                         persistence.rollback(Persistence.META_CTX);
                      }
                      catch (PersistenceException ignore)
                      {
                      }
                   }
                   else
                   {
                      // propagate exception to the outer block.
                      throw exc;
                   }
                }
                finally
                {
                   if (dmo != null)
                   {
                      RecordIdentifier<String> metaIdent = new RecordIdentifier<>(dmoClass.getName(),
                                                                                  dmo.primaryKey());
                      try
                      {
                         persistence.lock(LockType.NONE, metaIdent, true, Persistence.META_CTX);
                      }
                      catch (LockUnavailableException exc)
                      {
                         // won't be thrown from lock release
                      }
                   }
                }
                return false;
             }
          }

          /**
           * Lock record id
           */
          private class LockRecId
          implements Comparable<LockRecId>
          {
             /** lock Record Id */
             public final long lockRec;

             /** lock User Id */
             public final int lockUsr;

             /**
              * Constructor
              *
              * @param   event
              *          Lock event
              */
             public LockRecId(RecordLockEvent event)
             {
                this.lockRec = getLockRecordID(event);
                this.lockUsr = event.getUserid().intValue();
             }

             /**
              * Compare with the given record id
              *
              * @return  -1 if other record id is greater, 1 if other record is less, 0 if ids are equal
              */
             @Override
             public int compareTo(LockRecId o)
             {
                return this.lockRec < o.lockRec ? -1 :
                       this.lockRec > o.lockRec ?  1 :
                          this.lockUsr < o.lockUsr ? -1 :
                          this.lockUsr > o.lockUsr ?  1 : 0;
             }
          }

          /**
           * Get number of processed lock events.
           *
           * @return  number of processed lock events.
           */
          @Override
          public long getLockEvents()
          {
             return lockEvents.get();
          }

          /**
           * Get number of active locks
           *
           * @return  number of active locks.
           */
          @Override
          public long getActiveLocks()
          {
             return locks.size();
          }

          /**
           * Get number of locks in VST.
           *
           * @return  number of locks in VST.
           */
          @Override
          public long getPersistedLocks()
          {
             return persistedLocks.get();
          }

          /**
           * Get number of VST records deleted not in batch.
           *
           * @return  number of VST records deleted not in batch.
           */
          @Override
          public long getDeletesFromVST()
          {
             return deletesFromVST.get();
          }

          /**
           * Get number of 'persist' calls.
           *
           * @return  number of 'persist' calls.
           */
          @Override
          public long getPersistCalls()
          {
             return persistCalls.get();
          }

          /**
           * Force 'persist' call
           */
          @Override
          public void flush()
          {
             persist();
          }
       }

       /**
        * Interface which defines the minimally required methods of the <code>MetaLock</code>
        * interface. This interface defines the proxy used to create and manipulate metadata lock
        * DMO instances. The DMO's methods cannot be invoked directly from code here, since the
        * <code>MetaLock</code> interface resides in an application-specific package, which is not
        * known at compile time.
        */
       public interface MinimalLock
       {
          /**
           * Getter: lockId
           *
           * @return  lockId
           */
          public int64 getLockId();

          /**
           * Setter: lockId
           *
           * @param   lockId
           *          lockId
           */
          public void setLockId(NumberType lockId);

          /**
           * Getter: Usr
           *
           * @return  Usr
           */
          public integer getLockUsr();

          /**
           * Setter: Usr
           *
           * @param   lockUsr
           *          Usr
           */
          public void setLockUsr(NumberType lockUsr);

          /**
           * Getter: Name
           *
           * @return  Name
           */
          public character getLockName();

          /**
           * Setter: Name
           *
           * @param   lockName
           *          Name
           */
          public void setLockName(Text lockName);

          /**
           * Getter: Type
           *
           * @return  Type
           */
          public character getLockType();

          /**
           * Setter: Type
           *
           * @param   lockType
           *          Type
           */
          public void setLockType(Text lockType);

          /**
           * Getter: Table
           *
           * @return  Table
           */
          public integer getLockTable();

          /**
           * Setter: Table
           *
           * @param   lockTable
           *          Table
           */
          public void setLockTable(NumberType lockTable);

          /**
           * Getter: RECID
           *
           * @return  RECID
           */
          public int64 getLockRecId();

          /**
           * Setter: RECID
           *
           * @param   lockRecId
           *          RECID
           */
          public void setLockRecId(NumberType lockRecId);

          /**
           * Getter: Chain
           *
           * @return  Chain
           */
          public integer getLockChain();

          /**
           * Setter: Chain
           *
           * @param   lockChain
           *          Chain
           */
          public void setLockChain(NumberType lockChain);

          /**
           * Getter: Flags
           *
           * @return  Flags
           */
          public character getLockFlags();

          /**
           * Setter: Flags
           *
           * @param   lockFlags
           *          Flags
           */
          public void setLockFlags(Text lockFlags);
       }
    }