=== modified file 'src/com/goldencode/p2j/jmx/FwdJMX.java'
--- src/com/goldencode/p2j/jmx/FwdJMX.java	2021-12-16 18:36:18 +0000
+++ src/com/goldencode/p2j/jmx/FwdJMX.java	2022-03-28 09:44:42 +0000
@@ -2,7 +2,7 @@
 ** Module   : FwdJMX.java
 ** Abstract : FWD JMX instrumentation. 
 **
-** Copyright (c) 2020-2021, Golden Code Development Corporation.
+** Copyright (c) 2020-2022, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- ---------------------------------Description----------------------------------
 ** 001 IAS 20200930 Created initial version.
@@ -10,6 +10,7 @@
 **     HC  20201010 Implemented map counter.
 ** 003 SBI 20211104 Added ThreadsCpuTimers to monitor CPU utilization by threads.
 **     CA  20211216 Added a task counter for the legacy service worker threads.
+**     IAS 20220327 Added 'registerMBean' method.
 */ 
 
 /*
@@ -153,6 +154,27 @@
    }
    
    /**
+    * Register MBean
+    * 
+    * @param mbean
+    *        MBean to be registered 
+    * @param name
+    *        MBean name 
+    */
+   public void registerMBean(Object mbean, String name)
+   {
+      try
+      {
+         MBeanServer server = ManagementFactory.getPlatformMBeanServer();
+         server.registerMBean(mbean, new ObjectName("com.goldencode.p2j:name=" + name));
+      } 
+      catch (Exception e)
+      {
+         LOG.log(Level.SEVERE, "MBean registratiion faild", e);
+      }
+   }
+
+   /**
     * Initialize singleton
     */
    public static void init()
@@ -161,6 +183,19 @@
    }
    
    /**
+    * Register MBean
+    * 
+    * @param mbean
+    *        MBean to be registered 
+    * @param name
+    *        MBean name 
+    */
+   public static void register(Object mbean, String name)
+   {
+      Holder.INSTANCE.registerMBean(mbean, name);
+   }
+
+   /**
     * Instance holder (Lazy initialization)
     */
    private static class Holder
@@ -209,11 +244,15 @@
       /**
        * Return the string representation of the counter
        * 
-       * @param ts
-       * @param comment
-       *        comment
-       * @return string representation of the counter
-       * @throws IOException
+       * @param    ts
+       *           The measurement time stamp
+       * @param    comment
+       *           The comment
+       * 
+       * @return   The string representation of the counter
+       * 
+       * @throws   IOException
+       *           If IO exception occurs
        */
       @Override
       public String toString(String ts, String comment) throws IOException
@@ -430,10 +469,12 @@
        * Return the string representation of the counter
        * 
        * @param ts
+       *        The measurement time stamp
        * @param comment
        *        comment
        * @return string representation of the counter
        * @throws IOException
