org.projectnessie.versioned.impl.AbstractITTieredVersionStore Maven / Gradle / Ivy
/*
* Copyright (C) 2020 Dremio
*
* 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 org.projectnessie.versioned.impl;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;
import org.projectnessie.versioned.BranchName;
import org.projectnessie.versioned.Delete;
import org.projectnessie.versioned.Hash;
import org.projectnessie.versioned.ImmutableBranchName;
import org.projectnessie.versioned.ImmutableKey;
import org.projectnessie.versioned.ImmutablePut;
import org.projectnessie.versioned.ImmutableTagName;
import org.projectnessie.versioned.Key;
import org.projectnessie.versioned.NamedRef;
import org.projectnessie.versioned.Operation;
import org.projectnessie.versioned.Put;
import org.projectnessie.versioned.Ref;
import org.projectnessie.versioned.ReferenceAlreadyExistsException;
import org.projectnessie.versioned.ReferenceConflictException;
import org.projectnessie.versioned.ReferenceNotFoundException;
import org.projectnessie.versioned.Serializer;
import org.projectnessie.versioned.SerializerWithPayload;
import org.projectnessie.versioned.StringSerializer;
import org.projectnessie.versioned.TagName;
import org.projectnessie.versioned.Unchanged;
import org.projectnessie.versioned.VersionStore;
import org.projectnessie.versioned.WithHash;
import org.projectnessie.versioned.WithType;
import org.projectnessie.versioned.impl.InconsistentValue.InconsistentValueException;
import org.projectnessie.versioned.store.Id;
import org.projectnessie.versioned.store.Store;
import org.projectnessie.versioned.store.ValueType;
public abstract class AbstractITTieredVersionStore {
private AbstractTieredStoreFixture, ?> fixture;
@BeforeEach
void setup() {
fixture = createNewFixture();
}
@AfterEach
void deleteResources() throws Exception {
fixture.close();
}
public VersionStore versionStore() {
return fixture;
}
public Store store() {
return fixture.getStore();
}
protected abstract AbstractTieredStoreFixture, ?> createNewFixture();
@Test
void checkValueEntityType() throws Exception {
BranchName branch = BranchName.of("entity-types");
versionStore().create(branch, Optional.empty());
doReturn((byte) 24).when(StringSerializer.getInstance()).getPayload("world");
versionStore()
.commit(
branch, Optional.empty(), "metadata", ImmutableList.of(Put.of(Key.of("hi"), "world")));
assertEquals("world", versionStore().getValue(branch, Key.of("hi")));
List> values =
versionStore().getValues(branch, Lists.newArrayList(Key.of("hi")));
assertEquals(1, values.size());
assertTrue(values.get(0).isPresent());
List> keys =
versionStore().getKeys(branch).collect(Collectors.toList());
assertEquals(1, keys.size());
assertEquals(Key.of("hi"), keys.get(0).getValue());
assertEquals(StringSerializer.TestEnum.NO, keys.get(0).getType());
}
@Test
void checkValueEntityTypeWithRemoval() throws Exception {
BranchName branch = BranchName.of("entity-types-with-removal");
versionStore().create(branch, Optional.empty());
doReturn((byte) 24).when(StringSerializer.getInstance()).getPayload("world");
versionStore()
.commit(
branch, Optional.empty(), "metadata", ImmutableList.of(Put.of(Key.of("hi"), "world")));
versionStore()
.commit(branch, Optional.empty(), "metadata", ImmutableList.of(Delete.of(Key.of("hi"))));
List> keys =
versionStore().getKeys(branch).collect(Collectors.toList());
assertTrue(keys.isEmpty());
}
@Test
void checkValueEntityTypeWithModification() throws Exception {
BranchName branch = BranchName.of("entity-types-with-removal");
versionStore().create(branch, Optional.empty());
doReturn((byte) 24).when(StringSerializer.getInstance()).getPayload("world");
versionStore()
.commit(
branch, Optional.empty(), "metadata", ImmutableList.of(Put.of(Key.of("hi"), "world")));
List> keys =
versionStore().getKeys(branch).collect(Collectors.toList());
assertEquals(1, keys.size());
assertEquals(Key.of("hi"), keys.get(0).getValue());
assertEquals(StringSerializer.TestEnum.NO, keys.get(0).getType());
doReturn((byte) 80).when(StringSerializer.getInstance()).getPayload("world-weary");
versionStore()
.commit(
branch,
Optional.empty(),
"metadata",
ImmutableList.of(Put.of(Key.of("hi"), "world-weary")));
keys = versionStore().getKeys(branch).collect(Collectors.toList());
assertEquals(1, keys.size());
assertEquals(Key.of("hi"), keys.get(0).getValue());
assertEquals(StringSerializer.TestEnum.YES, keys.get(0).getType());
}
@Test
void checkDuplicateValueCommit() throws Exception {
BranchName branch = BranchName.of("dupe-values");
versionStore().create(branch, Optional.empty());
versionStore()
.commit(
branch,
Optional.empty(),
"metadata",
ImmutableList.of(Put.of(Key.of("hi"), "world"), Put.of(Key.of("no"), "world")));
assertEquals("world", versionStore().getValue(branch, Key.of("hi")));
assertEquals("world", versionStore().getValue(branch, Key.of("no")));
}
@Test
void mergeToEmpty() throws Exception {
BranchName branch1 = BranchName.of("b1");
BranchName branch2 = BranchName.of("b2");
versionStore().create(branch1, Optional.empty());
versionStore().create(branch2, Optional.empty());
versionStore()
.commit(
branch2,
Optional.empty(),
"metadata",
ImmutableList.of(Put.of(Key.of("hi"), "world"), Put.of(Key.of("no"), "world")));
versionStore()
.merge(
versionStore().toHash(branch2), branch1, Optional.of(versionStore().toHash(branch1)));
}
@Test
void mergeNoConflict() throws Exception {
BranchName branch1 = BranchName.of("b1");
BranchName branch2 = BranchName.of("b2");
Hash initial1 = versionStore().create(branch1, Optional.empty());
Hash commit1 =
versionStore()
.commit(
branch1,
Optional.empty(),
"metadata",
ImmutableList.of(Put.of(Key.of("foo"), "world1"), Put.of(Key.of("bar"), "world2")));
assertNotEquals(initial1, commit1);
Hash initial2 = versionStore().create(branch2, Optional.empty());
Hash commit2 =
versionStore()
.commit(
branch2,
Optional.empty(),
"metadata",
ImmutableList.of(Put.of(Key.of("hi"), "world3"), Put.of(Key.of("no"), "world4")));
assertNotEquals(initial2, commit2);
versionStore()
.merge(
versionStore().toHash(branch2), branch1, Optional.of(versionStore().toHash(branch1)));
assertEquals("world1", versionStore().getValue(branch1, Key.of("foo")));
assertEquals("world2", versionStore().getValue(branch1, Key.of("bar")));
assertEquals("world3", versionStore().getValue(branch1, Key.of("hi")));
assertEquals("world4", versionStore().getValue(branch1, Key.of("no")));
}
@Test
void mergeConflict() throws Exception {
BranchName branch1 = BranchName.of("b1");
BranchName branch2 = BranchName.of("b2");
Hash initial1 = versionStore().create(branch1, Optional.empty());
Hash commit1 =
versionStore()
.commit(
branch1,
Optional.empty(),
"metadata",
ImmutableList.of(Put.of(Key.of("conflictKey"), "world1")));
assertNotEquals(initial1, commit1);
Hash initial2 = versionStore().create(branch2, Optional.empty());
Hash commit2 =
versionStore()
.commit(
branch2,
Optional.empty(),
"metadata2",
ImmutableList.of(Put.of(Key.of("conflictKey"), "world2")));
assertNotEquals(initial2, commit2);
ReferenceConflictException ex =
assertThrows(
ReferenceConflictException.class,
() ->
versionStore()
.merge(
versionStore().toHash(branch2),
branch1,
Optional.of(versionStore().toHash(branch1))));
assertThat(ex.getMessage()).contains("conflictKey");
}
@Test
void checkKeyList() throws Exception {
BranchName branch = BranchName.of("my-key-list");
versionStore().create(branch, Optional.empty());
assertEquals(0, EntityType.L2.loadSingle(store(), InternalL2.EMPTY_ID).size());
versionStore()
.commit(
branch,
Optional.empty(),
"metadata",
ImmutableList.of(
Put.of(Key.of("hi"), "world"),
Put.of(Key.of("no"), "world"),
Put.of(Key.of("mad mad"), "world")));
assertEquals(0, EntityType.L2.loadSingle(store(), InternalL2.EMPTY_ID).size());
assertThat(versionStore().getKeys(branch).map(WithType::getValue).map(Key::toString))
.containsExactlyInAnyOrder("hi", "no", "mad mad");
}
@Test
void ensureKeyCheckpointsAndMultiFragmentsWork() throws Exception {
BranchName branch = BranchName.of("lots-of-keys");
Hash initial = versionStore().create(branch, Optional.empty());
Hash current = versionStore().toHash(branch);
assertEquals(current, initial);
Random r = new Random(1234);
char[] longName = new char[4096];
Arrays.fill(longName, 'a');
String prefix = new String(longName);
List names = new LinkedList<>();
Hash last = current;
for (int i = 1; i < 200; i++) {
Hash commitHash;
if (i % 5 == 0) {
// every so often, remove a key.
Key removal = names.remove(r.nextInt(names.size()));
commitHash =
versionStore()
.commit(
branch,
Optional.of(current),
"commit " + i,
Collections.singletonList(Delete.of(removal)));
} else {
Key name = Key.of(prefix + i);
names.add(name);
commitHash =
versionStore()
.commit(
branch,
Optional.of(current),
"commit " + i,
Collections.singletonList(Put.of(name, "bar")));
}
current = versionStore().toHash(branch);
assertEquals(current, commitHash);
assertNotEquals(last, current);
last = current;
}
List keysFromStore =
versionStore().getKeys(branch).map(WithType::getValue).collect(Collectors.toList());
// ensure that our total key size is greater than a single dynamo page.
assertThat(keysFromStore.size() * longName.length).isGreaterThan(400000);
// ensure that keys stored match those expected.
assertThat(keysFromStore).containsExactlyInAnyOrder(names.toArray(new Key[0]));
}
@Test
void multiload() throws Exception {
BranchName branch = BranchName.of("my-key-list");
Hash initialHash = versionStore().create(branch, Optional.empty());
Hash commitHash =
versionStore()
.commit(
branch,
Optional.empty(),
"metadata",
ImmutableList.of(
Put.of(Key.of("hi"), "world1"),
Put.of(Key.of("no"), "world2"),
Put.of(Key.of("mad mad"), "world3")));
assertNotEquals(initialHash, commitHash);
assertEquals(
Arrays.asList("world1", "world2", "world3"),
versionStore()
.getValues(branch, Arrays.asList(Key.of("hi"), Key.of("no"), Key.of("mad mad")))
.stream()
.map(Optional::get)
.collect(Collectors.toList()));
}
@Test
void ensureValidEmptyBranchState()
throws ReferenceNotFoundException, ReferenceAlreadyExistsException {
BranchName branch = BranchName.of("empty_branch");
versionStore().create(branch, Optional.empty());
Hash hash = versionStore().toHash(branch);
assertEquals(null, versionStore().getValue(hash, Key.of("arbitrary")));
}
@Test
void createAndDeleteTag() throws Exception {
TagName tag = TagName.of("foo");
// check that we can't assign an empty tag.
assertThrows(
IllegalArgumentException.class, () -> versionStore().create(tag, Optional.empty()));
// create a tag using the default empty hash.
versionStore().create(tag, Optional.of(InternalL1.EMPTY_ID.toHash()));
assertEquals(InternalL1.EMPTY_ID.toHash(), versionStore().toHash(tag));
// avoid dupe
assertThrows(
ReferenceAlreadyExistsException.class,
() -> versionStore().create(tag, Optional.of(InternalL1.EMPTY_ID.toHash())));
// delete without condition
versionStore().delete(tag, Optional.empty());
// create a tag using the default empty hash.
versionStore().create(tag, Optional.of(InternalL1.EMPTY_ID.toHash()));
// check that wrong id is rejected
assertThrows(
ReferenceConflictException.class,
() -> versionStore().delete(tag, Optional.of(Id.EMPTY.toHash())));
// delete with correct id.
versionStore().delete(tag, Optional.of(InternalL1.EMPTY_ID.toHash()));
// avoid create to invalid l1.
assertThrows(
ReferenceNotFoundException.class,
() -> versionStore().create(tag, Optional.of(Id.generateRandom().toHash())));
// fail on delete of non-existent.
assertThrows(
ReferenceNotFoundException.class, () -> versionStore().delete(tag, Optional.empty()));
}
@Test
void createAndDeleteBranch() throws Exception {
BranchName branch = BranchName.of("foo");
// create a tag using the default empty hash.
versionStore().create(branch, Optional.of(InternalL1.EMPTY_ID.toHash()));
assertEquals(InternalL1.EMPTY_ID.toHash(), versionStore().toHash(branch));
// delete without condition
versionStore().delete(branch, Optional.empty());
// create a tag using no commit.
versionStore().create(branch, Optional.empty());
// avoid dupe
assertThrows(
ReferenceAlreadyExistsException.class,
() -> versionStore().create(branch, Optional.empty()));
assertThrows(
ReferenceAlreadyExistsException.class,
() -> versionStore().create(branch, Optional.of(InternalL1.EMPTY_ID.toHash())));
// check that wrong id is rejected for deletion (non-existing)
assertThrows(
ReferenceConflictException.class,
() -> versionStore().delete(branch, Optional.of(Id.EMPTY.toHash())));
// delete with correct id.
versionStore().delete(branch, Optional.of(InternalL1.EMPTY_ID.toHash()));
// avoid create to invalid l1
assertThrows(
ReferenceNotFoundException.class,
() -> versionStore().create(branch, Optional.of(Id.generateRandom().toHash())));
// fail on delete of non-existent.
assertThrows(
ReferenceNotFoundException.class, () -> versionStore().delete(branch, Optional.empty()));
versionStore().create(branch, Optional.empty());
versionStore()
.commit(
branch, Optional.empty(), "metadata", ImmutableList.of(Put.of(Key.of("hi"), "world")));
// check that wrong id is rejected for deletion (valid but not matching)
assertThrows(
ReferenceConflictException.class,
() -> versionStore().delete(branch, Optional.of(InternalL1.EMPTY_ID.toHash())));
// can't use tag delete on branch.
assertThrows(
ReferenceConflictException.class,
() -> versionStore().delete(TagName.of("foo"), Optional.empty()));
}
/** Use case simulation: single branch, multiple users, each user updating a separate table. */
@Test
void singleBranchManyUsersDistinctTables() throws Exception {
singleBranchTest(
"singleBranchManyUsersDistinctTables", user -> String.format("user-table-%d", user), false);
}
/** Use case simulation: single branch, multiple users, all users updating a single table. */
@Test
void singleBranchManyUsersSingleTable() throws Exception {
singleBranchTest("singleBranchManyUsersSingleTable", user -> "single-table", true);
}
private void singleBranchTest(
String branchName, IntFunction tableNameGen, boolean allowInconsistentValueException)
throws Exception {
BranchName branch = BranchName.of(branchName);
int numUsers = 5;
int numCommits = 50;
Hash[] hashesKnownByUser = new Hash[numUsers];
Hash createHash = versionStore().create(branch, Optional.empty());
Arrays.fill(hashesKnownByUser, createHash);
List expectedValues = new ArrayList<>();
for (int commitNum = 0; commitNum < numCommits; commitNum++) {
for (int user = 0; user < numUsers; user++) {
Hash hashKnownByUser = hashesKnownByUser[user];
String msg = String.format("user %03d/commit %03d", user, commitNum);
expectedValues.add(msg);
String value = String.format("data_file_%03d_%03d", user, commitNum);
Put put = Put.of(Key.of(tableNameGen.apply(user)), value);
Hash commitHash;
try {
commitHash =
versionStore()
.commit(branch, Optional.of(hashKnownByUser), msg, ImmutableList.of(put));
} catch (InconsistentValueException inconsistentValueException) {
if (allowInconsistentValueException) {
hashKnownByUser = versionStore().toHash(branch);
commitHash =
versionStore()
.commit(branch, Optional.of(hashKnownByUser), msg, ImmutableList.of(put));
} else {
throw inconsistentValueException;
}
}
assertNotEquals(hashKnownByUser, commitHash);
hashesKnownByUser[user] = commitHash;
}
}
// Verify that all commits are there and that the order of the commits is correct
List committedValues =
versionStore().getCommits(branch).map(WithHash::getValue).collect(Collectors.toList());
Collections.reverse(expectedValues);
assertEquals(expectedValues, committedValues);
}
@Test
void checkpointWithUnsavedL1() throws Exception {
// KeyList.IncrementalList.generateNewCheckpoint collects parent L1s for a branch
// via HistoryRetriever to "checkpoint" the keylist. However, this only works if
// HistoryRetriever has access to both saved AND unsaved L1s, so L1s that are persisted
// and those that are still in the branch's REF. This test verifies that generateNewCheckpoint
// does not fail in that case.
BranchName branch = BranchName.of("checkpointWithUnsavedL1");
versionStore().create(branch, Optional.empty());
InternalRefId ref = InternalRefId.of(branch);
// generate MAX_DELTAS-1 keys in the key-list - just enough to *NOT*
// trigger KeyList.IncrementalList.generateNewCheckpoint
for (int i = 1; i < KeyList.IncrementalList.MAX_DELTAS; i++) {
InternalBranch internalBranch = simulateCommit(ref, i);
// verify that the branch has an unsaved L1
assertEquals(i, internalBranch.getCommits().stream().filter(c -> !c.isSaved()).count());
KeyList keyList = internalBranch.getUpdateState(store()).unsafeGetL1().getKeyList();
assertFalse(keyList.isFull());
assertFalse(keyList.isEmptyIncremental());
KeyList.IncrementalList incrementalList = (KeyList.IncrementalList) keyList;
assertEquals(i, incrementalList.getDistanceFromCheckpointCommits());
}
InternalBranch internalBranch = simulateCommit(ref, KeyList.IncrementalList.MAX_DELTAS);
KeyList keyList = internalBranch.getUpdateState(store()).unsafeGetL1().getKeyList();
assertTrue(keyList.isFull());
assertFalse(keyList.isEmptyIncremental());
}
/**
* This is a copy of {@link TieredVersionStore#commit(BranchName, Optional, Object, List)} that
* allows the test {@link #checkpointWithUnsavedL1()} to produce commits to a branch with unsaved
* commits and without collapsing the intention log.
*
* It is not particularly great to have a "stripped down" and "heavily adjusted" version of the
* original {@link TieredVersionStore#commit(BranchName, Optional, Object, List)} in a unit test,
* but the other option to prepare the pre-requisites for {@link #checkpointWithUnsavedL1()},
* namely unsaved commits + uncollapsed branch, would have been to refactor the original method
* and add a bunch of hooks, which felt too heavy.
*
* @param ref branch ID
* @param num number of the commit
* @return the updated branch
*/
private InternalBranch simulateCommit(InternalRefId ref, int num) {
List> ops =
Collections.singletonList(Put.of(Key.of("key" + num), "foo" + num));
List keys =
ops.stream().map(op -> new InternalKey(op.getKey())).collect(Collectors.toList());
SerializerWithPayload serializer =
AbstractTieredStoreFixture.WORKER.getValueSerializer();
Serializer metadataSerializer =
AbstractTieredStoreFixture.WORKER.getMetadataSerializer();
PartialTree current = PartialTree.of(serializer, ref, keys);
String incomingCommit = "metadata";
InternalCommitMetadata metadata =
InternalCommitMetadata.of(metadataSerializer.toBytes(incomingCommit));
store()
.load(
current.getLoadChain(
b -> {
InternalBranch.UpdateState updateState = b.getUpdateState(store());
return updateState.unsafeGetL1();
},
PartialTree.LoadType.NO_VALUES));
// do updates.
ops.forEach(
op ->
current.setValueForKey(
new InternalKey(op.getKey()), Optional.of(((Put) op).getValue())));
// save all but l1 and branch.
store()
.save(
Stream.concat(
current.getMostSaveOps(),
Stream.of(EntityType.COMMIT_METADATA.createSaveOpForEntity(metadata)))
.collect(Collectors.toList()));
PartialTree.CommitOp commitOp =
current.getCommitOp(metadata.getId(), Collections.emptyList(), true, true);
InternalRef.Builder> builder = EntityType.REF.newEntityProducer();
boolean updated =
store()
.update(
ValueType.REF,
ref.getId(),
commitOp.getUpdateWithCommit(),
Optional.of(commitOp.getTreeCondition()),
Optional.of(builder));
assertTrue(updated);
return builder.build().getBranch();
}
@Test
void conflictingCommit() throws Exception {
BranchName branch = BranchName.of("conflictingCommit");
Hash createHash = versionStore().create(branch, Optional.empty());
// first commit.
Hash initialHash =
versionStore()
.commit(
branch,
Optional.empty(),
"metadata",
ImmutableList.of(Put.of(Key.of("hi"), "hello world")));
assertNotEquals(createHash, initialHash);
// first hash.
Hash originalHash = versionStore().getCommits(branch).findFirst().get().getHash();
assertEquals(initialHash, originalHash);
// second commit.
Hash firstHash =
versionStore()
.commit(
branch,
Optional.empty(),
"metadata",
ImmutableList.of(Put.of(Key.of("hi"), "goodbye world")));
assertNotEquals(originalHash, firstHash);
// do an extra commit to make sure it has a different hash even though it has the same value.
Hash secondHash =
versionStore()
.commit(
branch,
Optional.empty(),
"metadata",
ImmutableList.of(Put.of(Key.of("hi"), "goodbye world")));
assertNotEquals(originalHash, secondHash);
assertNotEquals(firstHash, secondHash);
// attempt commit using first hash which has conflicting key change.
assertThrows(
InconsistentValueException.class,
() ->
versionStore()
.commit(
branch,
Optional.of(originalHash),
"metadata",
ImmutableList.of(Put.of(Key.of("hi"), "my world"))));
}
@Test
void conflictingCommitWithHash() throws Exception {
BranchName branch = BranchName.of("conflictingCommitWithHash");
Hash createHash = versionStore().create(branch, Optional.empty());
// first commit.
Hash initialHash =
versionStore()
.commit(
branch,
Optional.of(createHash),
"metadata",
ImmutableList.of(Put.of(Key.of("hi"), "hello world")));
assertNotEquals(createHash, initialHash);
// first hash.
Hash originalHash = versionStore().getCommits(branch).findFirst().get().getHash();
assertEquals(initialHash, originalHash);
// second commit.
Hash firstHash =
versionStore()
.commit(
branch,
Optional.of(originalHash),
"metadata",
ImmutableList.of(Put.of(Key.of("hi"), "goodbye world")));
assertNotEquals(originalHash, firstHash);
// do an extra commit to make sure it has a different hash even though it has the same value.
Hash secondHash =
versionStore()
.commit(
branch,
Optional.of(firstHash),
"metadata",
ImmutableList.of(Put.of(Key.of("hi"), "goodbye world")));
assertNotEquals(originalHash, secondHash);
assertNotEquals(firstHash, secondHash);
// attempt commit using first hash which has conflicting key change.
assertThrows(
InconsistentValueException.class,
() ->
versionStore()
.commit(
branch,
Optional.of(originalHash),
"metadata",
ImmutableList.of(Put.of(Key.of("hi"), "my world"))));
}
@Test
void checkRefs() throws Exception {
versionStore().create(BranchName.of("b1"), Optional.empty());
versionStore().create(BranchName.of("b2"), Optional.empty());
versionStore().create(TagName.of("t1"), Optional.of(InternalL1.EMPTY_ID.toHash()));
versionStore().create(TagName.of("t2"), Optional.of(InternalL1.EMPTY_ID.toHash()));
try (Stream> str = versionStore().getNamedRefs()) {
assertEquals(
ImmutableSet.of("b1", "b2", "t1", "t2"),
str.map(wh -> wh.getValue().getName()).collect(Collectors.toSet()));
}
}
@Test
void commitRetryCountExceeded() throws Exception {
BranchName branch = BranchName.of("commitRetryCountExceeded");
versionStore().create(branch, Optional.empty());
String c1 = "c1";
String c2 = "c2";
Key k1 = Key.of("hi");
String v1 = "hello world";
Key k2 = Key.of("my", "friend");
String v2 = "not here";
doReturn(false)
.when(store())
.update(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
assertEquals(
"Unable to complete commit due to conflicting events. Retried 5 times before failing.",
assertThrows(
ReferenceConflictException.class,
() ->
versionStore()
.commit(
branch,
Optional.empty(),
c1,
ImmutableList.of(Put.of(k1, v1), Put.of(k2, v2))))
.getMessage());
}
@ParameterizedTest
@ValueSource(ints = {0, 1, 2, 4})
void checkCommits(int numStoreUpdateFailures) throws Exception {
BranchName branch = BranchName.of("checkCommits" + numStoreUpdateFailures);
versionStore().create(branch, Optional.empty());
String c1 = "c1";
String c2 = "c2";
Key k1 = Key.of("hi");
String v1 = "hello world";
String v1p = "goodbye world";
Key k2 = Key.of("my", "friend");
String v2 = "not here";
AtomicInteger commitUpdateTry = new AtomicInteger();
doAnswer(
invocationOnMock -> {
if (commitUpdateTry.getAndIncrement() < numStoreUpdateFailures) {
return false;
}
return invocationOnMock.callRealMethod();
})
.when(store())
.update(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
versionStore()
.commit(branch, Optional.empty(), c1, ImmutableList.of(Put.of(k1, v1), Put.of(k2, v2)));
commitUpdateTry.set(0);
versionStore().commit(branch, Optional.empty(), c2, ImmutableList.of(Put.of(k1, v1p)));
List> commits = versionStore().getCommits(branch).collect(Collectors.toList());
assertEquals(
ImmutableList.of(c2, c1),
commits.stream().map(WithHash::getValue).collect(Collectors.toList()));
// changed across commits
assertEquals(v1, versionStore().getValue(commits.get(1).getHash(), k1));
assertEquals(v1p, versionStore().getValue(commits.get(0).getHash(), k1));
// not changed across commits
assertEquals(v2, versionStore().getValue(commits.get(0).getHash(), k2));
assertEquals(v2, versionStore().getValue(commits.get(1).getHash(), k2));
assertEquals(2, versionStore().getCommits(commits.get(0).getHash()).count());
TagName tag = TagName.of("tag1");
versionStore().create(tag, Optional.of(commits.get(0).getHash()));
assertEquals(2, versionStore().getCommits(tag).count());
}
@Test
void assignments() throws Exception {
BranchName branch = BranchName.of("foo");
final Key k1 = Key.of("p1");
versionStore().create(branch, Optional.empty());
versionStore().commit(branch, Optional.empty(), "c1", ImmutableList.of(Put.of(k1, "v1")));
Hash c1 = versionStore().toHash(branch);
versionStore().commit(branch, Optional.empty(), "c1", ImmutableList.of(Put.of(k1, "v2")));
Hash c2 = versionStore().toHash(branch);
TagName t1 = TagName.of("t1");
BranchName b2 = BranchName.of("b2");
// ensure tag create assignment is correct.
versionStore().create(t1, Optional.of(c1));
assertEquals("v1", versionStore().getValue(t1, k1));
// ensure branch create non-assignment works
versionStore().create(b2, Optional.empty());
assertEquals(null, versionStore().getValue(b2, k1));
// ensure tag reassignment is correct.
versionStore().assign(t1, Optional.of(c1), c2);
assertEquals("v2", versionStore().getValue(t1, k1));
// ensure branch assignment (no current) is correct
versionStore().assign(b2, Optional.empty(), c1);
assertEquals("v1", versionStore().getValue(b2, k1));
// ensure branch assignment (with current) is current
versionStore().assign(b2, Optional.of(c1), c2);
assertEquals("v2", versionStore().getValue(b2, k1));
}
@Test
void delete() throws Exception {
BranchName branch = BranchName.of("foo");
final Key k1 = Key.of("p1");
versionStore().create(branch, Optional.empty());
versionStore().commit(branch, Optional.empty(), "c1", ImmutableList.of(Put.of(k1, "v1")));
assertEquals("v1", versionStore().getValue(branch, k1));
versionStore().commit(branch, Optional.empty(), "c1", ImmutableList.of(Delete.of(k1)));
assertEquals(null, versionStore().getValue(branch, k1));
}
@Test
void unchangedOperation() throws Exception {
BranchName branch = BranchName.of("foo");
versionStore().create(branch, Optional.empty());
// first commit.
versionStore()
.commit(
branch,
Optional.empty(),
"metadata",
ImmutableList.of(Put.of(Key.of("hi"), "hello world")));
// first hash.
Hash originalHash = versionStore().getCommits(branch).findFirst().get().getHash();
// second commit.
versionStore()
.commit(
branch,
Optional.empty(),
"metadata",
ImmutableList.of(Put.of(Key.of("hi"), "goodbye world")));
versionStore()
.commit(
branch,
Optional.empty(),
"metadata",
ImmutableList.of(Put.of(Key.of("hi"), "goodbye world")));
// attempt commit using first hash which has conflicting key change.
assertThrows(
InconsistentValueException.class,
() ->
versionStore()
.commit(
branch,
Optional.of(originalHash),
"metadata",
ImmutableList.of(Put.of(Key.of("hi"), "my world"))));
// attempt commit using first hash, put on on-conflicting key, unchanged on conflicting key.
assertThrows(
ReferenceConflictException.class,
() ->
versionStore()
.commit(
branch,
Optional.of(originalHash),
"metadata",
ImmutableList.of(Put.of(Key.of("bar"), "mellow"), Unchanged.of(Key.of("hi")))));
}
@Test
void checkEmptyHistory() throws Exception {
BranchName branch = BranchName.of("foo");
versionStore().create(branch, Optional.empty());
assertEquals(0L, versionStore().getCommits(branch).count());
}
@Disabled
@Test
void completeFlow() throws Exception {
final BranchName branch = ImmutableBranchName.builder().name("main").build();
final BranchName branch2 = ImmutableBranchName.builder().name("b2").build();
final TagName tag = ImmutableTagName.builder().name("t1").build();
final Key p1 = ImmutableKey.builder().addElements("my.path").build();
final String commit1 = "my commit 1";
final String commit2 = "my commit 2";
final String v1 = "my.value";
final String v2 = "my.value2";
// create a branch
versionStore().create(branch, Optional.empty());
try {
versionStore().create(branch, Optional.empty());
assertFalse(
true,
"Creating the a branch with the same name as an existing one should fail but didn't.");
} catch (ReferenceAlreadyExistsException ex) {
// expected.
}
versionStore()
.commit(
branch,
Optional.empty(),
commit1,
ImmutableList.of(
ImmutablePut.builder().key(p1).shouldMatchHash(false).value(v1).build()));
assertEquals(v1, versionStore().getValue(branch, p1));
versionStore().create(tag, Optional.of(versionStore().toHash(branch)));
versionStore()
.commit(
branch,
Optional.empty(),
commit2,
ImmutableList.of(
ImmutablePut.builder().key(p1).shouldMatchHash(false).value(v2).build()));
assertEquals(v2, versionStore().getValue(branch, p1));
assertEquals(v1, versionStore().getValue(tag, p1));
List> commits = versionStore().getCommits(branch).collect(Collectors.toList());
assertEquals(v1, versionStore().getValue(commits.get(1).getHash(), p1));
assertEquals(commit1, commits.get(1).getValue());
assertEquals(v2, versionStore().getValue(commits.get(0).getHash(), p1));
assertEquals(commit2, commits.get(0).getValue());
versionStore().assign(tag, Optional.of(commits.get(1).getHash()), commits.get(0).getHash());
assertEquals(commits, versionStore().getCommits(tag).collect(Collectors.toList()));
assertEquals(
commits, versionStore().getCommits(commits.get(0).getHash()).collect(Collectors.toList()));
try (Stream> str = versionStore().getNamedRefs()) {
assertEquals(2, str.count());
}
versionStore().create(branch2, Optional.of(commits.get(1).getHash()));
versionStore().delete(branch, Optional.of(commits.get(0).getHash()));
try (Stream> str = versionStore().getNamedRefs()) {
assertEquals(2, str.count());
}
assertEquals(v1, versionStore().getValue(branch2, p1));
}
@Test
void unknownRef() throws Exception {
BranchName branch = BranchName.of("bar");
versionStore().create(branch, Optional.empty());
versionStore()
.commit(
branch,
Optional.empty(),
"metadata",
ImmutableList.of(Put.of(Key.of("hi"), "hello world")));
TagName tag = TagName.of("foo");
Hash expected = versionStore().toHash(branch);
versionStore().create(tag, Optional.of(expected));
testRefMatchesToRef(branch, expected, branch.getName());
testRefMatchesToRef(tag, expected, tag.getName());
testRefMatchesToRef(expected, expected, expected.asString());
}
private void testRefMatchesToRef(Ref ref, Hash hash, String name)
throws ReferenceNotFoundException {
WithHash val = versionStore().toRef(name);
assertEquals(ref, val.getValue());
assertEquals(hash, val.getHash());
}
}