/* * INESC-ID, Instituto de Engenharia de Sistemas e Computadores Investigação e Desevolvimento em Lisboa * Copyright 2013 INESC-ID and/or its affiliates and other * contributors as indicated by the @author tags. All rights reserved. * See the copyright.txt in the distribution for a full listing of * individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 3.0 of * the License, or (at your option) any later version. * * This software 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.infinispan.transaction.gmu.manager; import org.infinispan.commands.tx.GMUCommitCommand; import org.infinispan.container.versioning.gmu.GMUVersion; import org.infinispan.transaction.xa.CacheTransaction; import org.infinispan.transaction.xa.GlobalTransaction; import org.infinispan.util.logging.Log; import org.infinispan.util.logging.LogFactory; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import static org.infinispan.transaction.gmu.GMUHelper.toGMUVersion; /** * // TODO: Document this * * @author Pedro Ruivo * @since 5.2 */ public class SortedTransactionQueue { private static final Log log = LogFactory.getLog(SortedTransactionQueue.class); private final ConcurrentHashMap<GlobalTransaction, Node> concurrentHashMap; private final Node firstEntry; private final Node lastEntry; public SortedTransactionQueue() { this.concurrentHashMap = new ConcurrentHashMap<GlobalTransaction, Node>(); this.firstEntry = new Node() { private Node first; @Override public void commitVersion(GMUVersion commitCommand) {} @Override public GMUVersion getVersion() { throw new IllegalStateException("Cannot return the version from the first node"); } @Override public boolean isReady() { return false; } @Override public boolean isCommitted() { return false; } @Override public GlobalTransaction getGlobalTransaction() { throw new IllegalStateException("Cannot return the global transaction from the first node"); } @Override public Node getPrevious() { throw new IllegalStateException("Cannot return the previous node from the first node"); } @Override public void setPrevious(Node previous) { throw new IllegalStateException("Cannot set the previous node from the first node"); } @Override public Node getNext() { return first; } @Override public void setNext(Node next) { this.first = next; } @Override public int compareTo(Node o) { //the first node is always lower return -1; } @Override public void awaitUntilCommitted(GMUCommitCommand commitCommand) throws InterruptedException {} @Override public CacheTransaction getCacheTransactionForCommit() { throw new IllegalStateException("Cannot return the cache transaction from the first node"); } @Override public void committed() {} @Override public String toString() { return "FIRST_ENTRY"; } }; this.lastEntry = new Node() { private Node last; @Override public void commitVersion(GMUVersion commitCommand) {} @Override public GMUVersion getVersion() { throw new IllegalStateException("Cannot return the version from the last node"); } @Override public boolean isReady() { return false; } @Override public boolean isCommitted() { return false; } @Override public GlobalTransaction getGlobalTransaction() { throw new IllegalStateException("Cannot return the global transaction from the last node"); } @Override public Node getPrevious() { return last; } @Override public void setPrevious(Node previous) { this.last = previous; } @Override public Node getNext() { throw new IllegalStateException("Cannot return the next node from the last node"); } @Override public void setNext(Node next) { throw new IllegalStateException("Cannot set the next node from the last node"); } @Override public int compareTo(Node o) { //the last node is always higher return 1; } @Override public void awaitUntilCommitted(GMUCommitCommand commitCommand) throws InterruptedException {} @Override public CacheTransaction getCacheTransactionForCommit() { throw new IllegalStateException("Cannot return the cache transaction from the last node"); } @Override public void committed() {} @Override public String toString() { return "LAST_ENTRY"; } }; firstEntry.setNext(lastEntry); lastEntry.setPrevious(firstEntry); } public final void prepare(CacheTransaction cacheTransaction) { GlobalTransaction globalTransaction = cacheTransaction.getGlobalTransaction(); if (concurrentHashMap.contains(globalTransaction)) { log.warnf("Duplicated prepare for %s", globalTransaction); } Node entry = new TransactionEntryImpl(cacheTransaction); concurrentHashMap.put(globalTransaction, entry); addNew(entry); } public final void rollback(CacheTransaction cacheTransaction) { remove(concurrentHashMap.remove(cacheTransaction.getGlobalTransaction())); notifyIfNeeded(); } //return true if it is a read-write transaction public final boolean commit(CacheTransaction cacheTransaction, GMUVersion commitVersion) { Node entry = concurrentHashMap.get(cacheTransaction.getGlobalTransaction()); if (entry == null) { if (log.isDebugEnabled()) { log.debugf("Cannot commit transaction %s. Maybe it is a read-only on this node", cacheTransaction.getGlobalTransaction().prettyPrint()); } return false; } update(entry, commitVersion); notifyIfNeeded(); return true; } public final synchronized void populateToCommit(List<TransactionEntry> transactionEntryList) throws InterruptedException { removeCommitted(); while (!firstEntry.getNext().isReady()) { if (log.isTraceEnabled()) { log.tracef("get transactions to commit. First is not ready! %s", firstEntry.getNext()); } wait(); } //if (log.isDebugEnabled()) { // log.debugf("Try to commit transaction. Queue is %s", queueToString()); //} Node firstTransaction = firstEntry.getNext(); Node transactionToCheck = firstTransaction.getNext(); while (transactionToCheck != lastEntry) { boolean isSameVersion = transactionToCheck.compareTo(firstTransaction) == 0; if (!isSameVersion) { //commit until this transaction commitUntil(transactionToCheck, transactionEntryList); return; } else if (!transactionToCheck.isReady()) { if (log.isTraceEnabled()) { log.tracef("Transaction with the same version not ready. %s and %s", firstTransaction, transactionToCheck); } wait(); return; } transactionToCheck = transactionToCheck.getNext(); } //commit until this transaction commitUntil(transactionToCheck, transactionEntryList); } public final TransactionEntry getTransactionEntry(GlobalTransaction globalTransaction) { return concurrentHashMap.get(globalTransaction); } public final synchronized String queueToString() { Node node = firstEntry.getNext(); if (node == lastEntry) { return "[]"; } StringBuilder builder = new StringBuilder("["); builder.append(node); node = node.getNext(); while (node != lastEntry) { builder.append(",").append(node); node.getNext(); } builder.append("]"); return builder.toString(); } private void commitUntil(Node exclusive, List<TransactionEntry> transactionEntryList) { Node transaction = firstEntry.getNext(); while (transaction != exclusive) { transactionEntryList.add(transaction); transaction = transaction.getNext(); } } private void removeCommitted() { Node node = firstEntry.getNext(); while (node != lastEntry) { if (node.isCommitted()) { node = node.getNext(); } else { break; } } Node newFirst = node; node = newFirst.getPrevious(); while (node != firstEntry) { node.getNext().setPrevious(null); node.setNext(null); node = node.getPrevious(); } firstEntry.setNext(newFirst); newFirst.setPrevious(firstEntry); } private synchronized void update(Node entry, GMUVersion commitVersion) { if (log.isTraceEnabled()) { log.tracef("Update %s with %s", entry, commitVersion); } entry.commitVersion(commitVersion); if (entry.compareTo(entry.getNext()) > 0) { Node insertBefore = entry.getNext().getNext(); remove(entry); while (entry.compareTo(insertBefore) > 0) { insertBefore = insertBefore.getNext(); } addBefore(insertBefore, entry); } } private synchronized void addNew(Node entry) { if (log.isTraceEnabled()) { log.tracef("Add new entry: %s", entry); } Node insertAfter = lastEntry.getPrevious(); while (insertAfter != firstEntry) { if (insertAfter.compareTo(entry) <= 0) { break; } insertAfter = insertAfter.getPrevious(); } addAfter(insertAfter, entry); if (log.isTraceEnabled()) { log.tracef("After add, first entry is %s", firstEntry.getNext()); } } private synchronized void remove(Node entry) { if (entry == null) { return; } if (log.isTraceEnabled()) { log.tracef("remove entry: %s", entry); } Node previous = entry.getPrevious(); Node next = entry.getNext(); entry.setPrevious(null); entry.setNext(null); previous.setNext(next); next.setPrevious(previous); if (log.isTraceEnabled()) { log.tracef("After remove, first entry is %s", firstEntry.getNext()); } } private synchronized void addAfter(Node insertAfter, Node entry) { entry.setNext(insertAfter.getNext()); insertAfter.getNext().setPrevious(entry); entry.setPrevious(insertAfter); insertAfter.setNext(entry); if (log.isTraceEnabled()) { log.tracef("add after: %s --> [%s] --> %s", insertAfter, entry, entry.getNext()); } } private void addBefore(Node insertBefore, Node entry) { entry.setPrevious(insertBefore.getPrevious()); insertBefore.getPrevious().setNext(entry); entry.setNext(insertBefore); insertBefore.setPrevious(entry); if (log.isTraceEnabled()) { log.tracef("add before: %s --> [%s] --> %s", entry.getPrevious(), entry, insertBefore); } } private synchronized void notifyIfNeeded() { if (firstEntry.getNext().isReady()) { notify(); } } private class TransactionEntryImpl implements Node { private final CacheTransaction cacheTransaction; private GMUVersion entryVersion; private boolean ready; private boolean committed; private GMUCommitCommand commitCommand; private Node previous; private Node next; private TransactionEntryImpl(CacheTransaction cacheTransaction) { this.cacheTransaction = cacheTransaction; this.entryVersion = toGMUVersion(cacheTransaction.getTransactionVersion()); } public synchronized void commitVersion(GMUVersion commitVersion) { this.entryVersion = commitVersion; this.ready = true; if (log.isTraceEnabled()) { log.tracef("Set transaction commit version: %s", this); } } public synchronized GMUVersion getVersion() { return entryVersion; } public CacheTransaction getCacheTransaction() { return cacheTransaction; } public synchronized boolean isReady() { return ready; } @Override public synchronized boolean isCommitted() { return committed; } public GlobalTransaction getGlobalTransaction() { return cacheTransaction.getGlobalTransaction(); } public synchronized void committed() { if (log.isTraceEnabled()) { log.tracef("Mark transaction committed: %s", this); } committed = true; if (commitCommand != null) { commitCommand.sendReply(null, false); } notifyAll(); } @Override public synchronized void awaitUntilCommitted(GMUCommitCommand commitCommand) throws InterruptedException { if (log.isTraceEnabled()) { log.tracef("await until this [%s] is committed.", this); } if (committed && commitCommand != null) { commitCommand.sendReply(null, false); if (log.isTraceEnabled()) { log.tracef("Done! This [%s] is committed.", this); } return; } if (commitCommand != null) { this.commitCommand = commitCommand; if (log.isTraceEnabled()) { log.tracef("Don't wait. It is remote. Reply will be sent when this [%s] is committed.", this); } return; } while (!committed) { wait(); } if (log.isTraceEnabled()) { log.tracef("Done! This [%s] is committed.", this); } } @Override public CacheTransaction getCacheTransactionForCommit() { cacheTransaction.setTransactionVersion(entryVersion); return cacheTransaction; } @Override public String toString() { return "TransactionEntry{" + "version=" + getVersion() + ", ready=" + isReady() + ", gtx=" + cacheTransaction.getGlobalTransaction().prettyPrint() + '}'; } @Override public int compareTo(Node otherNode) { if (otherNode == null) { return -1; } else if (otherNode == firstEntry) { return 1; } else if (otherNode == lastEntry) { return -1; } Long my = getVersion().getThisNodeVersionValue(); Long other = otherNode.getVersion().getThisNodeVersionValue(); int compareResult = my.compareTo(other); if (log.isTraceEnabled()) { log.tracef("Comparing this[%s] with other[%s]. compare(%s,%s) ==> %s", this, otherNode, my, other, compareResult); } return compareResult; } @Override public Node getPrevious() { return previous; } @Override public void setPrevious(Node previous) { this.previous = previous; } @Override public Node getNext() { return next; } @Override public void setNext(Node next) { this.next = next; } } private interface Node extends TransactionEntry, Comparable<Node> { void commitVersion(GMUVersion commitCommand); GMUVersion getVersion(); boolean isReady(); boolean isCommitted(); GlobalTransaction getGlobalTransaction(); Node getPrevious(); void setPrevious(Node previous); Node getNext(); void setNext(Node next); } public static interface TransactionEntry { void awaitUntilCommitted(GMUCommitCommand commitCommand) throws InterruptedException; CacheTransaction getCacheTransactionForCommit(); void committed(); } }