package pt.ist.fenixframework.pstm; import java.sql.SQLException; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.Lock; import jvstm.ActiveTransactionsRecord; import jvstm.CommitException; import jvstm.ResumeException; import jvstm.VBoxBody; import jvstm.cps.ConsistencyCheckTransaction; import jvstm.cps.ConsistentTopLevelTransaction; import jvstm.cps.DependenceRecord; import jvstm.util.Cons; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.ojb.broker.PersistenceBroker; import org.apache.ojb.broker.PersistenceBrokerFactory; import org.apache.ojb.broker.accesslayer.LookupException; import pt.ist.fenixframework.DomainObject; import pt.ist.fenixframework.TxIntrospector; import pt.ist.fenixframework.pstm.DBChanges.AttrChangeLog; public class TopLevelTransaction extends ConsistentTopLevelTransaction implements FenixTransaction, TxIntrospector { private static final Logger logger = LoggerFactory.getLogger(TopLevelTransaction.class); private static int NUM_READS_THRESHOLD = 10000000; private static int NUM_WRITES_THRESHOLD = 100000; private static final Object COMMIT_LISTENERS_LOCK = new Object(); private static volatile Cons<CommitListener> COMMIT_LISTENERS = Cons.empty(); public static void addCommitListener(CommitListener listener) { synchronized (COMMIT_LISTENERS_LOCK) { COMMIT_LISTENERS = COMMIT_LISTENERS.cons(listener); } } public static void removeCommitListener(CommitListener listener) { synchronized (COMMIT_LISTENERS_LOCK) { COMMIT_LISTENERS = COMMIT_LISTENERS.removeFirst(listener); } } private static void notifyBeforeCommit(TopLevelTransaction tx) { for (CommitListener cl : COMMIT_LISTENERS) { cl.beforeCommit(tx); } } private static void notifyAfterCommit(TopLevelTransaction tx) { for (CommitListener cl : COMMIT_LISTENERS) { cl.afterCommit(tx); } } public static Lock getCommitlock() { return COMMIT_LOCK; } // The following variable is not updated atomically (with a CAS, // or something similar), because the accuracy of its value is not // critical. It may happen that a starting transaction puts a // lower value into this variable, when racing to update it, but // that means that a new transaction may open a new dbConnection // when it was not needed, but that is OK. protected volatile static long lastDbConnectionTimestamp = 0; static boolean lastDbConnectionWithin(long millis) { return (System.currentTimeMillis() - lastDbConnectionTimestamp) < millis; } // Each TopLevelTx has its DBChanges // If this slot is changed to null, it is an indication that the // transaction does not allow more changes private DBChanges dbChanges = null; private PersistenceBroker broker; // for statistics protected int numBoxReads = 0; protected int numBoxWrites = 0; // used by the DataAccessPatterns module private String contextURI = ""; TopLevelTransaction(ActiveTransactionsRecord record) { super(record); initDbConnection(false); initDbChanges(); initContext(); } // initialize the information necessary for the identification of // the surrounding context for the acquisition of statistical data protected void initContext() { String uri = RequestInfo.getRequestURI(); if (uri != null) { this.contextURI = uri; } } protected String getContext() { return contextURI; } protected void initDbConnection(boolean resuming) { // first, get a new broker that will give access to the DB connection this.broker = PersistenceBrokerFactory.defaultPersistenceBroker(); // update the lastDbConnectionTimestamp with the current time long now = System.currentTimeMillis(); if (now > lastDbConnectionTimestamp) { lastDbConnectionTimestamp = now; } // open a connection to the database and set this tx number to the // number that // corresponds to that connection number. The connection number should // always be // greater than the current number, because the current number is // obtained from // Transaction.getCommitted, which is set only after the commit to the // database ActiveTransactionsRecord newRecord = updateFromTxLogsOnDatabase(this.activeTxRecord); if (newRecord != this.activeTxRecord) { // if a new record is returned, that means that this transaction // will belong // to that new record, so we must take it off from its current // record and set // it properly // but, if we are resuming, we must ensure first that the // transaction is still valid for the new transaction // record if (resuming) { checkValidity(newRecord); } newRecord.incrementRunning(); this.activeTxRecord.decrementRunning(); this.activeTxRecord = newRecord; setNumber(newRecord.transactionNumber); } } protected void checkValidity(ActiveTransactionsRecord record) { // we must see whether any of the boxes read by this // transaction was changed by some transaction upto the one // corresponding to the new record (newer ones don't matter) int newTxNumber = record.transactionNumber; for (Map.Entry<jvstm.VBox, VBoxBody> entry : this.bodiesRead.entrySet()) { if (entry.getKey().body.getBody(newTxNumber) != entry.getValue()) { throw new ResumeException("Transaction is no longer valid for resuming"); } } } @Override protected void suspendTx() { // close the broker to release the db connection on suspension if (broker != null) { broker.close(); broker = null; } super.suspendTx(); } @Override protected void resumeTx() { super.resumeTx(); initDbConnection(true); } protected void initDbChanges() { this.dbChanges = new DBChanges(); } @Override public PersistenceBroker getOJBBroker() { return broker; } @Override public DomainObject readDomainObject(String classname, int oid) { return TransactionChangeLogs.readDomainObject(broker, classname, oid); } @Override public void setReadOnly() { // a null dbChanges indicates a read-only tx this.dbChanges = null; } @Override public Transaction makeNestedTransaction(boolean readOnly) { throw new Error("Nested transactions not supported yet..."); } private ActiveTransactionsRecord updateFromTxLogsOnDatabase(ActiveTransactionsRecord record) { try { return TransactionChangeLogs.updateFromTxLogsOnDatabase(getOJBBroker(), record); } catch (Exception sqle) { // sqle.printStackTrace(); throw new Error("Error while updating from FF$TX_CHANGE_LOGS: Cannot proceed: " + sqle.getMessage(), sqle); } } @Override protected void finish() { super.finish(); if (broker != null) { broker.close(); broker = null; } dbChanges = null; } @Override protected void doCommit() { if (isWriteTransaction()) { Transaction.STATISTICS.incWrites(this); } else { Transaction.STATISTICS.incReads(this); } if ((numBoxReads > NUM_READS_THRESHOLD) || (numBoxWrites > NUM_WRITES_THRESHOLD)) { logger.warn(String.format("WARN: Very-large transaction (reads = %d, writes = %d, uri = %s)", numBoxReads, numBoxWrites, RequestInfo.getRequestURI())); } // reset statistics counters numBoxReads = 0; numBoxWrites = 0; notifyBeforeCommit(this); super.doCommit(); notifyAfterCommit(this); } @Override protected boolean validateCommit() { boolean result = super.validateCommit(); if (!result) { Transaction.STATISTICS.incConflicts(); } return result; } public ReadSet getReadSet() { return new ReadSet(bodiesRead); } @Override public <T> void setBoxValue(jvstm.VBox<T> vbox, T value) { if (dbChanges == null) { throw new IllegalWriteException(); } else { numBoxWrites++; super.setBoxValue(vbox, value); } } @Override public <T> void setPerTxValue(jvstm.PerTxBox<T> box, T value) { if (dbChanges == null) { throw new IllegalWriteException(); } else { super.setPerTxValue(box, value); } } @Override public <T> T getBoxValue(VBox<T> vbox, Object obj, String attr) { numBoxReads++; T value = getLocalValue(vbox); if (value == null) { // no local value for the box VBoxBody<T> body = vbox.body.getBody(number); if (body.value == VBox.NOT_LOADED_VALUE) { synchronized (body) { if (body.value == VBox.NOT_LOADED_VALUE) { vbox.reload(obj, attr); // after the reload, the same body should have a new // value // if not, then something gone wrong and its better to // abort if (body.value == VBox.NOT_LOADED_VALUE) { System.out.println("Couldn't load the attribute " + attr + " for class " + obj.getClass()); throw new VersionNotAvailableException(); } } } } if (bodiesRead == EMPTY_MAP) { bodiesRead = new HashMap<jvstm.VBox, VBoxBody>(); } bodiesRead.put(vbox, body); value = body.value; } return (value == NULL_VALUE) ? null : value; } @Override public boolean isBoxValueLoaded(VBox vbox) { Object localValue = getLocalValue(vbox); if (localValue == VBox.NOT_LOADED_VALUE) { return false; } if (localValue != null) { return true; } VBoxBody body = vbox.body.getBody(number); return (body.value != VBox.NOT_LOADED_VALUE); } @Override public DBChanges getDBChanges() { if (dbChanges == null) { // if it is null, it means that the transaction is a read-only // transaction throw new IllegalWriteException(); } else { return dbChanges; } } @Override public boolean isWriteTransaction() { return ((dbChanges != null) && dbChanges.needsWrite()) || super.isWriteTransaction(); } @Override protected Cons<VBoxBody> performValidCommit() { // in memory everything is ok, but we need to check against the db PersistenceBroker pb = getOJBBroker(); int currentPriority = Thread.currentThread().getPriority(); try { Thread.currentThread().setPriority(Thread.MAX_PRIORITY); try { if (!pb.isInTransaction()) { pb.beginTransaction(); } try { // the updateFromTxLogs is made with the txNumber minus 1 to // ensure that the select // for update will return at least a record, and, therefore, // lock the record // otherwise, the mysql server may allow the select for // update to continue // concurrently with other executing commits in other // servers ActiveTransactionsRecord myRecord = this.activeTxRecord; if (TransactionChangeLogs.updateFromTxLogsOnDatabase(pb, myRecord, true) != myRecord) { // the cache may have been updated, so perform the // tx-validation again if (!validateCommit()) { System.out.println("Invalid commit. Restarting."); throw new jvstm.CommitException(); } } } catch (SQLException sqlex) { System.out.println("SqlException: " + sqlex.getMessage()); throw new CommitException(); } catch (LookupException le) { throw new Error("Error while obtaining database connection", le); } Cons<VBoxBody> newBodies = super.performValidCommit(); // ensure that changes are visible to other TXs before releasing // lock try { pb.commitTransaction(); } catch (Throwable t) { t.printStackTrace(); System.out.println("Error while commiting exception. Terminating server."); System.err.flush(); System.out.flush(); System.exit(-1); } pb = null; return newBodies; } finally { if (pb != null) { pb.abortTransaction(); } } } finally { Thread.currentThread().setPriority(currentPriority); } } @Override protected Cons<VBoxBody> doCommit(int newTxNumber) { persistTransaction(newTxNumber); TransactionCommitRecords.addCommitRecord(newTxNumber, dbChanges.getModifiedObjects()); return super.doCommit(newTxNumber); } protected void persistTransaction(int newTxNumber) { try { dbChanges.makePersistent(getOJBBroker(), newTxNumber); } catch (SQLException sqle) { throw new Error("Error while accessing database", sqle); } catch (LookupException le) { throw new Error("Error while obtaining database connection", le); } // calling the dbChanges.cache() method is no longer needed, // given that we are caching objects as soon as they are // instantiated // dbChanges.cache(); } // consistency-predicates-system methods @Override protected void checkConsistencyPredicates() { // check all the consistency predicates for the objects modified in this // transaction for (Object obj : getDBChanges().getModifiedObjects()) { checkConsistencyPredicates(obj); } super.checkConsistencyPredicates(); } @Override protected void checkConsistencyPredicates(Object obj) { if (getDBChanges().isDeleted(obj)) { // don't check deleted objects return; } else { super.checkConsistencyPredicates(obj); } } @Override protected ConsistencyCheckTransaction makeConsistencyCheckTransaction(Object obj) { return new FenixConsistencyCheckTransaction(this, obj); } @Override protected Iterator<DependenceRecord> getDependenceRecordsToRecheck() { // for now, just return an empty iterator return Util.emptyIterator(); } // implement the TxIntrospector interface @Override public Set<DomainObject> getNewObjects() { Set<DomainObject> emptySet = Collections.emptySet(); return isWriteTransaction() ? getDBChanges().getNewObjects() : emptySet; } @Override public Set<DomainObject> getModifiedObjects() { Set<DomainObject> emptySet = Collections.emptySet(); return isWriteTransaction() ? getDBChanges().getModifiedObjects() : emptySet; } public boolean isDeleted(Object obj) { return isWriteTransaction() ? getDBChanges().isDeleted(obj) : false; } @Override public Set<Entry> getReadSetLog() { throw new Error("getReadSetLog not implemented yet"); // Set<Entry> entries = new HashSet<Entry>(bodiesRead.size()); // for (Map.Entry<VBox,VBoxBody> entry : bodiesRead.entrySet()) { // entries.add(new Entry()); // } // return entries; } @Override public Set<Entry> getWriteSetLog() { Set<AttrChangeLog> attrChangeLogs = getDBChanges().getAttrChangeLogs(); Set<Entry> entries = new HashSet<Entry>(attrChangeLogs.size()); for (AttrChangeLog log : attrChangeLogs) { AbstractDomainObject obj = (AbstractDomainObject) log.obj; entries.add(new Entry(obj, log.attr, obj.getCurrentValueFor(log.attr))); } return entries; } // --------------------------------------------------------------- // keep a log of all relation changes, which are more // coarse-grained and semantically meaningfull than looking only // at changes made to the objects that implement the relations private HashMap<RelationTupleChange, RelationTupleChange> relationTupleChanges = null; @Override public void logRelationAdd(String relationName, DomainObject o1, DomainObject o2) { logRelationTuple(relationName, o1, o2, false); } @Override public void logRelationRemove(String relationName, DomainObject o1, DomainObject o2) { logRelationTuple(relationName, o1, o2, true); } private void logRelationTuple(String relationName, DomainObject o1, DomainObject o2, boolean remove) { if (relationTupleChanges == null) { relationTupleChanges = new HashMap<RelationTupleChange, RelationTupleChange>(); } RelationTupleChange log = new RelationTupleChange(relationName, o1, o2, remove); relationTupleChanges.put(log, log); } @Override public Set<RelationChangelog> getRelationsChangelog() { Set<RelationChangelog> entries = new HashSet<RelationChangelog>(); if (relationTupleChanges != null) { for (RelationTupleChange log : relationTupleChanges.values()) { entries.add(new RelationChangelog(log.relationName, log.obj1, log.obj2, log.remove)); } } return entries; } private static class RelationTupleChange { final String relationName; final DomainObject obj1; final DomainObject obj2; final boolean remove; RelationTupleChange(String relationName, DomainObject obj1, DomainObject obj2, boolean remove) { this.relationName = relationName; this.obj1 = obj1; this.obj2 = obj2; this.remove = remove; } @Override public int hashCode() { return relationName.hashCode() + obj1.hashCode() + obj2.hashCode(); } @Override public boolean equals(Object obj) { if ((obj != null) && (obj.getClass() == this.getClass())) { RelationTupleChange other = (RelationTupleChange) obj; return this.relationName.equals(other.relationName) && this.obj1.equals(other.obj1) && this.obj2.equals(other.obj2); } else { return false; } } } }