/* * Copyright 2012 NGDATA nv * * 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.lilyproject.tools.tester; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Option; import org.apache.commons.cli.OptionBuilder; import org.apache.commons.io.IOUtils; import org.apache.zookeeper.KeeperException; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.JsonParser; import org.codehaus.jackson.JsonToken; import org.codehaus.jackson.node.ObjectNode; import org.joda.time.DateTime; import org.lilyproject.cli.OptionUtil; import org.lilyproject.client.NoServersException; import org.lilyproject.repository.api.FieldType; import org.lilyproject.repository.api.QName; import org.lilyproject.repository.api.RecordType; import org.lilyproject.repository.api.RepositoryException; import org.lilyproject.testclientfw.BaseRepositoryTestTool; import org.lilyproject.testclientfw.Util; import org.lilyproject.tools.import_.cli.DefaultImportListener; import org.lilyproject.tools.import_.cli.ImportConflictException; import org.lilyproject.tools.import_.cli.ImportException; import org.lilyproject.tools.import_.cli.JsonImport; import org.lilyproject.tools.import_.json.JsonFormatException; import org.lilyproject.tools.import_.json.QNameConverter; import org.lilyproject.util.io.Closer; import org.lilyproject.util.json.JsonFormat; import org.lilyproject.util.json.JsonUtil; public class Tester extends BaseRepositoryTestTool { private Option configFileOption; private Option dumpSampleConfigOption; private RecordType recordType; private int maximumRunTime; private int maximumFailures; private String failuresFileName; private PrintStream errorStream; private long startTime; private int failureCount = 0; private Option iterationsOption; private int nrOfIterations; private TestActionFactory testActionFactory = new TestActionFactory(); private List<TestAction> workersTestActions[] = null; private List<JsonNode> recordSpacesConfig = new ArrayList<JsonNode>(); private List<RecordSpaces> workersRecordSpaces = null; private Map<QName, TestRecordType> recordTypes = new HashMap<QName, TestRecordType>(); private Map<QName, TestFieldType> fieldTypes = new HashMap<QName, TestFieldType>(); private JsonImport jsonImport; public static void main(String[] args) throws Exception { new Tester().start(args); } @Override protected String getCmdName() { return "lily-tester"; } @Override protected String getVersion() { return org.lilyproject.util.Version.readVersion("org.lilyproject", "lily-tester"); } @Override @SuppressWarnings("static-access") public List<Option> getOptions() { List<Option> options = super.getOptions(); configFileOption = OptionBuilder .withArgName("config.json") .hasArg() .withDescription("Test tool configuration file") .withLongOpt("config") .create("c"); options.add(configFileOption); dumpSampleConfigOption = OptionBuilder .withDescription("Dumps a sample configuration to standard out") .withLongOpt("dump-sample-config") .create("d"); options.add(dumpSampleConfigOption); iterationsOption = OptionBuilder .withArgName("iterations") .hasArg() .withDescription("Number of times to run the scenario") .withLongOpt("iterations") .create("i"); options.add(iterationsOption); return options; } @Override public int run(CommandLine cmd) throws Exception { int result = super.run(cmd); if (result != 0) { return result; } if (cmd.hasOption(dumpSampleConfigOption.getOpt())) { return dumpSampleConfig(); } if (!cmd.hasOption(configFileOption.getOpt())) { printHelp(); return 1; } setupLily(); setupMetrics(); String configFileName = cmd.getOptionValue(configFileOption.getOpt()); workersRecordSpaces = new ArrayList<RecordSpaces>(workers); workersTestActions = new ArrayList[workers]; for (int i = 0; i < workers; i++) { workersTestActions[i] = new ArrayList<TestAction>(); } InputStream is = new FileInputStream(configFileName); loadConfig(is); is.close(); try { System.out.println("Running tests..."); System.out.println("Tail the output files if you wonder what is happening."); nrOfIterations = OptionUtil.getIntOption(cmd, iterationsOption, 1000); test(); } finally { closeStreams(); } finishMetrics(); System.out.println("Test done."); return 0; } private void loadConfig(InputStream is) throws IOException, JsonFormatException, RepositoryException, ImportConflictException, ImportException, InterruptedException, SecurityException, IllegalArgumentException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { jsonImport = new JsonImport(table, repository, new DefaultImportListener()); JsonParser jp = JsonFormat.JSON_FACTORY_NON_STD.createJsonParser(is); JsonToken current; current = jp.nextToken(); if (current != JsonToken.START_OBJECT) { System.out.println("Error: expected object node as root of the input. Giving up."); return; } while (jp.nextToken() != JsonToken.END_OBJECT) { String fieldName = jp.getCurrentName(); current = jp.nextToken(); // move from field name to field value if (fieldName.equals("namespaces")) { if (current == JsonToken.START_OBJECT) { jsonImport.readNamespaces((ObjectNode) jp.readValueAsTree()); } else { System.out.println("Error: namespaces property should be an object. Skipping."); jp.skipChildren(); } } else if (fieldName.equals("failuresFile")) { if (current == JsonToken.VALUE_STRING) { openStreams(jp.getText()); } } else if (fieldName.equals("fieldTypes")) { if (current == JsonToken.START_ARRAY) { while (jp.nextToken() != JsonToken.END_ARRAY) { importFieldType(jp.readValueAsTree()); } } else { System.out.println("Error: fieldTypes property should be an array. Skipping."); jp.skipChildren(); } } else if (fieldName.equals("recordTypes")) { if (current == JsonToken.START_ARRAY) { while (jp.nextToken() != JsonToken.END_ARRAY) { importRecordType(jp.readValueAsTree()); } } else { System.out.println("Error: recordTypes property should be an array. Skipping."); jp.skipChildren(); } } else if (fieldName.equals("recordSpaces")) { if (current == JsonToken.START_ARRAY) { while (jp.nextToken() != JsonToken.END_ARRAY) { recordSpacesConfig.add(jp.readValueAsTree()); } for (int i = 0; i < workers; i++) { workersRecordSpaces.add(new RecordSpaces(recordSpacesConfig)); } } else { System.out.println("Error: recordSpaces property should be an array. Skipping."); jp.skipChildren(); } } else if (fieldName.equals("scenario")) { if (current == JsonToken.START_ARRAY) { while (jp.nextToken() != JsonToken.END_ARRAY) { JsonNode actionNode = jp.readValueAsTree(); prepareAction(actionNode); } } else { System.out.println("Error: recordSpaces property should be an array. Skipping."); jp.skipChildren(); } } else if (fieldName.equals("stopConditions")) { if (current == JsonToken.START_OBJECT) { readStopConditions((ObjectNode) jp.readValueAsTree()); } else { System.out.println("Error: stopConditions property should be an object. Skipping."); jp.skipChildren(); } } } } private void importFieldType(JsonNode fieldTypeNode) throws RepositoryException, ImportConflictException, ImportException, JsonFormatException, InterruptedException { int times = 0; JsonNode timesNode = fieldTypeNode.get("times"); if (timesNode != null) { times = timesNode.getIntValue(); } JsonNode propertiesNode = fieldTypeNode.get("properties"); if (times == 0) { FieldType importFieldType = jsonImport.importFieldType(fieldTypeNode); fieldTypes.put(importFieldType.getName(), new TestFieldType(importFieldType, table, repository, propertiesNode)); } else { List<FieldType> importFieldTypes = jsonImport.importFieldTypes(fieldTypeNode, times); for (FieldType importFieldType : importFieldTypes) { fieldTypes.put(importFieldType.getName(), new TestFieldType(importFieldType, table, repository, propertiesNode)); } } } private void importRecordType(JsonNode recordTypeNode) throws JsonFormatException, RepositoryException, ImportException, InterruptedException { String recordTypeName = JsonUtil.getString(recordTypeNode, "name"); QName recordTypeQName = QNameConverter.fromJson(recordTypeName, jsonImport.getNamespaces()); recordType = repository.getTypeManager().newRecordType(recordTypeQName); TestRecordType testRecordType = new TestRecordType(); // Fields for (JsonNode fieldNode : recordTypeNode.get("fields")) { String fieldName = JsonUtil.getString(fieldNode, "name"); int times = 0; JsonNode timesNode = fieldNode.get("times"); if (timesNode != null) { times = timesNode.getIntValue(); } if (times == 0) { TestFieldType fieldType = fieldTypes .get(QNameConverter.fromJson(fieldName, jsonImport.getNamespaces())); recordType.addFieldTypeEntry(fieldType.getFieldType().getId(), false); testRecordType.addFieldType(fieldType); } else { for (int i = 0; i < times; i++) { TestFieldType fieldType = fieldTypes.get(QNameConverter.fromJson(fieldName + i, jsonImport.getNamespaces())); recordType.addFieldTypeEntry(fieldType.getFieldType().getId(), false); testRecordType.addFieldType(fieldType); } } } testRecordType.setRecordType(typeManager.createOrUpdateRecordType(recordType)); recordTypes.put(recordType.getName(), testRecordType); } private int dumpSampleConfig() throws IOException { InputStream is = getClass().getClassLoader().getResourceAsStream("org/lilyproject/tools/tester/config.json"); try { IOUtils.copy(is, System.out); } finally { Closer.close(is); } return 0; } private void prepareAction(JsonNode actionNode) throws IOException, SecurityException, IllegalArgumentException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { final RoundRobinPrefixGenerator roundRobinPrefixGenerator = actionNode.get("recordIdPrefixNbrOfChars") != null ? new RoundRobinPrefixGenerator(actionNode.get("recordIdPrefixNbrOfChars").getIntValue()) : null; for (int i = 0; i < workers; i++) { new RecordSpaces(recordSpacesConfig); TestActionContext testActionContext = new TestActionContext(recordTypes, fieldTypes, jsonImport.getNamespaces(), workersRecordSpaces.get(i), table, repository, metrics, errorStream, roundRobinPrefixGenerator); TestAction testAction = testActionFactory.getTestAction(actionNode, testActionContext); workersTestActions[i].add(testAction); } } private void readStopConditions(JsonNode stopConditions) { maximumRunTime = JsonUtil.getInt(stopConditions, "maximumRunTime"); maximumFailures = JsonUtil.getInt(stopConditions, "maximumFailures"); } private void createSchema(JsonNode configNode) throws IOException, RepositoryException, ImportConflictException, ImportException, JsonFormatException, NoServersException, InterruptedException, KeeperException { JsonImport jsonImport = new JsonImport(table, repository, new DefaultImportListener()); // Namespaces ObjectNode namespacesNode = JsonUtil.getObject(configNode, "namespaces", null); if (namespacesNode != null) { jsonImport.readNamespaces(namespacesNode); } // Fields JsonNode fieldTypesNode = configNode.get("fieldTypes"); if (fieldTypesNode != null && fieldTypesNode.isArray()) { for (JsonNode fieldTypeNode : fieldTypesNode) { FieldType importFieldType = jsonImport.importFieldType(fieldTypeNode); JsonNode propertiesNode = fieldTypeNode.get("properties"); fieldTypes.put(importFieldType.getName(), new TestFieldType(importFieldType, table, repository, propertiesNode)); } } // Record type JsonNode recordTypesNode = configNode.get("recordTypes"); if (recordTypesNode != null && recordTypesNode.isArray()) { for (JsonNode recordTypeNode : recordTypesNode) { String recordTypeName = JsonUtil.getString(recordTypeNode, "name"); QName recordTypeQName = QNameConverter.fromJson(recordTypeName, jsonImport.getNamespaces()); recordType = repository.getTypeManager().newRecordType(recordTypeQName); TestRecordType testRecordType = new TestRecordType(); // Fields for (JsonNode fieldNode : recordTypeNode.get("fields")) { String fieldName = JsonUtil.getString(fieldNode, "name"); TestFieldType fieldType = fieldTypes.get(fieldName); recordType.addFieldTypeEntry(fieldType.getFieldType().getId(), false); testRecordType.addFieldType(fieldType); } testRecordType.setRecordType(typeManager.createOrUpdateRecordType(recordType)); recordTypes.put(recordType.getName(), testRecordType); } } } private void openStreams(String failuresFileName) throws IOException { errorStream = new PrintStream(Util.getOutputFileRollOldOne(failuresFileName)); errorStream.println(new DateTime() + " Opening file"); } private void closeStreams() { errorStream.println(new DateTime() + " Closing file"); Closer.close(errorStream); } private void test() throws InterruptedException { startTime = System.currentTimeMillis(); HashSet<Thread> threads = new HashSet<Thread>(workers); for (int i = 0; i < workers; i++) { threads.add(new WorkerThread(workersTestActions[i])); } for (Thread thread : threads) { thread.start(); } for (Thread thread : threads) { thread.join(); } } private class WorkerThread extends Thread { private final List<TestAction> testActions; WorkerThread(List<TestAction> testActions) { this.testActions = testActions; } @Override public void run() { for (int j = 0; j < nrOfIterations; j++) { for (TestAction testAction : testActions) { incFailureCount(testAction.run()); } if (checkStopConditions()) { return; } } } } private synchronized void incFailureCount(int amount) { failureCount = failureCount + amount; } private synchronized int getFailureCount() { return failureCount; } private boolean checkStopConditions() { if (getFailureCount() >= maximumFailures) { System.out.println("Stopping because maximum number of failures is reached: " + maximumFailures); return true; } int ran = (int) Math.floor((System.currentTimeMillis() - startTime) / 1000 / 60); if (ran >= maximumRunTime) { System.out.println("Stopping because maximum running time is reached: " + maximumRunTime + " minutes."); return true; } return false; } }