package rocks.inspectit.server.diagnosis.engine.session; import static com.google.common.base.Preconditions.checkNotNull; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import org.apache.commons.collections.CollectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import rocks.inspectit.server.diagnosis.engine.DiagnosisEngineConfiguration; import rocks.inspectit.server.diagnosis.engine.DiagnosisEngineException; import rocks.inspectit.server.diagnosis.engine.IDiagnosisEngine; import rocks.inspectit.server.diagnosis.engine.rule.FireCondition; import rocks.inspectit.server.diagnosis.engine.rule.RuleDefinition; import rocks.inspectit.server.diagnosis.engine.rule.RuleInput; import rocks.inspectit.server.diagnosis.engine.rule.RuleOutput; import rocks.inspectit.server.diagnosis.engine.rule.factory.Rules; import rocks.inspectit.server.diagnosis.engine.rule.store.DefaultRuleOutputStorage; import rocks.inspectit.server.diagnosis.engine.rule.store.IRuleOutputStorage; import rocks.inspectit.server.diagnosis.engine.session.exception.SessionException; import rocks.inspectit.server.diagnosis.engine.tag.Tag; import rocks.inspectit.server.diagnosis.engine.tag.Tags; /** * The Session is the core class of the {@link IDiagnosisEngine}. It executes all rules, stores * interim results, and prepares the final results by utilizing the {@link ISessionResultCollector}. * To ensure a proper execution it defines an explicit life cycle. To ensure a compliance with the * life cycle the current state of session is held in a {@link State} object. Additional runtime * information is stored in a {@link SessionContext}. * * * The lifecycle of a session is as follows: * <ul> * <li>{@link #activate(Object, SessionVariables)} Prepares the session for the next execution.</li> * <li>{@link #process()} Executes all rules until no more rule can be executed.</li> * <li>{@link #collectResults()} Invokes the {@link ISessionResultCollector} to gather and provide * results.</li> * <li>{@link #passivate()} Cleans the session and removes all data from the latest execution. The * session is now ready to be reactivated by invoking {@link #activate(Object)} again.</li> * <li>{@link #destroy()} Destroys the session. Session can not be revived anymore.</li> * </ul> * * <p> * In order to facilitate compliance with the life cycle it is strongly recommended to use the * provided {@link SessionPool} in combination with {@link ExecutorService}. * <p> * * <pre> * { * SessionPool<String, DefaultSessionResult<String>> pool = new SessionPool<>(configuration); * Session<String, DefaultSessionResult<String>> session = pool.borrowObject("Input", new SessionVariables()); * DefaultSessionResult<String> result = executorService.submit(input).get(); * } * </pre> * * @param <I> * The type of input to be analyzed. * @param <R> * The expected result type. * @author Claudio Waldvogel, Alexander Wert * @see IDiagnosisEngine * @see ISessionResultCollector * @see SessionContext */ public final class Session<I, R> implements Callable<R> { /** * Constant for empty session variables. */ public static final Map<String, Object> EMPTY_SESSION_VARIABLES = Collections.emptyMap(); /** * The slf4j Logger. */ private static final Logger LOG = LoggerFactory.getLogger(Session.class); /** * The current state of this Session. A session can enter 6 states: * <p> * NEW, ACTIVATED, PROCESSED, PASSIVATED, DESTROYED, FAILURE * * @see State */ private State state = State.NEW; /** * The {@link SessionContext} which is associated with this Session. It is created, executed, * and destroyed in accordance with the session itself. */ private SessionContext<I> sessionContext; /** * The {@link ISessionResultCollector} which produces the results of a session execution. The * {@link ISessionResultCollector} is configurable from the {@link DiagnosisEngineConfiguration} * . */ private ISessionResultCollector<I, R> resultCollector; // ------------------------------------------------------------- // Methods: Construction // ------------------------------------------------------------- /** * Constructor. * * @param ruleDefinitions * Set of {@link RuleDefinition} instances. * @param sessionResultCollector * The {@link ISessionResultCollector} for the results of the session. */ public Session(Set<RuleDefinition> ruleDefinitions, ISessionResultCollector<I, R> sessionResultCollector) { this(ruleDefinitions, sessionResultCollector, new DefaultRuleOutputStorage()); } /** * Constructor. * * @param ruleDefinitions * Set of {@link RuleDefinition} instances. * @param sessionResultCollector * The {@link ISessionResultCollector} for the results of the session. * @param storage * The storage for the rule outputs. */ public Session(Set<RuleDefinition> ruleDefinitions, ISessionResultCollector<I, R> sessionResultCollector, IRuleOutputStorage storage) { checkNotNull(ruleDefinitions); checkNotNull(storage); checkNotNull(sessionResultCollector); this.sessionContext = new SessionContext<>(ruleDefinitions, storage); this.resultCollector = sessionResultCollector; } /** * Protected Constructor for testing purposes. * * @param sessionContext * The {@link SessionContext}. * @param resultCollector * The {@link ISessionResultCollector}. */ protected Session(SessionContext<I> sessionContext, ISessionResultCollector<I, R> resultCollector) { this.sessionContext = sessionContext; this.resultCollector = resultCollector; } // ------------------------------------------------------------- // Interface Implementation: Callable // ------------------------------------------------------------- /** * Executes the Session. Invocation is exclusively possible if {@link Session} is in ACTIVATED * state, any other state forces a {@link SessionException}. Processing is enabled inserting a * initial RuleOutput to the {@link IRuleOutputStorage} which will act as input to further * rules. If processing completes without errors the {@link Session} enters PROCESSED state. In * any case of error it enters FAILURE state. * * @throws Exception * If Session lifecycle is in an invalid state. * @return The diagnosis result. */ @Override public R call() throws Exception { // Processes and collect results. // If a Session is used as Callable this call might horribly fail if sessions are not // retrieved from SessionPool and a sessions lifeCycle is neglected. But we have not chance // to activate a // session internally due to missing input information. So simply fail switch (state) { case ACTIVATED: sessionContext.getStorage().store(Rules.triggerRuleOutput(sessionContext.getInput())); doProcess(); state = State.PROCESSED; break; default: throw new SessionException("Session can not enter process stated from: " + state + " state. Ensure that Session is in ACTIVATED state before processing."); } return resultCollector.collect(sessionContext); } // ------------------------------------------------------------- // Methods: LifeCycle -> reflects the life cycle of a // org.apache.commons.pool.impl.GenericObjectPool // ------------------------------------------------------------- /** * Tries to activate the Session for the given input object and SessionVariables. Activation * means that state is changes to ACTIVATED and SessionContext is activated as well. Activation * is only possible if the session is currently in NEW or PASSIVATED state, any other state * forces a SessionException. * * @param input * The input to be processed. * @param variables * The session variables to be used. * @return The Session itself * @throws SessionException * If Session lifecycle is in an invalid state. */ public Session<I, R> activate(I input, Map<String, ?> variables) throws SessionException { switch (state) { case NEW: case PASSIVATED: // All we need to do is to reactivate the SessionContext sessionContext.activate(input, variables); state = State.ACTIVATED; break; case DESTROYED: case FAILURE: case ACTIVATED: case PROCESSED: default: throw new SessionException("Session can not enter ACTIVATED state from: " + state + " state. Ensure Session is in NEW or PASSIVATED state when activating."); } return this; } /** * Cleans the {@link Session} by means of cleaning the {@link SessionContext} and removing all * stale data. Valid transitions to PASSIVATED state are from PROCESSED and Failure. * * @return The Session itself/ */ public Session<I, R> passivate() { if (!State.PROCESSED.equals(state)) { LOG.warn("Not processed Session gets passivated!"); } // Passivate is always possible. Also it is important to passivate the Session in any case. // This ensures a reusable clean Session. sessionContext.passivate(); state = State.PASSIVATED; return this; } /** * Destroys this session. If the session was not yet passivated, it will be passivated in * advance. While the session is destroyed, the ExecutorService is shutdown and the * SessionContext is destroyed. After the session is destroyed it is unusable! * */ public void destroy() { switch (state) { case PROCESSED: // We can destroy the session but it was not yet passivated. To stay in sync with the // state lifeCycle we passivate first passivate(); break; case DESTROYED: LOG.warn("Failed destroying session. Session has already been destroyed!"); return; case NEW: case ACTIVATED: LOG.warn("Session is destroy before it was processed."); break; case FAILURE: case PASSIVATED: default: break; } state = State.DESTROYED; sessionContext = null; // NOPMD } // ------------------------------------------------------------- // Methods: Internals // ------------------------------------------------------------- /** * Internal processing routine to execute all rules. This methods blocks as long as further * rules can be executed. If this method returns it is assured that all possible rules are * executed and all possible results are available in the IRuleOutputStorage. * * @throws SessionException * If processing fails. * */ private void doProcess() throws SessionException { // identify initial set of rules that can be executed Collection<RuleDefinition> nextRules = findNextRules(sessionContext.getStorage().getAvailableTagTypes(), sessionContext.getRuleSet()); // execute rules as long as there are any in the pipe while (!nextRules.isEmpty()) { boolean anyRuleExecuted = false; // execute all rules in the pipe for (RuleDefinition ruleDef : nextRules) { try { // Collect all available inputs for the selected rule Collection<RuleInput> inputs = collectInputs(ruleDef, sessionContext.getStorage()); // Filter out all inputs that have already been processed by the selected rule // in the past inputs = filterProcessedInputs(sessionContext.getExecutions(), ruleDef, inputs); if (CollectionUtils.isNotEmpty(inputs)) { // Execute selected rule for each input and collect corresponding rule // outputs Collection<RuleOutput> outputs = ruleDef.execute(inputs, Session.this.sessionContext.getSessionVariables()); // store results sessionContext.getStorage().store(outputs); anyRuleExecuted = true; // track execution for subsequent checks for (RuleInput ruleInput : inputs) { sessionContext.addExecution(ruleDef, ruleInput); } } } catch (DiagnosisEngineException ex) { failure(ex); } } // Continue looping only if new rule executions are available. // Identify next rules that have been activated by the results of the previously // executed set of rules. if (anyRuleExecuted) { nextRules = findNextRules(sessionContext.getStorage().getAvailableTagTypes(), sessionContext.getRuleSet()); } else { break; } } } /** * Marks the session as failed and passivates it. * * @param cause * The root cause of failure. * @throws SessionException * If diagnosis fails with errors. */ private void failure(DiagnosisEngineException cause) throws SessionException { // enter failure state state = State.FAILURE; // ensure that Session gets passivated to enable reuse passivate(); // Propagate the cause of failure throw new SessionException("Diagnosis Session failed with error(s)", cause); } /** * Filters the {@link RuleInput}s that have already been processed for the given * {@link RuleDefinition} before. * * @param executions * Map of rule definitions and previously executed rule inputs. * @param ruleDef * {@link RuleDefinition} to check against. * @param inputs * a collection of {@link RuleInput}s to be filtered. {@link RuleInput}s that have * already been processed for the given {@link RuleDefinition} before are removed * from this collection. * @return a filtered collection of rule inputs. */ Collection<RuleInput> filterProcessedInputs(Multimap<RuleDefinition, RuleInput> executions, RuleDefinition ruleDef, Collection<RuleInput> inputs) { if (CollectionUtils.isEmpty(inputs)) { return Collections.emptyList(); } ArrayList<RuleInput> filteredList = new ArrayList<>(); for (RuleInput input : inputs) { if (!executions.containsEntry(ruleDef, input)) { filteredList.add(input); } } return filteredList; } /** * Utility method to determine the next executable rules. The next rules are determined by * comparing all, so far collected, types of tags in {@link IRuleOutputStorage} and the * {@link FireCondition} of each {@link RuleDefinition}. * * @param availableTagTypes * Set of strings determining the available tag types for input of potential next * rules. * @param ruleDefinitions * Set of available rule definitions. * * @return Collection of {@link RuleDefinition}s. * @see rocks.inspectit.server.diagnosis.engine.rule.FireCondition * @see RuleDefinition * @see IRuleOutputStorage */ Collection<RuleDefinition> findNextRules(Set<String> availableTagTypes, Set<RuleDefinition> ruleDefinitions) { Set<RuleDefinition> nextRules = new HashSet<>(); Iterator<RuleDefinition> iterator = ruleDefinitions.iterator(); while (iterator.hasNext()) { RuleDefinition rule = iterator.next(); if (rule.getFireCondition().canFire(availableTagTypes)) { nextRules.add(rule); } } return nextRules; } /** * Collects all available inputs for a single {@link RuleDefinition} from the passed storage. * Each RuleInput is equivalent to an execution of the RuleInput. * * @param definition * The {@link RuleDefinition} to be executed. * @param storage * The {@link IRuleOutputStorage} to derive the {@link RuleInput} instances from. * @return A Collection of RuleInputs * @see RuleInput * @see RuleDefinition */ Collection<RuleInput> collectInputs(RuleDefinition definition, IRuleOutputStorage storage) { // retrieve all tag types that are required to execute the passed rule definition Set<String> requiredInputTags = definition.getFireCondition().getTagTypes(); // Find all outputs that match the required tag types Collection<RuleOutput> leafOutputs = storage.findLatestResultsByTagType(requiredInputTags); Set<RuleInput> inputs = Sets.newHashSet(); // A single rule can produce n inputs (of the same tag type) for the next rule. Each // embedded tag in ruleOutput.getTags() will be // reflected in a new RuleInput // Although this is an O(n²) loop the iterated lists are expected to be rather short. // Also the nested while loop is expected to be very short. ruleOutputLoop: for (RuleOutput output : leafOutputs) { for (Tag leafTag : output.getTags()) { Collection<Tag> tags = Tags.unwrap(leafTag, requiredInputTags); if (tags.size() == requiredInputTags.size()) { // Create and store a new RuleInput inputs.add(new RuleInput(leafTag, tags)); } else { // If any of the leaf tags of an ruleOutput does not result in a valid unwrapped // tag set, then none of the leaf tags of THIS ruleOutput can produce a valid // unwrapped tag set. So we can continue with the next ruleOutput. continue ruleOutputLoop; } } } return inputs; } // ------------------------------------------------------------- // Methods: Accessors // ------------------------------------------------------------- /** * Gets {@link #state}. * * @return {@link #state} */ public State getState() { return state; } /** * Gets {@link #sessionContext}. * * @return {@link #sessionContext} */ public SessionContext<I> getSessionContext() { return sessionContext; } // ------------------------------------------------------------- // Inner classes // ------------------------------------------------------------- /** * Internal enum representing the current state of this session. */ enum State { /** * The initial State of each Session. */ NEW, /** * The state as soon as an {@link Session} gets activated. This state can be entered from * <code>NEW</code> and <code>PASSIVATED</code> states. */ ACTIVATED, /** * A {@link Session} enters the <code>PROCESSED</code> state after all applicable rules were * executed. */ PROCESSED, /** * An {@link Session} can enter the <code>PASSIVATED</code> state only from * <code>PROCESSED</code> state. <code>PASSIVATED</code> is the only state which enables a * transition back to <code>ACTIVATED</code>. */ PASSIVATED, /** * {@link Session} is destroyed and not longer usable. */ DESTROYED, /** * {@link Session} encountered an error and is in a failure state. */ FAILURE } }