+       *         If IO exception occurs
        */
       public String toString(String ts, String comment) throws IOException
       {
@@ -492,12 +533,16 @@
 
       /**
        * Return the string representation of the counter
-       *
-       * @param ts
-       * @param comment
-       *        comment
-       * @return string representation of the counter
-       * @throws IOException
+       * 
+       * @param    ts
+       *           The measurement time stamp
+       * @param    comment
+       *           The comment
+       * 
+       * @return   The string representation of the counter
+       * 
+       * @throws   IOException
+       *           If IO exception occurs
        */
       public String toString(String ts, String comment) throws IOException
       {
@@ -519,6 +564,7 @@
     * @param file 
     *        output file name (will be appended if exists)
     * @throws IOException 
+    *         If IO exception occurs
     */
    @Override
    public synchronized void dumpAll(String file) throws IOException

=== added file 'src/com/goldencode/p2j/jmx/LockTableUpdaterStat.java'
--- src/com/goldencode/p2j/jmx/LockTableUpdaterStat.java	1970-01-01 00:00:00 +0000
+++ src/com/goldencode/p2j/jmx/LockTableUpdaterStat.java	2022-03-27 15:58:12 +0000
@@ -0,0 +1,145 @@
+/*
+** Module   : LockTableUpdaterStat.java
+** Abstract : LockTableUpdater JMX instrumentation. 
+**
+** Copyright (c) 2022, Golden Code Development Corporation.
+**
+** -#- -I- --Date-- ---------------------------------Description----------------------------------
+** 001 IAS 20230327 Created initial version.
+*/ 
+
+/*
+** 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.jmx;
+
+/** LockTableUpdater JMX instrumentation */
+public class LockTableUpdaterStat
+implements LockTableUpdaterStatMBean
+{
+   /** LockTableUpdater worker */
+   private final LockTableUpdaterStatMBean worker;
+   
+   
+   /**
+    * Constructor.
+    * @param worker
+    *        LockTableUpdater worker
+    */
+   public LockTableUpdaterStat(LockTableUpdaterStatMBean worker)
+   {
+      super();
+      this.worker = worker;
+   }
+
+   /**
+    * Get number of processed lock events.
+    * @return number of processed lock events.
+    */
+   @Override
+   public long getLockEvents()
+   {
+      return worker.getLockEvents();
+   }
+
+   /**
+    * Get number of active locks
+    * @return number of active locks.
+    */
+   @Override
+   public long getActiveLocks()
+   {
+      return worker.getActiveLocks();
+   }
+
+   /**
+    * Get number of locks in VST.
+    * @return number of locks in VST.
+    */
+   @Override
+   public long getPersistedLocks()
+   {
+      return worker.getPersistedLocks();
+   }
+
+   /**
+    * Get number of VST records deleted not in batch.
+    * @return number of VST records deleted not in batch.
+    */
+   @Override
+   public long getDeletesFromVST()
+   {
+      return worker.getDeletesFromVST();
+   }
+
+   /**
+    * Get number of 'persist' calls.
+    * @return number of 'persist' calls.
+    */
+   @Override
+   public long getPersistCalls()
+   {
+      return worker.getPersistCalls();
+   }
+
+   /**
+    * Force 'persist' call
+    */
+   @Override
+   public void flush()
+   {
+      worker.flush();
+   }
+   
+}
+

=== added file 'src/com/goldencode/p2j/jmx/LockTableUpdaterStatMBean.java'
--- src/com/goldencode/p2j/jmx/LockTableUpdaterStatMBean.java	1970-01-01 00:00:00 +0000
+++ src/com/goldencode/p2j/jmx/LockTableUpdaterStatMBean.java	2022-03-27 15:58:13 +0000
@@ -0,0 +1,103 @@
+/*
+** Module   : LockTableUpdaterStatMBean.java
+** Abstract : LockTableUpdater JMX instrumentation interface. 
+**
+** Copyright (c) 2022, Golden Code Development Corporation.
+**
+** -#- -I- --Date-- ---------------------------------Description----------------------------------
+** 001 IAS 20230327 Created initial version.
+*/ 
+
+/*
+** 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.jmx;
+
+/** LockTableUpdater JMX instrumentation interface. */
+public interface LockTableUpdaterStatMBean
+{
+   /**
+    * Get number of processed lock events.
+    * @return number of processed lock events.
+    */
+   public long getLockEvents();
+ 
+   /**
+    * Get number of active locks
+    * @return number of active locks.
+    */
+   public long getActiveLocks();
+   
+   /**
+    * Get number of locks in VST.
+    * @return number of locks in VST.
+    */
+   public long getPersistedLocks();
+
+   /**
+    * Get number of VST records deleted not in batch.
+    * @return number of VST records deleted not in batch.
+    */
+   public long getDeletesFromVST();
+ 
+   /**
+    * Get number of 'persist' calls.
+    * @return number of 'persist' calls.
+    */
+   public long getPersistCalls();
+
+   /**
+    * Force 'persist' call
+    */
+   public void flush();
+}

=== modified file 'src/com/goldencode/p2j/persist/DatabaseManager.java'
--- src/com/goldencode/p2j/persist/DatabaseManager.java	2021-12-28 15:24:58 +0000
+++ src/com/goldencode/p2j/persist/DatabaseManager.java	2022-04-08 21:02:27 +0000
@@ -2,7 +2,7 @@
 ** Module   : DatabaseManager.java
 ** Abstract : Registers and configures databases and manages session factories
 **
-** Copyright (c) 2004-2021, Golden Code Development Corporation.
+** Copyright (c) 2004-2022, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
 ** 001 ECF 20050901   @22473 Created initial version. Extracted and
@@ -261,6 +261,7 @@
 **                           implemented - unknown is not an acceptable returned value for this function.
 **                           Replaced the UnimplementedFeature.missing from tenant-related APIs with a TODO 
 **                           comment.
+**     IAS 20220324          Re-worked LockTableUpdater.
 */
 
 /*
@@ -1238,7 +1239,7 @@
          
          if (lockTableUpdaters != null)
          {
-            LockTableUpdater ltu = new LockTableUpdater(metaDB);
+            LockTableUpdater ltu = new LockTableUpdater(primaryDB, metaDB);
             lockTableUpdaters.put(primaryDB, ltu);
          }
       }
@@ -1534,7 +1535,7 @@
     *          if lock metadata is used by the current application, but no lock table updater is
     *          found for the given database.
     */
-   static LockTableUpdater getLockTableUpdater(Database database)
+   public static LockTableUpdater getLockTableUpdater(Database database)
    {
       if (lockTableUpdaters == null)
       {

=== modified file 'src/com/goldencode/p2j/persist/Persistence.java'
--- src/com/goldencode/p2j/persist/Persistence.java	2022-03-21 13:37:30 +0000
+++ src/com/goldencode/p2j/persist/Persistence.java	2022-03-24 18:35:30 +0000
@@ -564,6 +564,7 @@
 **     ECF 20210927          Minor cleanup.
 **     IAS 20211223          Special processing of errors raised by UDFs. 
 **     IAS 20220221          Call a correct version of the getFWDVersion (Java or SQL) 
+**     IAS 20220324          Re-worked LockTableUpdater.
 */
 
 /*
@@ -1114,20 +1115,6 @@
    }
    
    /**
-    * Force the lock table updater to process all record lock events which are pending at the time
-    * this method is invoked. Additional events may be added by other threads while this is
-    * happening, but any events pending at the start of this method will be processed before this
-    * method returns.
-    */
-   public void flushMetaLockEvents()
-   {
-      if (lockTableUpdater != null)
-      {
-         lockTableUpdater.flushQueue();
-      }
-   }
-   
-   /**
     * Get the identity manager associated with this <code>Persistence</code>
     * instance.  For temp tables, this will be <code>null</code>, since for
     * temp tables unique primary keys generated using {@link

=== modified file 'src/com/goldencode/p2j/persist/RecordBuffer.java'
--- src/com/goldencode/p2j/persist/RecordBuffer.java	2022-03-28 22:11:32 +0000
+++ src/com/goldencode/p2j/persist/RecordBuffer.java	2022-04-05 20:29:52 +0000
@@ -1198,6 +1198,7 @@
 **     OM  20220228          Added byOuterQuery flag for marking situations when the buffer is empty because
 **                           it is set by an OUTER-JOIN table.
 **     OM  20220301          Merged [unknownMode] and [byOuterQuery] flags.
+**     IAS 20220324          Re-worked LockTableUpdater.
 **     OM  20220328          Count and return errors in [endBatch] method.
 */
 
@@ -3206,6 +3207,7 @@
    public static int endBatch(boolean error)
    {
       BufferManager bufMan = BufferManager.get();
+      
       Set<RecordBuffer> bufs = bufMan.endBatchAssignMode();
       int fails = 0;
       
@@ -3319,7 +3321,6 @@
                {
                   fails++;
                }
-               
                if (commitPending)
                {
                   buffer.setCommitPending(false);
@@ -3349,7 +3350,6 @@
       {
          cleanupBatchMode(bufs);
       }
-      
       return fails;
    }
    
@@ -6705,11 +6705,7 @@
          ErrorManager.recordOrShowError(14378, msg, true);
       }
       
-      if (legacyName.equalsIgnoreCase("_Lock"))
-      {
-         persistence.flushMetaLockEvents();
-      } 
-      else if (legacyName.equalsIgnoreCase("_Trans"))
+      if (legacyName.equalsIgnoreCase("_Trans"))
       {
          // flush the transaction id to persistence if required
          TransactionManager.flushTransMetaData(getPersistence());

=== modified file 'src/com/goldencode/p2j/persist/meta/LockTableUpdater.java'
--- src/com/goldencode/p2j/persist/meta/LockTableUpdater.java	2020-10-14 21:06:19 +0000
+++ src/com/goldencode/p2j/persist/meta/LockTableUpdater.java	2022-03-28 09:38:35 +0000
@@ -2,7 +2,7 @@
 ** Module   : LockTableUpdater.java
 ** Abstract : Manages metadata lock records for a particular, primary database
 **
-** Copyright (c) 2013-2020, Golden Code Development Corporation.
+** Copyright (c) 2013-2022, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
 ** 001 ECF 20131101 Created initial version.
@@ -23,6 +23,7 @@
 ** 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.
 */
 
 /*
@@ -82,8 +83,16 @@
 
 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 javax.management.loading.*;
+
+import org.apache.jasper.tagplugins.jstl.core.*;
+
+import com.goldencode.p2j.jmx.*;
 import com.goldencode.p2j.persist.*;
 import com.goldencode.p2j.persist.lock.*;
 import com.goldencode.p2j.persist.orm.*;
@@ -168,36 +177,25 @@
    
    /**
     * Constructor.
-    * 
+    * @param   primary 
+    *          Primary database
     * @param   metaDatabase
     *          Metadata database.
     */
-   public LockTableUpdater(Database metaDatabase)
-   {
-      this.worker = new UpdateWorker(PersistenceFactory.getInstance(metaDatabase));
-   }
-   
-   /**
-    * This method is called within a user context to force any pending lock events to be
-    * processed. It guarantees that any events that are pending in the queue at the time this
-    * method is invoked will have been processed by the time this method returns. However,
-    * other events may be added by other user contexts in the meantime, and these are not
-    * guaranteed to have been processed.
-    */
-   public void flushQueue()
-   {
-      worker.forceFlush();
-   }
-   
-   /**
-    * Stop processing record lock events for the associated, primary database. This will terminate
-    * the update worker and its processing thread.
-    */
-   public void terminate()
-   {
-      worker.terminate();
-   }
-   
+   public LockTableUpdater(Database primary, Database metaDatabase)
+   {
+      this.worker = new UpdateWorker(
+                        PersistenceFactory.getInstance(metaDatabase),
+                        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
@@ -244,19 +242,15 @@
     * thread processes the events.
     */
    private static class UpdateWorker
-   implements Runnable
+   implements LockTableUpdaterStatMBean
    {
       /** Metadata lock table name */
       private static final String LOCK_TABLE = "meta_lock";
       
-      /** SQL query to find a lock record by lock recid and user */
+      /** 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.lock_recid = ?1 and m.lock_usr = ?2";
-      
-      /** SQL query to find a file number by converted table name (used by lock manager) */
-      private static final String SQL_FILENUM =
-         "select m.file_number from meta_file m where m.user_misc = ?1";
+               "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;
@@ -304,9 +298,48 @@
          dmoClass = implementingClass;
       }
       
-      /** Event queue to which many threads can write and from which only one thread reads */
-      private final List<RecordLockEvent> eventQueue = new LinkedList<>();
-      
+      /** 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);
+      
+      /** 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;
       
@@ -316,29 +349,8 @@
       /** Invocation handler which maps proxy method calls to underlying DMO methods */
       private final Handler handler = new Handler();
       
-      /** Update worker thread */
-      private final AssociatedThread workThread;
-      
-      /** Cache of converted table names to file numbers */
-      private final Map<String, Integer> fileNums = new HashMap<>();
-      
-      /** Termination flag */
-      private boolean done = false;
-      
-      /** Lock needed to read from queue; must be obtained first if write lock is needed also */
-      private final Object readLock = new Object();
-      
-      /** Lock needed to write to queue; must be obtained second if read lock is needed also */
-      private final Object writeLock = new Object();
-      
-      /** Unique and sequential ID of the last event added to the queue */
-      private long lastAddedID = -1L;
-      
-      /** Unique and sequential ID of the last event read from the queue and processed */
-      private long lastProcessedID = -1L;
-      
       /** Next available primary key for a new lock metadata record */
-      private long nextPrimaryKey = 1L;
+      private AtomicLong nextPrimaryKey = new AtomicLong(1L);
       
       /** Dummy empty class to be used as the super class for proxy. */
       public static class Empty {}
@@ -349,59 +361,74 @@
        * @param   persistence
        *          Persistence helper object for metadata database.
        */
-      UpdateWorker(Persistence persistence)
+      UpdateWorker(Persistence persistence, Database primary)
       {
          super();
          
          this.persistence = persistence;
+         this.primary = primary;
          this.proxy = ProxyFactory.getProxy(Empty.class, new Class<?>[] { MinimalLock.class }, handler);
+         FwdJMX.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();
+         guard.writeLock().lock();
+         try
+         {
+            if (databaseVersion.get() >= mapVersion.get())
+            {
+               return;
+            }
+            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();
+               }
+            }
+            databaseVersion.set(mapVersion.get());
+         }
+         finally
+         {
+            guard.writeLock().unlock();
+         }
          
-         workThread = new AssociatedThread(this);
-         workThread.setDaemon(true);
-         workThread.setName("Lock metadata update thread");
-         workThread.start();
-      }
-      
-      /**
-       * This method executes in the worker thread, reading record lock events from the queue when
-       * they become available, and updating the metadata database accordingly.
-       */
-      @Override
-      public void run()
-      {
-         while (true)
-         {
-            // check if we should terminate event processing
-            synchronized (writeLock)
-            {
-               if (done)
-               {
-                  break;
-               }
-            }
-            
-            // process any pending events
-            processPendingEvents();
-            
-            // wait for new events to be added to the queue if it is still empty
-            synchronized (writeLock)
-            {
-               while (!done && eventQueue.isEmpty())
-               {
-                  try
-                  {
-                     writeLock.wait();
-                  }
-                  catch (InterruptedException exc)
-                  {
-                     // may be spurious, ignore;  if interrupted due to termination, we will
-                     // detect this in the termination check above
-                  }
-               }
-            }
-         }
-      }
-      
+      }
       /**
        * 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.
@@ -411,238 +438,44 @@
        */
       void addEvent(RecordLockEvent event)
       {
-         synchronized (writeLock)
-         {
-            eventQueue.add(event);
-            // DBG: System.out.println("-> " + event);
-            lastAddedID = event.getEventID();
-            writeLock.notify();
-         }
-      }
-      
-      /**
-       * This method is called within a user context to force any pending lock events to be
-       * processed. It guarantees that any events that are pending in the queue at the time this
-       * method is invoked will have been processed by the time this method returns. However,
-       * other events may be added by other user contexts in the meantime, and these are not
-       * guaranteed to have been processed.
-       */
-      void forceFlush()
-      {
-         long markerID = -1L;
-         long lastProcessed = -1L;
-         
-         synchronized (writeLock)
-         {
-            markerID = lastAddedID;
-            lastProcessed = lastProcessedID;
-         }
-         
-         synchronized (readLock)
-         {
-            while (lastProcessed < markerID)
-            {
-               try
-               {
-                  writeLock.notify();
-                  readLock.wait();
-               }
-               catch (InterruptedException exc)
-               {
-                  if (done)
-                  {
-                     break;
-                  }
-               }
-               
-               synchronized (writeLock)
-               {
-                  lastProcessed = lastProcessedID;
-               }
-            }
-         }
-      }
-      
-      /**
-       * Set a flag to terminate the queue, removing any pending events which have not yet been
-       * processed. The worker thread is interrupted.
-       * <p>
-       * This method should invoked when the primary database associated with the enclosing lock
-       * table updater is deregistered, as part of cleaning up the associated metadata database.
-       */
-      void terminate()
-      {
-         synchronized (writeLock)
-         {
-            done = true;
-            eventQueue.clear();
-            workThread.interrupt();
-         }
-      }
-      
-      /**
-       * Process all pending record lock events in the queue. Events are read and removed from the
-       * queue synchronously, such that no new events may be added during the read/remove
-       * operation. However, they are then processed asynchronously, so new events may be added
-       * while lock metadata is being updated.
-       */
-      private void processPendingEvents()
-      {
-         RecordLockEvent[] pending = null;
-         
-         synchronized (readLock)
-         {
-            // collect all pending events (if any)
-            synchronized (writeLock)
-            {
-               int size = eventQueue.size();
-               if (size > 0)
-               {
-                  pending = new RecordLockEvent[size];
-                  Iterator<RecordLockEvent> iter = eventQueue.iterator();
-                  for (int i = 0; iter.hasNext(); i++)
-                  {
-                     pending[i] = iter.next();
-                     iter.remove();
-                  }
-               }
-            }
-            
-            // process pending events (if any); note that new events can be added to the queue
-            // while we are doing this
-            if (pending != null)
-            {
-               int len = pending.length;
-               for (int i = 0; i < len; i++)
-               {
-                  processEvent(pending[i]);
-               }
-               
-               synchronized (writeLock)
-               {
-                  lastProcessedID = pending[len - 1].getEventID();
-               }
-            }
-            
-            // give any threads waiting in forceFlush a chance to proceed
-            readLock.notifyAll();
-         }
-      }
-      
-      /**
-       * Process an individual record lock event, updating the metadata lock table as appropriate.
-       * If the lock event represents a newly acquired lock, create a new lock record, otherwise
-       * find the existing lock record associated with the event, which must represent a change of
-       * the state of an existing lock. If that change of state represents a lock release, delete
-       * the associated lock record from the lock table.
-       * 
-       * @param   event
-       *          Record lock event describing a new lock or a change in state of an existing
-       *          lock.
-       */
-      private void processEvent(RecordLockEvent event)
-      {
-         Record dmo = null;
-         boolean commit = false;
-         
+         lockEvents.incrementAndGet();
+         guard.readLock().lock();
          try
          {
-            // DBG: System.out.println("<- " + event);
-            commit = persistence.beginTransaction();
-            
-            LockType oldType = event.getOldType();
-            LockType newType = event.getNewType();
-            
-            if (LockType.NONE.equals(oldType))
-            {
-               dmo = createRecord(event);
-            }
-            else
-            {
-               dmo = findRecord(event);
-            }
-            
-            if (LockType.NONE.equals(newType))
-            {
-               persistence.delete(dmo);
-            }
-            else
-            {
-               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.
-               proxy.setLockFlags(new character(code));
-               
-               boolean isTable = (event.getIdentifier() instanceof TableIdentifier);
-               proxy.setLockType(new character(isTable ? "TAB" : "REC"));
-               
-               persistence.save(dmo, dmo.primaryKey());
-            }
-            
-            persistence.flush();
-            
-            if (commit)
-            {
-               commit = false;   // to avoid double-rollback in catch block if commit fails
-               persistence.commit();
-            }
-            
-            if (log.isLoggable(Level.FINEST))
-            {
-               log.log(Level.FINEST, event.toString());
-            }
-         }
-         catch (Exception exc)
-         {
-            try
-            {
-               if (commit)
-               {
-                  persistence.rollback();
-               }
-            }
-            catch (PersistenceException pe)
-            {
-               // error already logged in finally block
-            }
-            finally
-            {
-               if (log.isLoggable(Level.SEVERE))
-               {
-                  String msg = String.format("Error updating _lock table for %s (%s)",
-                                             persistence.getDatabase().toString(),
-                                             event.toString());
-                  log.log(Level.SEVERE, msg, exc);
-               }
+            mapVersion.incrementAndGet();
+            LockRecId lrId = new LockRecId(event);
+            if (event.getNewType() == LockType.NONE )
+            {
+               MinimalLockImpl ml = locks.remove(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())
+                        {
+                           deletesFromVST.incrementAndGet();
+                        }
+                     }
+                     catch(Throwable t)
+                     {
+                       log.log(Level.WARNING, "Cannot delete lock data for " + ml.eventString, t); 
+                     }
+                  });
+               }
+               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
          {
-            if (dmo != null)
-            {
-               RecordIdentifier<String> metaIdent = new RecordIdentifier<>(dmoClass.getName(),
-                                                                           dmo.primaryKey());
-               try
-               {
-                  persistence.lock(LockType.NONE, metaIdent, true);
-               }
-               catch (LockUnavailableException exc)
-               {
-                  // won't be thrown from lock release
-               }
-            }
+            guard.readLock().unlock();
          }
       }
       
@@ -655,8 +488,7 @@
        * 
        * @return  See above.
        */
-      private long getLockRecordID(RecordLockEvent event)
-      throws PersistenceException
+      public long getLockRecordID(RecordLockEvent event) 
       {
          Long recordID = event.getIdentifier().getRecordID();
          long recID = (recordID != null) ? (Long) recordID : -getFileNum(event);
@@ -665,115 +497,6 @@
       }
       
       /**
-       * Find the metadata lock record associated with the user ID and lock record ID specified by
-       * the given record lock event.
-       * 
-       * @param   event
-       *          Record lock event.
-       * 
-       * @return  Metadata lock record, if found.
-       * 
-       * @throws  PersistenceException
-       *          if a lock record cannot be found for the given event, or if there is an error
-       *          executing the query.
-       */
-      private Record findRecord(RecordLockEvent event)
-      throws PersistenceException
-      {
-         long lockRec = getLockRecordID(event);
-         int lockUsr = event.getUserid().intValue();
-         Object[] args = new Object[] { lockRec, lockUsr };
-         
-         Long id = persistence.getSingleSQLResult(SQL_LOCK, args);
-         Record dmo = null;
-         
-         if (id != null)
-         {
-            RecordIdentifier<String> metaIdent = new RecordIdentifier<>(dmoClass.getName(), id);
-            persistence.lock(LockType.EXCLUSIVE, metaIdent, true);
-            dmo = persistence.quickLoad(metaIdent);
-         }
-         
-         if (dmo == null)
-         {
-            throw new PersistenceException("No lock record found for lock event:  " + event);
-         }
-         
-         handler.setDelegate(dmo);
-         
-         return dmo;
-      }
-      
-      /**
-       * Create a metadata lock record for the given record lock event.
-       * 
-       * @param   event
-       *          Event which specifies information to be stored in the lock record.
-       * 
-       * @return  New lock record.
-       * 
-       * @throws  PersistenceException
-       *          if there is any error querying related metadata, or if there is a reflection
-       *          error instantiating the DMO.
-       */
-      private Record createRecord(RecordLockEvent event)
-      throws PersistenceException
-      {
-         Record dmo = null;
-         
-         try
-         {
-            // create new DMO instance and back the proxy with it
-            dmo = dmoClass.newInstance();
-            dmo.initialize(null, true);
-            handler.setDelegate(dmo);
-            
-            // assign primary key
-            Long key = nextPrimaryKey++;
-            dmo.primaryKey(key);
-            
-            // get file number for table
-            Integer fileNum = getFileNum(event);
-            
-            // lock meta record for writing
-            RecordIdentifier<String> metaIdent = new RecordIdentifier<>(dmoClass.getName(), dmo.primaryKey());
-            persistence.lock(LockType.EXCLUSIVE, metaIdent, true);
-            
-            // set user name and user id
-            String subject = event.getOsUserName();
-            if (subject == null)
-            {
-               subject = event.getLocker().getSubject();
-            }
-            character userName = new character(subject);
-            proxy.setLockName(userName);
-            proxy.setLockUsr(event.getUserid());
-            
-            // assign other properties that will not change with new events
-            proxy.setLockId(new int64(key));    // re-use primary key
-            proxy.setLockTable(new integer(fileNum));
-            proxy.setLockRecid(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.
-            proxy.setLockChain(new integer(fileNum + CHAIN_OFFSET));
-         }
-         catch (InstantiationException | IllegalAccessException exc)
-         {
-            String msg = String.format("Error creating meta lock record for event %s",
-                                       event.toString());
-            
-            throw new PersistenceException(msg, exc);
-         }
-         
-         return dmo;
-      }
-      
-      /**
        * Get the file number associated with a lock event.
        * 
        * @param   event
@@ -782,35 +505,26 @@
        * @return  File number for the table associated with the event.
        */
       private Integer getFileNum(RecordLockEvent event)
-      throws PersistenceException
       {
          RecordIdentifier<String> ident = event.getIdentifier();
          String table = ident.getTable();
-         Integer fileNum = fileNums.get(table);
-         if (fileNum == null)
+         Integer fileNum;
+         if (MetadataManager.inUse(MetadataManager._FILE))
          {
-            if (MetadataManager.inUse(MetadataManager._FILE))
-            {
-               // converted table name is stored in MetaFile.userMisc
-               Object[] args = new Object[] { table };
-               fileNum = persistence.getSingleSQLResult(SQL_FILENUM, args);
-               
-               if (fileNum == null)
-               {
-                  String msg = "Table '" + table + "' not found in metadata [" + event.toString() + "]";
-                  throw new PersistenceException(msg);
-               }
-            }
-            else
-            {
-               // the _File metadata is not available
-               fileNum = 0;
-            }
+            // converted table name is stored in MetaFile.userMisc
+            fileNum = MetadataManager.getFileNum(primary, table);
             
-            // cache this to avoid future query by userMisc, which is not indexed
-            fileNums.put(table, fileNum);
-         }
-         
+            if (fileNum == null)
+            {
+               String msg = "Table '" + table + "' not found in metadata [" + event.toString() + "]";
+               throw new IllegalStateException(msg);
+            }
+         }
+         else
+         {
+            // the _File metadata is not available
+            fileNum = 0;
+         }
          return fileNum;
       }
       
@@ -855,6 +569,443 @@
             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;
+         
+         /** 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();
+            }
+            character userName = new character(subject);
+            lockName = userName;
+            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.toString(), 
+                     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.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);
+               proxy.setLockName(lockName);
+               proxy.setLockUsr(lockUsr);
+               proxy.setLockId(lockId); // re-use primary key
+               proxy.setLockTable(lockTable);
+               proxy.setLockRecid(lockRecid);
+               proxy.setLockChain(lockChain);
+            }
+            catch (InstantiationException | IllegalAccessException 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, new Object[] { key });
+            Record dmo = null;
+
+            if (id != null)
+            {
+               RecordIdentifier<String> metaIdent = new RecordIdentifier<>(dmoClass.getName(),
+                        id);
+               persistence.lock(LockType.EXCLUSIVE, metaIdent, true);
+               dmo = persistence.quickLoad(metaIdent);
+            }
+
+            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
+          */
+         public boolean persist()
+         {
+            guard.writeLock().lock();
+            try
+            {
+               return doPersist();
+            }
+            finally
+            {
+               guard.writeLock().unlock();
+            }
+         }
+
+         /** 
+          * Persist lock data in the _Lock VST. 
+          * @return <code>true</code> if success
+          */
+         private boolean doPersist()
+         {
+            if (persisted.get() >= version.get() || deleted)
+            {
+               return false;
+            }
+            Record dmo = null;
+            boolean commit = false;
+            
+            try
+            {
+               commit = persistence.beginTransaction();
+               
+               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)
+                        {
+                           persistence.rollback();
+                        }
+                     }
+                     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();
+               
+               if (commit)
+               {
+                  commit = false;   // to avoid double-rollback in catch block if commit fails
+                  persistence.commit();
+               }
+               if (!deleted)
+               {
+                  persisted.set(version.get());
+               }
+               return true;
+            }
+            catch (Exception exc)
+            {
+               try
+               {
+                  if (commit)
+                  {
+                     persistence.rollback();
+                  }
+               }
+               catch (PersistenceException pe)
+               {
+                  // error already logged in finally block
+               }
+               finally
+               {
+                  if (log.isLoggable(Level.SEVERE))
+                  {
+                     String msg = String.format("Error updating _lock table for %s (%s)",
+                                                persistence.getDatabase().toString(),
+                                                eventString);
+                     log.log(Level.SEVERE, msg, exc);
+                  }
+               }
+            }
+            finally
+            {
+               if (dmo != null)
+               {
+                  RecordIdentifier<String> metaIdent = new RecordIdentifier<>(dmoClass.getName(),
+                                                                              dmo.primaryKey());
+                  try
+                  {
+                     persistence.lock(LockType.NONE, metaIdent, true);
+                  }
+                  catch (LockUnavailableException exc)
+                  {
+                     // won't be thrown from lock release
+                  }
+               }
+            }
+            return false;
+         }
+      }
+
+      /** Lock record id */
+      private class LockRecId
+      implements Comparable<LockRecId>
+      {
+         public final long lockRec;
+         public final int lockUsr;
+         
+         /**
+          * Constructor
+          * @param event
+          *        Lock event
+          */
+         public LockRecId(RecordLockEvent event) 
+         {
+            this.lockRec = getLockRecordID(event);
+            this.lockUsr = event.getUserid().intValue();
+         }
+
+         /**
+          * Compare with ther 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();
+      }
    }
    
    /**

=== modified file 'src/com/goldencode/p2j/persist/meta/MetadataManager.java'
--- src/com/goldencode/p2j/persist/meta/MetadataManager.java	2022-03-01 23:56:49 +0000
+++ src/com/goldencode/p2j/persist/meta/MetadataManager.java	2022-03-24 18:40:04 +0000
@@ -39,7 +39,8 @@
 **     CA  20210709 Set the _File.desc from the Table.description() annotation.
 **     ECF 20210910 Code cleanup.
 **     OM  20220301 Correctly set unknown value as initial value of the field.
-*/
+**     IAS 20220324 Added FILE_NUM_BY_TBL_NAME map.
+**/
 
 /*
 ** This program is free software: you can redistribute it and/or modify
@@ -301,6 +302,10 @@
    private static final ConcurrentMap<Database, ConcurrentMap<Class<?>, integer>> FILE_NUM =
          new ConcurrentHashMap<>();
    
+   /** _File._File_num values for the primary databases by table name */
+   private static final ConcurrentMap<Database, ConcurrentMap<String, Integer>> FILE_NUM_BY_TBL_NAME =
+         new ConcurrentHashMap<>();
+
    /** Word tables' names for the primary databases */
    private static final ConcurrentMap<String, Map<String, Map<String, WordIndexData>>> WORD_TABLES =
          new ConcurrentHashMap<>();
@@ -353,6 +358,20 @@
    }
    
    /**
+    * Get _File._File_num value for the table
+    *  
+    * @param db
+    *        primary db
+    * @param tableName
+    *        Table nbame
+    * @return _File._File_num value for the table
+    */
+   public static Integer getFileNum(Database db, String tableName)
+   {
+      return FILE_NUM_BY_TBL_NAME.computeIfAbsent(db, d -> new ConcurrentHashMap<>()).get(tableName);
+   }
+
+   /**
     * Initialize this class and start up a secure database server to allow remote P2J servers to
     * access metadata.
     * 
@@ -821,6 +840,8 @@
       templateRecords.put(dmoImpl, -fileNum);
       FILE_NUM.computeIfAbsent(primary, d -> new ConcurrentHashMap<>()).
             putIfAbsent(dmoImpl, new integer(fileNum));
+      FILE_NUM_BY_TBL_NAME.computeIfAbsent(primary, d -> new ConcurrentHashMap<>()).
+            putIfAbsent(dmoIface.getAnnotation(Table.class).name(), (int)fileNum);
       saveChildren(p, children);
    }
    

=== modified file 'src/com/goldencode/p2j/persist/orm/FqlToSqlConverter.java'
--- src/com/goldencode/p2j/persist/orm/FqlToSqlConverter.java	2022-02-12 01:08:04 +0000
+++ src/com/goldencode/p2j/persist/orm/FqlToSqlConverter.java	2022-03-24 18:37:44 +0000
@@ -30,6 +30,7 @@
 **     IAS 20210905 Re-working re-writing queries with the values of the mutable SESSION attributes.
 **     OM  20210908 Used SchemaDictionary.TEMP_TABLE_DB instead of hardcoded constant.
 **     OM  20220212 Use ReservedProperty static method for testing reserved properties names.
+**     IAS 20220324 Re-worked LockTableUpdater.
 */
 
 /*
@@ -203,6 +204,9 @@
    /** Flag indicating that the query reads from _UserTableStat VST */
    private boolean userTableStatRead = false;
 
