package org.infinispan.server.test.client.hotrod;
import static org.infinispan.server.test.util.ITestUtils.SERVER1_MGMT_PORT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import javax.management.ObjectName;
import javax.management.openmbean.CompositeDataSupport;
import org.infinispan.arquillian.core.InfinispanResource;
import org.infinispan.arquillian.core.RemoteInfinispanServer;
import org.infinispan.arquillian.core.RunningServer;
import org.infinispan.arquillian.core.WithRunningServer;
import org.infinispan.arquillian.utils.MBeanServerConnectionProvider;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.client.hotrod.StreamingRemoteCache;
import org.infinispan.client.hotrod.configuration.Configuration;
import org.infinispan.client.hotrod.configuration.ConfigurationBuilder;
import org.jboss.arquillian.container.test.api.ContainerController;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* The basic set of tests for Streaming API over HottRod client
*
* @author zhostasa
*
*/
@RunWith(Arquillian.class)
@WithRunningServer({ @RunningServer(name = "default-clustered-manual-1"),
@RunningServer(name = "default-clustered-manual-2") })
public class HotRodRemoteStreamingIT {
private static final String SERVER_1_NAME = "default-clustered-manual-1",
SERVER_2_NAME = "default-clustered-manual-2";
private static final String USED_MEMORY_KEY = "used";
@InfinispanResource(SERVER_1_NAME)
private static RemoteInfinispanServer server1;
private static RemoteCacheManager rcm1;
@InfinispanResource(SERVER_2_NAME)
private static RemoteInfinispanServer server2;
private static RemoteCacheManager rcm2;
private StreamingRemoteCache<Object> src1, src2;
@ArquillianResource
private ContainerController controller;
private Configuration conf1, conf2;
private Boolean finalized = new Boolean(false);
private static Random random = new Random();
/**
* Refresh the resources for each test
*/
@Before
public void setUp() {
if (conf1 == null || conf2 == null) {
conf1 = new ConfigurationBuilder().addServer().host(server1.getHotrodEndpoint().getInetAddress().getHostName())
.port(server1.getHotrodEndpoint().getPort()).build();
conf2 = new ConfigurationBuilder().addServer().host(server2.getHotrodEndpoint().getInetAddress().getHostName())
.port(server2.getHotrodEndpoint().getPort()).build();
}
rcm1 = new RemoteCacheManager(conf1);
rcm2 = new RemoteCacheManager(conf2);
src1 = rcm1.getCache("streamingTestCache").streaming();
src2 = rcm2.getCache("streamingTestCache").streaming();
}
/**
* Check the server status for tests which involve shutting servers down
*/
private void checkServers() {
if (!controller.isStarted(SERVER_1_NAME)) {
controller.start(SERVER_1_NAME);
}
if (!controller.isStarted(SERVER_2_NAME)) {
controller.start(SERVER_2_NAME);
}
}
/**
* Test for basic functionality on multiple streams
*
* @throws Exception
*/
@Test
public void testBasicFunctionality() throws Exception {
// this value is good balance between good test, length and memory
// consumption, increase may necessitate JVm memory adjustment
int streamCount = 100;
List<RandomInserter> inserterList = new ArrayList<RandomInserter>();
for (int i = 0; i < streamCount; i++) {
inserterList.add(new RandomInserter(random.nextLong(), i % 2 == 0 ? src1 : src2, i % 2 == 0 ? src2 : src1));
}
boolean result = true;
while (result) {
Collections.shuffle(inserterList);
for (RandomInserter r : inserterList) {
result = result && r.process();
}
result = !result;
}
}
/**
* Test behaviour of cache if stream object if garbageCollected
*
* @throws Exception
*/
@Test
public void testGCOpenStream() throws Exception {
Long seed = random.nextLong();
RandomInserter ri = new RandomInserter(seed, src1, src1);
ri.process();
ri.process();
ri = null;
System.gc();
for (int i = 0; i < 10; i++) {
if (isFinalized())
break;
Thread.sleep(1000);
}
if (!isFinalized())
fail("Testing object was not garbage collected in time limit");
assertNull("Partial object found in cache1", src1.get(seed));
assertNull("Partial object found in cache2", src2.get(seed));
}
/**
* Test stream reaction on negative one value in stream
*
* @throws IOException
*/
@Test
public void testNegativeOneInStream() throws IOException {
Long seed = random.nextLong();
byte[] ba = new byte[1000];
random.nextBytes(ba);
for (int i = 1; i < 100; i++) {
ba[i * 10] = -1;
}
OutputStream out = src1.put(seed);
for (int i = 0; i < 1000; i++) {
out.write(ba[i]);
}
out.close();
InputStream in = src2.get(seed);
for (int i = 0; i < 1000; i++) {
assertEquals(ba[i], (byte) in.read());
}
in.close();
}
/**
* Test cache behaviour if same key is being manipulated multiple times
*
* @throws IOException
*/
@Test
public void testKeyConcurency() throws IOException {
int val1 = 123, val2 = 234;
Long seed = random.nextLong();
OutputStream out1 = src1.put(seed);
out1.write(val1);
OutputStream out2 = src2.put(seed);
out2.write(val2);
out1.close();
out2.close();
InputStream in = src1.get(seed);
assertEquals("", val2, in.read());
in.close();
seed = random.nextLong();
out1 = src1.putIfAbsent(seed);
out1.write(val1);
out2 = src2.putIfAbsent(seed);
out2.write(val2);
out1.close();
out2.close();
in = src1.get(seed);
assertEquals("", val1, in.read());
in.close();
}
/**
* Test correct behaviour for RemoteCacheManagers start/stop function <br>
* Current operations can be completed, but no new operations are supposed to
* be issued
*
* @throws IOException
* @throws InterruptedException
*/
@Test
public void RCMStopTest() throws IOException, InterruptedException {
byte[] value = new byte[100];
byte[] ret = new byte[100];
random.nextBytes(value);
Long key = random.nextLong();
OutputStream out = src1.put(key);
out.write(value, 0, 50);
rcm1.stop();
out.write(value, 50, 50);
out.close();
try {
src1.get(key);
src1.put(key);
fail("No exception returned with stopped RemoteCacheManager");
} catch (Exception e) {
}
rcm1.start();
src1.get(key).read(ret);
assertTrue("Returned value incorrect", Arrays.equals(ret, value));
}
/**
* Test behaviour if one server is gracefully shutdown
*
* @throws IOException
* @throws InterruptedException
*/
@Test
public void serverShutdownTest() throws IOException, InterruptedException {
byte[] value = new byte[5000];
random.nextBytes(value);
try {
for (int i = 0; i < 10; i++) {
Long key = random.nextLong();
OutputStream out = src1.put(key);
out.write(value);
out.write(value);
stopServer(i);
try {
out.write(value);
out.close();
} catch (Exception e) {
startServer(i);
if (src1.get(key) == null) {
continue;
} else
fail("Failed key found in te cache");
}
startServer(i);
}
} finally {
checkServers();
}
}
/**
* Test behaviour if one server is killed
*
* @throws IOException
* @throws InterruptedException
*/
@Test
public void serverKillTest() throws IOException, InterruptedException {
byte[] value = new byte[5000];
random.nextBytes(value);
try {
for (int i = 0; i < 10; i++) {
Long key = random.nextLong();
OutputStream out = src1.put(key);
out.write(value);
out.write(value);
killServer(i);
try {
out.write(value);
out.close();
} catch (Exception e) {
startServer(i);
if (src1.get(key) == null) {
continue;
} else
fail("Failed key found in te cache");
}
startServer(i);
}
} finally {
checkServers();
}
}
/**
* Test basic memory consumption difference between standard API and
* streaming API
*
* @throws Exception
*/
@Test
public void performanceTest() throws Exception {
try {
byte[] ba = new byte[1024 * 1024 * 10];
byte[] baForStream = new byte[1024];
controller.stop(SERVER_2_NAME);
// Server memory consumption data are also gathered, but do not
// influence result
MBeanServerConnectionProvider provider = new MBeanServerConnectionProvider(
server1.getHotrodEndpoint().getInetAddress().getHostName(), SERVER1_MGMT_PORT);
RemoteCache<String, byte[]> cache = rcm1.getCache("streamingTestCache");
CompositeDataSupport attribute = (CompositeDataSupport) provider.getConnection()
.getAttribute(new ObjectName("java.lang:type=Memory"), "HeapMemoryUsage");
Runtime runtime = Runtime.getRuntime();
Long averageMemoryConsumptionStatistic = new Long(0);
Long totalMemoryConsumptionStatistic = new Long(0);
// test is ran 10 times to get reasonable result pool
for (int top = 0; top < 10; top++) {
System.gc();
MemoryUsage serverMem = new MemoryUsage((long) attribute.get(USED_MEMORY_KEY));
MemoryUsage clientMem = new MemoryUsage(runtime.totalMemory() - runtime.freeMemory());
for (int i = 0; i < 10; i++) {
Long key = random.nextLong();
random.nextBytes(ba);
cache.put(key.toString(), ba);
serverMem.update((Long) ((CompositeDataSupport) provider.getConnection()
.getAttribute(new ObjectName("java.lang:type=Memory"), "HeapMemoryUsage")).get(USED_MEMORY_KEY));
clientMem.update(runtime.totalMemory() - runtime.freeMemory());
System.gc();
cache.remove(key.toString());
}
System.gc();
MemoryUsage serverMemStreaming = new MemoryUsage((long) attribute.get(USED_MEMORY_KEY));
MemoryUsage clientMemStreaming = new MemoryUsage(runtime.totalMemory() - runtime.freeMemory());
StreamingRemoteCache<String> streamingCache = cache.streaming();
for (int i = 0; i < 10; i++) {
Long key = random.nextLong();
OutputStream out = streamingCache.put(key.toString());
for (int y = 0; y < 1024 * 10; y++) {
random.nextBytes(baForStream);
out.write(baForStream);
}
out.close();
serverMemStreaming.update((Long) ((CompositeDataSupport) provider.getConnection()
.getAttribute(new ObjectName("java.lang:type=Memory"), "HeapMemoryUsage")).get(USED_MEMORY_KEY));
clientMemStreaming.update(runtime.totalMemory() - runtime.freeMemory());
System.gc();
cache.remove(key.toString());
}
Long averageMemoryConsumptionDifference = (clientMemStreaming.getAverage() - clientMemStreaming.getMin())
/ ((clientMem.getAverage() - clientMem.getMin()) / 100);
Long totalMemoryConsumptionDifference = (clientMemStreaming.getMax() - clientMemStreaming.getMin())
/ ((clientMem.getMax() - clientMem.getMin()) / 100);
if (averageMemoryConsumptionStatistic == 0)
averageMemoryConsumptionStatistic = averageMemoryConsumptionDifference;
else
averageMemoryConsumptionStatistic = (averageMemoryConsumptionStatistic
+ averageMemoryConsumptionDifference) / 2;
if (totalMemoryConsumptionStatistic == 0)
totalMemoryConsumptionStatistic = totalMemoryConsumptionDifference;
else
totalMemoryConsumptionStatistic = (totalMemoryConsumptionStatistic + totalMemoryConsumptionDifference)
/ 2;
}
assertTrue("Average memory consumption difference outside limit, max 15, actual "
+ averageMemoryConsumptionStatistic, averageMemoryConsumptionStatistic < 15);
assertTrue(
"Total memory consumption difference outside limit, max 30, actual " + totalMemoryConsumptionStatistic,
totalMemoryConsumptionStatistic < 30);
} finally {
checkServers();
}
}
private boolean isFinalized() {
return finalized;
}
private void setFinalized(boolean finalized) {
this.finalized = finalized;
}
/**
* Convenience method for killing servers according to number parity
*
* @param i
*/
private void killServer(int i) {
if (i % 2 == 0) {
controller.kill(SERVER_1_NAME);
} else {
controller.kill(SERVER_2_NAME);
}
rcm1 = null;
rcm2 = null;
}
/**
* Convenience method for stopping servers according to number parity
*
* @param i
*/
private void stopServer(int i) {
if (i % 2 == 0) {
controller.stop(SERVER_1_NAME);
} else {
controller.stop(SERVER_2_NAME);
}
rcm1 = null;
rcm2 = null;
}
/**
* Convenience method for starting servers according to number parity
*
* @param i
*/
private void startServer(int i) {
if (i % 2 == 0) {
controller.start(SERVER_1_NAME);
} else {
controller.start(SERVER_2_NAME);
}
setUp();
}
/**
* Testing class that will put object via stream into a cache (random key and
* size) and then retrieve it and check for consistency
*
* @author zhostasa
*
*/
private class RandomInserter {
private Long seed;
private int size = random.nextInt(1000000);
private Random randForData;
private StreamingRemoteCache<Object> cache1;
private StreamingRemoteCache<Object> cache2;
private OutputStream outStream;
private InputStream inStream;
private int count = 0;
private Boolean state;
/**
* Both cache instances can be identical
*
* @param seed
* seed for object data, can be null
* @param cache1
* cache to put object to
* @param cache2
* cache to get object from
*/
public RandomInserter(Long seed, StreamingRemoteCache<Object> cache1, StreamingRemoteCache<Object> cache2) {
this.seed = seed != null ? seed : random.nextLong();
this.cache1 = cache1;
this.cache2 = cache2;
randForData = new Random(this.seed);
}
/**
*
* @return true if the test was finished successfully, false otherwise
* @throws Exception
* Exception is to be considered a failed test
*/
public boolean process() throws Exception {
if (count == size && state == false)
return true;
else {
int randomInt = random.nextInt(100);
if (state == null) {
state = true;
outStream = cache1.put(seed);
}
if (state) {
if (randomInt + count > size)
randomInt = size - count;
byte[] arr = new byte[randomInt];
randomBytes(arr, arr.length);
outStream.write(arr);
count = count + arr.length;
} else {
byte[] arr = new byte[randomInt];
int ret = inStream.read(arr);
byte[] fromrand = new byte[arr.length];
randomBytes(fromrand, ret);
if (ret < arr.length)
for (int i = ret; i < fromrand.length; i++)
fromrand[i] = 0;
if (!Arrays.equals(arr, fromrand)) {
throw new Exception("Data returned from stream were not correct");
}
count = count + ret;
}
if (count == size) {
if (state) {
state = false;
outStream.close();
inStream = cache2.get(seed);
randForData = new Random(seed);
count = 0;
} else {
inStream.close();
return true;
}
}
}
return false;
}
/**
* Sets finalized flag in parent class for GC test
*
* @throws Throwable
*/
@Override
public void finalize() throws Throwable {
super.finalize();
setFinalized(true);
}
/**
* Because Random.nextBytes(byte[]) will drop bytes between calls
*
* @param ba
* byte[] to fill
* @param count
* number or bytes to fill in
*/
private void randomBytes(byte[] ba, int count) {
for (int i = 0; i < count; i++) {
ba[i] = (byte) randForData.nextInt();
}
}
}
/**
* Simple class for computing memory usage
*
* @author zhostasa
*
*/
private class MemoryUsage {
private Long min, max, average;
public MemoryUsage(Long startValue) {
this(startValue, startValue, startValue);
}
public MemoryUsage(Long min, Long max, Long average) {
this.min = min;
this.max = max;
this.average = average;
}
public void update(Long value) {
setMax(value);
setMin(value);
addToAverage(value);
}
public Long getMax() {
return max;
}
private void setMax(Long max) {
if (this.max < max)
this.max = max;
}
public Long getMin() {
return min;
}
private void setMin(Long min) {
if (this.min > min)
this.min = min;
}
public Long getAverage() {
return average;
}
private void addToAverage(Long average) {
this.average = (this.average + average) / 2;
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("MemoryStats:\n");
sb.append("Max memory: " + getMax() + "\n");
sb.append("Min memory: " + getMin() + "\n");
sb.append("Avg memory: " + getAverage() + "\n");
return sb.toString();
}
}
}