/*
* 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;
import org.ehcache.clustered.client.config.ClusteredResourcePool;
import org.ehcache.clustered.client.config.builders.ClusteredResourcePoolBuilder;
import org.ehcache.clustered.client.internal.ClusterTierManagerClientEntityFactory;
import org.ehcache.clustered.client.internal.ClusterTierManagerClientEntityService;
import org.ehcache.clustered.client.internal.UnitTestConnectionService;
import org.ehcache.clustered.client.internal.UnitTestConnectionService.PassthroughServerBuilder;
import org.ehcache.clustered.client.internal.lock.VoltronReadWriteLockEntityClientService;
import org.ehcache.clustered.common.Consistency;
import org.ehcache.clustered.common.ServerSideConfiguration;
import org.ehcache.clustered.common.internal.ServerStoreConfiguration;
import org.ehcache.clustered.common.internal.messages.ServerStoreMessageFactory;
import org.ehcache.clustered.common.internal.store.Chain;
import org.ehcache.clustered.lock.server.VoltronReadWriteLockServerEntityService;
import org.ehcache.clustered.server.ClusterTierManagerServerEntityService;
import org.ehcache.clustered.server.store.ObservableClusterTierServerEntityService;
import org.ehcache.config.units.MemoryUnit;
import org.ehcache.impl.serialization.LongSerializer;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.terracotta.connection.Connection;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static org.ehcache.clustered.common.internal.store.Util.chainsEqual;
import static org.ehcache.clustered.common.internal.store.Util.createPayload;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.fail;
public class EventualServerStoreProxyTest {
private static final String CACHE_IDENTIFIER = "testCache";
private static final URI CLUSTER_URI = URI.create("terracotta://localhost:9510");
private static ClusterTierClientEntity clientEntity1;
private static ClusterTierClientEntity clientEntity2;
private static EventualServerStoreProxy serverStoreProxy1;
private static EventualServerStoreProxy serverStoreProxy2;
private static ObservableClusterTierServerEntityService observableClusterTierServerEntityService = new ObservableClusterTierServerEntityService();
@BeforeClass
public static void setUp() throws Exception {
UnitTestConnectionService.add(CLUSTER_URI,
new PassthroughServerBuilder()
.serverEntityService(new ClusterTierManagerServerEntityService())
.clientEntityService(new ClusterTierManagerClientEntityService())
.serverEntityService(observableClusterTierServerEntityService)
.clientEntityService(new ClusterTierClientEntityService())
.serverEntityService(new VoltronReadWriteLockServerEntityService())
.clientEntityService(new VoltronReadWriteLockEntityClientService())
.resource("defaultResource", 128, MemoryUnit.MB)
.build());
UnitTestConnectionService unitTestConnectionService = new UnitTestConnectionService();
Connection connection1 = unitTestConnectionService.connect(CLUSTER_URI, new Properties());
Connection connection2 = unitTestConnectionService.connect(CLUSTER_URI, new Properties());
ClusterTierManagerClientEntityFactory entityFactory1 = new ClusterTierManagerClientEntityFactory(connection1);
ClusterTierManagerClientEntityFactory entityFactory2 = new ClusterTierManagerClientEntityFactory(connection2);
entityFactory1.create("TestCacheManager",
new ServerSideConfiguration("defaultResource", Collections.<String, ServerSideConfiguration.Pool>emptyMap()));
entityFactory2.retrieve("TestCacheManager", null);
ClusteredResourcePool resourcePool = ClusteredResourcePoolBuilder.clusteredDedicated(16L, MemoryUnit.MB);
ServerStoreConfiguration serverStoreConfiguration = new ServerStoreConfiguration(resourcePool.getPoolAllocation(), Long.class.getName(),
Long.class.getName(), LongSerializer.class.getName(), LongSerializer.class
.getName(), Consistency.EVENTUAL);
clientEntity1 = entityFactory1.fetchOrCreateClusteredStoreEntity(UUID.randomUUID(), "TestCacheManager", CACHE_IDENTIFIER, serverStoreConfiguration, true);
clientEntity2 = entityFactory2.fetchOrCreateClusteredStoreEntity(UUID.randomUUID(), "TestCacheManager", CACHE_IDENTIFIER, serverStoreConfiguration, false);
// required to attach the store to the client
clientEntity1.validate(serverStoreConfiguration);
clientEntity2.validate(serverStoreConfiguration);
serverStoreProxy1 = new EventualServerStoreProxy(CACHE_IDENTIFIER, new ServerStoreMessageFactory(clientEntity1.getClientId()), clientEntity1);
serverStoreProxy2 = new EventualServerStoreProxy(CACHE_IDENTIFIER, new ServerStoreMessageFactory(clientEntity2.getClientId()), clientEntity2);
}
@AfterClass
public static void tearDown() throws Exception {
serverStoreProxy1 = null;
if (clientEntity1 != null) {
clientEntity1.close();
clientEntity1 = null;
}
serverStoreProxy2 = null;
if (clientEntity2 != null) {
clientEntity2.close();
clientEntity2 = null;
}
UnitTestConnectionService.remove(CLUSTER_URI);
}
@Test
public void testServerSideEvictionFiresInvalidations() throws Exception {
final List<Long> store1InvalidatedHashes = new CopyOnWriteArrayList<Long>();
final List<Long> store2InvalidatedHashes = new CopyOnWriteArrayList<Long>();
ServerStoreProxy.InvalidationListener listener1 = new ServerStoreProxy.InvalidationListener() {
@Override
public void onInvalidateHash(long hash) {
store1InvalidatedHashes.add(hash);
}
@Override
public void onInvalidateAll() {
fail("should not be called");
}
};
ServerStoreProxy.InvalidationListener listener2 = new ServerStoreProxy.InvalidationListener() {
@Override
public void onInvalidateHash(long hash) {
store2InvalidatedHashes.add(hash);
}
@Override
public void onInvalidateAll() {
fail("should not be called");
}
};
serverStoreProxy1.addInvalidationListener(listener1);
serverStoreProxy2.addInvalidationListener(listener2);
final int ITERATIONS = 40;
for (int i = 0; i < ITERATIONS; i++) {
serverStoreProxy1.append(i, createPayload(i, 512 * 1024));
}
int evictionCount = 0;
int entryCount = 0;
for (int i = 0; i < ITERATIONS; i++) {
Chain elements1 = serverStoreProxy1.get(i);
Chain elements2 = serverStoreProxy2.get(i);
assertThat(chainsEqual(elements1, elements2), is(true));
if (!elements1.isEmpty()) {
entryCount++;
} else {
evictionCount++;
}
}
// there has to be server-side evictions, otherwise this test is useless
assertThat(store1InvalidatedHashes.size(), greaterThan(0));
// test that each time the server evicted, the originating client got notified
assertThat(store1InvalidatedHashes.size(), is(ITERATIONS - entryCount));
// test that each time the server evicted, the other client got notified on top of normal invalidations
assertThat(store2InvalidatedHashes.size(), is(ITERATIONS + evictionCount));
assertThatClientsWaitingForInvalidationIsEmpty();
serverStoreProxy1.removeInvalidationListener(listener1);
serverStoreProxy2.removeInvalidationListener(listener2);
}
@Test
public void testHashInvalidationListenerWithAppend() throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
final AtomicReference<Long> invalidatedHash = new AtomicReference<Long>();
ServerStoreProxy.InvalidationListener listener = new ServerStoreProxy.InvalidationListener() {
@Override
public void onInvalidateHash(long hash) {
invalidatedHash.set(hash);
latch.countDown();
}
@Override
public void onInvalidateAll() {
throw new AssertionError("Should not be called");
}
};
serverStoreProxy1.addInvalidationListener(listener);
serverStoreProxy2.append(1L, createPayload(1L));
latch.await(5, TimeUnit.SECONDS);
assertThat(invalidatedHash.get(), is(1L));
assertThatClientsWaitingForInvalidationIsEmpty();
serverStoreProxy1.removeInvalidationListener(listener);
}
@Test
public void testHashInvalidationListenerWithGetAndAppend() throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
final AtomicReference<Long> invalidatedHash = new AtomicReference<Long>();
ServerStoreProxy.InvalidationListener listener = new ServerStoreProxy.InvalidationListener() {
@Override
public void onInvalidateHash(long hash) {
invalidatedHash.set(hash);
latch.countDown();
}
@Override
public void onInvalidateAll() {
throw new AssertionError("Should not be called");
}
};
serverStoreProxy1.addInvalidationListener(listener);
serverStoreProxy2.getAndAppend(1L, createPayload(1L));
latch.await(5, TimeUnit.SECONDS);
assertThat(invalidatedHash.get(), is(1L));
assertThatClientsWaitingForInvalidationIsEmpty();
serverStoreProxy1.removeInvalidationListener(listener);
}
@Test
public void testAllInvalidationListener() throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
final AtomicBoolean invalidatedAll = new AtomicBoolean();
ServerStoreProxy.InvalidationListener listener = new ServerStoreProxy.InvalidationListener() {
@Override
public void onInvalidateHash(long hash) {
throw new AssertionError("Should not be called");
}
@Override
public void onInvalidateAll() {
invalidatedAll.set(true);
latch.countDown();
}
};
serverStoreProxy1.addInvalidationListener(listener);
serverStoreProxy2.clear();
latch.await(5, TimeUnit.SECONDS);
assertThat(invalidatedAll.get(), is(true));
assertThatClientsWaitingForInvalidationIsEmpty();
serverStoreProxy1.removeInvalidationListener(listener);
}
private static void assertThatClientsWaitingForInvalidationIsEmpty() throws Exception {
ObservableClusterTierServerEntityService.ObservableClusterTierActiveEntity activeEntity = observableClusterTierServerEntityService.getServedActiveEntities().get(0);
CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(() -> {
while (true) {
try {
if (activeEntity.getClientsWaitingForInvalidation().size() == 0) {
return true;
}
} catch (Exception e) {
}
}
});
assertThat(future.get(5, TimeUnit.SECONDS), is(true));
}
}