/*
* Copyright Terracotta, 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 org.ehcache.clustered.client.internal.store.operations;
import org.ehcache.clustered.client.TestTimeSource;
import org.ehcache.clustered.client.internal.store.ChainBuilder;
import org.ehcache.clustered.client.internal.store.ResolvedChain;
import org.ehcache.clustered.client.internal.store.operations.codecs.OperationsCodec;
import org.ehcache.clustered.common.internal.store.Chain;
import org.ehcache.clustered.common.internal.store.Element;
import org.ehcache.expiry.Duration;
import org.ehcache.expiry.Expirations;
import org.ehcache.impl.serialization.LongSerializer;
import org.ehcache.impl.serialization.StringSerializer;
import org.hamcrest.collection.IsIterableContainingInOrder;
import org.junit.BeforeClass;
import org.junit.Test;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
public class ChainResolverTest {
private static OperationsCodec<Long, String> codec = null;
private static TestTimeSource timeSource = null;
@BeforeClass
public static void initialSetup() {
codec = new OperationsCodec<Long, String>(new LongSerializer(), new StringSerializer());
timeSource = new TestTimeSource();
}
@Test
public void testResolveMaintainsOtherKeysInOrder() throws Exception {
ArrayList<Operation<Long, String>> list = new ArrayList<Operation<Long, String>>();
list.add(new PutOperation<Long, String>(1L, "Albin", timeSource.getTimeMillis()));
list.add(new PutOperation<Long, String>(2L, "Albin", timeSource.getTimeMillis()));
Operation<Long, String> expected = new PutOperation<Long, String>(1L, "Suresh", timeSource.getTimeMillis());
list.add(expected);
list.add(new PutOperation<Long, String>(2L, "Suresh", timeSource.getTimeMillis()));
list.add(new PutOperation<Long, String>(2L, "Mathew", timeSource.getTimeMillis()));
Chain chain = getChainFromOperations(list);
ChainResolver<Long, String> resolver = new ChainResolver<Long, String>(codec, Expirations.noExpiration());
ResolvedChain<Long, String> resolvedChain = resolver.resolve(chain, 1L, timeSource.getTimeMillis());
Result<String> result = resolvedChain.getResolvedResult(1L);
assertEquals(expected, result);
assertThat(resolvedChain.isCompacted(), is(true));
Chain compactedChain = resolvedChain.getCompactedChain();
List<Operation<Long, String>> operations = getOperationsListFromChain(compactedChain);
List<Operation<Long, String>> expectedOps = new ArrayList<Operation<Long, String>>();
expectedOps.add(new PutOperation<Long, String>(2L, "Albin", timeSource.getTimeMillis()));
expectedOps.add(new PutOperation<Long, String>(2L, "Suresh", timeSource.getTimeMillis()));
expectedOps.add(new PutOperation<Long, String>(2L, "Mathew", timeSource.getTimeMillis()));
expectedOps.add(new PutOperation<Long, String>(1L, "Suresh", timeSource.getTimeMillis()));
assertThat(operations, IsIterableContainingInOrder.contains(expectedOps.toArray()));
}
@Test
public void testResolveEmptyChain() throws Exception {
Chain chain = (new ChainBuilder()).build();
ChainResolver<Long, String> resolver = new ChainResolver<Long, String>(codec, Expirations.noExpiration());
ResolvedChain<Long, String> resolvedChain = resolver.resolve(chain, 1L, timeSource.getTimeMillis());
Result<String> result = resolvedChain.getResolvedResult(1L);
assertNull(result);
assertThat(resolvedChain.isCompacted(), is(false));
}
@Test
public void testResolveChainWithNonExistentKey() throws Exception {
ArrayList<Operation<Long, String>> list = new ArrayList<Operation<Long, String>>();
list.add(new PutOperation<Long, String>(1L, "Albin", timeSource.getTimeMillis()));
list.add(new PutOperation<Long, String>(2L, "Suresh", timeSource.getTimeMillis()));
list.add(new PutOperation<Long, String>(2L, "Mathew", timeSource.getTimeMillis()));
Chain chain = getChainFromOperations(list);
ChainResolver<Long, String> resolver = new ChainResolver<Long, String>(codec, Expirations.noExpiration());
ResolvedChain<Long, String> resolvedChain = resolver.resolve(chain, 3L, timeSource.getTimeMillis());
Result<String> result = resolvedChain.getResolvedResult(3L);
assertNull(result);
assertThat(resolvedChain.isCompacted(), is(false));
}
@Test
public void testResolveSinglePut() throws Exception {
ArrayList<Operation<Long, String>> list = new ArrayList<Operation<Long, String>>();
Operation<Long, String> expected = new PutOperation<Long, String>(1L, "Albin", timeSource.getTimeMillis());
list.add(expected);
Chain chain = getChainFromOperations(list);
ChainResolver<Long, String> resolver = new ChainResolver<Long, String>(codec, Expirations.noExpiration());
ResolvedChain<Long, String> resolvedChain = resolver.resolve(chain, 1L, timeSource.getTimeMillis());
Result<String> result = resolvedChain.getResolvedResult(1L);
assertEquals(expected, result);
assertThat(resolvedChain.isCompacted(), is(false));
}
@Test
public void testResolvePutsOnly() throws Exception {
ArrayList<Operation<Long, String>> list = new ArrayList<Operation<Long, String>>();
list.add(new PutOperation<Long, String>(1L, "Albin", timeSource.getTimeMillis()));
list.add(new PutOperation<Long, String>(1L, "Suresh", timeSource.getTimeMillis()));
Operation<Long, String> expected = new PutOperation<Long, String>(1L, "Mathew", timeSource.getTimeMillis());
list.add(expected);
Chain chain = getChainFromOperations(list);
ChainResolver<Long, String> resolver = new ChainResolver<Long, String>(codec, Expirations.noExpiration());
ResolvedChain<Long, String> resolvedChain = resolver.resolve(chain, 1L, timeSource.getTimeMillis());
Result<String> result = resolvedChain.getResolvedResult(1L);
assertEquals(expected, result);
assertThat(resolvedChain.isCompacted(), is(true));
assertThat(resolvedChain.getCompactionCount(), is(3));
}
@Test
public void testResolveSingleRemove() throws Exception {
ArrayList<Operation<Long, String>> list = new ArrayList<Operation<Long, String>>();
list.add(new RemoveOperation<Long, String>(1L, timeSource.getTimeMillis()));
Chain chain = getChainFromOperations(list);
ChainResolver<Long, String> resolver = new ChainResolver<Long, String>(codec, Expirations.noExpiration());
ResolvedChain<Long, String> resolvedChain = resolver.resolve(chain, 1L, timeSource.getTimeMillis());
Result<String> result = resolvedChain.getResolvedResult(1L);
assertNull(result);
assertThat(resolvedChain.isCompacted(), is(true));
assertThat(resolvedChain.getCompactionCount(), is(1));
}
@Test
public void testResolveRemovesOnly() throws Exception {
ArrayList<Operation<Long, String>> list = new ArrayList<Operation<Long, String>>();
list.add(new RemoveOperation<Long, String>(1L, timeSource.getTimeMillis()));
list.add(new RemoveOperation<Long, String>(1L, timeSource.getTimeMillis()));
Chain chain = getChainFromOperations(list);
ChainResolver<Long, String> resolver = new ChainResolver<Long, String>(codec, Expirations.noExpiration());
ResolvedChain<Long, String> resolvedChain = resolver.resolve(chain, 1L, timeSource.getTimeMillis());
Result<String> result = resolvedChain.getResolvedResult(1L);
assertNull(result);
assertThat(resolvedChain.isCompacted(), is(true));
assertThat(resolvedChain.getCompactionCount(), is(2));
}
@Test
public void testPutAndRemove() throws Exception {
ArrayList<Operation<Long, String>> list = new ArrayList<Operation<Long, String>>();
list.add(new PutOperation<Long, String>(1L, "Albin", timeSource.getTimeMillis()));
list.add(new RemoveOperation<Long, String>(1L, timeSource.getTimeMillis()));
Chain chain = getChainFromOperations(list);
ChainResolver<Long, String> resolver = new ChainResolver<Long, String>(codec, Expirations.noExpiration());
ResolvedChain<Long, String> resolvedChain = resolver.resolve(chain, 1L, timeSource.getTimeMillis());
Result<String> result = resolvedChain.getResolvedResult(1L);
assertNull(result);
assertThat(resolvedChain.isCompacted(), is(true));
}
@Test
public void testResolvePutIfAbsentOnly() throws Exception {
ArrayList<Operation<Long, String>> list = new ArrayList<Operation<Long, String>>();
Operation<Long, String> expected = new PutIfAbsentOperation<Long, String>(1L, "Mathew", timeSource.getTimeMillis());
list.add(expected);
Chain chain = getChainFromOperations(list);
ChainResolver<Long, String> resolver = new ChainResolver<Long, String>(codec, Expirations.noExpiration());
ResolvedChain<Long, String> resolvedChain = resolver.resolve(chain, 1L, timeSource.getTimeMillis());
Result<String> result = resolvedChain.getResolvedResult(1L);
assertEquals(expected, result);
assertThat(resolvedChain.isCompacted(), is(false));
}
@Test
public void testResolvePutIfAbsentsOnly() throws Exception {
ArrayList<Operation<Long, String>> list = new ArrayList<Operation<Long, String>>();
Operation<Long, String> expected = new PutIfAbsentOperation<Long, String>(1L, "Albin", timeSource.getTimeMillis());
list.add(expected);
list.add(new PutIfAbsentOperation<Long, String>(1L, "Suresh", timeSource.getTimeMillis()));
list.add(new PutIfAbsentOperation<Long, String>(1L, "Mathew", timeSource.getTimeMillis()));
Chain chain = getChainFromOperations(list);
ChainResolver<Long, String> resolver = new ChainResolver<Long, String>(codec, Expirations.noExpiration());
ResolvedChain<Long, String> resolvedChain = resolver.resolve(chain, 1L, timeSource.getTimeMillis());
Result<String> result = resolvedChain.getResolvedResult(1L);
assertEquals(expected, result);
assertThat(resolvedChain.isCompacted(), is(true));
}
@Test
public void testResolvePutIfAbsentSucceeds() throws Exception {
ArrayList<Operation<Long, String>> list = new ArrayList<Operation<Long, String>>();
list.add(new PutOperation<Long, String>(1L, "Albin", timeSource.getTimeMillis()));
list.add(new RemoveOperation<Long, String>(1L, timeSource.getTimeMillis()));
Operation<Long, String> expected = new PutIfAbsentOperation<Long, String>(1L, "Mathew", timeSource.getTimeMillis());
list.add(expected);
Chain chain = getChainFromOperations(list);
ChainResolver<Long, String> resolver = new ChainResolver<Long, String>(codec, Expirations.noExpiration());
ResolvedChain<Long, String> resolvedChain = resolver.resolve(chain, 1L, timeSource.getTimeMillis());
Result<String> result = resolvedChain.getResolvedResult(1L);
assertEquals(expected, result);
assertThat(resolvedChain.isCompacted(), is(true));
}
@Test
public void testResolveForSingleOperationDoesNotCompact() {
ArrayList<Operation<Long, String>> list = new ArrayList<Operation<Long, String>>();
list.add(new PutOperation<Long, String>(1L, "Albin", timeSource.getTimeMillis()));
Chain chain = getChainFromOperations(list);
ChainResolver<Long, String> resolver = new ChainResolver<Long, String>(codec, Expirations.noExpiration());
ResolvedChain<Long, String> resolvedChain = resolver.resolve(chain, 1L, timeSource.getTimeMillis());
assertThat(resolvedChain.isCompacted(), is(false));
assertThat(resolvedChain.getCompactionCount(), is(0));
}
@Test
public void testResolveForMultiplesOperationsAlwaysCompact() {
//create a random mix of operations
ArrayList<Operation<Long, String>> list = new ArrayList<Operation<Long, String>>();
list.add(new PutIfAbsentOperation<Long, String>(1L, "Albin", timeSource.getTimeMillis()));
list.add(new PutOperation<Long, String>(1L, "Suresh", timeSource.getTimeMillis()));
list.add(new PutOperation<Long, String>(1L, "Mathew", timeSource.getTimeMillis()));
list.add(new PutOperation<Long, String>(2L, "Melbin", timeSource.getTimeMillis()));
list.add(new ReplaceOperation<Long, String>(1L, "Joseph", timeSource.getTimeMillis()));
list.add(new RemoveOperation<Long, String>(2L, timeSource.getTimeMillis()));
list.add(new ConditionalRemoveOperation<Long, String>(1L, "Albin", timeSource.getTimeMillis()));
list.add(new PutOperation<Long, String>(1L, "Gregory", timeSource.getTimeMillis()));
list.add(new ConditionalReplaceOperation<Long, String>(1L, "Albin", "Abraham", timeSource.getTimeMillis()));
list.add(new RemoveOperation<Long, String>(1L, timeSource.getTimeMillis()));
list.add(new PutIfAbsentOperation<Long, String>(2L, "Albin", timeSource.getTimeMillis()));
Chain chain = getChainFromOperations(list);
ChainResolver<Long, String> resolver = new ChainResolver<Long, String>(codec, Expirations.noExpiration());
ResolvedChain<Long, String> resolvedChain = resolver.resolve(chain, 1L, timeSource.getTimeMillis());
assertThat(resolvedChain.isCompacted(), is(true));
assertThat(resolvedChain.getCompactionCount(), is(8));
}
@Test
public void testResolveForMultipleOperationHasCorrectIsFirstAndTimeStamp() {
ArrayList<Operation<Long, String>> list = new ArrayList<Operation<Long, String>>();
TestTimeSource testTimeSource = new TestTimeSource();
list.add(new PutOperation<Long, String>(1L, "Albin1", testTimeSource.getTimeMillis()));
testTimeSource.advanceTime(1L);
list.add(new PutOperation<Long, String>(1L, "Albin2", testTimeSource.getTimeMillis()));
testTimeSource.advanceTime(1L);
list.add(new RemoveOperation<Long, String>(1L, testTimeSource.getTimeMillis()));
testTimeSource.advanceTime(1L);
list.add(new PutOperation<Long, String>(1L, "AlbinAfterRemove", testTimeSource.getTimeMillis()));
Chain chain = getChainFromOperations(list);
ChainResolver<Long, String> resolver = new ChainResolver<Long, String>(codec, Expirations.noExpiration());
ResolvedChain<Long, String> resolvedChain = resolver.resolve(chain, 1L, testTimeSource.getTimeMillis());
Operation<Long, String> operation = getOperationsListFromChain(resolvedChain.getCompactedChain()).get(0);
assertThat(operation.isExpiryAvailable(), is(true));
assertThat(operation.expirationTime(), is(Long.MIN_VALUE));
try {
operation.timeStamp();
fail();
} catch (Exception ex) {
assertThat(ex.getMessage(), is("Timestamp not available"));
}
assertThat(resolvedChain.isCompacted(), is(true));
}
@Test
public void testResolveForMultipleOperationHasCorrectIsFirstAndTimeStampWithExpiry() {
ArrayList<Operation<Long, String>> list = new ArrayList<Operation<Long, String>>();
TestTimeSource testTimeSource = new TestTimeSource();
list.add(new PutOperation<Long, String>(1L, "Albin1", testTimeSource.getTimeMillis()));
testTimeSource.advanceTime(1L);
list.add(new PutOperation<Long, String>(1L, "Albin2", testTimeSource.getTimeMillis()));
testTimeSource.advanceTime(1L);
list.add(new PutOperation<Long, String>(1L, "Albin3", testTimeSource.getTimeMillis()));
testTimeSource.advanceTime(1L);
list.add(new PutOperation<Long, String>(1L, "Albin4", testTimeSource.getTimeMillis()));
Chain chain = getChainFromOperations(list);
ChainResolver<Long, String> resolver = new ChainResolver<Long, String>(codec, Expirations.timeToLiveExpiration(new Duration(1l, TimeUnit.MILLISECONDS)));
ResolvedChain<Long, String> resolvedChain = resolver.resolve(chain, 1L, testTimeSource.getTimeMillis());
Operation<Long, String> operation = getOperationsListFromChain(resolvedChain.getCompactedChain()).get(0);
assertThat(operation.isExpiryAvailable(), is(true));
assertThat(operation.expirationTime(), is(4L));
try {
operation.timeStamp();
fail();
} catch (Exception ex) {
assertThat(ex.getMessage(), is("Timestamp not available"));
}
assertThat(resolvedChain.isCompacted(), is(true));
}
@Test
public void testResolveDoesNotDecodeOtherKeyOperationValues() throws Exception {
ArrayList<Operation<Long, String>> list = new ArrayList<Operation<Long, String>>();
list.add(new PutOperation<Long, String>(2L, "Albin", timeSource.getTimeMillis()));
list.add(new PutOperation<Long, String>(2L, "Suresh", timeSource.getTimeMillis()));
list.add(new PutOperation<Long, String>(2L, "Mathew", timeSource.getTimeMillis()));
Chain chain = getChainFromOperations(list);
CountingLongSerializer keySerializer = new CountingLongSerializer();
CountingStringSerializer valueSerializer = new CountingStringSerializer();
OperationsCodec<Long, String> customCodec = new OperationsCodec<Long, String>(keySerializer, valueSerializer);
ChainResolver<Long, String> resolver = new ChainResolver<Long, String>(customCodec, Expirations.noExpiration());
resolver.resolve(chain, 1L, timeSource.getTimeMillis());
assertThat(keySerializer.decodeCount, is(3));
assertThat(valueSerializer.decodeCount, is(0));
assertThat(keySerializer.encodeCount, is(0));
assertThat(valueSerializer.encodeCount, is(0));
}
@Test
public void testResolveDecodesOperationValueOnlyOnDemand() throws Exception {
ArrayList<Operation<Long, String>> list = new ArrayList<Operation<Long, String>>();
list.add(new PutOperation<Long, String>(1L, "Albin", 1));
list.add(new PutOperation<Long, String>(1L, "Suresh", 2));
list.add(new PutOperation<Long, String>(1L, "Mathew", 3));
Chain chain = getChainFromOperations(list);
CountingLongSerializer keySerializer = new CountingLongSerializer();
CountingStringSerializer valueSerializer = new CountingStringSerializer();
OperationsCodec<Long, String> customCodec = new OperationsCodec<Long, String>(keySerializer, valueSerializer);
ChainResolver<Long, String> resolver = new ChainResolver<Long, String>(customCodec, Expirations.noExpiration());
resolver.resolve(chain, 1L, timeSource.getTimeMillis());
assertThat(keySerializer.decodeCount, is(3));
assertThat(valueSerializer.decodeCount, is(1)); //Only one decode on creation of the resolved operation
assertThat(valueSerializer.encodeCount, is(1)); //One encode from encoding the resolved operation's key
assertThat(keySerializer.encodeCount, is(1)); //One encode from encoding the resolved operation's key
}
private Chain getChainFromOperations(List<Operation<Long, String>> operations) {
ChainBuilder chainBuilder = new ChainBuilder();
for(Operation<Long, String> operation: operations) {
chainBuilder = chainBuilder.add(codec.encode(operation));
}
return chainBuilder.build();
}
private List<Operation<Long, String>> getOperationsListFromChain(Chain chain) {
List<Operation<Long, String>> list = new ArrayList<Operation<Long, String>>();
for (Element element : chain) {
Operation<Long, String> operation = codec.decode(element.getPayload());
list.add(operation);
}
return list;
}
private static class CountingLongSerializer extends LongSerializer {
private int encodeCount = 0;
private int decodeCount = 0;
@Override
public ByteBuffer serialize(final Long object) {
encodeCount++;
return super.serialize(object);
}
@Override
public Long read(final ByteBuffer binary) throws ClassNotFoundException {
decodeCount++;
return super.read(binary);
}
@Override
public boolean equals(final Long object, final ByteBuffer binary) throws ClassNotFoundException {
return super.equals(object, binary);
}
}
private static class CountingStringSerializer extends StringSerializer {
private int encodeCount = 0;
private int decodeCount = 0;
@Override
public ByteBuffer serialize(final String object) {
encodeCount++;
return super.serialize(object);
}
@Override
public String read(final ByteBuffer binary) throws ClassNotFoundException {
decodeCount++;
return super.read(binary);
}
@Override
public boolean equals(final String object, final ByteBuffer binary) throws ClassNotFoundException {
return super.equals(object, binary);
}
}
}