/**
* 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 org.junit.Assert.assertEquals;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import com.persistit.exception.PersistitException;
import com.persistit.exception.RollbackException;
public class MVCCPruneTest extends MVCCTestBase {
final static String KEY = "a";
final static String VALUE = "A";
final static String VALUE_TRX1 = "A_trx1";
final static String VALUE_TRX2 = "A_trx2";
@Test
public void testPruneNonExistingKey() throws PersistitException {
ex1.getValue().clear();
ex1.clear().append(KEY);
ex1.prune();
assertEquals("key unchanged", KEY, ex1.getKey().decode());
assertEquals("value unchanged/defined", false, ex1.getValue().isDefined());
assertEquals("key is defined", false, ex1.isValueDefined());
ex1.fetch();
assertEquals("key found by fetch", false, ex1.getValue().isDefined());
}
@Test
public void testPrunePrimordial() throws PersistitException {
// get a primordial by storing outside of transaction
store(ex1, KEY, VALUE);
assertEquals("initial primordial fetch", VALUE, fetch(ex1, KEY));
ex1.clear().append(KEY);
ex1.prune();
assertEquals("value after prune", VALUE, fetch(ex1, KEY));
assertEquals("version count after prune", 1, storedVersionCount(ex1, KEY));
}
@Test
public void testPrunePrimordialAndOneConcurrent() throws PersistitException {
storePrimordial(ex1, KEY, VALUE);
trx1.begin();
try {
store(ex1, KEY, VALUE_TRX1);
assertEquals("fetch after trx store", VALUE_TRX1, fetch(ex1, KEY));
prune(ex2, KEY);
assertEquals("fetch after prune", VALUE_TRX1, fetch(ex1, KEY));
assertEquals("version count after prune", 2, storedVersionCount(ex1, KEY));
} finally {
trx1.end();
}
}
@Test
public void testPrunePrimordialAndCommitted() throws PersistitException {
storePrimordial(ex1, KEY, VALUE);
trx1.begin();
try {
store(ex1, KEY, VALUE_TRX1);
trx1.commit();
} finally {
trx1.end();
}
assertEquals("version count after trx store", 2, storedVersionCount(ex1, KEY));
prune(ex1, KEY);
assertEquals("version count after prune", 1, storedVersionCount(ex1, KEY));
}
@Test
public void testPruneManyCommitted() throws PersistitException {
final int TRX1_COUNT = 5;
storePrimordial(ex1, KEY, VALUE);
for (int i = 1; i <= TRX1_COUNT; ++i) {
trx1.begin();
try {
store(ex1, KEY, VALUE + i);
trx1.commit();
} finally {
trx1.end();
}
}
final String highestCommitted = VALUE + TRX1_COUNT;
trx2.begin();
try {
assertEquals("value after many commits", highestCommitted, fetch(ex2, KEY));
store(ex2, KEY, VALUE_TRX2);
assertEquals("value from trx2 after store", VALUE_TRX2, fetch(ex2, KEY));
prune(ex1, KEY);
assertEquals("value from trx2 after prune", VALUE_TRX2, fetch(ex2, KEY));
assertEquals("value from no trx after prune", highestCommitted, fetch(ex1, KEY));
assertEquals("version count after prune, trx1&2 active", 2, storedVersionCount(ex1, KEY));
trx1.begin();
try {
assertEquals("value from trx1 after prune", highestCommitted, fetch(ex1, KEY));
trx1.commit();
} finally {
trx1.end();
}
trx2.commit();
} finally {
trx2.end();
}
assertEquals("value from no trx post second commit", VALUE_TRX2, fetch(ex1, KEY));
prune(ex2, KEY);
assertEquals("value from no trx post second prune", VALUE_TRX2, fetch(ex1, KEY));
assertEquals("version count post second prune, no trx active", 1, storedVersionCount(ex2, KEY));
}
@Test
public void testPruneAborted() throws PersistitException {
storePrimordial(ex1, KEY, VALUE);
trx1.begin();
try {
store(ex1, KEY, VALUE_TRX1);
assertEquals("value from trx1 store", VALUE_TRX1, fetch(ex1, KEY));
trx1.rollback();
} catch (final RollbackException e) {
// Expected
} finally {
trx1.end();
}
assertEquals("version count post-rollback", 2, storedVersionCount(ex1, KEY));
assertEquals("value post rollback pre-prune no trx", VALUE, fetch(ex2, KEY));
trx2.begin();
try {
assertEquals("value post rollback pre-prune in trx", VALUE, fetch(ex2, KEY));
trx2.commit();
} finally {
trx2.end();
}
prune(ex1, KEY);
assertEquals("version count post-rollback post prune", 1, storedVersionCount(ex1, KEY));
assertEquals("value post-rollback post-prune", VALUE, fetch(ex1, KEY));
}
@Test
public void testPruneRemoved() throws PersistitException {
storePrimordial(ex1, KEY, VALUE);
trx1.begin();
try {
assertEquals("key removed", true, remove(ex1, KEY));
assertEquals("key exists post-remove", false, ex1.isValueDefined());
assertEquals("version count post-remove pre-prune pre-commit", 2, storedVersionCount(ex2, KEY));
prune(ex2, KEY);
assertEquals("version count post-remove post-prune pre-commit", 2, storedVersionCount(ex2, KEY));
trx1.commit();
} finally {
trx1.end();
}
prune(ex2, KEY);
// NOTE: Next assert is confirming non-removal of a key that can't be
// quick deleted.
// Will need to be adjusted if that changes (directly, pruner thread,
// etc).
assertEquals("version count prune after commit", 1, storedVersionCount(ex2, KEY));
ex2.clear().append(KEY);
assertEquals("value prune after commit not in txn", false, ex2.isValueDefined());
trx1.begin();
try {
assertEquals("key exists prune after commit in trx", false, ex1.isValueDefined());
trx1.commit();
} finally {
trx1.end();
}
}
/*
* Tests currently heuristic for pruning on split. That is, always prune
* entire page when deciding to split. Will need updated if that changes.
*/
@Test
public void testPruneOnSplit() throws PersistitException {
final int MAX_KEYS = 2500;
String curKey = "";
boolean hadSplit = false;
for (int i = 0; i < MAX_KEYS; ++i) {
_persistit.getTransactionIndex().cleanup();
trx1.begin();
try {
curKey = String.format("k%4d", i);
store(ex1, curKey, i);
hadSplit = ex1.getStoreCausedSplit();
trx1.commit();
if (hadSplit) {
break;
}
} finally {
trx1.end();
}
}
assertEquals("had split before inserting max number of keys", true, hadSplit);
final Value ex1Value = ex1.getValue();
ex1.ignoreMVCCFetch(true);
ex1.clear().append(Key.BEFORE);
while (ex1.next()) {
final boolean isMVV = MVV.isArrayMVV(ex1Value.getEncodedBytes(), 0, ex1Value.getEncodedSize());
assertEquals("last key is MVV", ex1.getKey().decodeString().equals(curKey), isMVV);
}
ex1.ignoreMVCCFetch(false);
}
@Test
public void testPruneAlternatingAbortedAndCommittedVersions() throws PersistitException {
final char VERSIONS[] = { 'A', 'C', 'A', 'C', 'A' };
storePrimordial(ex1, KEY, VALUE);
for (int i = 0; i < VERSIONS.length; ++i) {
trx1.begin();
try {
store(ex1, KEY, VALUE + VERSIONS[i] + i);
if (VERSIONS[i] == 'A') {
trx1.rollback();
} else {
trx1.commit();
}
} finally {
trx1.end();
}
}
final int VERSIONS_NOW_REMOVED_BY_PRUNING_BEFORE_STORE = 2;
assertEquals("stored versions", VERSIONS.length + 1 - VERSIONS_NOW_REMOVED_BY_PRUNING_BEFORE_STORE,
storedVersionCount(ex2, KEY));
trx1.begin();
try {
final String value = VALUE + "_final";
store(ex1, KEY, value);
assertEquals("trx value fetched before pre-prune pre-commit", value, fetch(ex1, KEY));
prune(ex1, KEY);
assertEquals("trx value fetched before post-prune pre-commit", value, fetch(ex1, KEY));
trx1.commit();
} finally {
trx1.end();
}
assertEquals("stored versions", 2, storedVersionCount(ex2, KEY));
}
@Test
public void testPruneRunOfAbortedAndCommittedVersions() throws PersistitException {
final char VERSIONS[] = { 'A', 'A', 'A', 'C', 'A', 'A', 'C' };
for (int i = 0; i < VERSIONS.length; ++i) {
trx1.begin();
try {
store(ex1, KEY, VALUE + i + VERSIONS[i]);
if (VERSIONS[i] == 'A') {
trx1.rollback();
} else {
trx1.commit();
}
} finally {
trx1.end();
}
}
final int VERSIONS_NOW_REMOVED_BY_PRUNING_BEFORE_STORE = 5;
assertEquals("stored versions pre-prune", VERSIONS.length - VERSIONS_NOW_REMOVED_BY_PRUNING_BEFORE_STORE,
storedVersionCount(ex2, KEY));
prune(ex1, KEY);
assertEquals("stored versions post-prune", 1, storedVersionCount(ex2, KEY));
}
@Test
public void testStoreToPrimordialLongRecord() throws PersistitException {
final String LONG_STR = createString(ex1.getVolume().getPageSize());
storePrimordial(ex1, KEY, LONG_STR);
assertEquals("stored long record", true, ex1.isValueLongRecord());
trx1.begin();
try {
store(ex1, KEY, VALUE);
assertEquals("stored long mvv", false, ex1.isValueLongRecord());
trx1.commit();
} finally {
trx1.end();
}
trx1.begin();
try {
store(ex1, KEY, LONG_STR);
assertEquals("stored long mvv", false, ex1.isValueLongRecord());
trx1.commit();
} finally {
trx1.end();
}
prune(ex1, KEY);
assertEquals("value is long after pruning", true, ex1.isValueLongRecord());
}
/*
* Bug that could be triggered by having a primordial long record and then
* storing a short record (also, long mvv and storing a new short). The
* original long record chain would be incorrectly freed.
*/
@Test
public void testOverZealousLongRecordChainDeletion() throws PersistitException {
final String longStr = createString(ex1.getVolume().getPageSize());
storePrimordial(ex1, KEY, longStr);
trx1.begin();
try {
assertEquals("primordial long value fetch from trx1", longStr, fetch(ex1, KEY));
trx2.begin();
try {
store(ex2, KEY, VALUE_TRX2);
assertEquals("short value fetch from trx2", VALUE_TRX2, fetch(ex2, KEY));
assertEquals("old long value version fetch from trx1", longStr, fetch(ex1, KEY));
trx2.commit();
} finally {
trx2.end();
}
trx1.commit();
} finally {
trx1.end();
}
}
@Test
public void testLongRecordAndBufferMVVCount() throws PersistitException {
trx1.begin();
try {
storeLongMVV(ex1, KEY);
assertEquals("MVV count after storing LONG MVV", 1, ex1.fetchBufferCopy(0).getMvvCount());
trx1.commit();
} finally {
trx1.end();
}
_persistit.getTransactionIndex().cleanup();
ex1.clear().append(KEY).prune();
assertEquals("MVV count after commit and prune", 0, ex1.fetchBufferCopy(0).getMvvCount());
}
@Test
public void traverseWithMinBytesLongMVV() throws PersistitException {
trx1.begin();
try {
storeLongMVV(ex1, KEY);
trx1.commit();
} finally {
trx1.end();
}
int count = 0;
ex1.clear().append(Key.BEFORE);
while (ex1.traverse(Key.GT, true, 100)) {
++count;
}
assertEquals("Traversed count", 1, count);
ex1.clear();
final boolean hasChildren = ex1.hasChildren();
assertEquals("Has children", true, hasChildren);
}
//
// Test helper methods
//
private void prune(final Exchange ex, final Object k) throws PersistitException {
_persistit.getTransactionIndex().cleanup();
ex.clear().append(k);
ex.prune();
}
private class VersionInfoVisitor implements MVV.VersionVisitor {
List<Long> _versions = new ArrayList<Long>();
@Override
public void init() {
}
@Override
public void sawVersion(final long version, final int offset, final int valueLength) throws PersistitException {
_versions.add(version);
}
int sawCount() {
return _versions.size();
}
}
private int storedVersionCount(final Exchange ex, final Object k1) throws PersistitException {
ex.ignoreMVCCFetch(true);
try {
ex.clear().append(k1);
ex.fetch();
final VersionInfoVisitor visitor = new VersionInfoVisitor();
final Value value = ex.getValue();
MVV.visitAllVersions(visitor, value.getEncodedBytes(), 0, value.getEncodedSize());
ex.clear().getValue().clear();
return visitor.sawCount();
} finally {
ex.ignoreMVCCFetch(false);
}
}
private void storePrimordial(final Exchange ex, final Object k, final Object v) throws PersistitException {
if (trx1.isActive()) {
throw new IllegalStateException("Can only store primordial when outside transaction");
}
store(ex, k, v);
assertEquals("initial primordial fetch", v, fetch(ex, k));
}
}