/** * 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.mt; import com.foundationdb.qp.row.Row; import com.foundationdb.qp.rowtype.TableRowType; import com.foundationdb.qp.util.SchemaCache; import com.foundationdb.server.test.mt.util.MonitoredThread; import com.foundationdb.server.test.mt.util.ThreadHelper; import com.foundationdb.server.test.mt.util.ThreadMonitor.Stage; import com.foundationdb.server.test.mt.util.ConcurrentTestBuilderImpl; import com.foundationdb.server.test.mt.util.OperatorCreator; import com.foundationdb.server.test.mt.util.TimeMarkerComparison; import com.foundationdb.sql.parser.IsolationLevel; import org.junit.Assume; import org.junit.Before; import org.junit.Test; import java.util.List; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertEquals; /** Basic isolation between SELECTs and concurrent DML. */ public final class InsertUpdateDeleteMT extends MTBase { private static final String SCHEMA = "test"; private static final String TABLE = "t"; private int tID; private int pkID; private int xID; TableRowType tableRowType; List<Row> groupRows; List<Row> pkRows; List<Row> indexRows; @Before public void createAndLoad() { Assume.assumeThat( txnService().actualIsolationLevel(IsolationLevel.SNAPSHOT_ISOLATION_LEVEL), is(equalTo(IsolationLevel.SNAPSHOT_ISOLATION_LEVEL)) ); tID = createTable(SCHEMA, TABLE, "id INT NOT NULL PRIMARY KEY, x INT, y INT"); createIndex(SCHEMA, TABLE, "x", "x"); tableRowType = SchemaCache.globalSchema(ais()).tableRowType(tID); pkID = tableRowType.table().getPrimaryKey().getIndex().getIndexId(); xID = tableRowType.table().getIndex("x").getIndexId(); writeRows(row(tID, 3, 30, 300), row(tID, 4, 40, 400), row(tID, 6, 60, 600), row(tID, 7, 70, 700)); groupRows = runPlanTxn(groupScanCreator(tID)); pkRows = runPlanTxn(indexScanCreator(tID, pkID)); indexRows = runPlanTxn(indexScanCreator(tID, xID)); } // // Insert // @Test public void groupScanAndInsert() { Row row = testRow(tableRowType, 5, 50, 500); test(groupScanCreator(tID), insertCreator(tID, row), groupRows, insert(groupRows, 2, row)); } @Test public void pkScanAndInsert() { Row row = testRow(tableRowType, 5, 50, 500); Row pkRow = testRow(tableRowType.indexRowType(pkID), 5); test(indexScanCreator(tID, pkID), insertCreator(tID, row), pkRows, insert(pkRows, 2, pkRow)); } @Test public void indexScanAndInsert() { Row row = testRow(tableRowType, 5, 50, 500); Row xRow = testRow(tableRowType.indexRowType(xID), 50, 5); test(indexScanCreator(tID, xID), insertCreator(tID, row), indexRows, insert(indexRows, 2, xRow)); } // // Update // @Test public void groupScanAndUpdate() { Row oldRow = groupRows.get(0); Row newRow = testRow(tableRowType, 3, 30, 301); // Does not affect position test(groupScanCreator(tID), updateCreator(tID, oldRow, newRow), groupRows, replace(groupRows, 0, newRow)); } @Test public void pkScanAndUpdate() { Row oldRow = groupRows.get(0); Row newRow = testRow(tableRowType, 1, 30, 300); // Moves row prior to first Row newPKRow = testRow(tableRowType.indexRowType(pkID), 1); test(indexScanCreator(tID, pkID), updateCreator(tID, oldRow, newRow), pkRows, replace(pkRows, 0, newPKRow)); } @Test public void indexScanAndUpdate() { Row oldRow = groupRows.get(0); Row newRow = testRow(tableRowType, 3, 300, 300); // Moves row after last Row newXRow = testRow(tableRowType.indexRowType(xID), 300, 3); test(indexScanCreator(tID, xID), updateCreator(tID, oldRow, newRow), indexRows, combine(remove(indexRows, 0), newXRow)); } // // Delete // @Test public void groupScanAndDelete() { Row row = groupRows.get(0); test(groupScanCreator(tID), deleteCreator(tID, row), groupRows, groupRows.subList(1, groupRows.size())); } @Test public void pkScanAndDelete() { Row row = groupRows.get(0); test(indexScanCreator(tID, pkID), deleteCreator(tID, row), pkRows, pkRows.subList(1, pkRows.size())); } @Test public void indexScanAndDelete() { Row row = groupRows.get(0); test(indexScanCreator(tID, xID), deleteCreator(tID, row), indexRows, indexRows.subList(1, indexRows.size())); } // // Internal // private void test(OperatorCreator scan, OperatorCreator dml, List<Row> startingRows, List<Row> finalRows) { List<MonitoredThread> threads = readAndDML(scan, dml); assertEquals("dml scan size", 1, threads.get(1).getScannedRows().size()); compareRows(startingRows, threads.get(0).getScannedRows()); compareRows(finalRows, runPlanTxn(scan)); } /** Plan that ensures reader starts transaction first but doesn't perform any reads until write is finished. */ private List<MonitoredThread> readAndDML(OperatorCreator readPlan, OperatorCreator dmlPlan) { List<MonitoredThread> threads = ConcurrentTestBuilderImpl .create() .add("Scan", readPlan) .sync("a", Stage.POST_BEGIN) .sync("b", Stage.PRE_SCAN) .mark(Stage.PRE_BEGIN, Stage.PRE_SCAN) .add("DML", dmlPlan) .sync("a", Stage.PRE_BEGIN) .sync("b", Stage.FINISH) .mark(Stage.PRE_BEGIN, Stage.POST_COMMIT) .build(this); ThreadHelper.runAndCheck(threads); new TimeMarkerComparison(threads).verify("Scan:PRE_BEGIN", "DML:PRE_BEGIN", "DML:POST_COMMIT", "Scan:PRE_SCAN"); return threads; } }