/** * Copyright 2011-2012 Akiban Technologies, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.persistit; import static com.persistit.util.SequencerConstants.WRITE_WRITE_STORE_A; import static com.persistit.util.SequencerConstants.WRITE_WRITE_STORE_B; import static com.persistit.util.SequencerConstants.WRITE_WRITE_STORE_C; import static com.persistit.util.SequencerConstants.WRITE_WRITE_STORE_SCHEDULE; import static com.persistit.util.ThreadSequencer.addSchedules; import static com.persistit.util.ThreadSequencer.array; import static com.persistit.util.ThreadSequencer.describeHistory; import static com.persistit.util.ThreadSequencer.describePartialOrdering; import static com.persistit.util.ThreadSequencer.disableSequencer; import static com.persistit.util.ThreadSequencer.enableSequencer; import static com.persistit.util.ThreadSequencer.historyMeetsPartialOrdering; import static com.persistit.util.ThreadSequencer.out; import static com.persistit.util.ThreadSequencer.rawSequenceHistoryCopy; import static com.persistit.util.ThreadSequencer.sequence; import static org.junit.Assert.assertEquals; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import org.junit.Test; import com.persistit.exception.PersistitException; import com.persistit.unit.UnitTestProperties; public class Bug947182Test extends PersistitUnitTestCase { private static final String TREE_NAME = "Bug947182Test"; private static final String KEY = "key1"; private static final int A = WRITE_WRITE_STORE_A; private static final int B = WRITE_WRITE_STORE_B; private static final int C = WRITE_WRITE_STORE_C; @Override public void tearDown() throws Exception { disableSequencer(); super.tearDown(); } /* * Bug seen many times but only in a large (100 terminal), long running TPCC * test as a hung thread inside of Buffer#repack() due to _alloc appearing * to have a zero-ed out header. This turned out to be a secondary issue to * the real problem. * * The Exchange#storeInternal() method tracks a number of variables related * to long record chains. There are four in total: new and old for value * being stored and MVV itself. Logically, we don't need to keep new ones if * the store doesn't succeed and we can't free old ones until we have * committed the new. We also don't need create multiple long record chains * for the new one if we have to retry for any reason (e.g. split) as it * isn't visible to anyone else yet. Conversely, we do have to create * multiple long MVVs (if required) upon retry as the state of the MVV may * have changed in-between. * * That is where the real bug was. The storeInternal() method would * incorrectly track the old long MVV pointer across retries. Imagine this * sequence: 1) txn1 stores value at A 2) txn2 tries to store at A but is * stopped due to WW 3) txn2 releases latches and sleeps for short period 4) * txn1 aborts 5) txn2 wakes, sees tx1n aborted, and retries 6) A is * converted to short record (pruned, short stored, etc) 7) txn2 re-acquires * page and successfully stores value at A * * If the value during step 2 was a long MVV and was reduced to a short MVV * at step 6 (which would free the long record chain), then when txn2 * completed at step 7 it would still be holding onto the old long record * pointer. * * Ultimately, that resulted in the same page address being on the garbage * list twice. When the first came off and was being used for data and then * simultaneously being used for as the garbage root, we would see the end * result of _alloc appearing to be junk/zeroed out. */ @Test public void testDoubleFreeOfLongMvvChain() throws InterruptedException { enableSequencer(true); addSchedules(WRITE_WRITE_STORE_SCHEDULE); final Semaphore firstStore = new Semaphore(0); final ConcurrentLinkedQueue<Throwable> throwableList = new ConcurrentLinkedQueue<Throwable>(); final Thread thread1 = new Thread(new Runnable() { @Override public void run() { Exchange ex = null; try { final Transaction txn = _persistit.getTransaction(); ex = getExchange(_persistit); txn.begin(); try { ex.clear().append(KEY); storeLongMVV(ex); firstStore.release(); sequence(WRITE_WRITE_STORE_B); txn.rollback(); } finally { txn.end(); } /* * Store and rollback again but this time with a short value * so the long record chain gets de-allocated */ txn.begin(); try { ex.clear().append(KEY); ex.getValue().clear().put("in-between value"); ex.store(); txn.rollback(); } finally { txn.end(); } sequence(WRITE_WRITE_STORE_C); } catch (final Throwable t) { throwableList.add(t); } finally { if (ex != null) { _persistit.releaseExchange(ex); } } } }); final Thread thread2 = new Thread(new Runnable() { @Override public void run() { Exchange ex = null; try { if (!firstStore.tryAcquire(5, TimeUnit.SECONDS)) { throw new Exception("Timed out waiting for first store to complete"); } final Transaction txn = _persistit.getTransaction(); ex = getExchange(_persistit); txn.begin(); try { ex.clear().append(KEY).getValue().clear().put(KEY); ex.store(); txn.commit(); } finally { txn.end(); _persistit.releaseExchange(ex); } } catch (final Throwable t) { throwableList.add(t); } finally { _persistit.releaseExchange(ex); } } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); assertEquals("Threads had no exceptions", "[]", throwableList.toString()); final int[] history = rawSequenceHistoryCopy(); final int[][] expectedOrdering = { array(A, B), array(out(B)), array(C), array(out(A), out(C)) }; if (!historyMeetsPartialOrdering(history, expectedOrdering)) { assertEquals("History did not meet partial ordering", describePartialOrdering(expectedOrdering), describeHistory(history)); } } private static Exchange getExchange(final Persistit persistit) throws PersistitException { return persistit.getExchange(UnitTestProperties.VOLUME_NAME, TREE_NAME, true); } private static void storeLongMVV(final Exchange ex) throws PersistitException { final int size = ex.maxValueSize(ex.getKey().getEncodedSize()) - 1; final StringBuilder builder = new StringBuilder(size); while (builder.length() < size) { builder.append("0123456789"); } builder.setLength(size); ex.getValue().clear().put(builder.toString()); ex.store(); assertEquals("Did store long MVV", true, TestShim.isValueLongRecord(ex)); } }