TransactionTableUpdater.java

/*
** Module   : TransactionTableUpdater.java
** Abstract : Synchronizes metadata transaction records for a particular, primary database.
**
** Copyright (c) 2017-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 ECF 20171030 Created initial version.
** 002 ECF 20180819 Optimized common case of updates at the expense of reads.
** 003 OM  20190122 Rewrote the class to work with new TransactionManager.
** 004 ECF 20200906 New ORM implementation.
** 005 CA  20200924 Replaced Method.invoke with ReflectASM.
**     OM  20201012 Force use locally cached meta information instead of map lookup.
**     ECF 20220630 Fixed MinimalTrans getter/setter names to match conversion of historical names in _Trans
**                  table.
**     TJD 20220504 Upgrade do Java 11 minor changes
**     RAA 20230109 Changed inline statement(s) to prepared statement(s).
** 006 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 007 TJD 20240123 Java 17 compatibility updates
** 008 OM  20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
**                  based on [sharedDb] parameter.
*/

/*
** 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.logging.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.Record;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.proxy.*;

/**
 * An utility class which receives notifications from the {@link TransactionManager} when a flush
 * of {@code _Trans} metadata VST is needed. This involves dropping the expired records and 
 * creation of new transaction records.
 */
public final class TransactionTableUpdater
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(TransactionTableUpdater.class);
   
   /** Metadata transaction table name */
   private static final String TRANS_TABLE = "meta_trans";
   
   /** MetaTrans DMO implementation class */
   private static final Class<? extends Record> dmoClass;
   
   /** Map of {@code MinimalTrans} methods to {@code MetaTransImpl} 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 _Trans meta table is present configured in configuration file, the specialized
    * DMO interface - dmoName is already registered with DmoMetadataManager.
    */
   static
   {
      Class<? extends Record> implementingClass;
      
      // TODO: this assumes a particular DMO interface name conversion, which can change with configuration;
      // we should only allow a single, deterministic name conversion for the metaschema tables
      String dmoName = DmoMetadataManager.getDmoBasePackage() + "._meta.MetaTrans";
      try
      {
         DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(Class.forName(dmoName));
         implementingClass = dmoInfo.getImplementationClass();
         
         // map MinimalTrans methods to corresponding dmoClass methods; if we try to use
         // MinimalTrans methods with a MetaTransImpl object, we will raise
         // IllegalArgumentException during Method.invoke(), because our object will not be an
         // instance of MinimalTrans
         if (implementingClass != null)
         {
            for (Method ifcMeth : MinimalTrans.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 _trans table
         implementingClass = null; 
      }
      catch (NoSuchMethodException exc)
      {
         // our MinimalTrans does not match the full _Trans DMO from conversion time
         throw new RuntimeException("Error preparing MetaTrans implementation class", exc);
      }
      dmoClass = implementingClass;
   }
   
   /** Proxy object for the current metadata transaction DMO */
   private final MinimalTrans proxy;
   
   /** Invocation handler which maps proxy method calls to underlying DMO methods */
   private final Handler handler = new Handler();
   
   /** Dummy empty class to be used as the super class for proxies. */
   public static class Empty {}
   
   /**
    * Constructor which stores a persistence object for the associated metadata database, and
    * initializes a proxy for the application-specific DMO.
    */
   public TransactionTableUpdater()
   {
      this.proxy = ProxyFactory.getProxy(Empty.class,
                                         new Class<?>[] { MinimalTrans.class },
                                         handler);
   }
   
   /**
    * Check whether the project was configured with support for {@code _Trans}.  
    * 
    * @return  {@code true} when the project was configured with support for {@code _Trans}.
    */
   public static boolean isEnabled()
   {
      return dmoClass != null; 
   }
   
   /**
    * Synchronize metadata table with map of transaction IDs to session IDs, creating and
    * persisting a DMO for each record not already in the table.
    * 
    * @param   p
    *          The {@link Persistence} to be used when flushing data to database. 
    * @param   transactions
    *          The set of active transactions from the current database, mapped by the users
    *          (connection ID) that opened them.
    */
   public void flush(Persistence p, Map<Integer, Integer> transactions)
   {
      if (!isEnabled())
      {
         return;
      }
      
      String dbName = p.getDatabase(Persistence.META_CTX).getId().toLowerCase();
      boolean commit = false;
      try
      {
         commit = p.beginTransaction(Persistence.META_CTX);
         
         // drop all records that are no more valid
         StringBuilder buf = new StringBuilder(
               "delete from " + TRANS_TABLE + " where not trans_num in (");
         List<Object> args = new ArrayList<>();
         Iterator<Integer> it = transactions.values().iterator();
         for (int i = 0; it.hasNext(); i++)
         {
            if (i > 0)
            {
               buf.append(',');
            }
            
            args.add(it.next());
            buf.append('?');
         }
         buf.append(')');
         int delCounter = p.executeSQL(buf.toString(), Persistence.META_CTX, args.toArray());
         if (LOG.isLoggable(Level.FINE))
         {
            LOG.log(Level.FINE, "Dropped " + delCounter + " old _trans records from " + dbName);
         }
         
         // retrieve all records that are still in database and will be skipped: 
         Set<Long> savedIds = new HashSet<>();
         ScrollableResults<Long> sr = p.executeSQLQuery("select trans_num from " + TRANS_TABLE, 
                                                        Persistence.META_CTX, 
                                                        null);
         while (sr.next())
         {
            Long xid = sr.get(0, Long.class);
            savedIds.add(xid);
         }
         
         // save/update the records
         for (Map.Entry<Integer, Integer> entry : transactions.entrySet())
         {
            Integer trans_num = entry.getValue();
            if (savedIds.contains(trans_num.longValue()))
            {
               // this is already in database, leave it as it is
               continue;
            }
            
            // create new DMO instance and back the proxy with it
            Record dmo;
            try
            {
               dmo = dmoClass.getDeclaredConstructor().newInstance();
               dmo.initialize(null, true);
            }
            catch (ReflectiveOperationException e)
            {
               return;// failed to instantiate smo
            }
            handler.setDelegate(dmo);
            
            // assign primary key
            Long key = p.nextPrimaryKey(TRANS_TABLE, true);
            dmo.primaryKey(key);
            
            // assign transaction ID (required by unique index)
            proxy.setTransId(new int64(key));
            
            // assign fields with known values
            proxy.setTransNum(new integer(trans_num));
            proxy.setTransUsrnum(new integer(entry.getKey()));
            proxy.setTransFlags(new character("FWD"));
            // NOTE: all possibilities:
            //    FWD - regular forward (normal) process
            //    UNDO - rollback of the transaction is underway
            //    2PC, INDOUBT, COORD and FORCE - unknown
            proxy.setTransState(new character("BEGIN"));
            // NOTE: other possibilities:
            //    ALLOCATED - entered on long lasting transaction. When entering this state
            //                the start-time field is also filled-in
            //    BEGIN - the initial state of the transaction
            // more on: https://documentation.progress.com/output/ua/OpenEdge_latest/index.html#page/dmadm%2Fpromon-record-locking-table-option.html%23wwID0EE5XW
            
            // assign other fields which are mandatory and default to unknown
            proxy.setTransCoord(new character("?"));
            proxy.setTransCoordTx(new integer(0));
            proxy.setTransDuration(new integer(0));
            proxy.setTransTxtime(new character("?"));
            proxy.setTransTenantId(new integer(0));
            proxy.setTransCounter(new integer(0));
            proxy.setTransMisc(new integer(0));
            
            // save the new record
            p.save(dmo, key);
         }
         
         // flush explicitly at the end of batch
         p.flush(Persistence.META_CTX);
         
         if (commit)
         {
            commit = false;   // to avoid double-rollback in catch block if commit fails
            p.commit(Persistence.META_CTX);
         }
      }
      catch (PersistenceException exc)
      {
         try
         {
            if (commit)
            {
               p.rollback(Persistence.META_CTX);
            }
         }
         catch (PersistenceException pe)
         {
            // error already logged in finally block
         }
         finally
         {
            if (LOG.isLoggable(Level.SEVERE))
            {
               LOG.log(Level.SEVERE, "Error updating " + dbName + "._Trans meta table.", exc);
            }
         }
      }
   }
   
   /**
    * Invocation handler which delegates calls to the {@link MinimalTrans} 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 MinimalTrans} 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.
       * 
       * @throws  Throwable
       *          if the delegated method call throws an exception.
       */
      @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;
      }
   }
   
   /**
    * Interface which defines the minimally required methods of the <code>MetaTrans</code>
    * interface. This interface defines the proxy used to create and manipulate metadata
    * transaction DMO instances. The DMO's methods cannot be invoked directly from code here,
    * since the <code>MetaTrans</code> interface resides in an application-specific package,
    * which is not known at compile time.
    */
   public interface MinimalTrans
   {
      /**
       * Setter: transId
       * 
       * @param   transId
       *          transId
       */
      public void setTransId(NumberType transId);
      
      /**
       * Setter: Trans State
       * 
       * @param   transState
       *          Trans State
       */
      public void setTransState(Text transState);
      
      /**
       * Setter: Trans Flags
       * 
       * @param   transFlags
       *          Trans Flags
       */
      public void setTransFlags(Text transFlags);
      
      /**
       * Setter: Trans Usernum
       * 
       * @param   transUsrnum
       *          Trans Usernum
       */
      public void setTransUsrnum(NumberType transUsrnum);
      
      /**
       * Setter: Trans Num
       * 
       * @param   transNum
       *          Trans Num
       */
      public void setTransNum(NumberType transNum);
      
      /**
       * Setter: Trans rl counter
       * 
       * @param   transCounter
       *          Trans rl counter
       */
      public void setTransCounter(NumberType transCounter);
      
      /**
       * Setter: Trans Start Time
       * 
       * @param   transTxtime
       *          Trans Start Time
       */
      public void setTransTxtime(Text transTxtime);
      
      /**
       * Setter: Coord Name
       * 
       * @param   transCoord
       *          Coord Name
       */
      public void setTransCoord(Text transCoord);
      
      /**
       * Setter: Coordinator Transaction
       * 
       * @param   transCoordTx
       *          Coordinator Transaction
       */
      public void setTransCoordTx(NumberType transCoordTx);
      
      /**
       * Setter: Tx Duration (in secs)
       * 
       * @param   transDuration
       *          Tx Duration (in secs)
       */
      public void setTransDuration(NumberType transDuration);
      
      /**
       * Setter: Trans Misc
       * 
       * @param   transMisc
       *          Trans Misc
       */
      public void setTransMisc(NumberType transMisc);
      
      /**
       * Setter: Tenant Id
       * 
       * @param   transTenantId
       *          Tenant Id
       */
      public void setTransTenantId(NumberType transTenantId);
   }
}