package pt.ist.fenixframework.pstm;
import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import jvstm.ActiveTransactionsRecord;
import jvstm.VBoxBody;
import jvstm.util.Cons;
import org.apache.ojb.broker.Identity;
import org.apache.ojb.broker.PersistenceBroker;
import org.apache.ojb.broker.PersistenceBrokerFactory;
import org.apache.ojb.broker.accesslayer.LookupException;
import org.apache.ojb.broker.metadata.ClassDescriptor;
import org.apache.ojb.broker.metadata.DescriptorRepository;
import pt.ist.fenixframework.DomainObject;
public class TransactionChangeLogs {
private static class ClassInfo {
final ClassDescriptor classDescriptor;
final Class topLevelClass;
ClassInfo(ClassDescriptor classDescriptor, Class topLevelClass) {
this.classDescriptor = classDescriptor;
this.topLevelClass = topLevelClass;
}
}
private static final DescriptorRepository OJB_REPOSITORY = MetadataManager.getOjbMetadataManager().getRepository();
private static final Map<String,ClassInfo> CLASS_INFOS = new ConcurrentHashMap<String,ClassInfo>();
private static ClassInfo getClassInfo(String className) {
ClassInfo info = CLASS_INFOS.get(className);
if (info == null) {
try {
Class realClass = Class.forName(className);
ClassDescriptor cld = OJB_REPOSITORY.getDescriptorFor(realClass);
Class topLevelClass = OJB_REPOSITORY.getTopLevelClass(realClass);
info = new ClassInfo(cld, topLevelClass);
CLASS_INFOS.put(className, info);
} catch (ClassNotFoundException cnfe) {
throw new Error("Couldn't find class " + className + ": " + cnfe);
}
}
return info;
}
static DomainObject readDomainObject(PersistenceBroker pb, String className, int idInternal) {
ClassInfo info = getClassInfo(className);
//DomainObject obj = (DomainObject)Transaction.getCache().lookup(info.topLevelClass, idInternal);
// As the cache now only maps OIDs to objects, the previous
// method is no longer easy to implement. So, don't go to the
// cache first and always go to the database. This may be a
// performance problem if the readDomainObject is called many
// times, but this method should disappear, either way.
DomainObject obj = null;
if (obj == null) {
Identity oid = new Identity(null, info.topLevelClass, new Object[] { idInternal });
obj = (DomainObject) pb.getObjectByIdentity(oid);
}
if ((obj != null) && obj.isDeleted()) {
// if the object is deleted, then return null
obj = null;
}
return obj;
}
// ------------------------------------------------------------
private static class AlienTransaction {
final int txNumber;
// the set of objects is kept so that a strong reference exists
// for each of the objects modified by another server until no running
// transaction in the current VM may need to access it
private Map<AbstractDomainObject,List<String>> objectAttrChanges = new HashMap<AbstractDomainObject,List<String>>();
AlienTransaction(int txNumber) {
this.txNumber = txNumber;
}
void register(AbstractDomainObject obj, String attrName) {
List<String> allAttrs = objectAttrChanges.get(obj);
if (allAttrs == null) {
allAttrs = new LinkedList<String>();
objectAttrChanges.put(obj, allAttrs);
}
allAttrs.add(attrName);
}
Cons<VBoxBody> commit() {
Cons<VBoxBody> newBodies = Cons.empty();
for (Map.Entry<AbstractDomainObject,List<String>> entry : objectAttrChanges.entrySet()) {
AbstractDomainObject obj = entry.getKey();
List<String> allAttrs = entry.getValue();
for (String attr : allAttrs) {
VBoxBody newBody = obj.addNewVersion(attr, txNumber);
// the body may be null in some cases: see the
// comment on the VBox.addNewVersion method
if (newBody != null) {
newBodies = newBodies.cons(newBody);
}
}
}
return newBodies;
}
}
public static ActiveTransactionsRecord updateFromTxLogsOnDatabase(PersistenceBroker pb,
ActiveTransactionsRecord record)
throws SQLException,LookupException {
return updateFromTxLogsOnDatabase(pb, record, false);
}
public static ActiveTransactionsRecord updateFromTxLogsOnDatabase(PersistenceBroker pb,
ActiveTransactionsRecord record,
boolean forUpdate)
throws SQLException,LookupException {
Connection conn = pb.serviceConnectionManager().getConnection();
// ensure that the connection is up-to-date
conn.commit();
Statement stmt = null;
ResultSet rs = null;
try {
stmt = conn.createStatement();
// read tx logs
int maxTxNumber = record.transactionNumber;
rs = stmt.executeQuery("SELECT OBJ_OID,OBJ_ATTR,TX_NUMBER FROM FF$TX_CHANGE_LOGS WHERE TX_NUMBER > "
+ (forUpdate ? (maxTxNumber - 1) : maxTxNumber)
+ " ORDER BY TX_NUMBER"
+ (forUpdate ? " FOR UPDATE" : ""));
// if there are any results to be processed, process them
if (rs.next()) {
return processAlienTransaction(pb, rs, record);
} else {
return record;
}
} finally {
if (rs != null) {
rs.close();
}
if (stmt != null) {
stmt.close();
}
}
}
private static ActiveTransactionsRecord processAlienTransaction(PersistenceBroker pb, ResultSet rs, ActiveTransactionsRecord record)
throws SQLException {
// Acquire the JVSTM commit lock to process the result set, as
// doing so is semantically similar to commiting transactions.
// During the processing of an alien transaction, we may write
// back to boxes, as well as update the most recent committed
// record, so, as those things are supposed to be done one at
// a time, during the commit, we must ensure that we acquire
// the commit lock before proceeding.
// This may be changed in the future, when the new version of
// the JVSTM with a parallel commit becomes used in the
// fenix-framework.
Lock commitLock = TopLevelTransaction.getCommitlock();
commitLock.lock();
try {
// Here, after acquiring the lock, we know that no new transactions can start, because
// all transactions must call the updateFromTxLogsOnDatabase method with a number which
// is necessarily less than the number we are processing, and, therefore, will have to
// come into this method, blocking in the lock.
// Likewise for a commit of a write transaction.
int currentCommittedNumber = Transaction.getMostRecentCommitedNumber();
int txNum = rs.getInt(3);
// skip all the records already processed
while ((txNum <= currentCommittedNumber) && rs.next()) {
txNum = rs.getInt(3);
}
if (txNum <= currentCommittedNumber) {
// the records ended, so simply get out of here, with
// the record corresponding to the higher number that
// we got
return findActiveRecordForNumber(record, txNum);
}
// now, it's time to process the new changeLog records
AlienTransaction alienTx = new AlienTransaction(txNum);
while (alienTx != null) {
long oid = rs.getLong(1);
String attr = rs.getString(2);
if (oid != 0) {
// if the oid is 0, then this line
// doesn't represent a real change (see the
// comment on the DbChanges.writeAttrChangeLogs
// method)
AbstractDomainObject obj = AbstractDomainObject.fromOID(oid);
alienTx.register(obj, attr);
}
int nextTxNum = -1;
if (rs.next()) {
nextTxNum = rs.getInt(3);
}
if (nextTxNum != txNum) {
// finished the records for an alien transaction, so "commit" it
Cons<VBoxBody> newBodies = alienTx.commit();
// add it to the queue of CommitRecords to be GCed later
TransactionCommitRecords.addCommitRecord(alienTx.txNumber, alienTx);
ActiveTransactionsRecord newRecord = new ActiveTransactionsRecord(txNum, newBodies);
Transaction.setMostRecentActiveRecord(newRecord);
if (nextTxNum != -1) {
// there are more to process, create a new alien transaction
txNum = nextTxNum;
alienTx = new AlienTransaction(txNum);
} else {
// finish the loop
alienTx = null;
}
}
}
return findActiveRecordForNumber(record, txNum);
} finally {
commitLock.unlock();
}
}
private static ActiveTransactionsRecord findActiveRecordForNumber(ActiveTransactionsRecord rec, int number) {
while (rec.transactionNumber < number) {
rec = rec.getNext();
}
return rec;
}
public static int initializeTransactionSystem() {
// find the last committed transaction
PersistenceBroker broker = null;
try {
broker = PersistenceBrokerFactory.defaultPersistenceBroker();
broker.beginTransaction();
Connection conn = broker.serviceConnectionManager().getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT MAX(TX_NUMBER) FROM FF$TX_CHANGE_LOGS");
int maxTx = (rs.next() ? rs.getInt(1) : -1);
broker.commitTransaction();
broker.close();
broker = null;
stmt.close();
rs.close();
new CleanThread(maxTx).start();
new StatisticsThread().start();
return maxTx;
} catch (Exception e) {
throw new Error("Couldn't initialize the transaction system");
} finally {
if (broker != null) {
broker.close();
}
}
}
private static class CleanThread extends Thread {
private static final long SECONDS_BETWEEN_UPDATES = 120;
private String server;
private int lastTxNumber = -1;
CleanThread(int lastTxNumber) {
this.server = Util.getServerName();
this.lastTxNumber = lastTxNumber;
setDaemon(true);
}
public void run() {
try {
while (! initializeServerRecord()) {
// intentionally empty
}
while (true) {
try {
sleep(SECONDS_BETWEEN_UPDATES * 1000);
} catch (InterruptedException ie) {
// ignore it
}
updateServerRecord();
}
} finally {
System.out.println("Exiting CleanThread!");
System.err.flush();
System.out.flush();
}
}
private boolean initializeServerRecord() {
PersistenceBroker broker = null;
try {
broker = PersistenceBrokerFactory.defaultPersistenceBroker();
broker.beginTransaction();
Connection conn = broker.serviceConnectionManager().getConnection();
Statement stmt = conn.createStatement();
// delete previous record for this server and insert a new one
stmt.executeUpdate("DELETE FROM FF$LAST_TX_PROCESSED WHERE SERVER = '" + server + "' or LAST_UPDATE < (NOW() - 3600)");
stmt.executeUpdate("INSERT INTO FF$LAST_TX_PROCESSED VALUES ('" + server + "'," + lastTxNumber + ",null)");
broker.commitTransaction();
return true;
} catch (Exception e) {
e.printStackTrace();
System.out.println("Couldn't initialize the clean thread");
//throw new Error("Couldn't initialize the clean thread");
} finally {
if (broker != null) {
broker.close();
}
}
return false;
}
private void updateServerRecord() {
int currentTxNumber = Transaction.getMostRecentCommitedNumber();
PersistenceBroker broker = null;
try {
broker = PersistenceBrokerFactory.defaultPersistenceBroker();
broker.beginTransaction();
Connection conn = broker.serviceConnectionManager().getConnection();
Statement stmt = conn.createStatement();
// update record for this server
stmt.executeUpdate("UPDATE FF$LAST_TX_PROCESSED SET LAST_TX="
+ currentTxNumber
+ ",LAST_UPDATE=NULL WHERE SERVER = '"
+ server + "'");
// delete obsolete values
ResultSet rs = stmt.executeQuery("SELECT MIN(LAST_TX) FROM FF$LAST_TX_PROCESSED WHERE LAST_UPDATE > NOW() - "
+ (2 * SECONDS_BETWEEN_UPDATES));
int min = (rs.next() ? rs.getInt(1) : 0);
if (min > 0) {
stmt.executeUpdate("DELETE FROM FF$TX_CHANGE_LOGS WHERE TX_NUMBER < " + min);
}
broker.commitTransaction();
this.lastTxNumber = currentTxNumber;
} catch (Exception e) {
e.printStackTrace();
System.out.println("Couldn't update database in the clean thread");
//throw new Error("Couldn't update database in the clean thread");
} catch (Throwable t) {
t.printStackTrace();
System.out.println("Couldn't update database in the clean thread because of a Throwable.");
} finally {
if (broker != null) {
broker.close();
}
}
}
}
}