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);
}
}