/** Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016. All rights reserved. Contact: SYSTAP, LLC DBA Blazegraph 2501 Calvert ST NW #106 Washington, DC 20008 licenses@blazegraph.com This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /* * Created on Feb 17, 2007 */ package com.bigdata.btree.isolation; import java.util.Properties; import java.util.UUID; import junit.framework.TestCase2; import com.bigdata.btree.BTree; import com.bigdata.btree.IIndex; import com.bigdata.btree.ITuple; import com.bigdata.btree.IndexMetadata; import com.bigdata.btree.isolation.IConflictResolver; import com.bigdata.journal.BufferMode; import com.bigdata.journal.ITx; import com.bigdata.journal.Journal; import com.bigdata.journal.Options; import com.bigdata.journal.ValidationError; /** * Tests of write-write conflict resolution. * <p> * Write-write conflicts either result in successful reconcilation via * state-based conflict resolution or an abort of the transaction that is * validating. The tests in this suite verify that write-write conflicts can be * detected and provide versions of those tests where the conflict can and can * not be validated and verify the end state in each case. * <p> * State-based validation requires transparency at the object level, including * the ability to deserialize versions into objects, to compare objects for * consistency, to merge data into the most recent version where possible and * according to data type specific rules, and to destructively merge objects * when the conflict arises on <em>identity</em> rather than state. * <p> * An example of an identity based conflict is when two objects are created that * represent URIs in an RDF graph. Since the lexicon for an RDF graph generally * requires uniqueness those objects must be merged into a single object since * they have the same identity. For an RDFS store validation on the lexicon or * statements ALWAYS succeeds since they are always consistent. * * @todo Verify that we can handle the bank account example (this is state-based * conflict resolution altogether requiring that we carry a richer * representation of state in the objects and then use that additional * state to validate and resolve some kinds of data type specific * conflicts). * * @todo Do tests that verify that multiple conflicts are correctly detected and * resolved. * * @todo Verify that we can handle examples in which we have to traverse an * object graph during conflict resolution. (Really, two object graphs: a * readOnly view of the ground state for the transaction and the * readWriteTx that we are trying to validate.) This last issue is by far * the trickyest and may require support for concurrent modification of * the transaction indices during traveral (or more simply of reading from * a fused view of the resolved and unconflicting entries in the * read-write tx index views). * * @todo Destructive merging of objects in a graph can propagate changes other * objects. Unless the data structures provide for encapsulation, e.g., by * defining objects that serve as collectors for the link set members in a * given segment, that change could propagate beyond the segment in which * it is detected. If changes can propagate in that manner then care MUST * be taken to ensure that validation terminates. * * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a> * @version $Id$ */ public class TestConflictResolution extends TestCase2 { /** * */ public TestConflictResolution() { } /** * @param name */ public TestConflictResolution(String name) { super(name); } public Properties getProperties() { Properties properties = new Properties(super.getProperties()); properties.setProperty(Options.BUFFER_MODE,BufferMode.Transient.toString()); return properties; } /** * Test correct detection of a write-write conflict. An index is registered * and the journal is committed. Two transactions (tx1, tx2) are then * started. Both transactions write a value under the same key. tx1 prepares * and commits. tx2 attempts to prepare, and the test verifies that a * {@link ValidationError} is reported. */ public void test_writeWriteConflict_correctDetection() { final Journal journal = new Journal(getProperties()); try { String name = "abc"; final byte[] k1 = new byte[] { 1 }; final byte[] v1a = new byte[] { 1 }; final byte[] v1b = new byte[] { 2 }; { /* * register an index and commit the journal. */ IndexMetadata metadata = new IndexMetadata(name, UUID.randomUUID()); metadata.setIsolatable(true); // Note: No conflict resolver. journal.registerIndex(name, BTree.create(journal,metadata) ); journal.commit(); } /* * Create two transactions. */ final long tx1 = journal.newTx(ITx.UNISOLATED); final long tx2 = journal.newTx(ITx.UNISOLATED); /* * Write a value under the same key on the same index in both * transactions. */ journal.getIndex(name,tx1).insert(k1, v1a); journal.getIndex(name,tx2).insert(k1, v1b); journal.commit(tx1); /* * verify that the value from tx1 is found under the key on the * unisolated index. */ assertEquals(v1a,(byte[])journal.getIndex(name).lookup(k1)); // final ITx tmp = journal.getTx(tx2); try { journal.commit(tx2); fail("Expecting: "+ValidationError.class); } catch(ValidationError ex) { if(log.isInfoEnabled()) log.info("Ignoring expected exception: "+ex); // assertTrue(tmp.isAborted()); } } finally { journal.destroy(); } } /** * Test correct detection and resolution of a write-write conflict. An index * is registered with an {@link IConflictResolver} and the journal is * committed. Two transactions (tx1, tx2) are then started. Both * transactions write a value under the same key. tx1 prepares and commits. * tx2 attempts to prepare, and the test verifies that the conflict resolver * is invoked, that it may resolve the conflict causing validation to * succeed and that the value determined by conflict resolution is made * persistent when tx2 commits. */ public void test_writeWriteConflict_conflictIsResolved() { final Journal journal = new Journal(getProperties()); try { final String name = "abc"; final byte[] k1 = new byte[] { 1 }; final byte[] v1a = new byte[] { 1 }; final byte[] v1b = new byte[] { 2 }; final byte[] v1c = new byte[] { 3 }; { /* * register an index with a conflict resolver and commit the * journal. */ final IndexMetadata metadata = new IndexMetadata(name, UUID .randomUUID()); metadata.setIsolatable(true); metadata.setConflictResolver(new SingleValueConflictResolver( k1, v1c)); journal.registerIndex(name, BTree.create(journal, metadata)); journal.commit(); } /* * Create two transactions. */ final long tx1 = journal.newTx(ITx.UNISOLATED); final long tx2 = journal.newTx(ITx.UNISOLATED); /* * Write a value under the same key on the same index in both * transactions. */ journal.getIndex(name, tx1).insert(k1, v1a); journal.getIndex(name, tx2).insert(k1, v1b); journal.commit(tx1); /* * verify that the value from tx1 is found under the key on the * unisolated index. */ assertEquals(v1a, (byte[]) journal.getIndex(name).lookup(k1)); journal.commit(tx2); /* * verify that the resolved value is found under the key on the * unisolated index. */ assertEquals(v1c, (byte[]) journal.getIndex(name).lookup(k1)); } finally { journal.destroy(); } } // /** // * The concurrency control algorithm must not permit two transactions to // * prepare at the same time since that violates the basic rules of // * serializability. // * // * @todo javadoc and move into schedules test suite or its own test suite. // */ // public void test_serializability() { // // Properties properties = getProperties(); // // Journal journal = new Journal(properties); // // String name = "abc"; // // final byte[] k1 = new byte[] { 1 }; // // final byte[] v1a = new byte[] { 1 }; // final byte[] v1b = new byte[] { 2 }; // // { // // /* // * register an index and commit the journal. // */ // // journal.registerIndex(name, new UnisolatedBTree(journal)); // // journal.commit(); // // } // // /* // * Create two transactions. // */ // // final long tx1 = journal.newTx(IsolationEnum.ReadWrite); // // final long tx2 = journal.newTx(IsolationEnum.ReadWrite); // // /* // * Write a value under the same key on the same index in both // * transactions. // */ // // journal.getIndex(name,tx1).insert(k1, v1a); // // journal.getIndex(name,tx2).insert(k1, v1b); // // tx1.prepare(); // // try { // tx2.prepare(); // fail("Expecting: "+IllegalStateException.class); // } catch(IllegalStateException ex) { // System.err.println("Ignoring expected exception: "+ex); // } // // journal.close(); // // } // /** * Helper class used to resolve a predicted conflict to a known value. * * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a> * @version $Id$ */ public static class SingleValueConflictResolver implements IConflictResolver { private final byte[] expectedKey; private final byte[] resolvedValue; private static final long serialVersionUID = -1161201507073182976L; public SingleValueConflictResolver(byte[] expectedKey, byte[] resolvedValue) { this.expectedKey = expectedKey; this.resolvedValue = resolvedValue; } public boolean resolveConflict(IIndex writeSet, ITuple txTuple, ITuple currentTuple) throws Exception { // The key must be the same for both tuples. assertEquals(txTuple.getKey(),currentTuple.getKey()); // the key for the conflicting writes. final byte[] key = txTuple.getKey(); assertEquals("key", expectedKey, key ); writeSet.insert(key, resolvedValue); return true; } } }