/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.component.workflow.execution.internal; import java.util.ArrayList; import java.util.Deque; import java.util.LinkedList; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.CountDownLatch; import de.rcenvironment.core.communication.api.CommunicationService; import de.rcenvironment.core.communication.management.WorkflowHostService; import de.rcenvironment.core.component.execution.api.ConsoleRow; import de.rcenvironment.core.component.workflow.execution.api.ConsoleModelSnapshot; import de.rcenvironment.core.component.workflow.execution.api.ConsoleRowFilter; import de.rcenvironment.core.component.workflow.execution.api.ConsoleRowLogService; import de.rcenvironment.core.component.workflow.execution.api.ConsoleRowModelService; import de.rcenvironment.core.component.workflow.execution.api.GenericSubscriptionManager; import de.rcenvironment.core.component.workflow.execution.impl.ConsoleSubscriptionEventProcessor; import de.rcenvironment.core.toolkitbridge.transitional.ConcurrencyUtils; import de.rcenvironment.toolkit.modules.concurrency.api.TaskDescription; /** * Default {@link ConsoleRowModelService} implementation. * * @author Doreen Seider (initial version) * @author Robert Mischke (current) */ public class ConsoleRowModelServiceImpl implements ConsoleRowModelService, ConsoleRowProcessor { /** * The maximum total number of rows to keep. */ // TODO check limiting details; sufficient or add more detail? count chars instead? private static final int MAX_UNFILTERED_ROWS_RETENTION = 35000; /** * The maximum number of (filtered) rows to return in a snapshot. */ private static final int MAX_SNAPSHOT_SIZE = 25000; // TODO make non-static private static GenericSubscriptionManager subscriptionManager; // TODO make non-static private static CountDownLatch initialSubscriptionLatch; private Deque<ConsoleRow> allRows; /** * Note: The current concept is based on a single client view using this model; if required, this could be changed to a map of * registered filters. */ private ConsoleRowFilter currentFilter; private Deque<ConsoleRow> filteredRows; private SortedSet<String> workflows; private SortedSet<String> components; /** * Incremented on each model change; used for efficient change testing. Initialized with "+1" so a query with INITIAL_SEQUENCE_ID as * parameter will always signal an initial "change". * * May be changed in the future to filter-specific sequence ids. */ private int sequenceIdCounter = INITIAL_SEQUENCE_ID + 1; private int filteredListLastChanged; private int workflowListLastChanged; private int componentListLastChanged; private ConsoleRowLogService consoleRowLogService; private WorkflowHostService workflowHostService; private CommunicationService communicationService; public ConsoleRowModelServiceImpl() { // initialize internal model resetModel(); // set default filter currentFilter = new ConsoleRowFilter(); initialSubscriptionLatch = new CountDownLatch(1); } /** * OSGi-DS lifecycle method. */ public void activate() { ConcurrencyUtils.getAsyncTaskService().execute(new Runnable() { @Override @TaskDescription("Initial ConsoleRow model subscriptions") public void run() { subscriptionManager = new GenericSubscriptionManager(new ConsoleSubscriptionEventProcessor( ConsoleRowModelServiceImpl.this, consoleRowLogService), communicationService, workflowHostService); subscriptionManager.updateSubscriptionsForPrefixes(new String[] { ConsoleRow.NOTIFICATION_ID_PREFIX_CONSOLE_EVENT }); initialSubscriptionLatch.countDown(); } }); } @Override public void ensureConsoleCaptureIsInitialized() throws InterruptedException { initialSubscriptionLatch.await(); } /** * Updates subscriptions to known server instances. */ @Override public synchronized void updateSubscriptions() { try { initialSubscriptionLatch.await(); } catch (InterruptedException e) { // TODO better handling? throw new RuntimeException("Interrupted while waiting for initial subscriptions to complete", e); } subscriptionManager.updateSubscriptionsForPrefixes(new String[] { ConsoleRow.NOTIFICATION_ID_PREFIX_CONSOLE_EVENT }); } /** * Returns a new {@link ConsoleModelSnapshot} of the current model state if the model was modified since the given sequence id. The * typical source of this sequence id is calling getSequenceId() on a previously returned snapshot. If no change has occured, this * method returns null. * * @param sequenceId the last sequence id known to the caller * @return a new model snapshot, or null if no change has occured since the given sequence id */ @Override public synchronized ConsoleModelSnapshot getSnapshotIfModifiedSince(int sequenceId) { // any change at all? if (sequenceId == sequenceIdCounter) { return null; } // any relevant change? if (sequenceId >= filteredListLastChanged && sequenceId >= workflowListLastChanged && sequenceId >= componentListLastChanged) { return null; } // create & return snapshot object ConsoleModelSnapshotImpl snapshot = new ConsoleModelSnapshotImpl(); if (filteredListLastChanged > sequenceId) { // if modifed, set a copy of the filtered list snapshot.setFilteredRows(new ArrayList<ConsoleRow>(filteredRows)); } // if modified, set a copy of the workflow list if (workflowListLastChanged > sequenceId) { snapshot.setWorkflowList(new ArrayList<String>(workflows)); } // if modified, set a copy of the component list if (componentListLastChanged > sequenceId) { snapshot.setComponentList(new ArrayList<String>(components)); } snapshot.setSequenceId(sequenceIdCounter); return snapshot; } /** * Batch version of {@link #addConsoleRow(ConsoleRow)} to reduce synchronization overhead. * * @param rows the list of {@link ConsoleRow}s to add */ @Override public synchronized void processConsoleRows(List<ConsoleRow> rows) { sequenceIdCounter++; for (ConsoleRow row : rows) { if (accept(row)) { // add unfiltered allRows.addLast(row); // add to filtered list if filter matches if (currentFilter.accept(row)) { filteredRows.addLast(row); filteredListLastChanged = sequenceIdCounter; } // add to the set of workflows // note: currently, workflows are only purged on clearAll if (workflows.add(row.getWorkflowName())) { workflowListLastChanged = sequenceIdCounter; } // add to the set of components // note: currently, components are only purged on clearAll if (components.add(row.getComponentName())) { componentListLastChanged = sequenceIdCounter; } } } // trim model to retention limits trimUnfilteredModel(); // trim filtered list to max capacity trimFilteredList(); } private boolean accept(ConsoleRow row) { if (row.getType().equals(ConsoleRow.Type.WORKFLOW_ERROR)) { return row.getWorkflowName() != null && !row.getWorkflowName().isEmpty(); } else { return row.getWorkflowName() != null && !row.getWorkflowName().isEmpty() && row.getComponentName() != null && !row.getComponentName().isEmpty(); } } /** * Set the new {@link ConsoleRowFilter} for building future snapshots. Null is not permitted; set a permissive filter instead. * * @param newFilter the new {@link ConsoleRowFilter} */ @Override public synchronized void setRowFilter(ConsoleRowFilter newFilter) { // mark modification sequenceIdCounter++; // use a clone to prevent external modification currentFilter = newFilter.clone(); // rebuild filtered list with new filter filteredRows = new LinkedList<ConsoleRow>(); for (ConsoleRow row : allRows) { // add to filtered list if filter matches if (currentFilter.accept(row)) { filteredRows.addLast(row); } } filteredListLastChanged = sequenceIdCounter; // trim filtered list to max capacity trimFilteredList(); } /** * OSGi-DS bind method. * * @param newInstance the new service instance */ public void bindConsoleRowLogService(ConsoleRowLogService newInstance) { this.consoleRowLogService = newInstance; } private void resetModel() { allRows = new LinkedList<ConsoleRow>(); filteredRows = new LinkedList<ConsoleRow>(); filteredListLastChanged = sequenceIdCounter; workflows = new TreeSet<String>(); workflowListLastChanged = sequenceIdCounter; components = new TreeSet<String>(); componentListLastChanged = sequenceIdCounter; currentFilter = new ConsoleRowFilter(); } private void trimUnfilteredModel() { // TODO could be expanded to retention limits per type etc. while (allRows.size() > MAX_UNFILTERED_ROWS_RETENTION) { allRows.removeFirst(); } } private void trimFilteredList() { while (filteredRows.size() > MAX_SNAPSHOT_SIZE) { filteredRows.removeFirst(); } } /** * Removes all console rows. **/ @Override public synchronized void clearAll() { sequenceIdCounter++; resetModel(); } /** * OSGi-DS bind method. * * @param newInstance the new service instance */ public void bindCommunicationService(CommunicationService newInstance) { this.communicationService = newInstance; } /** * OSGi-DS bind method. * * @param newInstance the new service instance */ protected void bindWorkflowHostService(WorkflowHostService newWorkflowHostService) { workflowHostService = newWorkflowHostService; } }