+   /** Flag indicating that the query reads from _Lock VST */
+   private boolean lockTableRead = false;
+
    /** Re-write data for CONTAINS operators */
    private final List<ContainsRewriteData> containsRewriteData = new ArrayList<>(); 
    
@@ -334,6 +338,16 @@
    }
 
    /**
+    * Check if the query reads _Lock VST. 
+    * 
+    * @return <code>true</code> if the query reads _Lock VST.
+    */
+   public boolean isLockTableRead()
+   {
+      return lockTableRead;
+   }
+
+   /**
     * Re-write CONTAINS using word tables and CTE
     * 
     * @param   crwd
@@ -2881,6 +2895,10 @@
                {
                   userTableStatRead = true;
                }
+               if (dmoInfo.isVST(SystemTable._Lock))
+               {
+                  lockTableRead = true;
+               }
                if (alias == null || alias.getType() == ALIAS)
                {
                   String aliasName;

=== modified file 'src/com/goldencode/p2j/persist/orm/Query.java'
--- src/com/goldencode/p2j/persist/orm/Query.java	2022-03-04 19:39:40 +0000
+++ src/com/goldencode/p2j/persist/orm/Query.java	2022-03-24 18:38:09 +0000
@@ -18,6 +18,7 @@
 **     IAS 20211223 Make getDialect(Session session) package private.
 **     CA  20220304 Throw a IllegalStateException if the firstResult or maxResults parameter can't be set for
 **                  the query.
+**     IAS 20220324 Re-worked LockTableUpdater.
 */
 
 /*
@@ -77,6 +78,8 @@
 
 import java.util.*;
 import java.util.function.*;
+
+
 import com.goldencode.p2j.util.*;
 import com.goldencode.p2j.persist.*;
 import com.goldencode.p2j.persist.dialect.*;
@@ -102,6 +105,9 @@
    /** Flag indicating that the query reads from _UserTableStat VST */
    private boolean userTableStatRead = false;
 
+   /** Flag indicating that the query reads from _Lock VST */
+   private boolean lockTableRead = false;
+
    /**
     * The maximum number of results accepted by the (SELECT) query. Used with {@code firstResult}
     * to configure result pagination.
@@ -333,6 +339,7 @@
          paramCount = fql2sql.getLastConversionParamCount();
          wasRewritten = fql2sql.isQueryWasRewritten();
          userTableStatRead = fql2sql.isUserTableStatRead();
+         lockTableRead = fql2sql.isLockTableRead();
          if (!wasRewritten)
          {
             cache.accept(this);
@@ -375,6 +382,15 @@
       {
          UserTableStatUpdater.persist(db);
       }
+      if (lockTableRead)
+      {
+         LockTableUpdater ltu = DatabaseManager.getLockTableUpdater(db);
+         if (ltu != null)
+         {
+            ltu.persist();
+         }
+         UserTableStatUpdater.persist(db);
+      }
       return sqlQuery.expandPlaceholders();
    }
    

