package cz.cuni.mff.d3s.been.benchmarkapi; import java.io.InputStream; import java.util.Collection; import java.util.Map; import java.util.concurrent.TimeoutException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import cz.cuni.mff.d3s.been.core.benchmark.ResubmitHistoryItem; import cz.cuni.mff.d3s.been.core.jaxb.BindingParser; import cz.cuni.mff.d3s.been.core.jaxb.XSD; import cz.cuni.mff.d3s.been.core.task.*; import cz.cuni.mff.d3s.been.mq.MessagingException; import cz.cuni.mff.d3s.been.taskapi.Task; import cz.cuni.mff.d3s.been.util.JsonException; /** * This class is an abstract base class for benchmarks. To implement a benchmark * (which can generate task contexts) extend this class an implement the * abstract methods. The implementation then serves as a generator task for the * benchmark. * * Benchmark generator tasks are subclasses of {@link Task}, which means they * are submitted, scheduled and run the same way as any other task. However, the * generator task can (and/or are supposed) to run for a very long time, so they * support "resubmits". When the generator task fails for any reason, it will * get resubmit automatically. For correct behavior the generator should store * its internal state only using the {@link #storageSet} method, which will * persist the state in the cluster and retrieve it before resubmitting. * * When extending this class to implement a benchmark, you have to implement the * methods {@link #generateTaskContext}, {@link #onResubmit} and * {@link #onTaskContextFinished}. The later two are only "notify" methods and * can be implemented as no-operations. The first method ( * {@link #generateTaskContext}), however, serves as the main method that * "generates" all task contexts in the benchmark. This method will be called by * this class and is supposed to return a newly created context every time when * the benchmark can generate any. The method can block/wait when it does not * yet have any context to generate. When the method returns null, the benchmark * is ended. * * @author Kuba Brecka */ public abstract class Benchmark extends Task { /** Logging */ private static final Logger log = LoggerFactory.getLogger(Benchmark.class); /** Local storage which is synced to been */ private Map<String, String> storage; /** requestor that communicates with the host runtime for benchmark requests */ private BenchmarkRequestor benchmarkRequestor; /** * Method which drives the benchmark by generating contexts. The context is * submitted and run by BEEN. Subclasses are supposed to implement this * method, which will be called automatically by this class. The * implementation can wait/block when it doesn't have any content to run. * * To indicate end of the benchmark null must be returned. * * @return TaskContextDescriptor to be submitted, or null to indicate end of * the benchmark * @throws BenchmarkException * when an error has occurred in the generator task */ public abstract TaskContextDescriptor generateTaskContext() throws BenchmarkException; /** * Event handler that notifies the generator task that it has been resubmitted * after an error. */ public abstract void onResubmit(); /** * Event handler that notifies the generator task that a context with the * specified ID has finished with the specified state. * * @param taskContextId * ID of the context that has finished * @param state * state with which the context has finished */ public abstract void onTaskContextFinished(String taskContextId, TaskContextState state); /** * Retrieves a value for the given key from benchmark-wide storage. If no * value is found, null is returned instead. * * The values are preserved among runs of the generator for the same * benchmark. * * @param key * identification of the value * @return benchmark-wide value for the given key if it exists, null otherwise */ protected String storageGet(String key) { return storage.get(key); } /** * Retrieves a value for the given key from benchmark wide storage. If no * value is found, defaultValue is returned instead. * * The values are preserved among runs of the generator for the same * benchmark. * * @param key * identification of the value * @param defaultValue * value returned if no benchmark-value is found * @return benchmark-wide value for the given key if it exists, defaultValue * otherwise */ protected String storageGet(String key, String defaultValue) { return storage.get(key) != null ? storage.get(key) : defaultValue; } /** * Stores a value with the given key to Benchmark-wide storage. * * The values are preserved among runs of the generator for the same benchmark * and can be retrieved with {@link Benchmark#storageGet(String)} or * {@link Benchmark#storageGet(String, String)} * * @param key * identification of the value * @param value * value to store to the benchmark-wide storage */ protected void storageSet(String key, String value) { storage.put(key, value); } @Override public void run(String[] args) throws BenchmarkException, MessagingException { benchmarkRequestor = BenchmarkRequestor.create(); try { processContexts(); } catch (TimeoutException | JsonException e) { throw new BenchmarkException("Benchmark has encountered an internal exception.", e); } finally { benchmarkRequestor.close(); } } /** * Checks whether this generator task is the original one or whether it is * resubmitted. * * @return true if this generator is resubmitted, false otherwise */ private boolean isResubmittedBenchmark() { try { Collection<ResubmitHistoryItem> historyItems; historyItems = benchmarkRequestor.resubmitHistoryRetrieve(getBenchmarkId()); return historyItems.size() > 0; } catch (TimeoutException e) { throw new RuntimeException(e.getMessage(), e); } } /** * Returns the contexts ID of the currently running task context within this * benchmark. If there is no running task context, returns null. If there is * more than one running task contexts, this method throws an exception, * because this is an invalid state. * * @return ID of the currently running context or null if there is no such * context * @throws TimeoutException * when the request for listing contexts fails * @throws JsonException * when the request for listing contexts returns an unparsable * response */ private String benchmarkRunningContext() throws TimeoutException, JsonException { TaskContextStateInfo info = benchmarkRequestor.containedContextsRetrieve(getBenchmarkId()); int numberOfRunningContexts = 0; String contextId = null; for (TaskContextStateInfo.Item item : info.items) { if (item.state == TaskContextState.RUNNING) { contextId = item.taskContextId; numberOfRunningContexts++; } } if (numberOfRunningContexts > 1) { throw new RuntimeException(String.format("The benchmark %s has more than one running context!", getBenchmarkId())); } return contextId; } /** * The main generator task loop. This method first checks if it is being * resubmitted and performs additional checks and notifications when it is * resubmitted. Then it enters a loop that asks the subclass to provide a new * task context, submits it and waits for it to finish. The loop is ended when * the {@link #generateTaskContext} method returns null. * * The extra handling for resubmitted generators means that it first waits for * a (possibly) already running task context before entering the loop. * * @throws TimeoutException * when the host runtime requestor times out * @throws JsonException * when the requestor returns an unparsable response */ private void processContexts() throws TimeoutException, JsonException, BenchmarkException { try { this.storage = benchmarkRequestor.storageRetrieve(getBenchmarkId()); } catch (TimeoutException e) { throw new RuntimeException(e.getMessage(), e); } if (isResubmittedBenchmark()) { this.onResubmit(); String taskContextId = benchmarkRunningContext(); if (taskContextId != null) { TaskContextState state = benchmarkRequestor.contextWait(taskContextId); log.trace("Task context '{}' finished with state {}.", taskContextId, state); this.onTaskContextFinished(taskContextId, state); } } while (true) { TaskContextDescriptor taskContextDescriptor; taskContextDescriptor = generateTaskContext(); if (taskContextDescriptor == null) { return; } // First save the local storage, before submitting the context benchmarkRequestor.storagePersist(getBenchmarkId(), storage); log.trace("Submitting task context descriptor."); String taskContextId = benchmarkRequestor.contextSubmit(taskContextDescriptor, getBenchmarkId()); log.trace("Task context descriptor with ID '{}' submitted", taskContextId); TaskContextState state = benchmarkRequestor.contextWait(taskContextId); log.trace("Task context '{}' finished with state {}.", taskContextId, state); this.onTaskContextFinished(taskContextId, state); } } /** * Creates a task context descriptor from a XML descriptor file contained as a * resource for the current class. * * This method is deprecated, use {@link ContextBuilder} instead. * * @param resourceName * the resource name of the XML descriptor * @return a newly create task context * @throws BenchmarkException * when the descriptor cannot be created */ @Deprecated public TaskContextDescriptor getTaskContextFromResource(String resourceName) throws BenchmarkException { try { InputStream inputStream = this.getClass().getResourceAsStream(resourceName); TaskContextDescriptor taskContextDescriptor; BindingParser<TaskContextDescriptor> bindingComposer = XSD.TASK_CONTEXT_DESCRIPTOR.createParser(TaskContextDescriptor.class); taskContextDescriptor = bindingComposer.parse(inputStream); return taskContextDescriptor; } catch (Exception e) { throw new BenchmarkException("Cannot read resource.", e); } } /** * Sets a property inside the task context descriptor with the specified key * to the specified value. * * This method is deprecated, use {@link ContextBuilder} instead. * * @param descriptor * the task descriptor to modify * @param key * the key of the property to set * @param value * the value of the property to set */ @Deprecated public void setTaskContextProperty(TaskContextDescriptor descriptor, String key, String value) { Property p = new Property(); p.setName(key); p.setValue(value); if (!descriptor.isSetProperties()) descriptor.setProperties(new Properties()); descriptor.getProperties().getProperty().add(p); } }