/** * Copyright (C) 2009-2013 FoundationDB, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.foundationdb.server.test.it.dxl; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import com.foundationdb.ais.model.GroupIndex; import com.foundationdb.util.Exceptions; import org.junit.Test; import com.foundationdb.ais.model.TableName; import com.foundationdb.server.error.InvalidOperationException; import com.foundationdb.server.rowdata.RowDef; import com.foundationdb.server.service.session.Session; import com.foundationdb.server.test.it.ITBase; // Inspired by bug 1078331 public class WriteSkewIT extends ITBase { // Test case from bug 1078331, comment 2 @Test public void testHKeyMaintenance() throws InterruptedException { createDatabase(); writeRow(parentRowDef.getRowDefId(), 1, 100); writeRow(parentRowDef.getRowDefId(), 2, 200); writeRow(childRowDef.getRowDefId(), 1, 1, 1100); writeRow(grandchildRowDef.getRowDefId(), 1, 1, 11100); TestThread threadA = new TestThread("a") { @Override public void doAction() { writeRow(threadPrivateSession, childRowDef.getRowDefId(), 2, 2, 2200); } }; TestThread threadB = new TestThread("b") { @Override public void doAction() { writeRow(threadPrivateSession, grandchildRowDef.getRowDefId(), 2, 2, 22200); } }; runTest( threadA, threadB, new Runnable() { @Override public void run() { expectRows( getTable(parent).getGroup(), row(parent, 1, 100), row(child, 1, 1, 1100), row(grandchild, 1, 1, 11100), row(parent, 2, 200), row(child, 2, 2, 2200), row(grandchild, 2, 2, 22200) ); } } ); } // Test case from description of bug 1078331 @Test public void testGroupIndexMaintenance() throws InterruptedException { createDatabase(); writeRow(parentRowDef.getRowDefId(), 1, 100); writeRow(childRowDef.getRowDefId(), 11, 1, 1100); TestThread threadA = new TestThread("a") { @Override public void doAction() { writeRow(threadPrivateSession, parentRowDef.getRowDefId(), 2, 2200); } }; TestThread threadB = new TestThread("b") { @Override public void doAction() { updateRow(threadPrivateSession, row(threadPrivateSession, childRowDef.getRowDefId(), 11, 1, 1100), row(threadPrivateSession, childRowDef.getRowDefId(), 11, 2, 1100)); } }; runTest( threadA, threadB, new Runnable() { @Override public void run() { GroupIndex index = getTable(parent).getGroup().getIndex(groupIndexName); expectRows( index, row(index, 100, null, 1, null), row(index, 2200, 1100, 2, 11) ); } } ); } private void runTest(TestThread sessionA, TestThread sessionB, Runnable bothPassedCheck) throws InterruptedException { sessionA.start(); sessionB.start(); sessionA.semA.release(); sessionA.semB.tryAcquire(5, TimeUnit.SECONDS); sessionB.semA.release(); /** * Give session B time to conflict before A * commits. */ sessionB.semB.tryAcquire(1, TimeUnit.SECONDS); sessionA.semA.release(); sessionA.semB.tryAcquire(5, TimeUnit.SECONDS); sessionA.join(10000); sessionB.join(10000); assertNull(sessionA.termination()); if(sessionB.termination != null) { assertTrue("sessionB termination was rollback", Exceptions.isRollbackException(sessionB.termination())); } else { bothPassedCheck.run(); } } private void createDatabase() throws InvalidOperationException { final String SCHEMA = "schema"; parent = createTable(SCHEMA, "parent", "pid int not null", "x int", "primary key(pid)"); child = createTable(SCHEMA, "child", "cid int not null", "pid int", "y int", "primary key(cid)", "grouping foreign key(pid) references parent(pid)"); grandchild = createTable(SCHEMA, "grandchild", "gid int not null", "cid int", "z int", "primary key(gid)", "grouping foreign key(cid) references child(cid)"); parentRowDef = getRowDef(parent); childRowDef = getRowDef(child); grandchildRowDef = getRowDef(grandchild); createLeftGroupIndex(TableName.create(SCHEMA, "parent"), groupIndexName, "parent.x", "child.y"); } private int parent; private int child; private int grandchild; private RowDef parentRowDef; private RowDef childRowDef; private RowDef grandchildRowDef; private final AtomicBoolean exceptionInAnyThread = new AtomicBoolean(false); private final String groupIndexName = "idx_pxcy"; abstract private class TestThread extends Thread { TestThread(final String name) { super(name); } @Override public void run() { txnService().beginTransaction(threadPrivateSession); boolean committed = false; try { semA.tryAcquire(5, TimeUnit.SECONDS); doAction(); semB.release(); semA.tryAcquire(5, TimeUnit.SECONDS); txnService().commitTransaction(threadPrivateSession); semB.release(); committed = true; } catch (Exception e) { termination = e; exceptionInAnyThread.set(true); } finally { if (!committed) { txnService().rollbackTransactionIfOpen(threadPrivateSession); semB.release(); } } } abstract protected void doAction(); public Exception termination() { return termination; } protected final Session threadPrivateSession = serviceManager().getSessionService().createSession(); private Exception termination; private final Semaphore semA = new Semaphore(0); private final Semaphore semB = new Semaphore(0); } }