package pt.ist.fenixframework.backend.jvstm.pstm; import static jvstm.UtilUnsafe.UNSAFE; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import jvstm.ActiveTransactionsRecord; import jvstm.TopLevelTransaction; import jvstm.Transaction; import jvstm.TransactionSignaller; import jvstm.UtilUnsafe; import jvstm.WriteSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import pt.ist.fenixframework.backend.jvstm.lf.CommitRequest; import pt.ist.fenixframework.backend.jvstm.lf.CommitRequest.ValidationStatus; import pt.ist.fenixframework.backend.jvstm.lf.JvstmLockFreeBackEnd; import pt.ist.fenixframework.backend.jvstm.lf.SimpleReadSet; //import jvstm.VBox; public abstract class CommitOnlyTransaction extends TopLevelTransaction { private static final Logger logger = LoggerFactory.getLogger(CommitOnlyTransaction.class); private static final long commitTxRecordOffset = UtilUnsafe.objectFieldOffset(TopLevelTransaction.class, "commitTxRecord"); /** * Maps all node local {@link LockFreeTransaction}s using their id as key. Whoever completes the processing is * required to remove the entry from this map, lest it grow indefinitely with the number of transactions. * */ public final static ConcurrentHashMap<UUID, LockFreeTransaction> commitsMap = new ConcurrentHashMap<UUID, LockFreeTransaction>(); protected final CommitRequest commitRequest; // private final WriteSet writeSet = STUB_WRITE_SET; public static final ConcurrentHashMap<Integer, UUID> txVersionToCommitIdMap = new ConcurrentHashMap<Integer, UUID>(); // private boolean readOnly = false; // // for statistics // protected int numBoxReads = 0; // protected int numBoxWrites = 0; public CommitOnlyTransaction(ActiveTransactionsRecord record, CommitRequest commitRequest) { super(record); this.commitRequest = commitRequest; } @Override public boolean isWriteTransaction() { return true; } // @Override // public void setReadOnly() { // this.readOnly = true; // } // // @Override // public boolean txAllowsWrite() { // return !this.readOnly; // } // // @Override // public <T> T getBoxValue(VBox<T> vbox) { // return super.getBoxValue(vbox); // } // // @Override // public boolean isBoxValueLoaded(VBox vbox) { // // TODO Auto-generated method stub // throw new UnsupportedOperationException("not yet implemented"); // } // // @Override // public int getNumBoxReads() { // return numBoxReads; // } // // @Override // public int getNumBoxWrites() { // return numBoxWrites; // } // @Override /** * This is the commit algorithm that each CommitOnlyTransaction performs on each node, regardless of whether it is a * {@link LocalCommitOnlyTransaction} or a {@link RemoteCommitOnlyTransaction}. Note that {@link LockFreeTransaction}s * are decorated by {@link CommitOnlyTransaction}s. */ public void localCommit() { // save current Transaction savedTx = Transaction.current(); // set current Transaction.current.set(this); try { // enact the commit this.commitTx(false); // smf TODO: double-check whether we want to mess with ActiveTxRecords, thread-locals, etc. I guess 'false' is the way to go... } finally { // restore current Transaction.current.set(savedTx); } } // @Override // public int getNumber() { // return this.getUnderlyingTransaction().getNumber(); // } /** * Get the concrete transaction that will be committed. For local commits this should be the local tx instance, help by the * LocalCommitOnlyTransaction. For remote commits this should be the RemoteCommitOnlyTransaction instance itself. */ public abstract TopLevelTransaction getUnderlyingTransaction(); @Override protected void assignCommitRecord(int txNumber, WriteSet writeSet) { // Must set the correct commit number **BEFORE** setting the valid status super.assignCommitRecord(txNumber, writeSet); this.commitRequest.setValid(); } /* The validation for commit-only transactions follows the usual protocol: (1) helpCommitAll, (2) snapshotValidation, and (3) validateAndEnqueue. The difference is that now others may also be helping with the validation. So: - initially, check if validation already completed. This is also useful to avoid starting the validation when there is only one (already validated) entry in the commit requests queue, and we're just waiting until more requests arrive. Actually, I think that doing this would skip enqueuing if needed, so we should be doing the normal validation and just skipping when we really see them done. In short, if already valid just move along to enqueueing. - snapshot validation is a helped phase (split into buckets). if it fails the request is marked as failed. however, care must be taken to check that if a read vbox already contains a more recent entry, such entry may belong to this transaction already! (someone else helped quite a lot!). In that case, the whole commit for this transaction is done! :-) As usual, a more recent write by another to a VBox that was read, will cause validation to fail. - validateCommitAndEnqueue only needs to be attempted once. Given that snapshotValidation passed, then if enqueue fails it's because someone else did it already. Moving along... */ @Override protected void validate() { logger.debug("Validating commit request: {}", this.commitRequest.getId()); // // some other helper may have already done the work for us // ValidationStatus validationStatus = this.commitRequest.getValidationStatus(); // boolean alreadyValidated = validationStatus != ValidationStatus.UNSET; // // if (alreadyValidated) { // logger.debug("Commit request {} is already {}", this.commitRequest.getId(), // (validationStatus == ValidationStatus.VALID ? "VALID" : "INVALID")); // return; // } super.validate(); } @Override protected void snapshotValidation(int lastSeenCommittedTxNumber) { /* Notice that someone may have already enqueued this tx, in which case I may happen to have seen it already committed! But in that case the validation status must have been set already! So, the following test is a double take: it is necessary for correctness (to avoid enqueueing the same record twice and to throw an exception if already invalid), but it may also bring in the lucky benefit of the validation status for this tx already being set (either valid or invalid). */ ValidationStatus validationStatus = this.commitRequest.getValidationStatus(); boolean alreadyValidated = validationStatus != ValidationStatus.UNSET; if (alreadyValidated) { if (validationStatus == ValidationStatus.VALID) { logger.debug("Commit request {} was found already VALID", this.commitRequest.getId()); return; } else { logger.debug("Commit request {} was found already INVALID", this.commitRequest.getId()); /* Still, we throw exception to ensure that our own flow does not proceed to enqueueing */ TransactionSignaller.SIGNALLER.signalCommitFail(); throw new AssertionError("Impossible condition - Commit fail signalled!"); } } int myReadVersion = getNumber(); if (lastSeenCommittedTxNumber == myReadVersion) { logger.debug("Commit request {} is immediately VALID", this.commitRequest.getId()); assignCommitRecord(lastSeenCommittedTxNumber + 1, getWriteSet()); return; } SimpleReadSet readSet = this.commitRequest.getReadSet(); // smf: TODO implement the helping mechanism here. For now, just iterate all. JvstmLockFreeBackEnd backend = JvstmLockFreeBackEnd.getInstance(); for (String vboxId : readSet.getVBoxIds()) { VBox vbox = backend.vboxFromId(vboxId); // if (vbox == null) { // // smf: TODO this vbox is not cached locally. deal with this later // logger.error("not implemented yet. must deal with uncached vboxes in this node. cannot continue to commit deterministically. exiting"); // System.exit(-1); // /* We know that if this tx is valid then it will commit with // commitNumber=lastSeenCommittedTxNumber+1. So we need to check // if there is some version committed greater than this.getNumber() // and less than commitNumber. If it exists, this transaction is // invalid. If it doesn't exists, we may continue validation. // However, we can speed up the conclusion bu also checking whether // there is a commit for commitNumber. If so, then this transaction // is immediately valid or invalid depending on whether such // commitNumber's commitId is ours. Question: do we need to reload // everything from the most recent down to the lastSeenCommittedTxNumber? // Probably yes to ensure the invariant of the reload. Then we only // need to check the most recent loaded version :-). // // Another note: a simple reload here is not enough, because reloads // only load up to the mostRecentCommit seen by this node. We need // to know whether THIS request may have been already committed by // another node! // // NOTE: We never enter this branch of the if in a // LocalCommitOnlyTransaction, but it doesn't hurt to have this // code in the common CommitOnlyTransaction. // // In summary: If I'm not mistaken, after asking for a reload of // version mostRecentCommitted (whatever that may be), I'll be able // to run (exactly?) the same code as the one that runs when the // box is loaded... */ // // it is enough to reload the most recent version in order to decide about validation // vbox.reload(lastSeenCommittedTxNumber); // } /*else {*/ if (vbox.body.version == 0) { vbox.reload(lastSeenCommittedTxNumber); } // check whether the read was valid if (vbox.body.version > myReadVersion) { /* caution: it could be our own commit that we're seeing! But, in that case, validation must have finished! If validation hasn't finished, this implies that no more recent version could have been loaded and the version we're seeing is > myReadVersion but lower than my prospective commit version, which means I'm invalid. (true for local tx only?!) Can validation NOT have finished in this node, and what we see written is a box that another node already wrote. No, because our reload only loads versions that may be necessary in this node. By 'be necessary' I mean reload loads at highest, from the mostRecentlyCommitted version. So, for a box version to be higher than my read version, either a local commit or a remote commit higher than my read version (but lower than mostRecentCommitted) must have already been processed in this node, which ultimately implies that THIS commit request needs to have its validation status already set (after all, it was ahead in the queue!) So, if this commit request's validation state is unset after we already have seen a body with a version > myReadVersion, this commit request must be invalid. */ boolean validationFinished = this.commitRequest.getValidationStatus() != ValidationStatus.UNSET; if (!validationFinished) { /* this validation did not finish yet (thus neither did the write back) *AND* there is already a newer version written to the vbox. Thus, this transaction is invalid to commit. */ logger.debug("Commit request {} is INVALID", this.commitRequest.getId()); this.commitRequest.setInvalid(); TransactionSignaller.SIGNALLER.signalCommitFail(); throw new AssertionError("Impossible condition - Commit fail signalled!"); } else { // whatever the result, validation has finished already if (this.commitRequest.getValidationStatus() == ValidationStatus.VALID) { logger.debug("Some helper already found commit request {} to be VALID", this.commitRequest.getId()); return; } else { logger.debug("Some helper already found commit request {} to be INVALID", this.commitRequest.getId()); TransactionSignaller.SIGNALLER.signalCommitFail(); throw new AssertionError("Impossible condition - Commit fail signalled!"); } } } // } } logger.debug("Commit request {} is VALID", this.commitRequest.getId()); assignCommitRecord(lastSeenCommittedTxNumber + 1, getWriteSet()); } /** * Get the {@link WriteSet} for this transaction. * */ protected abstract WriteSet getWriteSet(); @Override public abstract WriteSet makeWriteSet(); @Override protected void validateCommitAndEnqueue(ActiveTransactionsRecord lastCheck) { enqueueValidCommit(lastCheck, this.getCommitTxRecord().getWriteSet()); updateOrecVersion(); } // @Override // public void updateOrecVersion() { // this.getUnderlyingTransaction().updateOrecVersion(); // } @Override protected void enqueueValidCommit(ActiveTransactionsRecord lastCheck, WriteSet writeSet) { ActiveTransactionsRecord commitRecord = this.getCommitTxRecord(); /* Here we know that our commit is valid. However, we may have concluded such result via some helper AND even have seen already our record enqueued and committed. So we need to check for that to skip enqueuing. */ if (lastCheck.transactionNumber >= commitRecord.transactionNumber) { logger.debug("Transaction {} for commit request {} was already enqueued AND even committed by another helper.", commitRecord.transactionNumber, this.commitRequest.getId()); } else { if (lastCheck.trySetNext(commitRecord)) { logger.debug("Enqueued record for valid transaction {} of commit request {}", commitRecord.transactionNumber, this.commitRequest.getId()); } else { logger.debug("Transaction {} of commit request {} was already enqueued by another helper.", commitRecord.transactionNumber, this.commitRequest.getId()); } } // EVERYONE MUST TRY THIS, to ensure visibility when looking it up ahead. txVersionToCommitIdMap.putIfAbsent(commitRecord.transactionNumber, this.commitRequest.getId()); } /* The commitTxRecord can only be set once */ @Override public void setCommitTxRecord(ActiveTransactionsRecord record) { if (UNSAFE.compareAndSwapObject(this.getUnderlyingTransaction(), this.commitTxRecordOffset, null, record)) { logger.debug("set commitTxRecord with version {}", record.transactionNumber); } else { logger.debug("commitTxRecord was already set with version {}", this.getCommitTxRecord().transactionNumber); } } // @Override // public ActiveTransactionsRecord getCommitTxRecord() { // return this.getUnderlyingTransaction().getCommitTxRecord(); // } @Override protected void helpCommit(ActiveTransactionsRecord recordToCommit) { if (!recordToCommit.isCommitted()) { logger.debug("Helping to commit version {}", recordToCommit.transactionNumber); int txVersion = recordToCommit.transactionNumber; UUID commitId = CommitOnlyTransaction.txVersionToCommitIdMap.get(txVersion); if (commitId != null) { // may be null if it was already persisted JvstmLockFreeBackEnd.getInstance().getRepository().mapTxVersionToCommitId(txVersion, commitId); CommitOnlyTransaction.txVersionToCommitIdMap.remove(txVersion); } super.helpCommit(recordToCommit); } else { logger.debug("Version {} was already fully committed", recordToCommit.transactionNumber); } } @Override protected void upgradeTx(ActiveTransactionsRecord newRecord) { // no op. /* This is not a required step in this type of transaction. The corresponding LockFreeTransaction will do this on its own node, after all the helping is done. */ } }