NIFI-250: Implemented fingerprinting and reloading of controller services and reporting tasks from flow.xml
Project: http://git-wip-us.apache.org/repos/asf/incubator-nifi/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-nifi/commit/2df4500c Tree: http://git-wip-us.apache.org/repos/asf/incubator-nifi/tree/2df4500c Diff: http://git-wip-us.apache.org/repos/asf/incubator-nifi/diff/2df4500c Branch: refs/heads/NIFI-250 Commit: 2df4500c05ac91d46614e9ca41974fed890d2646 Parents: 86d15f9 Author: Mark Payne <[email protected]> Authored: Mon Jan 26 15:17:04 2015 -0500 Committer: Mark Payne <[email protected]> Committed: Mon Jan 26 15:17:04 2015 -0500 ---------------------------------------------------------------------- .../cluster/manager/impl/WebClusterManager.java | 8 +- .../service/ControllerServiceProvider.java | 2 +- .../apache/nifi/controller/FlowController.java | 55 ++++++++++++- .../controller/StandardFlowSynchronizer.java | 70 ++++++++++++++++- .../service/ControllerServiceLoader.java | 3 +- .../StandardControllerServiceProvider.java | 6 +- .../nifi/fingerprint/FingerprintFactory.java | 81 ++++++++++++++++++++ .../nifi/web/controller/ControllerFacade.java | 8 +- 8 files changed, 219 insertions(+), 14 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/2df4500c/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-cluster/src/main/java/org/apache/nifi/cluster/manager/impl/WebClusterManager.java ---------------------------------------------------------------------- diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-cluster/src/main/java/org/apache/nifi/cluster/manager/impl/WebClusterManager.java b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-cluster/src/main/java/org/apache/nifi/cluster/manager/impl/WebClusterManager.java index 54c2b55..9d9640d 100644 --- a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-cluster/src/main/java/org/apache/nifi/cluster/manager/impl/WebClusterManager.java +++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-cluster/src/main/java/org/apache/nifi/cluster/manager/impl/WebClusterManager.java @@ -1287,6 +1287,10 @@ public class WebClusterManager implements HttpClusterManager, ProtocolHandler, C writeLock.unlock("handleControllerStartupFailure"); } } + + public ControllerServiceNode createControllerService(final String type, final boolean firstTimeAdded) { + return createControllerService(type, UUID.randomUUID().toString(), firstTimeAdded); + } /** * Adds an instance of a specified controller service. @@ -1297,8 +1301,8 @@ public class WebClusterManager implements HttpClusterManager, ProtocolHandler, C * @return */ @Override - public ControllerServiceNode createControllerService(final String type, final boolean firstTimeAdded) { - return controllerServiceProvider.createControllerService(type, firstTimeAdded); + public ControllerServiceNode createControllerService(final String type, final String id, final boolean firstTimeAdded) { + return controllerServiceProvider.createControllerService(type, id, firstTimeAdded); } @Override http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/2df4500c/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceProvider.java ---------------------------------------------------------------------- diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceProvider.java b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceProvider.java index 60ff6c9..1bc3964 100644 --- a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceProvider.java +++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceProvider.java @@ -34,7 +34,7 @@ public interface ControllerServiceProvider extends ControllerServiceLookup { * @param firstTimeAdded * @return */ - ControllerServiceNode createControllerService(String type, boolean firstTimeAdded); + ControllerServiceNode createControllerService(String type, String id, boolean firstTimeAdded); /** * Gets the controller service node for the specified identifier. Returns http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/2df4500c/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java ---------------------------------------------------------------------- diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java index 92975ad..47e26c0 100644 --- a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java +++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java @@ -2589,9 +2589,60 @@ public class FlowController implements EventAccess, ControllerServiceProvider, H } } - @Override + + /** + * <p> + * Starts any enabled Processors and Reporting Tasks that are referencing this Controller Service. If other Controller + * Services reference this Controller Service, will also enable those services and 'active' any components referencing + * them. + * </p> + * + * <p> + * NOTE: If any component cannot be started, an IllegalStateException will be thrown an no more components will + * be activated. This method provides no atomicity. + * </p> + * + * @param serviceNode + */ + public void activateReferencingComponents(final ControllerServiceNode serviceNode) { + final ControllerServiceReference ref = serviceNode.getReferences(); + final Set<ConfiguredComponent> components = ref.getReferencingComponents(); + + // First, activate any other controller services. We do this first so that we can + // avoid the situation where Processor X depends on Controller Services Y and Z; and + // Controller Service Y depends on Controller Service Z. In this case, if we first attempted + // to start Processor X, we would fail because Controller Service Y is disabled. THis way, we + // can recursively enable everything. + for ( final ConfiguredComponent component : components ) { + if (component instanceof ControllerServiceNode) { + final ControllerServiceNode componentNode = (ControllerServiceNode) component; + enableControllerService(componentNode); + activateReferencingComponents(componentNode); + } + } + + for ( final ConfiguredComponent component : components ) { + if (component instanceof ProcessorNode) { + final ProcessorNode procNode = (ProcessorNode) component; + if ( !procNode.isRunning() ) { + startProcessor(procNode.getProcessGroup().getIdentifier(), procNode.getIdentifier()); + } + } else if (component instanceof ReportingTaskNode) { + final ReportingTaskNode taskNode = (ReportingTaskNode) component; + if ( !taskNode.isRunning() ) { + startReportingTask(taskNode); + } + } + } + } + public ControllerServiceNode createControllerService(final String type, final boolean firstTimeAdded) { - return controllerServiceProvider.createControllerService(type, firstTimeAdded); + return createControllerService(type, UUID.randomUUID().toString(), firstTimeAdded); + } + + @Override + public ControllerServiceNode createControllerService(final String type, final String id, final boolean firstTimeAdded) { + return controllerServiceProvider.createControllerService(type, id, firstTimeAdded); } public void enableReportingTask(final ReportingTaskNode reportingTaskNode) { http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/2df4500c/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java ---------------------------------------------------------------------- diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java index 0964a91..dbf9b2c 100644 --- a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java +++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/StandardFlowSynchronizer.java @@ -238,7 +238,11 @@ public class StandardFlowSynchronizer implements FlowSynchronizer { if ( controllerServicesElement != null ) { final List<Element> serviceElements = DomUtils.getChildElementsByTagName(controllerServicesElement, "controllerService"); for ( final Element serviceElement : serviceElements ) { - addControllerService(controller, serviceElement, encryptor); + if ( !initialized || existingFlowEmpty ) { + addControllerService(controller, serviceElement, encryptor); + } else { + updateControllerService(controller, serviceElement, encryptor); + } } } @@ -246,7 +250,11 @@ public class StandardFlowSynchronizer implements FlowSynchronizer { if ( reportingTasksElement != null ) { final List<Element> taskElements = DomUtils.getChildElementsByTagName(reportingTasksElement, "reportingTask"); for ( final Element taskElement : taskElements ) { - addReportingTask(controller, taskElement, encryptor); + if ( !initialized || existingFlowEmpty ) { + addReportingTask(controller, taskElement, encryptor); + } else { + updateReportingTask(controller, taskElement, encryptor); + } } } } @@ -347,6 +355,21 @@ public class StandardFlowSynchronizer implements FlowSynchronizer { for ( final Map.Entry<String, String> property : dto.getProperties().entrySet() ) { node.setProperty(property.getKey(), property.getValue()); } + + if ( dto.getEnabled() == Boolean.TRUE ) { + controller.enableControllerService(node); + } + } + + private void updateControllerService(final FlowController controller, final Element controllerServiceElement, final StringEncryptor encryptor) { + final ControllerServiceDTO dto = FlowFromDOMFactory.getControllerService(controllerServiceElement, encryptor); + + final boolean enabled = controller.isControllerServiceEnabled(dto.getId()); + if (dto.getEnabled() && !enabled) { + controller.enableControllerService(controller.getControllerServiceNode(dto.getId())); + } else if (dto.getEnabled() == Boolean.FALSE && enabled) { + controller.disableControllerService(controller.getControllerServiceNode(dto.getId())); + } } private void addReportingTask(final FlowController controller, final Element reportingTaskElement, final StringEncryptor encryptor) throws ReportingTaskInstantiationException { @@ -367,6 +390,49 @@ public class StandardFlowSynchronizer implements FlowSynchronizer { } } + private void updateReportingTask(final FlowController controller, final Element reportingTaskElement, final StringEncryptor encryptor) { + final ReportingTaskDTO dto = FlowFromDOMFactory.getReportingTask(reportingTaskElement, encryptor); + final ReportingTaskNode taskNode = controller.getReportingTaskNode(dto.getId()); + + if (!taskNode.getScheduledState().name().equals(dto.getScheduledState())) { + try { + switch (ScheduledState.valueOf(dto.getScheduledState())) { + case DISABLED: + if ( taskNode.isRunning() ) { + controller.stopReportingTask(taskNode); + } + controller.disableReportingTask(taskNode); + break; + case RUNNING: + if ( taskNode.getScheduledState() == ScheduledState.DISABLED ) { + controller.enableReportingTask(taskNode); + } + controller.startReportingTask(taskNode); + break; + case STOPPED: + if (taskNode.getScheduledState() == ScheduledState.DISABLED) { + controller.enableReportingTask(taskNode); + } else if (taskNode.getScheduledState() == ScheduledState.RUNNING) { + controller.stopReportingTask(taskNode); + } + break; + } + } catch (final IllegalStateException ise) { + logger.error("Failed to change Scheduled State of {} from {} to {} due to {}", taskNode, taskNode.getScheduledState().name(), dto.getScheduledState(), ise.toString()); + logger.error("", ise); + + // create bulletin for the Processor Node + controller.getBulletinRepository().addBulletin(BulletinFactory.createBulletin("Node Reconnection", Severity.ERROR.name(), + "Failed to change Scheduled State of " + taskNode + " from " + taskNode.getScheduledState().name() + " to " + dto.getScheduledState() + " due to " + ise.toString())); + + // create bulletin at Controller level. + controller.getBulletinRepository().addBulletin(BulletinFactory.createBulletin("Node Reconnection", Severity.ERROR.name(), + "Failed to change Scheduled State of " + taskNode + " from " + taskNode.getScheduledState().name() + " to " + dto.getScheduledState() + " due to " + ise.toString())); + } + } + } + + private ProcessGroup updateProcessGroup(final FlowController controller, final ProcessGroup parentGroup, final Element processGroupElement, final StringEncryptor encryptor) throws ProcessorInstantiationException { // get the parent group ID http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/2df4500c/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/ControllerServiceLoader.java ---------------------------------------------------------------------- diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/ControllerServiceLoader.java b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/ControllerServiceLoader.java index 7504d13..db44b5f 100644 --- a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/ControllerServiceLoader.java +++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/ControllerServiceLoader.java @@ -122,10 +122,11 @@ public class ControllerServiceLoader { for (final Element serviceElement : serviceNodes) { //get properties for the specific controller task - id, name, class, //and schedulingPeriod must be set + final String serviceId = DomUtils.getChild(serviceElement, "identifier").getTextContent().trim(); final String serviceClass = DomUtils.getChild(serviceElement, "class").getTextContent().trim(); //set the class to be used for the configured controller task - final ControllerServiceNode serviceNode = provider.createControllerService(serviceClass, false); + final ControllerServiceNode serviceNode = provider.createControllerService(serviceClass, serviceId, false); //optional task-specific properties for (final Element optionalProperty : DomUtils.getChildElementsByTagName(serviceElement, "property")) { http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/2df4500c/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java ---------------------------------------------------------------------- diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java index 75de4b9..0263ee0 100644 --- a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java +++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceProvider.java @@ -28,7 +28,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -98,12 +97,11 @@ public class StandardControllerServiceProvider implements ControllerServiceProvi } @Override - public ControllerServiceNode createControllerService(final String type, final boolean firstTimeAdded) { - if (type == null) { + public ControllerServiceNode createControllerService(final String type, final String id, final boolean firstTimeAdded) { + if (type == null || id == null) { throw new NullPointerException(); } - final String id = UUID.randomUUID().toString(); final ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader(); try { final ClassLoader cl = ExtensionManager.getClassLoader(type); http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/2df4500c/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java ---------------------------------------------------------------------- diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java index 8575569..ee7cd54 100644 --- a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java +++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/fingerprint/FingerprintFactory.java @@ -41,12 +41,14 @@ import javax.xml.validation.SchemaFactory; import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.controller.FlowController; +import org.apache.nifi.controller.FlowFromDOMFactory; import org.apache.nifi.controller.Template; import org.apache.nifi.controller.exception.ProcessorInstantiationException; import org.apache.nifi.encrypt.StringEncryptor; import org.apache.nifi.processor.Processor; import org.apache.nifi.util.DomUtils; import org.apache.nifi.web.api.dto.ConnectionDTO; +import org.apache.nifi.web.api.dto.ControllerServiceDTO; import org.apache.nifi.web.api.dto.FlowSnippetDTO; import org.apache.nifi.web.api.dto.FunnelDTO; import org.apache.nifi.web.api.dto.LabelDTO; @@ -58,6 +60,7 @@ import org.apache.nifi.web.api.dto.ProcessorDTO; import org.apache.nifi.web.api.dto.RemoteProcessGroupContentsDTO; import org.apache.nifi.web.api.dto.RemoteProcessGroupDTO; import org.apache.nifi.web.api.dto.RemoteProcessGroupPortDTO; +import org.apache.nifi.web.api.dto.ReportingTaskDTO; import org.apache.nifi.web.api.dto.TemplateDTO; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -250,6 +253,22 @@ public final class FingerprintFactory { // root group final Element rootGroupElem = (Element) DomUtils.getChildNodesByTagName(flowControllerElem, "rootGroup").item(0); addProcessGroupFingerprint(builder, rootGroupElem, controller); + + final Element controllerServicesElem = DomUtils.getChild(flowControllerElem, "controllerServices"); + if ( controllerServicesElem != null ) { + for ( final Element serviceElem : DomUtils.getChildElementsByTagName(controllerServicesElem, "controllerService") ) { + addControllerServiceFingerprint(builder, serviceElem); + } + } + + final Element reportingTasksElem = DomUtils.getChild(flowControllerElem, "reportingTasks"); + if ( reportingTasksElem != null ) { + for ( final Element taskElem : DomUtils.getChildElementsByTagName(reportingTasksElem, "reportingTask") ) { + addReportingTaskFingerprint(builder, taskElem); + } + } + + return builder; } @@ -832,6 +851,68 @@ public final class FingerprintFactory { builder.append(funnel.getId()); return builder; } + + private void addControllerServiceFingerprint(final StringBuilder builder, final Element controllerServiceElem) { + final ControllerServiceDTO dto = FlowFromDOMFactory.getControllerService(controllerServiceElem, encryptor); + addControllerServiceFingerprint(builder, dto); + } + + private void addControllerServiceFingerprint(final StringBuilder builder, final ControllerServiceDTO dto) { + builder.append(dto.getId()); + builder.append(dto.getType()); + builder.append(dto.getName()); + builder.append(dto.getComment()); + builder.append(dto.getAvailability()); + builder.append(dto.getAnnotationData()); + + final Map<String, String> properties = dto.getProperties(); + if (properties == null) { + builder.append("NO_PROPERTIES"); + } else { + final SortedMap<String, String> sortedProps = new TreeMap<>(properties); + for (final Map.Entry<String, String> entry : sortedProps.entrySet()) { + final String propName = entry.getKey(); + final String propValue = entry.getValue(); + if (propValue == null) { + continue; + } + + builder.append(propName).append("=").append(propValue); + } + } + } + + private void addReportingTaskFingerprint(final StringBuilder builder, final Element element) { + final ReportingTaskDTO dto = FlowFromDOMFactory.getReportingTask(element, encryptor); + addReportingTaskFingerprint(builder, dto); + } + + private void addReportingTaskFingerprint(final StringBuilder builder, final ReportingTaskDTO dto) { + builder.append(dto.getId()); + builder.append(dto.getType()); + builder.append(dto.getName()); + builder.append(dto.getComment()); + builder.append(dto.getSchedulingPeriod()); + builder.append(dto.getSchedulingStrategy()); + builder.append(dto.getAvailability()); + builder.append(dto.getAnnotationData()); + + final Map<String, String> properties = dto.getProperties(); + if (properties == null) { + builder.append("NO_PROPERTIES"); + } else { + final SortedMap<String, String> sortedProps = new TreeMap<>(properties); + for (final Map.Entry<String, String> entry : sortedProps.entrySet()) { + final String propName = entry.getKey(); + final String propValue = entry.getValue(); + if (propValue == null) { + continue; + } + + builder.append(propName).append("=").append(propValue); + } + } + } private Comparator<Element> getIdsComparator() { return new Comparator<Element>() { http://git-wip-us.apache.org/repos/asf/incubator-nifi/blob/2df4500c/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java ---------------------------------------------------------------------- diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java index 5ed25e6..40d3b1b 100644 --- a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java +++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java @@ -404,9 +404,13 @@ public class ControllerFacade implements ControllerServiceProvider { return flowController.getControllerService(serviceIdentifier); } - @Override public ControllerServiceNode createControllerService(final String type, final boolean firstTimeAdded) { - return flowController.createControllerService(type, firstTimeAdded); + return flowController.createControllerService(type, firstTimeAdded); + } + + @Override + public ControllerServiceNode createControllerService(final String type, final String id, final boolean firstTimeAdded) { + return flowController.createControllerService(type, id, firstTimeAdded); } public void removeControllerService(ControllerServiceNode serviceNode) {
