/**
* diqube: Distributed Query Base.
*
* Copyright (C) 2015 Bastian Gloeckle
*
* This file is part of diqube.
*
* diqube is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diqube.itest.tests;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import org.diqube.config.ConfigKey;
import org.diqube.itest.AbstractDiqubeIntegrationTest;
import org.diqube.itest.annotations.NeedsServer;
import org.diqube.itest.util.ClusterFlattenServiceTestUtil;
import org.diqube.itest.util.ClusterFlattenServiceTestUtil.TestClusterFlattenService;
import org.diqube.itest.util.TestDataGenerator;
import org.diqube.itest.util.Waiter;
import org.diqube.remote.cluster.thrift.ClusterFlattenService;
import org.diqube.server.ControlFileManager;
import org.diqube.thrift.base.util.RUuidUtil;
import org.diqube.util.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
import org.testng.annotations.Test;
/**
* Integration test that interacts with {@link ClusterFlattenService}s on servers (= directly talks to "query remotes"
* !). It tests wether requests are merged correctly and how the services act if their cluster is divided into several
* parts, each processing a different request ID.
*
* @author Bastian Gloeckle
*/
public class ClusterFlattenIntegrationTest extends AbstractDiqubeIntegrationTest {
private static final Logger logger = LoggerFactory.getLogger(ClusterFlattenIntegrationTest.class);
private static final String BIG_TABLE = "big";
private static final String BIG0_CONTROL_FILE =
"/" + ClusterFlattenIntegrationTest.class.getSimpleName() + "/big0" + ControlFileManager.CONTROL_FILE_EXTENSION;
private static final String BIG10_CONTROL_FILE =
"/" + ClusterFlattenIntegrationTest.class.getSimpleName() + "/big10" + ControlFileManager.CONTROL_FILE_EXTENSION;
private static final String BIG_DATA_FILE_WORK = "big.json";
/**
* This test starts a first flatten process on all nodes and then before the first one is completed, a second, equal
* one is started. Tests whether the second one receives the same result as the first and validates therefore that the
* servers merged the two requests.
*/
@Test
@NeedsServer(servers = 2)
public void mergeTest() throws IOException, InterruptedException {
// GIVEN
TestDataGenerator.generateJsonTestData(work(BIG_DATA_FILE_WORK), 20, 2, new String[] { "a", "b" }, 30);
serverControl.get(0).deploy(cp(BIG0_CONTROL_FILE), work(BIG_DATA_FILE_WORK));
serverControl.get(1).deploy(cp(BIG10_CONTROL_FILE), work(BIG_DATA_FILE_WORK));
try (TestClusterFlattenService localCfs = ClusterFlattenServiceTestUtil
.createClusterFlattenService(serverControl.get(0).getServerMacKey() /* same MAC key for both */)) {
// first: request flattening from both servers using the first Request ID
UUID firstRequestId = UUID.randomUUID();
logger.info("Sending first request to flatten the table (request ID {})", firstRequestId);
serverControl.get(0).getSerivceTestUtil().clusterFlattenService(clusterFlattenService -> {
clusterFlattenService.flattenAllLocalShards(RUuidUtil.toRUuid(firstRequestId), BIG_TABLE, "a[*].a[*]",
Arrays.asList(serverControl.get(1).getAddr().toRNodeAddress()),
localCfs.getThisServicesAddr().toRNodeAddress());
});
serverControl.get(1).getSerivceTestUtil().clusterFlattenService(clusterFlattenService -> {
clusterFlattenService.flattenAllLocalShards(RUuidUtil.toRUuid(firstRequestId), BIG_TABLE, "a[*].a[*]",
Arrays.asList(serverControl.get(0).getAddr().toRNodeAddress()),
localCfs.getThisServicesAddr().toRNodeAddress());
});
// sleep for a second, both servers should get started to process the first request
Thread.sleep(200);
// second: start second flattenign request. As the first one should still be running, this request should be
// "merged" with the first one.
UUID secondRequestId = UUID.randomUUID();
logger.info("Sending second request to flatten the table (request ID {})", secondRequestId);
serverControl.get(0).getSerivceTestUtil().clusterFlattenService(clusterFlattenService -> {
clusterFlattenService.flattenAllLocalShards(RUuidUtil.toRUuid(secondRequestId), BIG_TABLE, "a[*].a[*]",
Arrays.asList(serverControl.get(1).getAddr().toRNodeAddress()),
localCfs.getThisServicesAddr().toRNodeAddress());
});
serverControl.get(1).getSerivceTestUtil().clusterFlattenService(clusterFlattenService -> {
clusterFlattenService.flattenAllLocalShards(RUuidUtil.toRUuid(secondRequestId), BIG_TABLE, "a[*].a[*]",
Arrays.asList(serverControl.get(0).getAddr().toRNodeAddress()),
localCfs.getThisServicesAddr().toRNodeAddress());
});
logger.info("Waiting for first flatten request to finish...");
new Waiter().waitUntil("First flattening request succeeded", 60, 500,
() -> localCfs.check() && //
localCfs.getNodeResults().values().stream().flatMap(lst -> lst.stream())
.filter(p -> p.getLeft().equals(firstRequestId)).count() == 2); // two results for first req
logger.info("Waiting for second flatten request to finish...");
new Waiter().waitUntil("Second flattening request succeeded", 60, 500,
() -> localCfs.check() && //
localCfs.getNodeResults().values().stream().flatMap(lst -> lst.stream())
.filter(p -> p.getLeft().equals(secondRequestId)).count() == 2);
logger.info("Flatten completed successfully, now inspecting the results.");
List<UUID> flattenTableIdForFirstRequest =
localCfs.getNodeResults().values().stream().flatMap(lst -> lst.stream())
.filter(p -> p.getLeft().equals(firstRequestId)).map(p -> p.getRight()).collect(Collectors.toList());
List<UUID> flattenTableIdForSecondRequest =
localCfs.getNodeResults().values().stream().flatMap(lst -> lst.stream())
.filter(p -> p.getLeft().equals(secondRequestId)).map(p -> p.getRight()).collect(Collectors.toList());
// If the flatten succeeds in a request, the id of the flattened table is equal to the request ID. Should be the
// case for the first flatten request.
Assert.assertEquals(flattenTableIdForFirstRequest, Arrays.asList(firstRequestId, firstRequestId),
"Expected first request succeeded and created a flattened table with the same ID");
// If the flatten request gets merged (to another request, because the latter is currently running and computing
// the same result), the result flatten ID should be the ID of that request that the new request was merged to.
Assert.assertEquals(flattenTableIdForSecondRequest, Arrays.asList(firstRequestId, firstRequestId),
"Expected second request succeeded and merged with first request.");
}
}
/**
* Tests what happens when two equal flatten requests are issued at the same time with different request IDs. It is
* expected that none of the query remotes returns successfully, but all fail. The query master then usually needs to
* retry its request - we though don't do this in the test here.
*
*
* @throws IOException
* @throws InterruptedException
*/
@Test
@NeedsServer(servers = 2, manualStart = true)
public void dividedClusterTest() throws IOException, InterruptedException {
// GIVEN
// override the timeout to actually receive an exception from the remotes.
serverControl.get(0).start(prop -> prop.setProperty(ConfigKey.FLATTEN_TIMEOUT_SECONDS, "60"));
serverControl.get(1).start(prop -> prop.setProperty(ConfigKey.FLATTEN_TIMEOUT_SECONDS, "60"));
TestDataGenerator.generateJsonTestData(work(BIG_DATA_FILE_WORK), 10, 2, new String[] { "a", "b" }, 10);
serverControl.get(0).deploy(cp(BIG0_CONTROL_FILE), work(BIG_DATA_FILE_WORK));
serverControl.get(1).deploy(cp(BIG10_CONTROL_FILE), work(BIG_DATA_FILE_WORK));
try (TestClusterFlattenService localCfs = ClusterFlattenServiceTestUtil
.createClusterFlattenService(serverControl.get(0).getServerMacKey() /* same MAC key for both */)) {
// first request is sent to first server
UUID firstRequestId = UUID.randomUUID();
logger.info("Sending first request to flatten the table (request ID {}) to first server", firstRequestId);
serverControl.get(0).getSerivceTestUtil().clusterFlattenService(clusterFlattenService -> {
clusterFlattenService.flattenAllLocalShards(RUuidUtil.toRUuid(firstRequestId), BIG_TABLE, "a[*].a[*]",
Arrays.asList(serverControl.get(1).getAddr().toRNodeAddress()),
localCfs.getThisServicesAddr().toRNodeAddress());
});
// second request to second server
UUID secondRequestId = UUID.randomUUID();
logger.info("Sending second request to flatten the table (request ID {}) to second server", secondRequestId);
serverControl.get(1).getSerivceTestUtil().clusterFlattenService(clusterFlattenService -> {
clusterFlattenService.flattenAllLocalShards(RUuidUtil.toRUuid(secondRequestId), BIG_TABLE, "a[*].a[*]",
Arrays.asList(serverControl.get(0).getAddr().toRNodeAddress()),
localCfs.getThisServicesAddr().toRNodeAddress());
});
// sleep for a second, both servers should get started to process their respective request
Thread.sleep(1000);
// now complete each request
logger.info("Sending first request to flatten the table (request ID {}) to second server", firstRequestId);
serverControl.get(1).getSerivceTestUtil().clusterFlattenService(clusterFlattenService -> {
clusterFlattenService.flattenAllLocalShards(RUuidUtil.toRUuid(firstRequestId), BIG_TABLE, "a[*].a[*]",
Arrays.asList(serverControl.get(0).getAddr().toRNodeAddress()),
localCfs.getThisServicesAddr().toRNodeAddress());
});
logger.info("Sending second request to flatten the table (request ID {}) to first server", secondRequestId);
serverControl.get(0).getSerivceTestUtil().clusterFlattenService(clusterFlattenService -> {
clusterFlattenService.flattenAllLocalShards(RUuidUtil.toRUuid(secondRequestId), BIG_TABLE, "a[*].a[*]",
Arrays.asList(serverControl.get(1).getAddr().toRNodeAddress()),
localCfs.getThisServicesAddr().toRNodeAddress());
});
// wait double the time of the flattentimeout we set for the servers!
// we will receive the exception for each remote and for each requestId on the remote -> 4 times.
new Waiter().waitUntil("Both remotes reported an exception", 120, 500,
() -> localCfs.getExceptions().size() == 2 * 2);
}
}
/**
* Tests what happens when two equal flatten requests are issued at the same time with different request IDs and with
* different "remoteFlatteners" - one of the requests having no remote request at all. In contrast to
* {@link #dividedClusterTest()} this will NOT fail, but the query master will receive one result (from the node which
* succeeds because there are no other flatteners).
*/
@Test
@NeedsServer(servers = 2, manualStart = true)
public void dividedClusterOneNoRemotesTest() throws IOException, InterruptedException {
// GIVEN
// override the timeout to actually receive an exception from the remotes.
serverControl.get(0).start(prop -> prop.setProperty(ConfigKey.FLATTEN_TIMEOUT_SECONDS, "60"));
serverControl.get(1).start(prop -> prop.setProperty(ConfigKey.FLATTEN_TIMEOUT_SECONDS, "60"));
TestDataGenerator.generateJsonTestData(work(BIG_DATA_FILE_WORK), 10, 2, new String[] { "a", "b" }, 10);
serverControl.get(0).deploy(cp(BIG0_CONTROL_FILE), work(BIG_DATA_FILE_WORK));
serverControl.get(1).deploy(cp(BIG10_CONTROL_FILE), work(BIG_DATA_FILE_WORK));
try (TestClusterFlattenService localCfs = ClusterFlattenServiceTestUtil
.createClusterFlattenService(serverControl.get(0).getServerMacKey() /* same MAC key for both */)) {
// first request is sent to first server
UUID firstRequestId = UUID.randomUUID();
logger.info("Sending first request to flatten the table (request ID {}) to first server", firstRequestId);
serverControl.get(0).getSerivceTestUtil().clusterFlattenService(clusterFlattenService -> {
clusterFlattenService.flattenAllLocalShards(RUuidUtil.toRUuid(firstRequestId), BIG_TABLE, "a[*].a[*]",
new ArrayList<>(), // no "other flatteners"
localCfs.getThisServicesAddr().toRNodeAddress());
});
// second request to second server
UUID secondRequestId = UUID.randomUUID();
logger.info("Sending second request to flatten the table (request ID {}) to second server", secondRequestId);
serverControl.get(1).getSerivceTestUtil().clusterFlattenService(clusterFlattenService -> {
clusterFlattenService.flattenAllLocalShards(RUuidUtil.toRUuid(secondRequestId), BIG_TABLE, "a[*].a[*]",
Arrays.asList(serverControl.get(0).getAddr().toRNodeAddress()),
localCfs.getThisServicesAddr().toRNodeAddress());
});
// second request to first server
logger.info("Sending second request to flatten the table (request ID {}) to first server", secondRequestId);
serverControl.get(0).getSerivceTestUtil().clusterFlattenService(clusterFlattenService -> {
clusterFlattenService.flattenAllLocalShards(RUuidUtil.toRUuid(secondRequestId), BIG_TABLE, "a[*].a[*]",
Arrays.asList(serverControl.get(1).getAddr().toRNodeAddress()),
localCfs.getThisServicesAddr().toRNodeAddress());
});
// wait double the time of the flattentimeout we set for the servers!
// we will receive the exception from server "1" for each requestId on the remote -> 1 time.
new Waiter().waitUntil("One remote is exception, the other has a result.", 120, 500,
() -> localCfs.getExceptions().size() == 1
&& localCfs.getNodeResults().containsKey(serverControl.get(0).getAddr().toRNodeAddress())
&& localCfs.getNodeResults().get(serverControl.get(0).getAddr().toRNodeAddress()).size() == 2);
Assert.assertEquals(new HashSet<>(localCfs.getNodeResults().get(serverControl.get(0).getAddr().toRNodeAddress())),
new HashSet<Pair<UUID, UUID>>(
Arrays.asList(new Pair<>(firstRequestId, firstRequestId), new Pair<>(secondRequestId, firstRequestId))),
"Expected to receive correct request/flattenId pair.");
}
}
}