YARN-5363. For AM containers, or for containers of running-apps, "yarn logs" incorrectly only (tries to) shows syslog file-type by default. (Contributed by Vinod Kumar Vavilapalli)

(cherry picked from commit 429347289c)
This commit is contained in:
Xuan 2016-07-14 10:20:08 -07:00
parent d5d68d98c6
commit 58088d27e6
2 changed files with 64 additions and 59 deletions

View File

@ -28,7 +28,9 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser; import org.apache.commons.cli.GnuParser;
@ -53,9 +55,8 @@ import org.apache.hadoop.yarn.api.records.YarnApplicationState;
import org.apache.hadoop.yarn.client.api.YarnClient; import org.apache.hadoop.yarn.client.api.YarnClient;
import org.apache.hadoop.yarn.conf.YarnConfiguration; import org.apache.hadoop.yarn.conf.YarnConfiguration;
import org.apache.hadoop.yarn.exceptions.YarnException; import org.apache.hadoop.yarn.exceptions.YarnException;
import org.apache.hadoop.yarn.logaggregation.LogCLIHelpers;
import org.apache.hadoop.yarn.logaggregation.ContainerLogsRequest; import org.apache.hadoop.yarn.logaggregation.ContainerLogsRequest;
import org.apache.hadoop.yarn.util.ConverterUtils; import org.apache.hadoop.yarn.logaggregation.LogCLIHelpers;
import org.apache.hadoop.yarn.util.Times; import org.apache.hadoop.yarn.util.Times;
import org.apache.hadoop.yarn.webapp.util.WebAppUtils; import org.apache.hadoop.yarn.webapp.util.WebAppUtils;
import org.codehaus.jettison.json.JSONArray; import org.codehaus.jettison.json.JSONArray;
@ -78,7 +79,7 @@ public class LogsCLI extends Configured implements Tool {
private static final String NODE_ADDRESS_OPTION = "nodeAddress"; private static final String NODE_ADDRESS_OPTION = "nodeAddress";
private static final String APP_OWNER_OPTION = "appOwner"; private static final String APP_OWNER_OPTION = "appOwner";
private static final String AM_CONTAINER_OPTION = "am"; private static final String AM_CONTAINER_OPTION = "am";
private static final String CONTAINER_LOG_FILES = "logFiles"; private static final String PER_CONTAINER_LOG_FILES_OPTION = "log_files";
private static final String LIST_NODES_OPTION = "list_nodes"; private static final String LIST_NODES_OPTION = "list_nodes";
private static final String SHOW_APPLICATION_LOG_INFO private static final String SHOW_APPLICATION_LOG_INFO
= "show_application_log_info"; = "show_application_log_info";
@ -146,8 +147,8 @@ public class LogsCLI extends Configured implements Tool {
return -1; return -1;
} }
} }
if (commandLine.hasOption(CONTAINER_LOG_FILES)) { if (commandLine.hasOption(PER_CONTAINER_LOG_FILES_OPTION)) {
logFiles = commandLine.getOptionValues(CONTAINER_LOG_FILES); logFiles = commandLine.getOptionValues(PER_CONTAINER_LOG_FILES_OPTION);
} }
if (commandLine.hasOption(SIZE_OPTION)) { if (commandLine.hasOption(SIZE_OPTION)) {
bytes = Long.parseLong(commandLine.getOptionValue(SIZE_OPTION)); bytes = Long.parseLong(commandLine.getOptionValue(SIZE_OPTION));
@ -359,12 +360,18 @@ public class LogsCLI extends Configured implements Tool {
} }
private boolean fetchAllLogFiles(String[] logFiles) { private boolean fetchAllLogFiles(String[] logFiles) {
if(logFiles != null) {
// If no value is specified for the PER_CONTAINER_LOG_FILES_OPTION option,
// we will assume all logs.
if (logFiles == null || logFiles.length == 0) {
return true;
}
List<String> logs = Arrays.asList(logFiles); List<String> logs = Arrays.asList(logFiles);
if (logs.contains("ALL") || logs.contains(".*")) { if (logs.contains("ALL") || logs.contains(".*")) {
return true; return true;
} }
}
return false; return false;
} }
@ -425,7 +432,7 @@ public class LogsCLI extends Configured implements Tool {
containerIdStr); containerIdStr);
try { try {
// fetch all the log files for the container // fetch all the log files for the container
// filter the log files based on the given --logFiles pattern // filter the log files based on the given -log_files pattern
List<PerLogFileInfo> allLogFileInfos= List<PerLogFileInfo> allLogFileInfos=
getContainerLogFiles(getConf(), containerIdStr, nodeHttpAddress); getContainerLogFiles(getConf(), containerIdStr, nodeHttpAddress);
List<String> fileNames = new ArrayList<String>(); List<String> fileNames = new ArrayList<String>();
@ -701,8 +708,8 @@ public class LogsCLI extends Configured implements Tool {
new Option(APPLICATION_ID_OPTION, true, "ApplicationId (required)"); new Option(APPLICATION_ID_OPTION, true, "ApplicationId (required)");
opts.addOption(appIdOpt); opts.addOption(appIdOpt);
opts.addOption(CONTAINER_ID_OPTION, true, "ContainerId. " opts.addOption(CONTAINER_ID_OPTION, true, "ContainerId. "
+ "By default, it will only print syslog if the application is running." + "By default, it will print all available logs."
+ " Work with -logFiles to get other logs. If specified, the" + " Work with -log_files to get only specific logs. If specified, the"
+ " applicationId can be omitted"); + " applicationId can be omitted");
opts.addOption(NODE_ADDRESS_OPTION, true, "NodeAddress in the format " opts.addOption(NODE_ADDRESS_OPTION, true, "NodeAddress in the format "
+ "nodename:port"); + "nodename:port");
@ -715,13 +722,13 @@ public class LogsCLI extends Configured implements Tool {
+ "the logs for the first AM Container as well as the second " + "the logs for the first AM Container as well as the second "
+ "AM Container. To get logs for all AM Containers, use -am ALL. " + "AM Container. To get logs for all AM Containers, use -am ALL. "
+ "To get logs for the latest AM Container, use -am -1. " + "To get logs for the latest AM Container, use -am -1. "
+ "By default, it will only print out syslog. Work with -logFiles " + "By default, it will print all available logs. Work with -log_files "
+ "to get other logs"); + "to get only specific logs.");
amOption.setValueSeparator(','); amOption.setValueSeparator(',');
amOption.setArgs(Option.UNLIMITED_VALUES); amOption.setArgs(Option.UNLIMITED_VALUES);
amOption.setArgName("AM Containers"); amOption.setArgName("AM Containers");
opts.addOption(amOption); opts.addOption(amOption);
Option logFileOpt = new Option(CONTAINER_LOG_FILES, true, Option logFileOpt = new Option(PER_CONTAINER_LOG_FILES_OPTION, true,
"Specify comma-separated value " "Specify comma-separated value "
+ "to get specified container log files. Use \"ALL\" to fetch all the " + "to get specified container log files. Use \"ALL\" to fetch all the "
+ "log files for the container. It also supports Java Regex."); + "log files for the container. It also supports Java Regex.");
@ -765,7 +772,7 @@ public class LogsCLI extends Configured implements Tool {
printOpts.addOption(commandOpts.getOption(NODE_ADDRESS_OPTION)); printOpts.addOption(commandOpts.getOption(NODE_ADDRESS_OPTION));
printOpts.addOption(commandOpts.getOption(APP_OWNER_OPTION)); printOpts.addOption(commandOpts.getOption(APP_OWNER_OPTION));
printOpts.addOption(commandOpts.getOption(AM_CONTAINER_OPTION)); printOpts.addOption(commandOpts.getOption(AM_CONTAINER_OPTION));
printOpts.addOption(commandOpts.getOption(CONTAINER_LOG_FILES)); printOpts.addOption(commandOpts.getOption(PER_CONTAINER_LOG_FILES_OPTION));
printOpts.addOption(commandOpts.getOption(LIST_NODES_OPTION)); printOpts.addOption(commandOpts.getOption(LIST_NODES_OPTION));
printOpts.addOption(commandOpts.getOption(SHOW_APPLICATION_LOG_INFO)); printOpts.addOption(commandOpts.getOption(SHOW_APPLICATION_LOG_INFO));
printOpts.addOption(commandOpts.getOption(SHOW_CONTAINER_LOG_INFO)); printOpts.addOption(commandOpts.getOption(SHOW_CONTAINER_LOG_INFO));
@ -808,13 +815,7 @@ public class LogsCLI extends Configured implements Tool {
private int fetchAMContainerLogs(ContainerLogsRequest request, private int fetchAMContainerLogs(ContainerLogsRequest request,
List<String> amContainersList, LogCLIHelpers logCliHelper) List<String> amContainersList, LogCLIHelpers logCliHelper)
throws Exception { throws Exception {
List<String> logFiles = request.getLogTypes();
// if we do not specify the value for CONTAINER_LOG_FILES option,
// we will only output syslog
if (logFiles == null || logFiles.isEmpty()) {
logFiles = Arrays.asList("syslog");
}
request.setLogTypes(logFiles);
// If the application is running, we will call the RM WebService // If the application is running, we will call the RM WebService
// to get the AppAttempts which includes the nodeHttpAddress // to get the AppAttempts which includes the nodeHttpAddress
// and containerId for all the AM Containers. // and containerId for all the AM Containers.
@ -857,7 +858,6 @@ public class LogsCLI extends Configured implements Tool {
String nodeAddress = request.getNodeId(); String nodeAddress = request.getNodeId();
String appOwner = request.getAppOwner(); String appOwner = request.getAppOwner();
boolean isAppFinished = request.isAppFinished(); boolean isAppFinished = request.isAppFinished();
List<String> logFiles = request.getLogTypes();
// if we provide the node address and the application is in the final // if we provide the node address and the application is in the final
// state, we could directly get logs from HDFS. // state, we could directly get logs from HDFS.
if (nodeAddress != null && isAppFinished) { if (nodeAddress != null && isAppFinished) {
@ -900,12 +900,6 @@ public class LogsCLI extends Configured implements Tool {
// we will provide the NodeHttpAddress and get the container logs // we will provide the NodeHttpAddress and get the container logs
// by calling NodeManager webservice. // by calling NodeManager webservice.
if (!isAppFinished) { if (!isAppFinished) {
// if we do not specify the value for CONTAINER_LOG_FILES option,
// we will only output syslog
if (logFiles == null || logFiles.isEmpty()) {
logFiles = Arrays.asList("syslog");
}
request.setLogTypes(logFiles);
resultCode = printContainerLogsFromRunningApplication(getConf(), request, resultCode = printContainerLogsFromRunningApplication(getConf(), request,
logCliHelper); logCliHelper);
} else { } else {
@ -1027,13 +1021,6 @@ public class LogsCLI extends Configured implements Tool {
newOptions.setNodeHttpAddress(httpAddress newOptions.setNodeHttpAddress(httpAddress
.replaceFirst(WebAppUtils.getHttpSchemePrefix(getConf()), "")); .replaceFirst(WebAppUtils.getHttpSchemePrefix(getConf()), ""));
} }
// if we do not specify the value for CONTAINER_LOG_FILES option,
// we will only output syslog
List<String> logFiles = newOptions.getLogTypes();
if (logFiles == null || logFiles.isEmpty()) {
logFiles = Arrays.asList("syslog");
newOptions.setLogTypes(logFiles);
}
newOptionsList.add(newOptions); newOptionsList.add(newOptions);
} }
return newOptionsList; return newOptionsList;

View File

@ -46,6 +46,7 @@ import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileStatus;
@ -74,6 +75,7 @@ import org.codehaus.jettison.json.JSONObject;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.ArgumentCaptor;
public class TestLogsCLI { public class TestLogsCLI {
@ -195,20 +197,20 @@ public class TestLogsCLI {
pw.println(" Container. To get logs for all AM"); pw.println(" Container. To get logs for all AM");
pw.println(" Containers, use -am ALL. To get logs for"); pw.println(" Containers, use -am ALL. To get logs for");
pw.println(" the latest AM Container, use -am -1. By"); pw.println(" the latest AM Container, use -am -1. By");
pw.println(" default, it will only print out syslog."); pw.println(" default, it will print all available");
pw.println(" Work with -logFiles to get other logs"); pw.println(" logs. Work with -log_files to get only");
pw.println(" specific logs.");
pw.println(" -appOwner <Application Owner> AppOwner (assumed to be current user if"); pw.println(" -appOwner <Application Owner> AppOwner (assumed to be current user if");
pw.println(" not specified)"); pw.println(" not specified)");
pw.println(" -containerId <Container ID> ContainerId. By default, it will only"); pw.println(" -containerId <Container ID> ContainerId. By default, it will print");
pw.println(" print syslog if the application is"); pw.println(" all available logs. Work with -log_files");
pw.println(" running. Work with -logFiles to get other"); pw.println(" to get only specific logs. If specified,");
pw.println(" logs. If specified, the applicationId can"); pw.println(" the applicationId can be omitted");
pw.println(" be omitted");
pw.println(" -help Displays help for all commands."); pw.println(" -help Displays help for all commands.");
pw.println(" -list_nodes Show the list of nodes that successfully"); pw.println(" -list_nodes Show the list of nodes that successfully");
pw.println(" aggregated logs. This option can only be"); pw.println(" aggregated logs. This option can only be");
pw.println(" used with finished applications."); pw.println(" used with finished applications.");
pw.println(" -logFiles <Log File Name> Specify comma-separated value to get"); pw.println(" -log_files <Log File Name> Specify comma-separated value to get");
pw.println(" specified container log files. Use \"ALL\""); pw.println(" specified container log files. Use \"ALL\"");
pw.println(" to fetch all the log files for the"); pw.println(" to fetch all the log files for the");
pw.println(" container. It also supports Java Regex."); pw.println(" container. It also supports Java Regex.");
@ -240,7 +242,7 @@ public class TestLogsCLI {
} }
@Test (timeout = 15000) @Test (timeout = 15000)
public void testFetchApplictionLogs() throws Exception { public void testFetchFinishedApplictionLogs() throws Exception {
String remoteLogRootDir = "target/logs/"; String remoteLogRootDir = "target/logs/";
Configuration configuration = new Configuration(); Configuration configuration = new Configuration();
configuration.setBoolean(YarnConfiguration.LOG_AGGREGATION_ENABLED, true); configuration.setBoolean(YarnConfiguration.LOG_AGGREGATION_ENABLED, true);
@ -337,7 +339,7 @@ public class TestLogsCLI {
sysOutStream.reset(); sysOutStream.reset();
exitCode = cli.run(new String[] {"-applicationId", appId.toString(), exitCode = cli.run(new String[] {"-applicationId", appId.toString(),
"-logFiles", ".*"}); "-log_files", ".*"});
assertTrue(exitCode == 0); assertTrue(exitCode == 0);
assertTrue(sysOutStream.toString().contains( assertTrue(sysOutStream.toString().contains(
"Hello container_0_0001_01_000001 in syslog!")); "Hello container_0_0001_01_000001 in syslog!"));
@ -351,7 +353,7 @@ public class TestLogsCLI {
sysOutStream.reset(); sysOutStream.reset();
exitCode = cli.run(new String[] {"-applicationId", appId.toString(), exitCode = cli.run(new String[] {"-applicationId", appId.toString(),
"-logFiles", "std*"}); "-log_files", "std*"});
assertTrue(exitCode == 0); assertTrue(exitCode == 0);
assertFalse(sysOutStream.toString().contains( assertFalse(sysOutStream.toString().contains(
"Hello container_0_0001_01_000001 in syslog!")); "Hello container_0_0001_01_000001 in syslog!"));
@ -364,7 +366,7 @@ public class TestLogsCLI {
sysOutStream.reset(); sysOutStream.reset();
exitCode = cli.run(new String[] {"-applicationId", appId.toString(), exitCode = cli.run(new String[] {"-applicationId", appId.toString(),
"-logFiles", "123"}); "-log_files", "123"});
assertTrue(exitCode == -1); assertTrue(exitCode == -1);
assertTrue(sysErrStream.toString().contains( assertTrue(sysErrStream.toString().contains(
"Can not find any log file matching the pattern: [123] " "Can not find any log file matching the pattern: [123] "
@ -374,7 +376,7 @@ public class TestLogsCLI {
// specify the bytes which is larger than the actual file size, // specify the bytes which is larger than the actual file size,
// we would get the full logs // we would get the full logs
exitCode = cli.run(new String[] {"-applicationId", appId.toString(), exitCode = cli.run(new String[] {"-applicationId", appId.toString(),
"-logFiles", ".*", "-size", "10000" }); "-log_files", ".*", "-size", "10000" });
assertTrue(exitCode == 0); assertTrue(exitCode == 0);
assertTrue(sysOutStream.toByteArray().length == fullSize); assertTrue(sysOutStream.toByteArray().length == fullSize);
sysOutStream.reset(); sysOutStream.reset();
@ -396,7 +398,7 @@ public class TestLogsCLI {
sysOutStream.reset(); sysOutStream.reset();
exitCode = cli.run(new String[] {"-applicationId", appId.toString(), exitCode = cli.run(new String[] {"-applicationId", appId.toString(),
"-containerId", containerId3.toString(), "-logFiles", "123" }); "-containerId", containerId3.toString(), "-log_files", "123" });
assertTrue(exitCode == -1); assertTrue(exitCode == -1);
assertTrue(sysErrStream.toString().contains( assertTrue(sysErrStream.toString().contains(
"Can not find any log file matching the pattern: [123] " "Can not find any log file matching the pattern: [123] "
@ -405,7 +407,7 @@ public class TestLogsCLI {
sysErrStream.reset(); sysErrStream.reset();
exitCode = cli.run(new String[] {"-applicationId", appId.toString(), exitCode = cli.run(new String[] {"-applicationId", appId.toString(),
"-containerId", containerId3.toString(), "-logFiles", "stdout" }); "-containerId", containerId3.toString(), "-log_files", "stdout" });
assertTrue(exitCode == 0); assertTrue(exitCode == 0);
int fullContextSize = sysOutStream.toByteArray().length; int fullContextSize = sysOutStream.toByteArray().length;
String fullContext = sysOutStream.toString(); String fullContext = sysOutStream.toString();
@ -419,7 +421,7 @@ public class TestLogsCLI {
// specify a position number, it would get the first n bytes from // specify a position number, it would get the first n bytes from
// container log // container log
exitCode = cli.run(new String[] {"-applicationId", appId.toString(), exitCode = cli.run(new String[] {"-applicationId", appId.toString(),
"-containerId", containerId3.toString(), "-logFiles", "stdout", "-containerId", containerId3.toString(), "-log_files", "stdout",
"-size", "5"}); "-size", "5"});
assertTrue(exitCode == 0); assertTrue(exitCode == 0);
Assert.assertEquals(new String(logMessage.getBytes(), 0, 5), Assert.assertEquals(new String(logMessage.getBytes(), 0, 5),
@ -430,7 +432,7 @@ public class TestLogsCLI {
// specify a negative number, it would get the last n bytes from // specify a negative number, it would get the last n bytes from
// container log // container log
exitCode = cli.run(new String[] {"-applicationId", appId.toString(), exitCode = cli.run(new String[] {"-applicationId", appId.toString(),
"-containerId", containerId3.toString(), "-logFiles", "stdout", "-containerId", containerId3.toString(), "-log_files", "stdout",
"-size", "-5"}); "-size", "-5"});
assertTrue(exitCode == 0); assertTrue(exitCode == 0);
Assert.assertEquals(new String(logMessage.getBytes(), Assert.assertEquals(new String(logMessage.getBytes(),
@ -441,7 +443,7 @@ public class TestLogsCLI {
long negative = (fullContextSize + 1000) * (-1); long negative = (fullContextSize + 1000) * (-1);
exitCode = cli.run(new String[] {"-applicationId", appId.toString(), exitCode = cli.run(new String[] {"-applicationId", appId.toString(),
"-containerId", containerId3.toString(), "-logFiles", "stdout", "-containerId", containerId3.toString(), "-log_files", "stdout",
"-size", Long.toString(negative)}); "-size", Long.toString(negative)});
assertTrue(exitCode == 0); assertTrue(exitCode == 0);
Assert.assertEquals(fullContext, sysOutStream.toString()); Assert.assertEquals(fullContext, sysOutStream.toString());
@ -473,12 +475,12 @@ public class TestLogsCLI {
"Hello container_0_0001_01_000003 in stdout!")); "Hello container_0_0001_01_000003 in stdout!"));
sysOutStream.reset(); sysOutStream.reset();
// set -logFiles option as stdout // set -log_files option as stdout
// should only print log with the name as stdout // should only print log with the name as stdout
exitCode = exitCode =
cli.run(new String[] { "-applicationId", appId.toString(), cli.run(new String[] { "-applicationId", appId.toString(),
"-nodeAddress", nodeId.toString(), "-containerId", "-nodeAddress", nodeId.toString(), "-containerId",
containerId3.toString() , "-logFiles", "stdout"}); containerId3.toString() , "-log_files", "stdout"});
assertTrue(exitCode == 0); assertTrue(exitCode == 0);
assertTrue(sysOutStream.toString().contains( assertTrue(sysOutStream.toString().contains(
"Hello container_0_0001_01_000003 in stdout!")); "Hello container_0_0001_01_000003 in stdout!"));
@ -527,11 +529,14 @@ public class TestLogsCLI {
@Test (timeout = 5000) @Test (timeout = 5000)
public void testFetchRunningApplicationLogs() throws Exception { public void testFetchRunningApplicationLogs() throws Exception {
UserGroupInformation ugi = UserGroupInformation.getCurrentUser(); UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
NodeId nodeId = NodeId.newInstance("localhost", 1234); NodeId nodeId = NodeId.newInstance("localhost", 1234);
ApplicationId appId = ApplicationId.newInstance(0, 1); ApplicationId appId = ApplicationId.newInstance(0, 1);
ApplicationAttemptId appAttemptId = ApplicationAttemptId ApplicationAttemptId appAttemptId = ApplicationAttemptId
.newInstance(appId, 1); .newInstance(appId, 1);
// Create a mock ApplicationAttempt Report // Create a mock ApplicationAttempt Report
ApplicationAttemptReport mockAttemptReport = mock( ApplicationAttemptReport mockAttemptReport = mock(
ApplicationAttemptReport.class); ApplicationAttemptReport.class);
@ -568,11 +573,24 @@ public class TestLogsCLI {
cli.setConf(new YarnConfiguration()); cli.setConf(new YarnConfiguration());
int exitCode = cli.run(new String[] {"-applicationId", appId.toString()}); int exitCode = cli.run(new String[] {"-applicationId", appId.toString()});
assertTrue(exitCode == 0); assertTrue(exitCode == 0);
ArgumentCaptor<ContainerLogsRequest> logsRequestCaptor =
ArgumentCaptor.forClass(ContainerLogsRequest.class);
// we have two container reports, so make sure we have called // we have two container reports, so make sure we have called
// printContainerLogsFromRunningApplication twice // printContainerLogsFromRunningApplication twice
verify(cli, times(2)).printContainerLogsFromRunningApplication( verify(cli, times(2)).printContainerLogsFromRunningApplication(
any(Configuration.class), any(ContainerLogsRequest.class), any(Configuration.class), logsRequestCaptor.capture(),
any(LogCLIHelpers.class)); any(LogCLIHelpers.class));
// Verify that the log-type is *
List<ContainerLogsRequest> capturedRequests =
logsRequestCaptor.getAllValues();
Assert.assertEquals(2, capturedRequests.size());
List<String> logTypes0 = capturedRequests.get(0).getLogTypes();
List<String> logTypes1 = capturedRequests.get(1).getLogTypes();
Assert.assertEquals(".*", logTypes0.get(0));
Assert.assertEquals(".*", logTypes1.get(0));
} }
@Test (timeout = 15000) @Test (timeout = 15000)