diff options
author | Jorge Hernandez <jh1730@att.com> | 2018-04-03 21:32:51 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@onap.org> | 2018-04-03 21:32:51 +0000 |
commit | 1d33f6b237c824c7f33b1ad137f6783d3d7d88df (patch) | |
tree | 9a3ad72967a53cab7a8a069d3a737c4eda8b2e29 /feature-pooling-dmaap | |
parent | 06018cabcdcdc96bbf1a186b1de9329f824612ce (diff) | |
parent | a3fa1c69a955af57f4e9023488bac3ef67a4fc3e (diff) |
Merge "Add pooling capability"
Diffstat (limited to 'feature-pooling-dmaap')
79 files changed, 13639 insertions, 0 deletions
diff --git a/feature-pooling-dmaap/assembly/assemble_zip.xml b/feature-pooling-dmaap/assembly/assemble_zip.xml new file mode 100644 index 00000000..9908a2b9 --- /dev/null +++ b/feature-pooling-dmaap/assembly/assemble_zip.xml @@ -0,0 +1,76 @@ +<!-- + ============LICENSE_START======================================================= + feature-pooling-dmaap + ================================================================================ + Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + ================================================================================ + 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. + ============LICENSE_END========================================================= + --> + +<!-- Defines how we build the .zip file which is our distribution. --> + +<assembly + xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd"> + <id>feature-pooling-dmaap</id> + <formats> + <format>zip</format> + </formats> + + <!-- we want "system" and related files right at the root level as this + file is suppose to be unzip on top of a karaf distro. --> + <includeBaseDirectory>false</includeBaseDirectory> + + <fileSets> + <fileSet> + <directory>target</directory> + <outputDirectory>lib/feature</outputDirectory> + <includes> + <include>feature-pooling-dmaap-${project.version}.jar</include> + </includes> + </fileSet> + <fileSet> + <directory>target/assembly/lib</directory> + <outputDirectory>lib/dependencies</outputDirectory> + <includes> + <include>*.jar</include> + </includes> + </fileSet> + <fileSet> + <directory>src/main/feature/config</directory> + <outputDirectory>config</outputDirectory> + <fileMode>0644</fileMode> + <excludes/> + </fileSet> + <fileSet> + <directory>src/main/feature/bin</directory> + <outputDirectory>bin</outputDirectory> + <fileMode>0744</fileMode> + <excludes/> + </fileSet> + <fileSet> + <directory>src/main/feature/db</directory> + <outputDirectory>db</outputDirectory> + <fileMode>0744</fileMode> + <excludes/> + </fileSet> + <fileSet> + <directory>src/main/feature/install</directory> + <outputDirectory>install</outputDirectory> + <fileMode>0744</fileMode> + <excludes/> + </fileSet> + </fileSets> +</assembly> diff --git a/feature-pooling-dmaap/pom.xml b/feature-pooling-dmaap/pom.xml new file mode 100644 index 00000000..4ae26b92 --- /dev/null +++ b/feature-pooling-dmaap/pom.xml @@ -0,0 +1,242 @@ +<!-- + ============LICENSE_START======================================================= + ONAP Policy Engine - Drools PDP + ================================================================================ + Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + ================================================================================ + 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. + ============LICENSE_END========================================================= + --> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.onap.policy.drools-pdp</groupId> + <artifactId>drools-pdp</artifactId> + <version>1.2.0-SNAPSHOT</version> + </parent> + + <artifactId>feature-pooling-dmaap</artifactId> + + <name>feature-pooling</name> + <description>Endpoints</description> + + <properties> + <maven.compiler.source>1.8</maven.compiler.source> + <maven.compiler.target>1.8</maven.compiler.target> + <jetty.version>9.3.20.v20170531</jetty.version> + <powermock.version>1.6.6</powermock.version> + </properties> + + <dependencies> + + <dependency> + <groupId>com.att.nsa</groupId> + <artifactId>cambriaClient</artifactId> + <version>${cambria.version}</version> + <exclusions> + <exclusion> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-log4j12</artifactId> + </exclusion> + <exclusion> + <groupId>com.att.nsa</groupId> + <artifactId>saClientLibrary</artifactId> + </exclusion> + </exclusions> + </dependency> + + <dependency> + <groupId>org.onap.dmaap.messagerouter.dmaapclient</groupId> + <artifactId>dmaapClient</artifactId> + <version>${dmaap.version}</version> + <exclusions> + <exclusion> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-log4j12</artifactId> + </exclusion> + <exclusion> + <groupId>log4j</groupId> + <artifactId>log4j</artifactId> + </exclusion> + </exclusions> + </dependency> + + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-server</artifactId> + <version>${jetty.version}</version> + </dependency> + + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-servlet</artifactId> + <version>${jetty.version}</version> + </dependency> + + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-server</artifactId> + <version>${jersey.version}</version> + </dependency> + + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-servlet-core</artifactId> + </dependency> + + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-json-jackson</artifactId> + <version>${jersey.version}</version> + </dependency> + + <dependency> + <groupId>org.glassfish.jersey.containers</groupId> + <artifactId>jersey-container-jetty-http</artifactId> + <version>${jersey.version}</version> + <exclusions> + <exclusion> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-util</artifactId> + </exclusion> + </exclusions> + </dependency> + + <dependency> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-client</artifactId> + <version>${jersey.version}</version> + </dependency> + + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-core</artifactId> + <version>${jackson.version}</version> + </dependency> + + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + <version>${jackson.version}</version> + </dependency> + + <dependency> + <groupId>com.fasterxml.jackson.datatype</groupId> + <artifactId>jackson-datatype-jsr310</artifactId> + <version>${jackson.version}</version> + </dependency> + + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-annotations</artifactId> + <version>${jackson.version}</version> + </dependency> + + <dependency> + <groupId>io.swagger</groupId> + <artifactId>swagger-jersey2-jaxrs</artifactId> + </dependency> + + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpcore</artifactId> + </dependency> + + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient</artifactId> + </dependency> + + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-collections4</artifactId> + <version>4.1</version> + </dependency> + + <dependency> + <groupId>ch.qos.logback</groupId> + <artifactId>logback-classic</artifactId> + </dependency> + + <dependency> + <groupId>org.onap.policy.drools-pdp</groupId> + <artifactId>policy-core</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>org.onap.policy.drools-pdp</groupId> + <artifactId>policy-management</artifactId> + <version>${project.version}</version> + </dependency> + + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.onap.policy.common</groupId> + <artifactId>utils-test</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.powermock</groupId> + <artifactId>powermock-api-mockito</artifactId> + <version>${powermock.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.powermock</groupId> + <artifactId>powermock-module-junit4</artifactId> + <version>${powermock.version}</version> + <scope>test</scope> + <exclusions> + <exclusion> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + </exclusion> + <exclusion> + <groupId>org.powermock</groupId> + <artifactId>powermock-core</artifactId> + </exclusion> + <exclusion> + <groupId>org.powermock</groupId> + <artifactId>powermock-reflect</artifactId> + </exclusion> + <exclusion> + <groupId>org.javassist</groupId> + <artifactId>javassist</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>org.javassist</groupId> + <artifactId>javassist</artifactId> + <version>3.21.0-GA</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.common</groupId> + <artifactId>utils</artifactId> + <version>${project.version}</version> + </dependency> + </dependencies> + +</project> diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/DmaapManager.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/DmaapManager.java new file mode 100644 index 00000000..98543f29 --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/DmaapManager.java @@ -0,0 +1,294 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling; + +import java.util.List; +import java.util.Properties; +import org.onap.policy.drools.event.comm.FilterableTopicSource; +import org.onap.policy.drools.event.comm.TopicEndpoint; +import org.onap.policy.drools.event.comm.TopicListener; +import org.onap.policy.drools.event.comm.TopicSink; +import org.onap.policy.drools.event.comm.TopicSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages the internal DMaaP topic. + */ +public class DmaapManager { + + private static final Logger logger = LoggerFactory.getLogger(DmaapManager.class); + + /** + * Factory used to construct objects. + */ + private static Factory factory = new Factory(); + + /** + * Name of the DMaaP topic. + */ + private final String topic; + + /** + * Topic source whose filter is to be manipulated. + */ + private final FilterableTopicSource topicSource; + + /** + * Where to publish messages. + */ + private final TopicSink topicSink; + + /** + * Topic sources. In theory, there's only one item in this list, the + * internal DMaaP topic. + */ + private final List<TopicSource> sources; + + /** + * Topic sinks. In theory, there's only one item in this list, the internal + * DMaaP topic. + */ + private final List<TopicSink> sinks; + + /** + * {@code True} if the consumer is running, {@code false} otherwise. + */ + private boolean consuming = false; + + /** + * {@code True} if the publisher is running, {@code false} otherwise. + */ + private boolean publishing = false; + + /** + * Constructs the manager, but does not start the source or sink. + * + * @param topic name of the internal DMaaP topic + * @param props properties to configure the topic source & sink + * @throws PoolingFeatureException if an error occurs + */ + public DmaapManager(String topic, Properties props) throws PoolingFeatureException { + + logger.info("initializing bus for topic {}", topic); + + try { + this.topic = topic; + this.sources = factory.initTopicSources(props); + this.sinks = factory.initTopicSinks(props); + + this.topicSource = findTopicSource(); + this.topicSink = findTopicSink(); + + // verify that we can set the filter + setFilter(null); + + } catch (IllegalArgumentException e) { + logger.error("failed to attach to topic {}", topic); + throw new PoolingFeatureException(e); + } + } + + protected static Factory getFactory() { + return factory; + } + + /** + * Used by junit tests to set the factory used to create various objects + * used by this class. + * + * @param factory the new factory + */ + protected static void setFactory(Factory factory) { + DmaapManager.factory = factory; + } + + public String getTopic() { + return topic; + } + + /** + * Finds the topic source associated with the internal DMaaP topic. + * + * @return the topic source + * @throws PoolingFeatureException if the source doesn't exist or is not + * filterable + */ + private FilterableTopicSource findTopicSource() throws PoolingFeatureException { + for (TopicSource src : sources) { + if (topic.equals(src.getTopic())) { + if (src instanceof FilterableTopicSource) { + return (FilterableTopicSource) src; + + } else { + throw new PoolingFeatureException("topic source " + topic + " is not filterable"); + } + } + } + + throw new PoolingFeatureException("missing topic source " + topic); + } + + /** + * Finds the topic sink associated with the internal DMaaP topic. + * + * @return the topic sink + * @throws PoolingFeatureException if the sink doesn't exist + */ + private TopicSink findTopicSink() throws PoolingFeatureException { + for (TopicSink sink : sinks) { + if (topic.equals(sink.getTopic())) { + return sink; + } + } + + throw new PoolingFeatureException("missing topic sink " + topic); + } + + /** + * Starts the publisher, if it isn't already running. + * + * @throws PoolingFeatureException if an error occurs + */ + public void startPublisher() throws PoolingFeatureException { + if (publishing) { + return; + } + + try { + topicSink.start(); + publishing = true; + + } catch (IllegalStateException e) { + throw new PoolingFeatureException("cannot start topic sink " + topic, e); + } + } + + /** + * Stops the publisher. + */ + public void stopPublisher() { + if (!publishing) { + return; + } + + try { + publishing = false; + topicSink.stop(); + + } catch (IllegalStateException e) { + logger.error("cannot stop sink for topic {}", topic, e); + } + } + + /** + * Starts the consumer, if it isn't already running. + * + * @param listener listener to register with the source + */ + public void startConsumer(TopicListener listener) { + if (consuming) { + return; + } + + topicSource.register(listener); + consuming = true; + } + + /** + * Stops the consumer. + * + * @param listener listener to unregister with the source + */ + public void stopConsumer(TopicListener listener) { + if (!consuming) { + return; + } + + consuming = false; + topicSource.unregister(listener); + } + + /** + * Sets the server-side filter to be used by the consumer. + * + * @param filter the filter string, or {@code null} if no filter is to be + * used + * @throws PoolingFeatureException if the topic is not filterable + */ + public void setFilter(String filter) throws PoolingFeatureException { + try { + topicSource.setFilter(filter); + + } catch (UnsupportedOperationException e) { + throw new PoolingFeatureException("cannot filter topic " + topic); + } + } + + /** + * Publishes a message to the sink. + * + * @param msg message to be published + * @throws PoolingFeatureException if an error occurs or the publisher isn't + * running + */ + public void publish(String msg) throws PoolingFeatureException { + if (!publishing) { + throw new PoolingFeatureException(new IllegalStateException("no topic sink " + topic)); + } + + try { + if (!topicSink.send(msg)) { + throw new PoolingFeatureException("failed to send to topic sink " + topic); + } + + } catch (IllegalStateException e) { + throw new PoolingFeatureException("cannot send to topic sink " + topic, e); + } + } + + /** + * Factory used to construct objects. + */ + public static class Factory { + + /** + * Initializes the topic sources. + * + * @param props properties used to configure the topics + * @return the topic sources + */ + public List<TopicSource> initTopicSources(Properties props) { + return TopicEndpoint.manager.addTopicSources(props); + } + + /** + * Initializes the topic sinks. + * + * @param props properties used to configure the topics + * @return the topic sinks + */ + public List<TopicSink> initTopicSinks(Properties props) { + return TopicEndpoint.manager.addTopicSinks(props); + } + + } +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/EventQueue.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/EventQueue.java new file mode 100644 index 00000000..0bed85b5 --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/EventQueue.java @@ -0,0 +1,121 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling; + +import java.util.Deque; +import java.util.LinkedList; +import org.onap.policy.drools.pooling.message.Forward; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Finite queue of events waiting to be processed once the buckets have been + * assigned. + */ +public class EventQueue { + + private static final Logger logger = LoggerFactory.getLogger(EventQueue.class); + + /** + * Maximum number of events allowed in the queue. When excess events are + * added, the older events are removed. + */ + private int maxEvents; + + /** + * Maximum age, in milliseconds, of events in the queue. Events that are + * older than this are discarded rather than being handed off when + * {@link #poll()} is invoked. + */ + private long maxAgeMs; + + /** + * The actual queue of events. + */ + private Deque<Forward> events = new LinkedList<>(); + + /** + * + * @param maxEvents maximum number of events to hold in the queue + * @param maxAgeMs maximum age of events in the queue + */ + public EventQueue(int maxEvents, long maxAgeMs) { + this.maxEvents = maxEvents; + this.maxAgeMs = maxAgeMs; + } + + /** + * + * @return {@code true} if the queue is empty, {@code false} otherwise + */ + public boolean isEmpty() { + return events.isEmpty(); + } + + /** + * Clears the queue. + */ + public void clear() { + events.clear(); + } + + /** + * + * @return the number of elements in the queue + */ + public int size() { + return events.size(); + } + + /** + * Adds an item to the queue. If the queue is full, the older item is + * removed and discarded. + * + * @param event + */ + public void add(Forward event) { + if (events.size() >= maxEvents) { + logger.warn("full queue - discarded event for topic {}", event.getTopic()); + events.remove(); + } + + events.add(event); + } + + /** + * Gets the oldest, un-expired event from the queue. + * + * @return the oldest, un-expired event + */ + public Forward poll() { + long tmin = System.currentTimeMillis() - maxAgeMs; + + Forward ev; + while ((ev = events.poll()) != null) { + if (!ev.isExpired(tmin)) { + break; + } + } + + return ev; + } + +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/FeatureEnabledChecker.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/FeatureEnabledChecker.java new file mode 100644 index 00000000..d2f32043 --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/FeatureEnabledChecker.java @@ -0,0 +1,144 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling; + +// TODO move to policy-utils + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.Properties; +import org.onap.policy.common.utils.properties.SpecPropertyConfiguration; +import org.onap.policy.common.utils.properties.exception.PropertyException; + +/** + * Checks whether or not a feature is enabled. The name of the "enable" property + * is assumed to be of the form accepted by a {@link SpecPropertyConfiguration}, + * which contains a substitution place-holder into which a "specializer" (e.g., + * controller or session name) is substituted. + */ +public class FeatureEnabledChecker { + + /** + * + */ + private FeatureEnabledChecker() { + super(); + } + + /** + * Determines if a feature is enabled for a particular specializer. + * + * @param props properties from which to extract the "enabled" flag + * @param specializer specializer to be substituted into the property name + * when extracting + * @param propName the name of the "enabled" property + * @return {@code true} if the feature is enabled, or {@code false} if it is + * not enabled (or if the property doesn't exist) + * @throws IllegalArgumentException if the "enabled" property is not a + * boolean value + */ + public static boolean isFeatureEnabled(Properties props, String specializer, String propName) { + + try { + return new Config(specializer, props, propName).isEnabled(); + + } catch (PropertyException e) { + throw new IllegalArgumentException("cannot check property " + propName, e); + } + } + + + /** + * Configuration used to extract the value. + */ + private static class Config extends SpecPropertyConfiguration { + + /** + * There is a bit of trickery here. This annotation is just a + * place-holder to get the superclass to invoke the + * {@link #setValue(java.lang.reflect.Field, Properties, Property) + * setValue()} method. When that's invoked, we'll substitute + * {@link #propOverride} instead of this annotation. + */ + @Property(name = "feature-enabled-property-place-holder") + private boolean enabled; + + /** + * Annotation that will actually be used to set the field. + */ + private Property propOverride; + + /** + * + * @param specializer specializer to be substituted into the property + * name when extracting + * @param props properties from which to extract the "enabled" flag + * @param propName the name of the "enabled" property + * @throws PropertyException if an error occurs + */ + public Config(String specializer, Properties props, String propName) throws PropertyException { + super(specializer); + + propOverride = new Property() { + + @Override + public String name() { + return propName; + } + + @Override + public String defaultValue() { + // feature is disabled by default + return "false"; + } + + @Override + public String accept() { + return ""; + } + + @Override + public Class<? extends Annotation> annotationType() { + return Property.class; + } + }; + + setAllFields(props); + } + + /** + * Substitutes {@link #propOverride} for "prop". + */ + @Override + protected boolean setValue(Field field, Properties props, Property prop) throws PropertyException { + return super.setValue(field, props, propOverride); + } + + /** + * + * @return {@code true} if the feature is enabled, {@code false} + * otherwise + */ + public boolean isEnabled() { + return enabled; + } + }; +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingFeature.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingFeature.java new file mode 100644 index 00000000..da47a031 --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingFeature.java @@ -0,0 +1,390 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling; + +import java.io.IOException; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import org.onap.policy.common.utils.properties.exception.PropertyException; +import org.onap.policy.drools.controller.DroolsController; +import org.onap.policy.drools.core.PolicySessionFeatureAPI; +import org.onap.policy.drools.event.comm.Topic.CommInfrastructure; +import org.onap.policy.drools.features.DroolsControllerFeatureAPI; +import org.onap.policy.drools.features.PolicyControllerFeatureAPI; +import org.onap.policy.drools.system.PolicyController; +import org.onap.policy.drools.utils.PropertyUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Controller/session pooling. Multiple hosts may be launched, all servicing the + * same controllers/sessions. When this feature is enabled, the requests are + * divided across the different hosts, instead of all running on a single, + * active host. + * <p> + * With each controller, there is an associated DMaaP topic that is used for + * internal communication between the different hosts serving the controller. + */ +public class PoolingFeature implements PolicyControllerFeatureAPI, DroolsControllerFeatureAPI, PolicySessionFeatureAPI { + + private static final Logger logger = LoggerFactory.getLogger(PoolingFeature.class); + + // TODO state-management doesn't allow more than one active host at a time + + /** + * Factory used to create objects. + */ + private static Factory factory; + + /** + * Entire set of feature properties, including those specific to various + * controllers. + */ + private Properties featProps = null; + + /** + * Maps a controller name to its associated manager. + */ + private ConcurrentHashMap<String, PoolingManagerImpl> ctlr2pool = new ConcurrentHashMap<>(107); + + /** + * Arguments passed to beforeOffer(), which are saved for when the + * beforeInsert() is called later. As multiple threads can be active within + * the methods at the same time, we must keep this in thread local storage. + */ + private ThreadLocal<OfferArgs> offerArgs = new ThreadLocal<>(); + + /** + * + */ + public PoolingFeature() { + super(); + } + + protected static Factory getFactory() { + return factory; + } + + /** + * Sets the factory to be used to create objects. Used by junit tests. + * + * @param factory the new factory to be used to create objects + */ + protected static void setFactory(Factory factory) { + PoolingFeature.factory = factory; + } + + @Override + public int getSequenceNumber() { + return 0; + } + + /** + * @throws PoolingFeatureRtException if the properties cannot be read or are + * invalid + */ + @Override + public void globalInit(String[] args, String configDir) { + logger.info("initializing pooling feature"); + + try { + featProps = PropertyUtil.getProperties(configDir + "/feature-pooling-dmaap.properties"); + + } catch (IOException ex) { + throw new PoolingFeatureRtException(ex); + } + } + + /** + * Adds the controller and a new pooling manager to {@link #ctlr2pool}. + * + * @throws PoolingFeatureRtException if an error occurs + */ + @Override + public boolean afterCreate(PolicyController controller) { + + if (featProps == null) { + logger.error("pooling feature properties have not been loaded"); + throw new PoolingFeatureRtException(new IllegalStateException("missing pooling feature properties")); + } + + String name = controller.getName(); + + if (FeatureEnabledChecker.isFeatureEnabled(featProps, name, PoolingProperties.FEATURE_ENABLED)) { + try { + // get & validate the properties + PoolingProperties props = new PoolingProperties(name, featProps); + + logger.info("pooling enabled for {}", name); + ctlr2pool.computeIfAbsent(name, xxx -> factory.makeManager(controller, props)); + + } catch (PropertyException e) { + logger.error("pooling disabled due to exception for {}", name, e); + throw new PoolingFeatureRtException(e); + } + + } else { + logger.info("pooling disabled for {}", name); + } + + + return false; + } + + @Override + public boolean beforeStart(PolicyController controller) { + return doManager(controller, mgr -> { + mgr.beforeStart(); + return false; + }); + } + + @Override + public boolean afterStart(PolicyController controller) { + return doManager(controller, mgr -> { + mgr.afterStart(); + return false; + }); + } + + @Override + public boolean beforeStop(PolicyController controller) { + return doManager(controller, mgr -> { + mgr.beforeStop(); + return false; + }); + } + + @Override + public boolean afterStop(PolicyController controller) { + + // NOTE: using doDeleteManager() instead of doManager() + + return doDeleteManager(controller, mgr -> { + + mgr.afterStop(); + return false; + }); + } + + @Override + public boolean beforeLock(PolicyController controller) { + return doManager(controller, mgr -> { + mgr.beforeLock(); + return false; + }); + } + + @Override + public boolean afterUnlock(PolicyController controller) { + return doManager(controller, mgr -> { + mgr.afterUnlock(); + return false; + }); + } + + @Override + public boolean beforeOffer(PolicyController controller, CommInfrastructure protocol, String topic2, String event) { + /* + * As this is invoked a lot, we'll directly call the manager's method + * instead of using the functional interface via doManager(). + */ + PoolingManagerImpl mgr = ctlr2pool.get(controller.getName()); + if (mgr == null) { + return false; + } + + if (mgr.beforeOffer(protocol, topic2, event)) { + return true; + } + + offerArgs.set(new OfferArgs(protocol, topic2, event)); + return false; + } + + @Override + public boolean beforeInsert(DroolsController droolsController, Object fact) { + + OfferArgs args = offerArgs.get(); + if (args == null) { + return false; + } + + PolicyController controller; + try { + controller = factory.getController(droolsController); + + } catch (IllegalArgumentException | IllegalStateException e) { + return false; + } + + if (controller == null) { + return false; + } + + /* + * As this is invoked a lot, we'll directly call the manager's method + * instead of using the functional interface via doManager(). + */ + PoolingManagerImpl mgr = ctlr2pool.get(controller.getName()); + if (mgr == null) { + return false; + } + + return mgr.beforeInsert(args.protocol, args.topic, args.event, fact); + } + + @Override + public boolean afterOffer(PolicyController controller, CommInfrastructure protocol, String topic, String event, + boolean success) { + + // clear any stored arguments + offerArgs.set(null); + + return false; + } + + /** + * Executes a function using the manager associated with the controller. + * Catches any exceptions from the function and re-throws it as a runtime + * exception. + * + * @param controller + * @param func function to be executed + * @return {@code true} if the function handled the request, {@code false} + * otherwise + * @throws PoolingFeatureRtException if an error occurs + */ + private boolean doManager(PolicyController controller, MgrFunc func) { + PoolingManagerImpl mgr = ctlr2pool.get(controller.getName()); + if (mgr == null) { + return false; + } + + try { + return func.apply(mgr); + + } catch (PoolingFeatureException e) { + throw e.toRuntimeException(); + } + } + + /** + * Executes a function using the manager associated with the controller and + * then deletes the manager. Catches any exceptions from the function and + * re-throws it as a runtime exception. + * + * @param controller + * @param func function to be executed + * @return {@code true} if the function handled the request, {@code false} + * otherwise + * @throws PoolingFeatureRtException if an error occurs + */ + private boolean doDeleteManager(PolicyController controller, Function<PoolingManagerImpl, Boolean> func) { + + // NOTE: using "remove()" instead of "get()" + + PoolingManagerImpl mgr = ctlr2pool.remove(controller.getName()); + + if (mgr == null) { + return false; + } + + return func.apply(mgr); + } + + /** + * Function that operates on a manager. + */ + @FunctionalInterface + private static interface MgrFunc { + + /** + * + * @param mgr + * @return {@code true} if the request was handled by the manager, + * {@code false} otherwise + * @throws PoolingFeatureException + */ + public boolean apply(PoolingManagerImpl mgr) throws PoolingFeatureException; + } + + /** + * Arguments captured from beforeOffer(). + */ + private static class OfferArgs { + + /** + * Protocol of the receiving topic. + */ + private CommInfrastructure protocol; + + /** + * Topic on which the event was received. + */ + private String topic; + + /** + * The event text that was received on the topic. + */ + private String event; + + /** + * + * @param protocol + * @param topic + * @param event the actual event data received on the topic + */ + public OfferArgs(CommInfrastructure protocol, String topic, String event) { + this.protocol = protocol; + this.topic = topic; + this.event = event; + } + } + + /** + * Used to create objects. + */ + public static class Factory { + + /** + * Makes a pooling manager for a controller. + * + * @param controller + * @param props properties to use to configure the manager + * @return a new pooling manager + */ + public PoolingManagerImpl makeManager(PolicyController controller, PoolingProperties props) { + return new PoolingManagerImpl(controller, props); + } + + /** + * Gets the policy controller associated with a drools controller. + * + * @param droolsController + * @return the policy controller associated with a drools controller + */ + public PolicyController getController(DroolsController droolsController) { + return PolicyController.factory.get(droolsController); + } + } +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingFeatureException.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingFeatureException.java new file mode 100644 index 00000000..5efd1414 --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingFeatureException.java @@ -0,0 +1,59 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling; + +/** + * Exception thrown by the pooling feature. + */ +public class PoolingFeatureException extends Exception { + private static final long serialVersionUID = 1L; + + public PoolingFeatureException() { + super(); + } + + public PoolingFeatureException(String message) { + super(message); + } + + public PoolingFeatureException(Throwable cause) { + super(cause); + } + + public PoolingFeatureException(String message, Throwable cause) { + super(message, cause); + } + + public PoolingFeatureException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + /** + * Converts the exception to a runtime exception. + * + * @return a new runtime exception, wrapping this exception + */ + public PoolingFeatureRtException toRuntimeException() { + return new PoolingFeatureRtException(this); + } + +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingFeatureRtException.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingFeatureRtException.java new file mode 100644 index 00000000..6fdb6c69 --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingFeatureRtException.java @@ -0,0 +1,50 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling; + +/** + * A runtime exception thrown by the pooling feature. + */ +public class PoolingFeatureRtException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public PoolingFeatureRtException() { + super(); + } + + public PoolingFeatureRtException(String message) { + super(message); + } + + public PoolingFeatureRtException(Throwable cause) { + super(cause); + } + + public PoolingFeatureRtException(String message, Throwable cause) { + super(message, cause); + } + + public PoolingFeatureRtException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingManager.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingManager.java new file mode 100644 index 00000000..de08d1e1 --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingManager.java @@ -0,0 +1,152 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ScheduledFuture; +import org.onap.policy.drools.pooling.message.BucketAssignments; +import org.onap.policy.drools.pooling.message.Forward; +import org.onap.policy.drools.pooling.message.Message; +import org.onap.policy.drools.pooling.state.State; +import org.onap.policy.drools.pooling.state.StateTimerTask; + +/** + * Pooling manager for a single PolicyController. + */ +public interface PoolingManager { + + /** + * Gets the properties used to configure the manager. + * + * @return + */ + public PoolingProperties getProperties(); + + /** + * Gets the host id. + * + * @return the host id + */ + public String getHost(); + + /** + * Gets the name of the internal DMaaP topic used by this manager to + * communicate with its other hosts. + * + * @return the name of the internal DMaaP topic + */ + public String getTopic(); + + /** + * Indicates that communication with internal DMaaP topic failed, typically + * due to a missed heart beat. Stops the PolicyController. + * + * @return a latch that can be used to determine when the controller's + * stop() method has completed + */ + public CountDownLatch internalTopicFailed(); + + /** + * Starts distributing requests according to the given bucket assignments. + * + * @param assignments must <i>not</i> be {@code null} + */ + public void startDistributing(BucketAssignments assignments); + + /** + * Gets the current bucket assignments. + * + * @return the current bucket assignments, or {@code null} if no assignments + * have been made + */ + public BucketAssignments getAssignments(); + + /** + * Publishes a message to the internal topic on the administrative channel. + * + * @param msg message to be published + */ + public void publishAdmin(Message msg); + + /** + * Publishes a message to the internal topic on a particular channel. + * + * @param channel channel on which the message should be published + * @param msg message to be published + */ + public void publish(String channel, Message msg); + + /** + * Handles a {@link Forward} event that was received from the internal + * topic. + * + * @param event + */ + public void handle(Forward event); + + /** + * Schedules a timer to fire after a delay. + * + * @param delayMs delay, in milliseconds + * @param task + * @return a future that can be used to cancel the timer + */ + public ScheduledFuture<?> schedule(long delayMs, StateTimerTask task); + + /** + * Schedules a timer to fire repeatedly. + * + * @param initialDelayMs initial delay, in milliseconds + * @param delayMs delay, in milliseconds + * @param task + * @return a future that can be used to cancel the timer + */ + public ScheduledFuture<?> scheduleWithFixedDelay(long initialDelayMs, long delayMs, StateTimerTask task); + + /** + * Transitions to the "start" state. + * + * @return the new state + */ + public State goStart(); + + /** + * Transitions to the "query" state. + * + * @return the new state + */ + public State goQuery(); + + /** + * Transitions to the "active" state. + * + * @return the new state + */ + public State goActive(); + + /** + * Transitions to the "inactive" state. + * + * @return the new state + */ + public State goInactive(); + +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingManagerImpl.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingManagerImpl.java new file mode 100644 index 00000000..cd71670d --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingManagerImpl.java @@ -0,0 +1,871 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Properties; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.onap.policy.drools.controller.DroolsController; +import org.onap.policy.drools.event.comm.Topic.CommInfrastructure; +import org.onap.policy.drools.event.comm.TopicListener; +import org.onap.policy.drools.pooling.extractor.ClassExtractors; +import org.onap.policy.drools.pooling.message.BucketAssignments; +import org.onap.policy.drools.pooling.message.Forward; +import org.onap.policy.drools.pooling.message.Leader; +import org.onap.policy.drools.pooling.message.Message; +import org.onap.policy.drools.pooling.state.ActiveState; +import org.onap.policy.drools.pooling.state.IdleState; +import org.onap.policy.drools.pooling.state.InactiveState; +import org.onap.policy.drools.pooling.state.QueryState; +import org.onap.policy.drools.pooling.state.StartState; +import org.onap.policy.drools.pooling.state.State; +import org.onap.policy.drools.pooling.state.StateTimerTask; +import org.onap.policy.drools.protocol.coders.EventProtocolCoder; +import org.onap.policy.drools.system.PolicyController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.core.JsonProcessingException; + +/** + * Implementation of a {@link PoolingManager}. Until bucket assignments have + * been made, events coming from external topics are saved in a queue for later + * processing. Once assignments are made, the saved events are processed. In + * addition, while the controller is locked, events are still forwarded to other + * hosts and bucket assignments are still updated, based on any {@link Leader} + * messages that it receives. + */ +public class PoolingManagerImpl implements PoolingManager, TopicListener { + + private static final Logger logger = LoggerFactory.getLogger(PoolingManagerImpl.class); + + // TODO metrics, audit logging + + /** + * Maximum number of times a message can be forwarded. + */ + public static final int MAX_HOPS = 5; + + /** + * Type of item that the extractors will be extracting. + */ + private static final String EXTRACTOR_TYPE = "requestId"; + + /** + * Prefix for extractor properties. + */ + private static final String PROP_EXTRACTOR_PREFIX = "extractor." + EXTRACTOR_TYPE; + + /** + * Factory used to create various objects. Can be overridden during junit + * testing. + */ + private static Factory factory = new Factory(); + + /** + * ID of this host. + */ + private final String host; + + /** + * Properties with which this was configured. + */ + private final PoolingProperties props; + + /** + * Associated controller. + */ + private final PolicyController controller; + + /** + * Where to offer events that have been forwarded to this host (i.e, the + * controller). + */ + private final TopicListener listener; + + /** + * Used to encode & decode request objects received from & sent to a rule + * engine. + */ + private final Serializer serializer; + + /** + * Internal DMaaP topic used by this controller. + */ + private final String topic; + + /** + * Manager for the internal DMaaP topic. + */ + private final DmaapManager dmaapMgr; + + /** + * Used to extract the request id from the decoded message. + */ + private final ClassExtractors extractors; + + /** + * Lock used while updating {@link #current}. In general, public methods + * must use this, while private methods assume the lock is already held. + */ + private final Object curLocker = new Object(); + + /** + * Current state. + * <p> + * This uses a finite state machine, wherein the state object contains all + * of the data relevant to that state. Each state object has a process() + * method, specific to each type of {@link Message} subclass. The method + * returns the next state object, or {@code null} if the state is to remain + * the same. + */ + private State current; + + /** + * Current bucket assignments or {@code null}. + */ + private BucketAssignments assignments = null; + + /** + * Pool used to execute timers. + */ + private ScheduledThreadPoolExecutor scheduler = null; + + /** + * Queue used when no bucket assignments are available. + */ + private EventQueue eventq; + + /** + * {@code True} if events offered by the controller should be intercepted, + * {@code false} otherwise. + */ + private boolean intercept = true; + + /** + * Constructs the manager, initializing all of the data structures. + * + * @param controller controller with which this is associated + * @param props feature properties specific to the controller + */ + public PoolingManagerImpl(PolicyController controller, PoolingProperties props) { + this.host = UUID.randomUUID().toString(); + this.controller = controller; + this.props = props; + + try { + this.listener = (TopicListener) controller; + this.serializer = new Serializer(); + this.topic = props.getPoolingTopic(); + this.eventq = factory.makeEventQueue(props); + + SpecProperties spec = new SpecProperties(PROP_EXTRACTOR_PREFIX, controller.getName()); + this.extractors = factory.makeClassExtractors(spec); + + this.dmaapMgr = factory.makeDmaapManager(props); + this.current = new IdleState(this); + + logger.info("allocating host {} to controller {} for topic {}", host, controller.getName(), topic); + + } catch (ClassCastException e) { + logger.error("not a topic listener, controller {}", controller.getName()); + throw new PoolingFeatureRtException(e); + + } catch (PoolingFeatureException e) { + logger.error("failed to attach internal DMaaP topic to controller {}", controller.getName()); + throw e.toRuntimeException(); + } + } + + protected static Factory getFactory() { + return factory; + } + + protected static void setFactory(Factory factory) { + PoolingManagerImpl.factory = factory; + } + + /** + * Should only be used by junit tests. + * + * @return the current state + */ + protected State getCurrent() { + synchronized (curLocker) { + return current; + } + } + + public String getHost() { + return host; + } + + public String getTopic() { + return topic; + } + + @Override + public PoolingProperties getProperties() { + return props; + } + + /** + * Indicates that the controller is about to start. Starts the publisher for + * the internal topic, and creates a thread pool for the timers. + * + * @throws PoolingFeatureException if the internal topic publisher cannot be + * started + */ + public void beforeStart() throws PoolingFeatureException { + synchronized (curLocker) { + if (scheduler == null) { + dmaapMgr.startPublisher(); + + scheduler = factory.makeScheduler(); + + /* + * Only a handful of timers at any moment, thus we can afford to + * take the time to remove them when they're cancelled. + */ + scheduler.setRemoveOnCancelPolicy(true); + scheduler.setMaximumPoolSize(1); + scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + scheduler.setContinueExistingPeriodicTasksAfterShutdownPolicy(false); + } + } + } + + /** + * Indicates that the controller has successfully started. Starts the + * consumer for the internal topic, enters the {@link StartState}, and sets + * the filter for the initial state. + */ + public void afterStart() { + synchronized (curLocker) { + if (current instanceof IdleState) { + dmaapMgr.startConsumer(this); + changeState(new StartState(this)); + } + } + } + + /** + * Indicates that the controller is about to stop. Stops the consumer, the + * scheduler, and the current state. + */ + public void beforeStop() { + ScheduledThreadPoolExecutor sched; + + synchronized (curLocker) { + sched = scheduler; + scheduler = null; + + if (!(current instanceof IdleState)) { + dmaapMgr.stopConsumer(this); + changeState(new IdleState(this)); + + // TODO + /* + * Need a brief delay here to allow "offline" message to be + * transmitted? + */ + } + } + + if (sched != null) { + sched.shutdownNow(); + } + } + + /** + * Indicates that the controller has stopped. Stops the publisher and logs a + * warning if any events are still in the queue. + */ + public void afterStop() { + synchronized (curLocker) { + if (!eventq.isEmpty()) { + logger.warn("discarded {} messages after stopping topic {}", eventq.size(), topic); + eventq.clear(); + } + + dmaapMgr.stopPublisher(); + } + } + + /** + * Indicates that the controller is about to be locked. Enters the idle + * state, as all it will be doing is forwarding messages. + */ + public void beforeLock() { + synchronized (curLocker) { + changeState(new IdleState(this)); + } + } + + /** + * Indicates that the controller has been unlocked. Enters the start state, + * if the controller is running. + */ + public void afterUnlock() { + synchronized (curLocker) { + if (controller.isAlive() && current instanceof IdleState && scheduler != null) { + changeState(new StartState(this)); + } + } + } + + /** + * Changes the finite state machine to a new state, provided the new state + * is not {@code null}. + * + * @param newState new state, or {@code null} if to remain unchanged + */ + private void changeState(State newState) { + if (newState != null) { + current.cancelTimers(); + current = newState; + + // set the filter before starting the state + setFilter(newState.getFilter()); + newState.start(); + } + } + + /** + * Sets the server-side filter for the internal topic. + * + * @param filter new filter to be used + */ + private void setFilter(Map<String, Object> filter) { + try { + dmaapMgr.setFilter(serializer.encodeFilter(filter)); + + } catch (JsonProcessingException e) { + logger.error("failed to encode server-side filter for topic {}, {}", topic, filter, e); + + } catch (PoolingFeatureException e) { + logger.error("failed to set server-side filter for topic {}, {}", topic, filter, e); + } + } + + @Override + public CountDownLatch internalTopicFailed() { + logger.error("communication failed for topic {}", topic); + + CountDownLatch latch = new CountDownLatch(1); + + /* + * We don't want to build up items in our queue if we can't forward them + * to other hosts, so we just stop the controller. + * + * Use a background thread to prevent deadlocks. + */ + new Thread() { + @Override + public void run() { + controller.stop(); + latch.countDown(); + } + }.start(); + + return latch; + } + + @Override + public ScheduledFuture<?> schedule(long delayMs, StateTimerTask task) { + return scheduler.schedule(new TimerAction(task), delayMs, TimeUnit.MILLISECONDS); + } + + @Override + public ScheduledFuture<?> scheduleWithFixedDelay(long initialDelayMs, long delayMs, StateTimerTask task) { + return scheduler.scheduleWithFixedDelay(new TimerAction(task), initialDelayMs, delayMs, TimeUnit.MILLISECONDS); + } + + @Override + public void publishAdmin(Message msg) { + publish(Message.ADMIN, msg); + } + + @Override + public void publish(String channel, Message msg) { + msg.setChannel(channel); + + try { + // ensure it's valid before we send it + msg.checkValidity(); + + String txt = serializer.encodeMsg(msg); + dmaapMgr.publish(txt); + + } catch (JsonProcessingException e) { + logger.error("failed to serialize message for topic {} channel {}", topic, channel, e); + + } catch (PoolingFeatureException e) { + logger.error("failed to publish message for topic {} channel {}", topic, channel, e); + } + } + + /** + * Handles an event from the internal topic. + * + * @param topic2 + * @param event + * @return {@code true} if the event was handled, {@code false} if the + * controller should handle it + */ + @Override + public void onTopicEvent(CommInfrastructure commType, String topic2, String event) { + + if (event == null) { + logger.error("null event on topic {}", topic); + return; + } + + synchronized (curLocker) { + // it's on the internal topic + handleInternal(event); + } + } + + /** + * Called by the PolicyController before it offers the event to the + * DroolsController. If the controller is locked, then it isn't processing + * events. However, they still need to be forwarded, thus in that case, they + * are decoded and forwarded. + * <p> + * On the other hand, if the controller is not locked, then we just return + * immediately and let {@link #beforeInsert(Object, String, String, Object) + * beforeInsert()} handle it instead, as it already has the decoded message. + * + * @param protocol + * @param topic2 + * @param event + * @return {@code true} if the event was handled by the manager, + * {@code false} if it must still be handled by the invoker + */ + public boolean beforeOffer(CommInfrastructure protocol, String topic2, String event) { + + if (!controller.isLocked() || !intercept) { + // we should NOT intercept this message - let the invoker handle it + return false; + } + + return handleExternal(protocol, topic2, event, extractRequestId(decodeEvent(topic2, event))); + } + + /** + * Called by the DroolsController before it inserts the event into the rule + * engine. + * + * @param protocol + * @param topic2 + * @param event original event text, as received from the Bus + * @param event2 event, as an object + * @return {@code true} if the event was handled by the manager, + * {@code false} if it must still be handled by the invoker + */ + public boolean beforeInsert(CommInfrastructure protocol, String topic2, String event, Object event2) { + + if (!intercept) { + // we should NOT intercept this message - let the invoker handle it + return false; + } + + return handleExternal(protocol, topic2, event, extractRequestId(event2)); + } + + /** + * Handles an event from an external topic. + * + * @param protocol + * @param topic2 + * @param event + * @param reqid request id extracted from the event, or {@code null} if it + * couldn't be extracted + * @return {@code true} if the event was handled by the manager, + * {@code false} if it must still be handled by the invoker + */ + private boolean handleExternal(CommInfrastructure protocol, String topic2, String event, String reqid) { + if (reqid == null) { + // no request id - let the invoker handle it + return false; + } + + if (reqid.isEmpty()) { + logger.warn("handle locally due to empty request id for topic {}", topic2); + // no request id - let the invoker handle it + return false; + } + + Forward ev = makeForward(protocol, topic2, event, reqid); + if (ev == null) { + // invalid args - consume the message + return true; + } + + synchronized (curLocker) { + return handleExternal(ev); + } + } + + /** + * Handles an event from an external topic. + * + * @param event + * @return {@code true} if the event was handled, {@code false} if the + * invoker should handle it + */ + private boolean handleExternal(Forward event) { + if (assignments == null) { + // no bucket assignments yet - add it to the queue + eventq.add(event); + + // we've consumed the event + return true; + + } else { + return handleEvent(event); + } + } + + /** + * Handles a {@link Forward} event, possibly forwarding it again. + * + * @param event + * @return {@code true} if the event was handled, {@code false} if the + * invoker should handle it + */ + private boolean handleEvent(Forward event) { + int bucket = Math.abs(event.getRequestId().hashCode()) % assignments.size(); + String target = assignments.getAssignedHost(bucket); + + if (target == null) { + /* + * This bucket has no assignment - just discard the event + */ + return true; + } + + if (target.equals(host)) { + /* + * Message belongs to this host - allow the controller to handle it. + */ + return false; + } + + // forward to a different host, if hop count has been exhausted + if (event.getNumHops() > MAX_HOPS) { + logger.warn("message discarded - hop count {} exceeded {} for topic {}", event.getNumHops(), MAX_HOPS, + topic); + + } else { + event.bumpNumHops(); + publish(target, event); + } + + // either way, consume the event + return true; + } + + /** + * Extract the request id from an event object. + * + * @param event the event object, or {@code null} + * @return the event's request id, or {@code null} if it can't be extracted + */ + private String extractRequestId(Object event) { + if (event == null) { + return null; + } + + Object reqid = extractors.extract(event); + return (reqid != null ? reqid.toString() : null); + } + + /** + * Decodes an event from a String into an event Object. + * + * @param topic2 + * @param event + * @return the decoded event object, or {@code null} if it can't be decoded + */ + private Object decodeEvent(String topic2, String event) { + DroolsController drools = controller.getDrools(); + + // check if this topic has a decoder + + if (!factory.canDecodeEvent(drools, topic2)) { + + logger.warn("{}: DECODING-UNSUPPORTED {}:{}:{}", drools, topic2, drools.getGroupId(), + drools.getArtifactId()); + return null; + } + + // decode + + try { + return factory.decodeEvent(drools, topic2, event); + + } catch (UnsupportedOperationException | IllegalStateException | IllegalArgumentException e) { + logger.debug("{}: DECODE FAILED: {} <- {} because of {}", drools, topic2, event, e.getMessage(), e); + return null; + } + } + + /** + * Makes a {@link Forward}, and validates its contents. + * + * @param protocol + * @param topic2 + * @param event + * @param reqid + * @return a new message, or {@code null} if the message was invalid + */ + private Forward makeForward(CommInfrastructure protocol, String topic2, String event, String reqid) { + try { + Forward ev = new Forward(host, protocol, topic2, event, reqid); + + // required for the validity check + ev.setChannel(host); + + ev.checkValidity(); + + return ev; + + } catch (PoolingFeatureException e) { + logger.error("invalid message for topic {}", topic2, e); + return null; + } + } + + @Override + public void handle(Forward event) { + synchronized (curLocker) { + if (!handleExternal(event)) { + // this host should handle it - inject it + inject(event); + } + } + } + + /** + * Injects an event into the controller. + * + * @param event + */ + private void inject(Forward event) { + intercept = false; + listener.onTopicEvent(event.getProtocol(), event.getTopic(), event.getPayload()); + + intercept = true; + } + + /** + * Handles an event from the internal topic. This uses reflection to + * identify the appropriate process() method to invoke, based on the type of + * Message that was decoded. + * + * @param event the serialized {@link Message} read from the internal topic + */ + private void handleInternal(String event) { + Class<?> clazz = null; + + try { + Message msg = serializer.decodeMsg(event); + + // get the class BEFORE checking the validity + clazz = msg.getClass(); + + msg.checkValidity(); + + Method meth = current.getClass().getMethod("process", msg.getClass()); + changeState((State) meth.invoke(current, msg)); + + } catch (IOException e) { + logger.warn("failed to decode message for topic {}", topic, e); + + } catch (NoSuchMethodException | SecurityException e) { + logger.error("no processor for message {} for topic {}", clazz, topic, e); + + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + logger.error("failed to process message {} for topic {}", clazz, topic, e); + + } catch (PoolingFeatureException e) { + logger.error("failed to process message {} for topic {}", clazz, topic, e); + } + } + + @Override + public void startDistributing(BucketAssignments assignments) { + if (assignments == null) { + return; + } + + synchronized (curLocker) { + this.assignments = assignments; + + // now that we have assignments, we can process the queue + Forward ev; + while ((ev = eventq.poll()) != null) { + handle(ev); + } + } + } + + @Override + public BucketAssignments getAssignments() { + return assignments; + } + + @Override + public State goStart() { + return new StartState(this); + } + + @Override + public State goQuery() { + return new QueryState(this); + } + + @Override + public State goActive() { + return new ActiveState(this); + } + + @Override + public State goInactive() { + return new InactiveState(this); + } + + /** + * Action to run a timer task. Only runs the task if the machine is still in + * the state that it was in when the timer was created. + */ + private class TimerAction implements Runnable { + + /** + * State of the machine when the timer was created. + */ + private State origState; + + /** + * Task to be executed. + */ + private StateTimerTask task; + + /** + * + * @param task task to execute when this timer runs + */ + public TimerAction(StateTimerTask task) { + this.origState = current; + this.task = task; + } + + @Override + public void run() { + synchronized (curLocker) { + if (current == origState) { + changeState(task.fire(null)); + } + } + } + } + + /** + * Factory used to create objects. + */ + public static class Factory { + + /** + * Creates an event queue. + * + * @param props properties used to configure the event queue + * @return a new event queue + */ + public EventQueue makeEventQueue(PoolingProperties props) { + return new EventQueue(props.getOfflineLimit(), props.getOfflineAgeMs()); + } + + /** + * Creates object extractors. + * + * @param props properties used to configure the extractors + * @return a new set of extractors + */ + public ClassExtractors makeClassExtractors(Properties props) { + return new ClassExtractors(props, PROP_EXTRACTOR_PREFIX, EXTRACTOR_TYPE); + } + + /** + * Creates a DMaaP manager. + * + * @param props properties used to configure the manager + * @return a new DMaaP manager + * @throws PoolingFeatureException if an error occurs + */ + public DmaapManager makeDmaapManager(PoolingProperties props) throws PoolingFeatureException { + return new DmaapManager(props.getPoolingTopic(), props.getSource()); + } + + /** + * Creates a scheduled thread pool. + * + * @return a new scheduled thread pool + */ + public ScheduledThreadPoolExecutor makeScheduler() { + return new ScheduledThreadPoolExecutor(1); + } + + /** + * Determines if the event can be decoded. + * + * @param drools drools controller + * @param topic topic on which the event was received + * @return {@code true} if the event can be decoded, {@code false} + * otherwise + */ + public boolean canDecodeEvent(DroolsController drools, String topic) { + return EventProtocolCoder.manager.isDecodingSupported(drools.getGroupId(), drools.getArtifactId(), topic); + } + + /** + * Decodes the event. + * + * @param drools drools controller + * @param topic topic on which the event was received + * @param event event text to be decoded + * @return the decoded event + * @throws IllegalArgumentException + * @throw UnsupportedOperationException + * @throws IllegalStateException + */ + public Object decodeEvent(DroolsController drools, String topic, String event) { + return EventProtocolCoder.manager.decode(drools.getGroupId(), drools.getArtifactId(), topic, event); + } + } +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingProperties.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingProperties.java new file mode 100644 index 00000000..1cbe5cb9 --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/PoolingProperties.java @@ -0,0 +1,162 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling; + +import java.util.Properties; +import org.onap.policy.common.utils.properties.SpecPropertyConfiguration; +import org.onap.policy.common.utils.properties.exception.PropertyException; + +/** + * Properties used by the pooling feature, specific to a controller. + */ +public class PoolingProperties extends SpecPropertyConfiguration { + + /** + * Feature properties all begin with this prefix. + */ + public static final String PREFIX = "pooling."; + + /* + * These properties REQUIRE a controller name, thus they use the "{$}" form. + */ + public static final String POOLING_TOPIC = PREFIX + "{$}.topic"; + + /* + * These properties allow the controller name to be left out, thus they use + * the "{prefix?suffix}" form. + */ + public static final String FEATURE_ENABLED = PREFIX + "{?.}enabled"; + public static final String OFFLINE_LIMIT = PREFIX + "{?.}offline.queue.limit"; + public static final String OFFLINE_AGE_MS = PREFIX + "{?.}offline.queue.age.milliseconds"; + public static final String START_HEARTBEAT_MS = PREFIX + "{?.}start.heartbeat.milliseconds"; + public static final String REACTIVATE_MS = PREFIX + "{?.}reactivate.milliseconds"; + public static final String IDENTIFICATION_MS = PREFIX + "{?.}identification.milliseconds"; + public static final String ACTIVE_HEARTBEAT_MS = PREFIX + "{?.}active.heartbeat.milliseconds"; + public static final String INTER_HEARTBEAT_MS = PREFIX + "{?.}inter.heartbeat.milliseconds"; + + /** + * Properties from which this was constructed. + */ + private Properties source; + + /** + * Topic used for inter-host communication. + */ + @Property(name = POOLING_TOPIC) + private String poolingTopic; + + /** + * Maximum number of events to retain in the queue while waiting for + * buckets to be assigned. + */ + @Property(name = OFFLINE_LIMIT, defaultValue = "1000") + private int offlineLimit; + + /** + * Maximum age, in milliseconds, of events to be retained in the queue. + * Events older than this are discarded. + */ + @Property(name = OFFLINE_AGE_MS, defaultValue = "60000") + private long offlineAgeMs; + + /** + * Time, in milliseconds, to wait for this host's heart beat during the + * start-up state. + */ + @Property(name = START_HEARTBEAT_MS, defaultValue = "50000") + private long startHeartbeatMs; + + /** + * Time, in milliseconds, to wait before attempting to re-active this + * host when it has no bucket assignments. + */ + @Property(name = REACTIVATE_MS, defaultValue = "50000") + private long reactivateMs; + + /** + * Time, in milliseconds, to wait for all Identification messages to + * arrive during the query state. + */ + @Property(name = IDENTIFICATION_MS, defaultValue = "50000") + private long identificationMs; + + /** + * Time, in milliseconds, to wait for heart beats from this host, or its + * predecessor, during the active state. + */ + @Property(name = ACTIVE_HEARTBEAT_MS, defaultValue = "50000") + private long activeHeartbeatMs; + + /** + * Time, in milliseconds, to wait between heart beat generations during + * the active state. + */ + @Property(name = INTER_HEARTBEAT_MS, defaultValue = "15000") + private long interHeartbeatMs; + + /** + * @param controllerName the name of the controller + * @param props set of properties used to configure this + * @throws PropertyException if an error occurs + * + */ + public PoolingProperties(String controllerName, Properties props) throws PropertyException { + super(controllerName, props); + + source = props; + } + + public Properties getSource() { + return source; + } + + public String getPoolingTopic() { + return poolingTopic; + } + + public int getOfflineLimit() { + return offlineLimit; + } + + public long getOfflineAgeMs() { + return offlineAgeMs; + } + + public long getStartHeartbeatMs() { + return startHeartbeatMs; + } + + public long getReactivateMs() { + return reactivateMs; + } + + public long getIdentificationMs() { + return identificationMs; + } + + public long getActiveHeartbeatMs() { + return activeHeartbeatMs; + } + + public long getInterHeartbeatMs() { + return interHeartbeatMs; + } +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/Serializer.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/Serializer.java new file mode 100644 index 00000000..63aefb7a --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/Serializer.java @@ -0,0 +1,79 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling; + +import java.io.IOException; +import java.util.Map; +import org.onap.policy.drools.pooling.message.Message; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Serialization helper functions. + */ +public class Serializer { + + /** + * Used to encode & decode JSON messages sent & received, respectively, on + * the internal DMaaP topic. + */ + private final ObjectMapper mapper = new ObjectMapper(); + + /** + * + */ + public Serializer() { + super(); + } + + /** + * Encodes a filter. + * + * @param filter filter to be encoded + * @return the filter, serialized as a JSON string + * @throws JsonProcessingException if it cannot be serialized + */ + public String encodeFilter(Map<String, Object> filter) throws JsonProcessingException { + return mapper.writeValueAsString(filter); + } + + /** + * Encodes a message. + * + * @param msg message to be encoded + * @return the message, serialized as a JSON string + * @throws JsonProcessingException if it cannot be serialized + */ + public String encodeMsg(Message msg) throws JsonProcessingException { + return mapper.writeValueAsString(msg); + } + + /** + * Decodes a JSON string into a Message. + * + * @param msg JSON string representing the message + * @return the message + * @throws IOException if it cannot be serialized + */ + public Message decodeMsg(String msg) throws IOException { + return mapper.readValue(msg, Message.class); + } +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/SpecProperties.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/SpecProperties.java new file mode 100644 index 00000000..e4557404 --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/SpecProperties.java @@ -0,0 +1,109 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling; + +import java.util.Properties; + +/** + * Properties with an optional specialization (e.g., session name, controller + * name). + */ +public class SpecProperties extends Properties { + private static final long serialVersionUID = 1L; + + /** + * The property prefix, ending with ".". + */ + private final String prefix; + + /** + * The specialized property prefix, ending with ".". + */ + private final String specPrefix; + + /** + * + * @param prefix the property name prefix that appears before any + * specialization + * @param specialization the property name specialization (e.g., session + * name) + */ + public SpecProperties(String prefix, String specialization) { + this.prefix = withTrailingDot(prefix); + this.specPrefix = withTrailingDot(this.prefix + specialization); + } + + /** + * + * @param prefix the property name prefix that appears before any + * specialization + * @param specialization the property name specialization (e.g., session + * name) + * @param props the default properties + */ + public SpecProperties(String prefix, String specialization, Properties props) { + super(props); + + this.prefix = withTrailingDot(prefix); + this.specPrefix = withTrailingDot(this.prefix + specialization); + } + + /** + * Adds a trailing "." to a String, if it doesn't already have one. + * + * @param text text to which the "." should be added + * @return the text, with a trailing "." + */ + private static String withTrailingDot(String text) { + return text.endsWith(".") ? text : text + "."; + } + + /** + * Gets the property whose value has the given key, looking first for the + * specialized property name, and then for the generalized property name. + * + * @param key property name, without the specialization + * @return the value from the property set, or {@code null} if the property + * set does not contain the value + */ + public String getProperty(String key) { + if (!key.startsWith(prefix)) { + return super.getProperty(key); + } + + String suffix = key.substring(prefix.length()); + + String val = super.getProperty(specPrefix + suffix); + if (val != null) { + return val; + } + + return super.getProperty(key); + } + + protected String getPrefix() { + return prefix; + } + + protected String getSpecPrefix() { + return specPrefix; + } +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/ClassExtractors.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/ClassExtractors.java new file mode 100644 index 00000000..cb12a6ac --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/ClassExtractors.java @@ -0,0 +1,466 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.extractor; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.commons.lang.StringUtils; +import org.onap.policy.drools.utils.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Extractors for each object class. Properties define how the data is to be + * extracted for a given class, where the properties are similar to the + * following: + * + * <pre> + * <code><a.prefix>.<class.name> = ${event.reqid}</code> + * </pre> + * + * If it doesn't find a property for the class, then it looks for a property for + * that class' super class or interfaces. Extractors are compiled and cached. + */ +public class ClassExtractors { + + private static final Logger logger = LoggerFactory.getLogger(ClassExtractors.class); + + /** + * Properties that specify how the data is to be extracted from a given + * class. + */ + private final Properties properties; + + /** + * Property prefix, including a trailing ".". + */ + private final String prefix; + + /** + * Type of item to be extracted. + */ + private final String type; + + /** + * Maps the class name to its extractor. + */ + private final ConcurrentHashMap<String, Extractor> class2extractor = new ConcurrentHashMap<>(); + + /** + * + * @param props properties that specify how the data is to be extracted from + * a given class + * @param prefix property name prefix, prepended before the class name + * @param type type of item to be extracted + */ + public ClassExtractors(Properties props, String prefix, String type) { + this.properties = props; + this.prefix = (prefix.endsWith(".") ? prefix : prefix + "."); + this.type = type; + } + + /** + * Gets the number of extractors in the map. + * + * @return gets the number of extractors in the map + */ + protected int size() { + return class2extractor.size(); + } + + /** + * Extracts the desired data item from an object. + * + * @param object object from which to extract the data item + * @return the extracted item, or {@code null} if it could not be extracted + */ + public Object extract(Object object) { + if (object == null) { + return null; + } + + Extractor ext = getExtractor(object); + + return ext.extract(object); + } + + /** + * Gets the extractor for the given type of object, creating one if it + * doesn't exist yet. + * + * @param object object whose extracted is desired + * @return an extractor for the object + */ + private Extractor getExtractor(Object object) { + Class<?> clazz = object.getClass(); + Extractor ext = class2extractor.get(clazz.getName()); + + if (ext == null) { + // allocate a new extractor, if another thread doesn't beat us to it + ext = class2extractor.computeIfAbsent(clazz.getName(), xxx -> buildExtractor(clazz)); + } + + return ext; + } + + /** + * Builds an extractor for the class. + * + * @param clazz class for which the extractor should be built + * + * @return a new extractor + */ + private Extractor buildExtractor(Class<?> clazz) { + String value = properties.getProperty(prefix + clazz.getName(), null); + if (value != null) { + // property has config info for this class - build the extractor + return buildExtractor(clazz, value); + } + + /* + * Get the extractor, if any, for the super class or interfaces, but + * don't add one if it doesn't exist + */ + Extractor ext = getClassExtractor(clazz, false); + if (ext != null) { + return ext; + } + + /* + * No extractor defined for for this class or its super class - we + * cannot extract data items from objects of this type, so just + * allocated a null extractor. + */ + logger.warn("missing property " + prefix + clazz.getName()); + return new NullExtractor(); + } + + /** + * Builds an extractor for the class, based on the config value extracted + * from the corresponding property. + * + * @param clazz class for which the extractor should be built + * @param value config value (e.g., "${event.request.id}" + * @return a new extractor + */ + private Extractor buildExtractor(Class<?> clazz, String value) { + if (!value.startsWith("${")) { + logger.warn("property value for " + prefix + clazz.getName() + " does not start with '${'"); + return new NullExtractor(); + } + + if (!value.endsWith("}")) { + logger.warn("property value for " + prefix + clazz.getName() + " does not end with '}'"); + return new NullExtractor(); + } + + // get the part in the middle + String val = value.substring(2, value.length() - 1); + if (val.startsWith(".")) { + logger.warn("property value for " + prefix + clazz.getName() + " begins with '.'"); + return new NullExtractor(); + } + + if (val.endsWith(".")) { + logger.warn("property value for " + prefix + clazz.getName() + " ends with '.'"); + return new NullExtractor(); + } + + // everything's valid - create the extractor + try { + ComponetizedExtractor ext = new ComponetizedExtractor(clazz, val.split("[.]")); + + /* + * If there's only one extractor, then just return it, otherwise + * return the whole extractor. + */ + return (ext.extractors.length == 1 ? ext.extractors[0] : ext); + + } catch (ExtractorException e) { + logger.warn("cannot build extractor for " + clazz.getName()); + return new NullExtractor(); + } + } + + /** + * Gets the extractor for a class, examining all super classes and + * interfaces. + * + * @param clazz class whose extractor is desired + * @param addOk {@code true} if the extractor may be added, provided the + * property is defined, {@code false} otherwise + * @return the extractor to be used for the class, or {@code null} if no + * extractor has been defined yet + */ + private Extractor getClassExtractor(Class<?> clazz, boolean addOk) { + if (clazz == null) { + return null; + } + + Extractor ext = null; + + if (addOk) { + String val = properties.getProperty(prefix + clazz.getName(), null); + + if (val != null) { + /* + * A property is defined for this class, so create the extractor + * for it. + */ + return class2extractor.computeIfAbsent(clazz.getName(), xxx -> buildExtractor(clazz)); + } + } + + // see if the superclass has an extractor + if ((ext = getClassExtractor(clazz.getSuperclass(), true)) != null) { + return ext; + } + + // check the interfaces, too + for (Class<?> clz : clazz.getInterfaces()) { + if ((ext = getClassExtractor(clz, true)) != null) { + break; + } + } + + return ext; + } + + /** + * Extractor that always returns {@code null}. Used when no extractor could + * be built for a given object type. + */ + private class NullExtractor implements Extractor { + + @Override + public Object extract(Object object) { + logger.warn("cannot extract " + type + " from " + object.getClass()); + return null; + } + } + + /** + * Component-ized extractor. Extracts an object that is referenced + * hierarchically, where each name identifies a particular component within + * the hierarchy. Supports retrieval from {@link Map} objects, as well as + * via getXxx() methods, or by direct field retrieval. + * <p> + * Note: this will <i>not</i> work if POJOs are contained within a Map. + */ + private class ComponetizedExtractor implements Extractor { + + /** + * Extractor for each component. + */ + private final Extractor[] extractors; + + /** + * + * @param clazz the class associated with the object at the root of the + * hierarchy + * @param names name associated with each component + * @throws ExtractorException + */ + public ComponetizedExtractor(Class<?> clazz, String[] names) throws ExtractorException { + this.extractors = new Extractor[names.length]; + + Class<?> clz = clazz; + + for (int x = 0; x < names.length; ++x) { + String comp = names[x]; + + Pair<Extractor, Class<?>> pair = buildExtractor(clz, comp); + + extractors[x] = pair.first(); + clz = pair.second(); + } + } + + /** + * Builds an extractor for the given component of an object. + * + * @param clazz type of object from which the component will be + * extracted + * @param comp name of the component to extract + * @return a pair containing the extractor and the extracted object's + * type + * @throws ExtractorException + */ + private Pair<Extractor, Class<?>> buildExtractor(Class<?> clazz, String comp) throws ExtractorException { + Pair<Extractor, Class<?>> pair = null; + + if (pair == null) { + pair = getMethodExtractor(clazz, comp); + } + + if (pair == null) { + pair = getFieldExtractor(clazz, comp); + } + + if (pair == null) { + pair = getMapExtractor(clazz, comp); + } + + + // didn't find an extractor + if (pair == null) { + throw new ExtractorException("class " + clazz + " contains no element " + comp); + } + + return pair; + } + + @Override + public Object extract(Object object) { + Object obj = object; + + for (Extractor ext : extractors) { + if (obj == null) { + break; + } + + obj = ext.extract(obj); + } + + return obj; + } + + /** + * Gets an extractor that invokes a getXxx() method to retrieve the + * object. + * + * @param clazz container's class + * @param name name of the property to be retrieved + * @return a new extractor, or {@code null} if the class does not + * contain the corresponding getXxx() method + * @throws ExtractorException if the getXxx() method is inaccessible + */ + private Pair<Extractor, Class<?>> getMethodExtractor(Class<?> clazz, String name) throws ExtractorException { + Method meth; + + String nm = "get" + StringUtils.capitalize(name); + + try { + meth = clazz.getMethod(nm); + + Class<?> retType = meth.getReturnType(); + if (retType == void.class) { + // it's a void method, thus it won't return an object + return null; + } + + return new Pair<>(new MethodExtractor(meth), retType); + + } catch (NoSuchMethodException expected) { + // no getXxx() method, maybe there's a field by this name + return null; + + } catch (SecurityException e) { + throw new ExtractorException("inaccessible method " + clazz + "." + nm, e); + } + } + + /** + * Gets an extractor for a field within the object. + * + * @param clazz container's class + * @param name name of the field whose value is to be extracted + * @return a new extractor, or {@code null} if the class does not + * contain the given field + * @throws ExtractorException if the field is inaccessible + */ + private Pair<Extractor, Class<?>> getFieldExtractor(Class<?> clazz, String name) throws ExtractorException { + + Field field = getClassField(clazz, name); + if (field == null) { + return null; + } + + return new Pair<>(new FieldExtractor(field), field.getType()); + } + + /** + * Gets an extractor for an item within a Map object. + * + * @param clazz container's class + * @param key item key within the map + * @return a new extractor, or {@code null} if the class is not a Map + * subclass + * @throws ExtractorException + */ + private Pair<Extractor, Class<?>> getMapExtractor(Class<?> clazz, String key) throws ExtractorException { + + if (!Map.class.isAssignableFrom(clazz)) { + return null; + } + + /* + * Don't know the value's actual type, so we'll assume it's a Map + * for now. Things should still work OK, as this is only used to + * direct the constructor on what type of extractor to create next. + * If the object turns out not to be a map, then the MapExtractor + * for the next component will just return null. + */ + return new Pair<>(new MapExtractor(key), Map.class); + } + + /** + * Gets field within a class, examining all super classes and + * interfaces. + * + * @param clazz class whose field is desired + * @param name name of the desired field + * @return the field within the class, or {@code null} if the field does + * not exist + * @throws ExtractorException if the field is inaccessible + */ + private Field getClassField(Class<?> clazz, String name) throws ExtractorException { + if (clazz == null) { + return null; + } + + try { + return clazz.getDeclaredField(name); + + } catch (NoSuchFieldException expected) { + // no field by this name - try super class & interfaces + + } catch (SecurityException e) { + throw new ExtractorException("inaccessible field " + clazz + "." + name, e); + } + + + Field field; + + // see if the superclass has an extractor + if ((field = getClassField(clazz.getSuperclass(), name)) != null) { + return field; + } + + // not necessary to check the interfaces + + return field; + } + } +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/Extractor.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/Extractor.java new file mode 100644 index 00000000..9ed32ae4 --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/Extractor.java @@ -0,0 +1,35 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.extractor; + +/** + * Used to extract an object contained within another object. + */ +public interface Extractor { + + /** + * Extracts an object contained within another object. + * + * @param object object from which to extract the contained object + * @return the extracted value, or {@code null} if it cannot be extracted + */ + public Object extract(Object object); +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/ExtractorException.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/ExtractorException.java new file mode 100644 index 00000000..a864672b --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/ExtractorException.java @@ -0,0 +1,49 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.extractor; + +/** + * Exception generated by extractors. + */ +public class ExtractorException extends Exception { + private static final long serialVersionUID = 1L; + + public ExtractorException() { + super(); + } + + public ExtractorException(String message) { + super(message); + } + + public ExtractorException(Throwable cause) { + super(cause); + } + + public ExtractorException(String message, Throwable cause) { + super(message, cause); + } + + public ExtractorException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/FieldExtractor.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/FieldExtractor.java new file mode 100644 index 00000000..132b8ed0 --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/FieldExtractor.java @@ -0,0 +1,59 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.extractor; + +import java.lang.reflect.Field; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Used to extract an object stored in one of the container's fields. + */ +public class FieldExtractor implements Extractor { + + private static final Logger logger = LoggerFactory.getLogger(FieldExtractor.class); + + /** + * Field containing the object. + */ + private final Field field; + + /** + * + * @param field field containing the object + */ + public FieldExtractor(Field field) { + this.field = field; + + field.setAccessible(true); + } + + @Override + public Object extract(Object object) { + try { + return field.get(object); + + } catch (IllegalAccessException | IllegalArgumentException e) { + logger.warn("cannot get {} from {}", field.getName(), object.getClass(), e); + return null; + } + } +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/MapExtractor.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/MapExtractor.java new file mode 100644 index 00000000..aff9d860 --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/MapExtractor.java @@ -0,0 +1,60 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.extractor; + +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Used to extract an object stored in a map. + */ +public class MapExtractor implements Extractor { + + private static final Logger logger = LoggerFactory.getLogger(MapExtractor.class); + + /** + * Key to the item to extract from the map. + */ + private final String key; + + /** + * + * @param key key to the item to extract from the map + */ + public MapExtractor(String key) { + this.key = key; + } + + @Override + public Object extract(Object object) { + + if (object instanceof Map) { + Map<?, ?> map = (Map<?, ?>) object; + + return map.get(key); + + } else { + logger.warn("expecting a map, instead of " + object.getClass()); + return null; + } + } +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/MethodExtractor.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/MethodExtractor.java new file mode 100644 index 00000000..20c4a1a7 --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/extractor/MethodExtractor.java @@ -0,0 +1,58 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.extractor; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Used to extract an object by invoking a method on the container. + */ +public class MethodExtractor implements Extractor { + + private static final Logger logger = LoggerFactory.getLogger(MethodExtractor.class); + + /** + * Method to invoke to extract the contained object. + */ + private final Method method; + + /** + * + * @param method method to invoke to extract the contained object + */ + public MethodExtractor(Method method) { + this.method = method; + } + + @Override + public Object extract(Object object) { + try { + return method.invoke(object); + + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + logger.warn("cannot invoke {} on {}", method.getName(), object.getClass(), e); + return null; + } + } +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/BucketAssignments.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/BucketAssignments.java new file mode 100644 index 00000000..8fd86c1e --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/BucketAssignments.java @@ -0,0 +1,215 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.message; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.onap.policy.drools.pooling.PoolingFeatureException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Bucket assignments, which is simply an array of host names. + */ +public class BucketAssignments { + + @JsonIgnore + private static final Logger logger = LoggerFactory.getLogger(BucketAssignments.class); + + /** + * Number of buckets. + */ + public static final int MAX_BUCKETS = 1024; + + /** + * Identifies the host serving a particular bucket. + */ + private String[] hostArray = null; + + /** + * + */ + public BucketAssignments() { + super(); + } + + /** + * + * @param hostArray maps a bucket number (i.e., array index) to a host. All + * values must be non-null + */ + public BucketAssignments(String[] hostArray) { + this.hostArray = hostArray; + } + + public String[] getHostArray() { + return hostArray; + } + + public void setHostArray(String[] hostArray) { + this.hostArray = hostArray; + } + + /** + * Gets the leader, which is the host with the minimum UUID. + * + * @return the assignment leader + */ + @JsonIgnore + public String getLeader() { + if (hostArray == null) { + return null; + } + + String leader = null; + + for (String host : hostArray) { + if (host != null && (leader == null || host.compareTo(leader) < 0)) { + leader = host; + } + } + + return leader; + + } + + /** + * Determines if a host has an assignment. + * + * @param host host to be checked + * @return {@code true} if the host has an assignment, {@code false} + * otherwise + */ + @JsonIgnore + public boolean hasAssignment(String host) { + if (hostArray == null) { + return false; + } + + for (String host2 : hostArray) { + if (host.equals(host2)) { + return true; + } + } + + return false; + } + + /** + * Gets all of the hosts that have an assignment. + * + * @return all of the hosts that have an assignment + */ + @JsonIgnore + public Set<String> getAllHosts() { + Set<String> set = new HashSet<>(); + if (hostArray == null) { + return set; + } + + for (String host : hostArray) { + if (host != null) { + set.add(host); + } + } + + return set; + } + + /** + * Gets the host assigned to a given bucket. + * + * @param bucket bucket number + * @return the assigned host, or {@code null} if the bucket has no assigned + * host + */ + @JsonIgnore + public String getAssignedHost(int bucket) { + if (hostArray == null) { + logger.error("no buckets have been assigned"); + return null; + } + + if (bucket < 0 || bucket >= hostArray.length) { + logger.error("invalid bucket number {} maximum {}", bucket, hostArray.length); + return null; + } + + return hostArray[bucket]; + } + + /** + * Gets the number of buckets. + * + * @return the number of buckets + */ + @JsonIgnore + public int size() { + return (hostArray != null ? hostArray.length : 0); + } + + /** + * Checks the validity of the assignments, verifying that all buckets have + * been assigned to a host. + * + * @throws PoolingFeatureException if the assignments are invalid + */ + @JsonIgnore + public void checkValidity() throws PoolingFeatureException { + if (hostArray == null || hostArray.length == 0) { + throw new PoolingFeatureException("missing hosts in message bucket assignments"); + } + + if (hostArray.length > MAX_BUCKETS) { + throw new PoolingFeatureException("too many hosts in message bucket assignments"); + } + + for (int x = 0; x < hostArray.length; ++x) { + if (hostArray[x] == null) { + throw new PoolingFeatureException("bucket " + x + " has no assignment"); + } + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(hostArray); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + BucketAssignments other = (BucketAssignments) obj; + if (!Arrays.equals(hostArray, other.hostArray)) + return false; + return true; + } +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Forward.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Forward.java new file mode 100644 index 00000000..b59cfbb2 --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Forward.java @@ -0,0 +1,180 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.message; + +import org.onap.policy.drools.event.comm.Topic.CommInfrastructure; +import org.onap.policy.drools.pooling.PoolingFeatureException; +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Message to forward an event to another host. + */ +public class Forward extends Message { + + /** + * Number of hops (i.e., number of times it's been forwarded) so far. + */ + private int numHops; + + /** + * Time, in milliseconds, at which the message was created. + */ + private long createTimeMs; + + /** + * Protocol of the receiving topic. + */ + private CommInfrastructure protocol; + + /** + * Topic on which the event was received. + */ + private String topic; + + /** + * The event pay load that was received on the topic. + */ + private String payload; + + /** + * The request id that was extracted from the event. + */ + private String requestId; + + /** + * + */ + public Forward() { + super(); + } + + /** + * + * @param source host on which the message originated + * @param protocol + * @param topic + * @param payload the actual event data received on the topic + * @param requestId + */ + public Forward(String source, CommInfrastructure protocol, String topic, String payload, String requestId) { + super(source); + + this.numHops = 0; + this.createTimeMs = System.currentTimeMillis(); + this.protocol = protocol; + this.topic = topic; + this.payload = payload; + this.requestId = requestId; + } + + /** + * Increments {@link #numHops}. + */ + public void bumpNumHops() { + ++numHops; + } + + public int getNumHops() { + return numHops; + } + + public void setNumHops(int numHops) { + this.numHops = numHops; + } + + public long getCreateTimeMs() { + return createTimeMs; + } + + public void setCreateTimeMs(long createTimeMs) { + this.createTimeMs = createTimeMs; + } + + public CommInfrastructure getProtocol() { + return protocol; + } + + public void setProtocol(CommInfrastructure protocol) { + this.protocol = protocol; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getPayload() { + return payload; + } + + public void setPayload(String payload) { + this.payload = payload; + } + + public String getRequestId() { + return requestId; + } + + public void setRequestId(String requestId) { + this.requestId = requestId; + } + + @JsonIgnore + public boolean isExpired(long minCreateTimeMs) { + return (createTimeMs < minCreateTimeMs); + + } + + @JsonIgnore + public void checkValidity() throws PoolingFeatureException { + + super.checkValidity(); + + if (protocol == null) { + throw new PoolingFeatureException("missing message protocol"); + } + + if (topic == null || topic.isEmpty()) { + throw new PoolingFeatureException("missing message topic"); + } + + /* + * Note: an empty pay load is OK, as an empty message could have been + * received on the topic. + */ + + if (payload == null) { + throw new PoolingFeatureException("missing message payload"); + } + + if (requestId == null || requestId.isEmpty()) { + throw new PoolingFeatureException("missing message requestId"); + } + + if (numHops < 0) { + throw new PoolingFeatureException("invalid message hop count"); + } + } + +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Heartbeat.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Heartbeat.java new file mode 100644 index 00000000..2a63a5be --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Heartbeat.java @@ -0,0 +1,60 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.message; + +/** + * Heart beat message sent to self, or to the succeeding host. + */ +public class Heartbeat extends Message { + + /** + * Time, in milliseconds, when this was created. + */ + private long timestampMs; + + /** + * + */ + public Heartbeat() { + super(); + + } + + /** + * + * @param source host on which the message originated + * @param timestampMs time, in milliseconds, associated with the message + */ + public Heartbeat(String source, long timestampMs) { + super(source); + + this.timestampMs = timestampMs; + } + + public long getTimestampMs() { + return timestampMs; + } + + public void setTimestampMs(long timestampMs) { + this.timestampMs = timestampMs; + } + +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Identification.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Identification.java new file mode 100644 index 00000000..5de6b8f9 --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Identification.java @@ -0,0 +1,45 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.message; + +/** + * Identifies the source host and the bucket assignments which it knows about. + */ +public class Identification extends MessageWithAssignments { + + /** + * + */ + public Identification() { + super(); + + } + + /** + * + * @param source host on which the message originated + * @param assignments + */ + public Identification(String source, BucketAssignments assignments) { + super(source, assignments); + } + +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Leader.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Leader.java new file mode 100644 index 00000000..0fc48c3c --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Leader.java @@ -0,0 +1,75 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.message; + +import org.onap.policy.drools.pooling.PoolingFeatureException; +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Indicates that the "source" of this message is now the "lead" host. + */ +public class Leader extends MessageWithAssignments { + + /** + * + */ + public Leader() { + super(); + } + + /** + * + * @param source host on which the message originated + * @param assignments + */ + public Leader(String source, BucketAssignments assignments) { + super(source, assignments); + } + + /** + * Also verifies that buckets have been assigned and that the source is + * indeed the leader. + */ + @Override + @JsonIgnore + public void checkValidity() throws PoolingFeatureException { + + super.checkValidity(); + + BucketAssignments assignments = getAssignments(); + if (assignments == null) { + throw new PoolingFeatureException("missing message bucket assignments"); + } + + String leader = getSource(); + + if(!assignments.hasAssignment(leader)) { + throw new PoolingFeatureException("leader " + leader + " has no bucket assignments"); + } + + for (String host : assignments.getHostArray()) { + if (host.compareTo(leader) < 0) { + throw new PoolingFeatureException("invalid leader " + leader + ", should be " + host); + } + } + } + +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Message.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Message.java new file mode 100644 index 00000000..e8a4671d --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Message.java @@ -0,0 +1,103 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.message; + +import org.onap.policy.drools.pooling.PoolingFeatureException; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * Messages sent on the internal topic. + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") +@JsonSubTypes({@Type(value = Forward.class, name = "forward"), @Type(value = Heartbeat.class, name = "heartbeat"), + @Type(value = Identification.class, name = "identification"), + @Type(value = Leader.class, name = "leader"), @Type(value = Offline.class, name = "offline"), + @Type(value = Query.class, name = "query")}) +public class Message { + + /** + * Name of the administrative channel. + */ + public static final String ADMIN = "_admin"; + + /** + * Host that originated the message. + */ + private String source; + + /** + * Channel on which the message is routed, which is either the target host + * or {@link #ADMIN}. + */ + private String channel; + + /** + * + */ + public Message() { + super(); + } + + /** + * + * @param source host on which the message originated + */ + public Message(String source) { + this.source = source; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public String getChannel() { + return channel; + } + + public void setChannel(String channel) { + this.channel = channel; + } + + /** + * Checks the validity of the message, including verifying that required + * fields are not missing. + * + * @throws PoolingFeatureException if the message is invalid + */ + @JsonIgnore + public void checkValidity() throws PoolingFeatureException { + if (source == null || source.isEmpty()) { + throw new PoolingFeatureException("missing message source"); + } + + if (channel == null || channel.isEmpty()) { + throw new PoolingFeatureException("missing message channel"); + } + } + +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/MessageWithAssignments.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/MessageWithAssignments.java new file mode 100644 index 00000000..9fded815 --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/MessageWithAssignments.java @@ -0,0 +1,77 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.message; + +import org.onap.policy.drools.pooling.PoolingFeatureException; +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * A Message that includes bucket assignments. + */ +public class MessageWithAssignments extends Message { + + /** + * Bucket assignments, as known by the source host. + */ + private BucketAssignments assignments; + + /** + * + */ + public MessageWithAssignments() { + super(); + } + + /** + * + * @param source host on which the message originated + * @param assignments + */ + public MessageWithAssignments(String source, BucketAssignments assignments) { + super(source); + + this.assignments = assignments; + + } + + public BucketAssignments getAssignments() { + return assignments; + } + + public void setAssignments(BucketAssignments assignments) { + this.assignments = assignments; + } + + /** + * If there are any assignments, it verifies there validity. + */ + @Override + @JsonIgnore + public void checkValidity() throws PoolingFeatureException { + + super.checkValidity(); + + if (assignments != null) { + assignments.checkValidity(); + } + } + +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Offline.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Offline.java new file mode 100644 index 00000000..297671ac --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Offline.java @@ -0,0 +1,45 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.message; + +/** + * Indicates that the source host is going offline and will be unable to process + * any further requests. + */ +public class Offline extends Message { + + /** + * + */ + public Offline() { + super(); + + } + + /** + * + * @param source host on which the message originated + */ + public Offline(String source) { + super(source); + } + +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Query.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Query.java new file mode 100644 index 00000000..c995a288 --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/message/Query.java @@ -0,0 +1,44 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.message; + +/** + * Query the other hosts for their identification. + */ +public class Query extends Message { + + /** + * + */ + public Query() { + super(); + + } + + /** + * + * @param source host on which the message originated + */ + public Query(String source) { + super(source); + } + +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/ActiveState.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/ActiveState.java new file mode 100644 index 00000000..5f503a3b --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/ActiveState.java @@ -0,0 +1,255 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.state; + +import java.util.Arrays; +import java.util.TreeSet; +import org.onap.policy.drools.pooling.PoolingManager; +import org.onap.policy.drools.pooling.message.Heartbeat; +import org.onap.policy.drools.pooling.message.Offline; +import org.onap.policy.drools.pooling.message.Query; + +/** + * The active state. In this state, this host has one more more bucket + * assignments and processes any events associated with one of its buckets. + * Other events are forwarded to appropriate target hosts. + */ +public class ActiveState extends ProcessingState { + + /** + * Set of hosts that have been assigned a bucket. + */ + private final TreeSet<String> assigned = new TreeSet<>(); + + /** + * Host that comes after this host, or {@code null} if it has no successor. + */ + private String succHost = null; + + /** + * Host that comes before this host, or "" if it has no predecessor. + */ + private String predHost = ""; + + /** + * {@code True} if we saw this host's heart beat since the last check, + * {@code false} otherwise. + */ + private boolean myHeartbeatSeen = false; + + /** + * {@code True} if we saw the predecessor's heart beat since the last check, + * {@code false} otherwise. + */ + private boolean predHeartbeatSeen = false; + + /** + * + * @param mgr + */ + public ActiveState(PoolingManager mgr) { + super(mgr, mgr.getAssignments().getLeader()); + + assigned.addAll(Arrays.asList(mgr.getAssignments().getHostArray())); + + detmNeighbors(); + } + + /** + * Determine this host's neighbors based on the order of the host UUIDs. + * Updates {@link #succHost} and {@link #predHost}. + */ + private void detmNeighbors() { + if (assigned.size() < 2) { + /* + * this host is the only one with any assignments - it has no + * neighbors + */ + succHost = null; + predHost = ""; + return; + } + + if ((succHost = assigned.higher(getHost())) == null) { + // wrapped around - successor is the first host in the set + succHost = assigned.first(); + } + + if ((predHost = assigned.lower(getHost())) == null) { + // wrapped around - predecessor is the last host in the set + predHost = assigned.last(); + } + } + + @Override + public void start() { + addTimers(); + genHeartbeat(); + } + + /** + * Adds the timers. + */ + private void addTimers() { + + /* + * heart beat generator + */ + long genMs = getProperties().getActiveHeartbeatMs(); + + scheduleWithFixedDelay(genMs, genMs, xxx -> { + genHeartbeat(); + return null; + }); + + /* + * my heart beat checker + */ + long interMs = getProperties().getInterHeartbeatMs(); + + scheduleWithFixedDelay(interMs, interMs, xxx -> { + if (myHeartbeatSeen) { + myHeartbeatSeen = false; + return null; + } + + // missed my heart beat + + return internalTopicFailed(); + }); + + /* + * predecessor heart beat checker + */ + if (!predHost.isEmpty()) { + + scheduleWithFixedDelay(interMs, interMs, xxx -> { + if (predHeartbeatSeen) { + predHeartbeatSeen = false; + return null; + } + + // missed the predecessor's heart beat + publish(makeQuery()); + + return goQuery(); + }); + } + } + + /** + * Generates a heart beat for this host and its successor. + */ + private void genHeartbeat() { + Heartbeat msg = makeHeartbeat(System.currentTimeMillis()); + publish(getHost(), msg); + + if (succHost != null) { + publish(succHost, msg); + } + } + + @Override + public State process(Heartbeat msg) { + String src = msg.getSource(); + + if (src == null) { + return null; + + } else if (src.equals(getHost())) { + myHeartbeatSeen = true; + + } else if (src.equals(predHost)) { + predHeartbeatSeen = true; + + } + + return null; + } + + @Override + public State process(Offline msg) { + String src = msg.getSource(); + + if (src == null) { + return null; + + } else if (!assigned.contains(src)) { + /* + * the offline host wasn't assigned any buckets, so just ignore the + * message + */ + return null; + + } else if (isLeader() || (predHost.equals(src) && predHost.equals(assigned.first()))) { + /* + * Case 1: We are the leader. + * + * Case 2: Our predecessor was the leader and it has gone offline - + * we should become the leader. + * + * In either case, we are now the leader and we must re-balance the + * buckets since one of the hosts has gone offline. + */ + + assigned.remove(src); + + return becomeLeader(assigned); + + } else { + /* + * Otherwise, we don't care right now - we'll wait for the leader to + * tell us it's been removed. + */ + return null; + } + } + + /** + * Transitions to the query state. + */ + @Override + public State process(Query msg) { + State next = super.process(msg); + if (next != null) { + return next; + } + + return goQuery(); + } + + protected String getSuccHost() { + return succHost; + } + + protected String getPredHost() { + return predHost; + } + + protected boolean isMyHeartbeatSeen() { + return myHeartbeatSeen; + } + + protected boolean isPredHeartbeatSeen() { + return predHeartbeatSeen; + } + +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/FilterUtils.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/FilterUtils.java new file mode 100644 index 00000000..a2da0ea2 --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/FilterUtils.java @@ -0,0 +1,96 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.state; + +import java.util.Map; +import java.util.TreeMap; + +/** + * Filter Utilities. These methods create <i>TreeMap</i> objects, because they + * should only contain a small number of items. + */ +public class FilterUtils { + // message element names + public static final String MSG_CHANNEL = "channel"; + public static final String MSG_TIMESTAMP = "timestampMs"; + + // json element names + protected static final String JSON_CLASS = "class"; + protected static final String JSON_FILTERS = "filters"; + protected static final String JSON_FIELD = "field"; + protected static final String JSON_VALUE = "value"; + + // values to be stuck into the "class" element + protected static final String CLASS_OR = "Or"; + protected static final String CLASS_AND = "And"; + protected static final String CLASS_EQUALS = "Equals"; + + /** + * + */ + private FilterUtils() { + super(); + } + + /** + * Makes a filter that verifies that a field equals a value. + * + * @param field name of the field to check + * @param value desired value + * @return a map representing an "equals" filter + */ + public static Map<String, Object> makeEquals(String field, String value) { + Map<String, Object> map = new TreeMap<>(); + map.put(JSON_CLASS, CLASS_EQUALS); + map.put(JSON_FIELD, field); + map.put(JSON_VALUE, value); + + return map; + } + + /** + * Makes an "and" filter, where all of the items must be true. + * + * @param items items to be checked + * @return an "and" filter + */ + public static Map<String, Object> makeAnd(@SuppressWarnings("unchecked") Map<String, Object>... items) { + Map<String, Object> map = new TreeMap<>(); + map.put(JSON_CLASS, CLASS_AND); + map.put(JSON_FILTERS, items); + + return map; + } + + /** + * Makes an "or" filter, where at least one of the items must be true. + * + * @param items items to be checked + * @return an "or" filter + */ + public static Map<String, Object> makeOr(@SuppressWarnings("unchecked") Map<String, Object>... items) { + Map<String, Object> map = new TreeMap<>(); + map.put(JSON_CLASS, CLASS_OR); + map.put(JSON_FILTERS, items); + + return map; + } +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/IdleState.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/IdleState.java new file mode 100644 index 00000000..27678360 --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/IdleState.java @@ -0,0 +1,85 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.state; + +import org.onap.policy.drools.pooling.PoolingManager; +import org.onap.policy.drools.pooling.message.Heartbeat; +import org.onap.policy.drools.pooling.message.Identification; +import org.onap.policy.drools.pooling.message.Leader; +import org.onap.policy.drools.pooling.message.Offline; +import org.onap.policy.drools.pooling.message.Query; + +/** + * Idle state, used when offline. + */ +public class IdleState extends State { + + public IdleState(PoolingManager mgr) { + super(mgr); + } + + @Override + public void stop() { + // do nothing - don't even send of "offline" message + } + + /** + * Discards the message. + */ + @Override + public State process(Heartbeat msg) { + return null; + } + + /** + * Discards the message. + */ + @Override + public State process(Identification msg) { + return null; + } + + /** + * Copies the assignments, but doesn't change states. + */ + @Override + public State process(Leader msg) { + super.process(msg); + return null; + } + + /** + * Discards the message. + */ + @Override + public State process(Offline msg) { + return null; + } + + /** + * Discards the message. + */ + @Override + public State process(Query msg) { + return null; + } + +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/InactiveState.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/InactiveState.java new file mode 100644 index 00000000..1c8e4dcc --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/InactiveState.java @@ -0,0 +1,55 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.state; + +import org.onap.policy.drools.pooling.PoolingManager; + +/** + * The inactive state. In this state, we just wait a bit and then try to + * re-activate. In the meantime, all messages are ignored. + */ +public class InactiveState extends State { + + /** + * + * @param mgr + */ + public InactiveState(PoolingManager mgr) { + super(mgr); + } + + @Override + public void start() { + + super.start(); + + schedule(getProperties().getReactivateMs(), xxx -> goStart()); + } + + /** + * Remains in this state. + */ + @Override + protected State goInactive() { + return null; + } + +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/ProcessingState.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/ProcessingState.java new file mode 100644 index 00000000..2f830c66 --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/ProcessingState.java @@ -0,0 +1,410 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.state; + +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import org.onap.policy.drools.pooling.PoolingManager; +import org.onap.policy.drools.pooling.message.BucketAssignments; +import org.onap.policy.drools.pooling.message.Identification; +import org.onap.policy.drools.pooling.message.Leader; +import org.onap.policy.drools.pooling.message.Query; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Any state in which events are being processed locally and forwarded, as + * appropriate. + */ +public class ProcessingState extends State { + + private static final Logger logger = LoggerFactory.getLogger(ProcessingState.class); + + /** + * Current known leader, never {@code null}. + */ + private String leader; + + /** + * + * @param mgr + * @param leader current known leader, which need not be the same as the + * assignment leader. Never {@code null} + * @throws IllegalArgumentException if an argument is invalid + */ + public ProcessingState(PoolingManager mgr, String leader) { + super(mgr); + + if (leader == null) { + throw new IllegalArgumentException("null leader"); + } + + BucketAssignments assignments = mgr.getAssignments(); + + if (assignments != null) { + String[] arr = assignments.getHostArray(); + if (arr != null && arr.length == 0) { + throw new IllegalArgumentException("zero-length bucket assignments"); + } + } + + this.leader = leader; + } + + /** + * Generates an Identification message and returns {@code null}. + */ + @Override + public State process(Query msg) { + publish(makeIdentification()); + return goQuery(); + } + + /** + * Makes an Identification message. + * + * @return a new message + */ + protected Identification makeIdentification() { + return new Identification(getHost(), getAssignments()); + } + + /** + * Sets the assignments. + * + * @param assignments new assignments, or {@code null} + */ + protected final void setAssignments(BucketAssignments assignments) { + if (assignments != null) { + startDistributing(assignments); + } + } + + public String getLeader() { + return leader; + } + + /** + * Sets the leader. + * + * @param leader the new leader + * @throws IllegalArgumentException if an argument is invalid + */ + protected void setLeader(String leader) { + if (leader == null) { + throw new IllegalArgumentException("null leader"); + } + + this.leader = leader; + } + + /** + * Determines if this host is the leader, based on the current assignments. + * + * @return {@code true} if this host is the leader, {@code false} otherwise + */ + public boolean isLeader() { + return getHost().equals(leader); + } + + /** + * Becomes the leader. Publishes a Leader message and enters the + * {@link ActiveState}. + * + * @param alive hosts that are known to be alive + * + * @return the new state + */ + protected State becomeLeader(SortedSet<String> alive) { + String leader = getHost(); + + if (!leader.equals(alive.first())) { + throw new IllegalArgumentException(leader + " cannot replace " + alive.first()); + } + + Leader msg = makeLeader(alive); + publish(msg); + + setAssignments(msg.getAssignments()); + + return goActive(); + } + + /** + * Makes a leader message. Assumes "this" host is the leader, and thus + * appears as the first host in the set of hosts that are still alive. + * + * @param alive hosts that are known to be alive + * + * @return a new message + */ + private Leader makeLeader(Set<String> alive) { + return new Leader(getHost(), makeAssignments(alive)); + } + + /** + * Makes a set of bucket assignments. Assumes "this" host is the leader. + * + * @param alive hosts that are known to be alive + * + * @return a new set of bucket assignments + */ + private BucketAssignments makeAssignments(Set<String> alive) { + + // make a working array from the CURRENT assignments + String[] bucket2host = makeBucketArray(); + + TreeSet<String> avail = new TreeSet<>(alive); + + // if we have more hosts than buckets, then remove the extra hosts + removeExcessHosts(bucket2host.length, avail); + + // create a host bucket for each available host + Map<String, HostBucket> host2hb = new HashMap<>(); + avail.forEach(host -> host2hb.put(host, new HostBucket(host))); + + // add bucket indices to the appropriate host bucket + addIndicesToHostBuckets(bucket2host, host2hb); + + // convert the collection back to an array + fillArray(host2hb.values(), bucket2host); + + // update bucket2host with new assignments + rebalanceBuckets(host2hb.values(), bucket2host); + + return new BucketAssignments(bucket2host); + } + + /** + * Makes a bucket array, copying the current assignments, if available. + * + * @return a new bucket array + */ + private String[] makeBucketArray() { + BucketAssignments asgn = getAssignments(); + if (asgn == null) { + return new String[BucketAssignments.MAX_BUCKETS]; + } + + String[] oldArray = asgn.getHostArray(); + if (oldArray.length == 0) { + return new String[BucketAssignments.MAX_BUCKETS]; + } + + String[] newArray = new String[oldArray.length]; + System.arraycopy(oldArray, 0, newArray, 0, oldArray.length); + + return newArray; + } + + /** + * Removes excess hosts from the set of available hosts. Assumes "this" host + * is the leader, and thus appears as the first host in the set. + * + * @param maxHosts maximum number of hosts to be retained + * @param avail available hosts + */ + private void removeExcessHosts(int maxHosts, SortedSet<String> avail) { + while (avail.size() > maxHosts) { + /* + * Don't remove this host, as it's the leader. Since the leader is + * always at the front of the sorted set, we'll just pick off hosts + * from the back of the set. + */ + String host = avail.last(); + avail.remove(host); + + logger.warn("not using extra host {} for topic {}", host, getTopic()); + } + } + + /** + * Adds bucket indices to {@link HostBucket} objects. Buckets that are + * unassigned or assigned to a host that does not appear within the map are + * re-assigned to a host that appears within the map. + * + * @param bucket2host bucket assignments + * @param host2data maps a host name to its {@link HostBucket} + */ + private void addIndicesToHostBuckets(String[] bucket2host, Map<String, HostBucket> host2data) { + LinkedList<Integer> nullBuckets = new LinkedList<Integer>(); + + for (int x = 0; x < bucket2host.length; ++x) { + String host = bucket2host[x]; + if (host == null) { + nullBuckets.add(x); + + } else { + HostBucket hb = host2data.get(host); + if (hb == null) { + nullBuckets.add(x); + + } else { + hb.add(x); + } + } + } + + // assign the null buckets to other hosts + assignNullBuckets(nullBuckets, host2data.values()); + } + + /** + * Assigns null buckets (i.e., those having no assignment) to available + * hosts. + * + * @param buckets available hosts + * @param coll collection of current host-bucket assignments + */ + private void assignNullBuckets(Queue<Integer> buckets, Collection<HostBucket> coll) { + // assign null buckets to the hosts with the fewest buckets + TreeSet<HostBucket> assignments = new TreeSet<>(coll); + + for (Integer index : buckets) { + // add it to the host with the shortest bucket list + HostBucket newhb = assignments.pollFirst(); + newhb.add(index); + + // put the item back into the queue, with its new count + assignments.add(newhb); + } + } + + /** + * Re-balances the buckets, taking from those that have a larger count and + * giving to those that have a smaller count. Populates an output array with + * the new assignments. + * + * @param coll current bucket assignment + * @param bucket2host array to be populated with the new assignments + */ + private void rebalanceBuckets(Collection<HostBucket> coll, String[] bucket2host) { + if (coll.size() <= 1) { + // only one hosts - nothing to rebalance + return; + } + + TreeSet<HostBucket> assignments = new TreeSet<>(coll); + + for (;;) { + HostBucket smaller = assignments.pollFirst(); + HostBucket larger = assignments.pollLast(); + + if (larger.size() - smaller.size() <= 1) { + // it's as balanced as it will get + break; + } + + // move the bucket from the larger to the smaller + Integer b = larger.remove(); + smaller.add(b); + + bucket2host[b] = smaller.host; + + // put the items back, with their new counts + assignments.add(larger); + assignments.add(smaller); + } + + } + + /** + * Fills the array with the host assignments. + * + * @param coll the host assignments + * @param bucket2host array to be filled + */ + private void fillArray(Collection<HostBucket> coll, String[] bucket2host) { + for (HostBucket hb : coll) { + for (Integer index : hb.buckets) { + bucket2host[index] = hb.host; + } + } + } + + /** + * Tracks buckets that have been assigned to a host. + */ + public static class HostBucket implements Comparable<HostBucket> { + /** + * Host to which the buckets have been assigned. + */ + private String host; + + /** + * Buckets that have been assigned to this host. + */ + private Queue<Integer> buckets = new LinkedList<>(); + + /** + * + * @param host + */ + public HostBucket(String host) { + this.host = host; + } + + /** + * Removes the next bucket from the list. + * + * @return the next bucket + */ + public final Integer remove() { + return buckets.remove(); + } + + /** + * Adds a bucket to the list. + * + * @param index index of the bucket to add + */ + public final void add(Integer index) { + buckets.add(index); + } + + /** + * + * @return the number of buckets assigned to this host + */ + public final int size() { + return buckets.size(); + } + + /** + * Compares host buckets, first by the number of buckets, and then by + * the host name. + */ + @Override + public final int compareTo(HostBucket other) { + int d = buckets.size() - other.buckets.size(); + if (d == 0) { + d = host.compareTo(other.host); + } + return d; + } + } +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/QueryState.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/QueryState.java new file mode 100644 index 00000000..57521960 --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/QueryState.java @@ -0,0 +1,209 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.state; + +import java.util.TreeSet; +import org.onap.policy.drools.pooling.PoolingManager; +import org.onap.policy.drools.pooling.message.BucketAssignments; +import org.onap.policy.drools.pooling.message.Identification; +import org.onap.policy.drools.pooling.message.Leader; +import org.onap.policy.drools.pooling.message.Offline; + +// TODO add logging + +/** + * The Query state. In this state, the host waits for the other hosts to + * identify themselves. Eventually, a leader should come forth. If not, it will + * transition to the active or inactive state, depending on whether or not it + * has an assignment in the current bucket assignments. The other possibility is + * that it may <i>become</i> the leader, in which case it will also transition + * to the active state. + */ +public class QueryState extends ProcessingState { + + /** + * Hosts that have sent an "Identification" message. Always includes this + * host. + */ + private TreeSet<String> alive = new TreeSet<>(); + + /** + * + * @param mgr + */ + public QueryState(PoolingManager mgr) { + // this host is the leader, until a better candidate identifies itself + super(mgr, mgr.getHost()); + + alive.add(getHost()); + } + + @Override + public void start() { + + super.start(); + + // start identification timer + awaitIdentification(); + } + + /** + * Starts a timer to wait for all Identification messages to arrive. + */ + private void awaitIdentification() { + + /* + * Once we've waited long enough for all Identification messages to + * arrive, become the leader, assuming we should. + */ + + schedule(getProperties().getIdentificationMs(), xxx -> { + + if (isLeader()) { + // "this" host is the new leader + return becomeLeader(alive); + + } else if (hasAssignment()) { + /* + * this host is not the new leader, but it does have an + * assignment - return to the active state while we wait for the + * leader + */ + return goActive(); + + } else { + // not the leader and no assignment yet + return goInactive(); + } + }); + } + + /** + * Remains in this state. + */ + @Override + public State goQuery() { + return null; + } + + /** + * Determines if this host has an assignment in the CURRENT assignments. + * + * @return {@code true} if this host has an assignment, {@code false} + * otherwise + */ + protected boolean hasAssignment() { + BucketAssignments asgn = getAssignments(); + return (asgn != null && asgn.hasAssignment(getHost())); + } + + @Override + public State process(Identification msg) { + + recordInfo(msg.getSource(), msg.getAssignments()); + + return null; + } + + /** + * If the message leader is better than the leader we have, then go active + * with it. Otherwise, simply treat it like an {@link Identification} + * message. + */ + @Override + public State process(Leader msg) { + BucketAssignments asgn = msg.getAssignments(); + if (asgn == null) { + return null; + } + + // ignore Leader messages from ourself + String source = msg.getSource(); + if (source == null || source.equals(getHost())) { + return null; + } + + // the new leader must equal the source + if (!source.equals(asgn.getLeader())) { + return null; + } + + // go active, if this has a better leader than the one we have + if (source.compareTo(getLeader()) < 0) { + return super.process(msg); + } + + /* + * The message does not have an acceptable leader, but we'll still + * record its info. + */ + recordInfo(msg.getSource(), msg.getAssignments()); + + return null; + } + + /** + * Records info from a message, adding the source host name to + * {@link #alive}, and updating the bucket assignments. + * + * @param source the message's source host + * @param assignments assignments, or {@code null} + */ + private void recordInfo(String source, BucketAssignments assignments) { + // add this message's source host to "alive" + if (source != null) { + alive.add(source); + setLeader(alive.first()); + } + + if (assignments == null || assignments.getLeader() == null) { + return; + } + + // record assignments, if we don't have any yet + BucketAssignments current = getAssignments(); + if (current == null) { + setAssignments(assignments); + return; + } + + /* + * Record assignments, if the new assignments have a better (i.e., + * lesser) leader. + */ + String curldr = current.getLeader(); + if (curldr == null || curldr.compareTo(assignments.getLeader()) > 0) { + setAssignments(assignments); + } + } + + @Override + public State process(Offline msg) { + String host = msg.getSource(); + + if (host != null && !host.equals(getHost())) { + alive.remove(host); + setLeader(alive.first()); + } + + return null; + } +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/StartState.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/StartState.java new file mode 100644 index 00000000..decbdfda --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/StartState.java @@ -0,0 +1,132 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.state; + +import static org.onap.policy.drools.pooling.state.FilterUtils.MSG_CHANNEL; +import static org.onap.policy.drools.pooling.state.FilterUtils.MSG_TIMESTAMP; +import static org.onap.policy.drools.pooling.state.FilterUtils.makeAnd; +import static org.onap.policy.drools.pooling.state.FilterUtils.makeEquals; +import static org.onap.policy.drools.pooling.state.FilterUtils.makeOr; +import java.util.Map; +import org.onap.policy.drools.pooling.PoolingManager; +import org.onap.policy.drools.pooling.message.Heartbeat; +import org.onap.policy.drools.pooling.message.Identification; +import org.onap.policy.drools.pooling.message.Leader; +import org.onap.policy.drools.pooling.message.Message; +import org.onap.policy.drools.pooling.message.Offline; +import org.onap.policy.drools.pooling.message.Query; + +/** + * The start state. Upon entry, a heart beat is generated and the event filter + * is changed to look for just that particular message. Once the message is + * seen, it goes into the {@link QueryState}. + */ +public class StartState extends State { + + /** + * Time stamp inserted into the heart beat message. + */ + private long hbTimestampMs = System.currentTimeMillis(); + + /** + * + * @param mgr + */ + public StartState(PoolingManager mgr) { + super(mgr); + } + + /** + * + * @return the time stamp inserted into the heart beat message + */ + public long getHbTimestampMs() { + return hbTimestampMs; + } + + @Override + public void start() { + + super.start(); + + publish(getHost(), makeHeartbeat(hbTimestampMs)); + + schedule(getProperties().getStartHeartbeatMs(), xxx -> internalTopicFailed()); + } + + /** + * Transitions to the query state if the heart beat originated from this + * host and its time stamp matches. + */ + @Override + public State process(Heartbeat msg) { + if (msg.getTimestampMs() == hbTimestampMs && getHost().equals(msg.getSource())) { + // saw our own heart beat - transition to query state + publish(makeQuery()); + return goQuery(); + } + + return null; + } + + /** + * Discards the message. + */ + @Override + public State process(Identification msg) { + return null; + } + + /** + * Processes the assignments, but remains in the current state. + */ + @Override + public State process(Leader msg) { + super.process(msg); + return null; + } + + /** + * Discards the message. + */ + @Override + public State process(Offline msg) { + return null; + } + + /** + * Discards the message. + */ + @Override + public State process(Query msg) { + return null; + } + + @SuppressWarnings("unchecked") + @Override + public Map<String, Object> getFilter() { + // ignore everything except our most recent heart beat message + return makeOr(makeEquals(MSG_CHANNEL, Message.ADMIN), makeAnd(makeEquals(MSG_CHANNEL, getHost()), + makeEquals(MSG_TIMESTAMP, String.valueOf(hbTimestampMs)))); + + } + +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/State.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/State.java new file mode 100644 index 00000000..1e3a907e --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/State.java @@ -0,0 +1,370 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.state; + +import static org.onap.policy.drools.pooling.state.FilterUtils.MSG_CHANNEL; +import static org.onap.policy.drools.pooling.state.FilterUtils.makeEquals; +import static org.onap.policy.drools.pooling.state.FilterUtils.makeOr; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import org.onap.policy.drools.pooling.PoolingManager; +import org.onap.policy.drools.pooling.PoolingProperties; +import org.onap.policy.drools.pooling.message.BucketAssignments; +import org.onap.policy.drools.pooling.message.Forward; +import org.onap.policy.drools.pooling.message.Heartbeat; +import org.onap.policy.drools.pooling.message.Identification; +import org.onap.policy.drools.pooling.message.Leader; +import org.onap.policy.drools.pooling.message.Message; +import org.onap.policy.drools.pooling.message.Offline; +import org.onap.policy.drools.pooling.message.Query; + +/** + * A state in the finite state machine. + * <p> + * A state may have several timers associated with it, which must be cancelled + * whenever the state is changed. Assumes that timers are not continuously added + * to the same state. + */ +public abstract class State { + + /** + * Host pool manager. + */ + private final PoolingManager mgr; + + /** + * Timers added by this state. + */ + private final List<ScheduledFuture<?>> timers = new LinkedList<>(); + + /** + * + * @param mgr + */ + public State(PoolingManager mgr) { + this.mgr = mgr; + } + + /** + * Gets the server-side filter to use when polling the DMaaP internal topic. + * The default method returns a filter that accepts messages on the admin + * channel and on the host's own channel. + * + * @return the server-side filter to use. + */ + @SuppressWarnings("unchecked") + public Map<String, Object> getFilter() { + return makeOr(makeEquals(MSG_CHANNEL, Message.ADMIN), makeEquals(MSG_CHANNEL, getHost())); + } + + /** + * Cancels the timers added by this state. + */ + public void cancelTimers() { + for (ScheduledFuture<?> fut : timers) { + fut.cancel(false); + } + } + + /** + * Starts the state. + */ + public void start() { + + } + + /** + * Indicates that the finite state machine is stopping. Sends an "offline" + * message to the other hosts. + */ + public void stop() { + publish(makeOffline()); + } + + /** + * Transitions to the "start" state. + * + * @return the new state + */ + public State goStart() { + return mgr.goStart(); + } + + /** + * Transitions to the "query" state. + * + * @return the new state + */ + public State goQuery() { + return mgr.goQuery(); + } + + /** + * Transitions to the "active" state. + * + * @return the new state + */ + public State goActive() { + return mgr.goActive(); + } + + /** + * Transitions to the "inactive" state. + * + * @return the new state + */ + protected State goInactive() { + return mgr.goInactive(); + } + + /** + * Processes a message. The default method passes it to the manager to + * handle and returns {@code null}. + * + * @param msg message to be processed + * @return the new state, or {@code null} if the state is unchanged + */ + public State process(Forward msg) { + mgr.handle(msg); + return null; + } + + /** + * Processes a message. The default method just returns {@code null}. + * + * @param msg message to be processed + * @return the new state, or {@code null} if the state is unchanged + */ + public State process(Heartbeat msg) { + return null; + } + + /** + * Processes a message. The default method just returns {@code null}. + * + * @param msg message to be processed + * @return the new state, or {@code null} if the state is unchanged + */ + public State process(Identification msg) { + return null; + } + + /** + * Processes a message. If this host has a new assignment, then it + * transitions to the active state. Otherwise, it transitions to the + * inactive state. + * + * @param msg message to be processed + * @return the new state, or {@code null} if the state is unchanged + */ + public State process(Leader msg) { + BucketAssignments asgn = msg.getAssignments(); + if (asgn == null) { + return null; + } + + String source = msg.getSource(); + if (source == null) { + return null; + } + + // the new leader must equal the source + if (source.equals(asgn.getLeader())) { + startDistributing(asgn); + + if (asgn.hasAssignment(getHost())) { + return goActive(); + + } else { + return goInactive(); + } + } + + return null; + } + + /** + * Processes a message. The default method just returns {@code null}. + * + * @param msg message to be processed + * @return the new state, or {@code null} if the state is unchanged + */ + public State process(Offline msg) { + return null; + } + + /** + * Processes a message. The default method just returns {@code null}. + * + * @param msg message to be processed + * @return the new state, or {@code null} if the state is unchanged + */ + public State process(Query msg) { + return null; + } + + /** + * Publishes a message. + * + * @param msg message to be published + */ + protected void publish(Identification msg) { + mgr.publishAdmin(msg); + } + + /** + * Publishes a message. + * + * @param msg message to be published + */ + protected void publish(Leader msg) { + mgr.publishAdmin(msg); + } + + /** + * Publishes a message. + * + * @param msg message to be published + */ + protected void publish(Offline msg) { + mgr.publishAdmin(msg); + } + + /** + * Publishes a message. + * + * @param msg message to be published + */ + protected void publish(Query msg) { + mgr.publishAdmin(msg); + } + + /** + * Publishes a message on the specified channel. + * + * @param channel + * @param msg message to be published + */ + protected void publish(String channel, Forward msg) { + mgr.publish(channel, msg); + } + + /** + * Publishes a message on the specified channel. + * + * @param channel + * @param msg message to be published + */ + protected void publish(String channel, Heartbeat msg) { + mgr.publish(channel, msg); + } + + /** + * Starts distributing messages using the specified bucket assignments. + * + * @param assignments + */ + protected void startDistributing(BucketAssignments assignments) { + if (assignments != null) { + mgr.startDistributing(assignments); + } + } + + /** + * Schedules a timer to fire after a delay. + * + * @param delayMs + * @param task + */ + protected void schedule(long delayMs, StateTimerTask task) { + timers.add(mgr.schedule(delayMs, task)); + } + + /** + * Schedules a timer to fire repeatedly. + * + * @param initialDelayMs + * @param delayMs + * @param task + */ + protected void scheduleWithFixedDelay(long initialDelayMs, long delayMs, StateTimerTask task) { + timers.add(mgr.scheduleWithFixedDelay(initialDelayMs, delayMs, task)); + } + + /** + * Indicates that the internal topic failed. + * + * @return a new {@link InactiveState} + */ + protected State internalTopicFailed() { + publish(makeOffline()); + mgr.internalTopicFailed(); + + return mgr.goInactive(); + } + + /** + * Makes a heart beat message. + * + * @param timestampMs time, in milliseconds, associated with the message + * + * @return a new message + */ + protected Heartbeat makeHeartbeat(long timestampMs) { + return new Heartbeat(getHost(), timestampMs); + } + + /** + * Makes an "offline" message. + * + * @return a new message + */ + protected Offline makeOffline() { + return new Offline(getHost()); + } + + /** + * Makes a query message. + * + * @return a new message + */ + protected Query makeQuery() { + return new Query(getHost()); + } + + public final BucketAssignments getAssignments() { + return mgr.getAssignments(); + } + + public final String getHost() { + return mgr.getHost(); + } + + public final String getTopic() { + return mgr.getTopic(); + } + + public final PoolingProperties getProperties() { + return mgr.getProperties(); + } +} diff --git a/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/StateTimerTask.java b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/StateTimerTask.java new file mode 100644 index 00000000..bd388b4e --- /dev/null +++ b/feature-pooling-dmaap/src/main/java/org/onap/policy/drools/pooling/state/StateTimerTask.java @@ -0,0 +1,37 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.state; + +/** + * Task to be executed when a timer fires within a {@link State}. + */ +@FunctionalInterface +public interface StateTimerTask { + + /** + * Fires the timer. + * + * @param arg always {@code null} + * @return the new state, or {@code null} if the state is unchanged + */ + public State fire(Void arg); + +} diff --git a/feature-pooling-dmaap/src/main/resources/META-INF/services/org.onap.policy.drools.core.PolicySessionFeatureAPI b/feature-pooling-dmaap/src/main/resources/META-INF/services/org.onap.policy.drools.core.PolicySessionFeatureAPI new file mode 100644 index 00000000..cd59e469 --- /dev/null +++ b/feature-pooling-dmaap/src/main/resources/META-INF/services/org.onap.policy.drools.core.PolicySessionFeatureAPI @@ -0,0 +1 @@ +org.onap.policy.drools.pooling.PoolingFeature diff --git a/feature-pooling-dmaap/src/main/resources/META-INF/services/org.onap.policy.drools.features.DroolsControllerFeatureAPI b/feature-pooling-dmaap/src/main/resources/META-INF/services/org.onap.policy.drools.features.DroolsControllerFeatureAPI new file mode 100644 index 00000000..cd59e469 --- /dev/null +++ b/feature-pooling-dmaap/src/main/resources/META-INF/services/org.onap.policy.drools.features.DroolsControllerFeatureAPI @@ -0,0 +1 @@ +org.onap.policy.drools.pooling.PoolingFeature diff --git a/feature-pooling-dmaap/src/main/resources/META-INF/services/org.onap.policy.drools.features.PolicyControllerFeatureAPI b/feature-pooling-dmaap/src/main/resources/META-INF/services/org.onap.policy.drools.features.PolicyControllerFeatureAPI new file mode 100644 index 00000000..cd59e469 --- /dev/null +++ b/feature-pooling-dmaap/src/main/resources/META-INF/services/org.onap.policy.drools.features.PolicyControllerFeatureAPI @@ -0,0 +1 @@ +org.onap.policy.drools.pooling.PoolingFeature diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/DmaapManagerTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/DmaapManagerTest.java new file mode 100644 index 00000000..f68f2395 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/DmaapManagerTest.java @@ -0,0 +1,355 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.Properties; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.onap.policy.drools.event.comm.FilterableTopicSource; +import org.onap.policy.drools.event.comm.TopicListener; +import org.onap.policy.drools.event.comm.TopicSink; +import org.onap.policy.drools.event.comm.TopicSource; +import org.onap.policy.drools.pooling.DmaapManager.Factory; + +public class DmaapManagerTest { + + private static String MY_TOPIC = "my.topic"; + private static String MSG = "a message"; + private static String FILTER = "a filter"; + + /** + * Original factory, to be restored when all tests complete. + */ + private static Factory saveFactory; + + private Properties props; + private Factory factory; + private TopicListener listener; + private FilterableTopicSource source; + private TopicSink sink; + private DmaapManager mgr; + + @BeforeClass + public static void setUpBeforeClass() throws Exception { + saveFactory = DmaapManager.getFactory(); + } + + @AfterClass + public static void tearDownAfterClass() throws Exception { + DmaapManager.setFactory(saveFactory); + } + + @Before + public void setUp() throws Exception { + props = new Properties(); + + listener = mock(TopicListener.class); + factory = mock(Factory.class); + source = mock(FilterableTopicSource.class); + sink = mock(TopicSink.class); + + DmaapManager.setFactory(factory); + + when(source.getTopic()).thenReturn(MY_TOPIC); + + when(sink.getTopic()).thenReturn(MY_TOPIC); + when(sink.send(any())).thenReturn(true); + + // three sources, with the desired one in the middle + when(factory.initTopicSources(props)) + .thenReturn(Arrays.asList(mock(TopicSource.class), source, mock(TopicSource.class))); + + // three sinks, with the desired one in the middle + when(factory.initTopicSinks(props)) + .thenReturn(Arrays.asList(mock(TopicSink.class), sink, mock(TopicSink.class))); + + mgr = new DmaapManager(MY_TOPIC, props); + } + + @Test + public void testDmaapManager() { + // verify that the init methods were called + verify(factory).initTopicSinks(props); + verify(factory).initTopicSinks(props); + } + + @Test(expected = PoolingFeatureException.class) + public void testDmaapManager_PoolingEx() throws PoolingFeatureException { + // force error by having no topics match + when(source.getTopic()).thenReturn(""); + + new DmaapManager(MY_TOPIC, props); + } + + @Test(expected = PoolingFeatureException.class) + public void testDmaapManager_IllegalArgEx() throws PoolingFeatureException { + // force error + when(factory.initTopicSources(props)).thenThrow(new IllegalArgumentException("expected")); + + new DmaapManager(MY_TOPIC, props); + } + + @Test(expected = PoolingFeatureException.class) + public void testDmaapManager_CannotFilter() throws PoolingFeatureException { + // force an error when setFilter() is called + doThrow(new UnsupportedOperationException("expected")).when(source).setFilter(any()); + + new DmaapManager(MY_TOPIC, props); + } + + @Test + public void testGetTopic() { + assertEquals(MY_TOPIC, mgr.getTopic()); + } + + @Test + public void testFindTopicSource() { + // getting here means it worked + } + + @Test(expected = PoolingFeatureException.class) + public void testFindTopicSource_NotFilterableTopicSource() throws PoolingFeatureException { + + // matching topic, but doesn't have the correct interface + TopicSource source2 = mock(TopicSource.class); + when(source2.getTopic()).thenReturn(MY_TOPIC); + + when(factory.initTopicSources(props)).thenReturn(Arrays.asList(source2)); + + new DmaapManager(MY_TOPIC, props); + } + + @Test(expected = PoolingFeatureException.class) + public void testFindTopicSource_NotFound() throws PoolingFeatureException { + // one item in list, and its topic doesn't match + when(factory.initTopicSources(props)).thenReturn(Arrays.asList(mock(TopicSource.class))); + + new DmaapManager(MY_TOPIC, props); + } + + @Test(expected = PoolingFeatureException.class) + public void testFindTopicSource_EmptyList() throws PoolingFeatureException { + // empty list + when(factory.initTopicSources(props)).thenReturn(new LinkedList<>()); + + new DmaapManager(MY_TOPIC, props); + } + + @Test + public void testFindTopicSink() { + // getting here means it worked + } + + @Test(expected = PoolingFeatureException.class) + public void testFindTopicSink_NotFound() throws PoolingFeatureException { + // one item in list, and its topic doesn't match + when(factory.initTopicSinks(props)).thenReturn(Arrays.asList(mock(TopicSink.class))); + + new DmaapManager(MY_TOPIC, props); + } + + @Test(expected = PoolingFeatureException.class) + public void testFindTopicSink_EmptyList() throws PoolingFeatureException { + // empty list + when(factory.initTopicSinks(props)).thenReturn(new LinkedList<>()); + + new DmaapManager(MY_TOPIC, props); + } + + @Test + public void testStartPublisher() throws PoolingFeatureException { + // not started yet + verify(sink, never()).start(); + + mgr.startPublisher(); + verify(sink).start(); + + // restart should have no effect + mgr.startPublisher(); + verify(sink).start(); + + // should be able to publish now + mgr.publish(MSG); + verify(sink).send(MSG); + } + + @Test + public void testStartPublisher_Exception() throws PoolingFeatureException { + // force exception when it starts + doThrow(new IllegalStateException("expected")).when(sink).start(); + + expectException("startPublisher,start", xxx -> mgr.startPublisher()); + expectException("startPublisher,publish", xxx -> mgr.publish(MSG)); + + // allow it to succeed this time + reset(sink); + when(sink.send(any())).thenReturn(true); + + mgr.startPublisher(); + verify(sink).start(); + + // should be able to publish now + mgr.publish(MSG); + verify(sink).send(MSG); + } + + @Test + public void testStopPublisher() throws PoolingFeatureException { + // not publishing yet, so stopping should have no effect + mgr.stopPublisher(); + verify(sink, never()).stop(); + + // now start it + mgr.startPublisher(); + + // this time, stop should do something + mgr.stopPublisher(); + verify(sink).stop(); + + // re-stopping should have no effect + mgr.stopPublisher(); + verify(sink).stop(); + } + + @Test + public void testStopPublisher_Exception() throws PoolingFeatureException { + mgr.startPublisher(); + + // force exception when it stops + doThrow(new IllegalStateException("expected")).when(sink).stop(); + + mgr.stopPublisher(); + } + + @Test + public void testStartConsumer() { + // not started yet + verify(source, never()).register(any()); + + mgr.startConsumer(listener); + verify(source).register(listener); + + // restart should have no effect + mgr.startConsumer(listener); + verify(source).register(listener); + } + + @Test + public void testStopConsumer() { + // not consuming yet, so stopping should have no effect + mgr.stopConsumer(listener); + verify(source, never()).unregister(any()); + + // now start it + mgr.startConsumer(listener); + + // this time, stop should do something + mgr.stopConsumer(listener); + verify(source).unregister(listener); + + // re-stopping should have no effect + mgr.stopConsumer(listener); + verify(source).unregister(listener); + } + + @Test + public void testSetFilter() throws PoolingFeatureException { + mgr.setFilter(FILTER); + } + + @Test(expected = PoolingFeatureException.class) + public void testSetFilter_Exception() throws PoolingFeatureException { + // force an error when setFilter() is called + doThrow(new UnsupportedOperationException("expected")).when(source).setFilter(any()); + + mgr.setFilter(FILTER); + } + + @Test + public void testPublish() throws PoolingFeatureException { + // cannot publish before starting + expectException("publish,pre", xxx -> mgr.publish(MSG)); + + mgr.startPublisher(); + + // publish several messages + mgr.publish(MSG); + verify(sink).send(MSG); + + mgr.publish(MSG+"a"); + verify(sink).send(MSG+"a"); + + mgr.publish(MSG+"b"); + verify(sink).send(MSG+"b"); + + // stop and verify we can no longer publish + mgr.stopPublisher(); + expectException("publish,stopped", xxx -> mgr.publish(MSG)); + } + + @Test(expected = PoolingFeatureException.class) + public void testPublish_SendFailed() throws PoolingFeatureException { + mgr.startPublisher(); + + // arrange for send() to fail + when(sink.send(MSG)).thenReturn(false); + + mgr.publish(MSG); + } + + @Test(expected = PoolingFeatureException.class) + public void testPublish_SendEx() throws PoolingFeatureException { + mgr.startPublisher(); + + // arrange for send() to throw an exception + doThrow(new IllegalStateException("expected")).when(sink).send(MSG); + + mgr.publish(MSG); + } + + private void expectException(String testnm, VFunction func) { + try { + func.apply(null); + fail(testnm + " missing exception"); + + } catch (PoolingFeatureException expected) { + // OK + } + } + + @FunctionalInterface + public static interface VFunction { + public void apply(Void arg) throws PoolingFeatureException; + } +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/EventQueueTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/EventQueueTest.java new file mode 100644 index 00000000..24144686 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/EventQueueTest.java @@ -0,0 +1,196 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import java.util.LinkedList; +import org.junit.Before; +import org.junit.Test; +import org.onap.policy.drools.event.comm.Topic.CommInfrastructure; +import org.onap.policy.drools.pooling.message.Forward; + +public class EventQueueTest { + + private static final int MAX_SIZE = 5; + private static final long MAX_AGE_MS = 3000L; + + private static final String MY_SOURCE = "my.source"; + private static final CommInfrastructure MY_PROTO = CommInfrastructure.UEB; + private static final String MY_TOPIC = "my.topic"; + private static final String MY_PAYLOAD = "my.payload"; + private static final String MY_REQID = "my.request.id"; + + private EventQueue queue; + + @Before + public void setUp() { + queue = new EventQueue(MAX_SIZE, MAX_AGE_MS); + + } + + @Test + public void testEventQueue() { + // shouldn't generate an exception + new EventQueue(1, 1); + } + + @Test + public void testClear() { + // add some items + queue.add(makeActive()); + queue.add(makeActive()); + + assertFalse(queue.isEmpty()); + + queue.clear(); + + // should be empty now + assertTrue(queue.isEmpty()); + } + + @Test + public void testIsEmpty() { + // test when empty + assertTrue(queue.isEmpty()); + + // all active + Forward msg1 = makeActive(); + Forward msg2 = makeActive(); + queue.add(msg1); + assertFalse(queue.isEmpty()); + + queue.add(msg2); + assertFalse(queue.isEmpty()); + + assertEquals(msg1, queue.poll()); + assertFalse(queue.isEmpty()); + + assertEquals(msg2, queue.poll()); + assertTrue(queue.isEmpty()); + + // active, expired, expired, active + queue.add(msg1); + queue.add(makeInactive()); + queue.add(makeInactive()); + queue.add(msg2); + + assertEquals(msg1, queue.poll()); + assertFalse(queue.isEmpty()); + + assertEquals(msg2, queue.poll()); + assertTrue(queue.isEmpty()); + } + + @Test + public void testSize() { + queue = new EventQueue(2, 1000L); + assertEquals(0, queue.size()); + + queue.add(makeActive()); + assertEquals(1, queue.size()); + + queue.poll(); + assertEquals(0, queue.size()); + + queue.add(makeActive()); + queue.add(makeActive()); + assertEquals(2, queue.size()); + + queue.poll(); + assertEquals(1, queue.size()); + + queue.poll(); + assertEquals(0, queue.size()); + } + + @Test + public void testAdd() { + int nextra = 3; + + // create excess messages + LinkedList<Forward> msgs = new LinkedList<>(); + for (int x = 0; x < MAX_SIZE + nextra; ++x) { + msgs.add(makeActive()); + } + + // add them to the queue + msgs.forEach(msg -> queue.add(msg)); + + // should not have added too many messages + assertEquals(MAX_SIZE, queue.size()); + + // should have discarded the first "nextra" items + for (int x = 0; x < MAX_SIZE; ++x) { + assertEquals("x=" + x, msgs.get(x + nextra), queue.poll()); + } + + assertEquals(null, queue.poll()); + } + + @Test + public void testPoll() { + // poll when empty + assertNull(queue.poll()); + + // all active + Forward msg1 = makeActive(); + Forward msg2 = makeActive(); + queue.add(msg1); + queue.add(msg2); + + assertEquals(msg1, queue.poll()); + assertEquals(msg2, queue.poll()); + assertEquals(null, queue.poll()); + + // active, expired, expired, active + queue.add(msg1); + queue.add(makeInactive()); + queue.add(makeInactive()); + queue.add(msg2); + + assertEquals(msg1, queue.poll()); + assertEquals(msg2, queue.poll()); + assertEquals(null, queue.poll()); + + // one that's close to the age limit + msg1 = makeActive(); + msg1.setCreateTimeMs(System.currentTimeMillis() - MAX_AGE_MS + 100); + queue.add(msg1); + assertEquals(msg1, queue.poll()); + assertEquals(null, queue.poll()); + } + + private Forward makeActive() { + return new Forward(MY_SOURCE, MY_PROTO, MY_TOPIC, MY_PAYLOAD, MY_REQID); + } + + private Forward makeInactive() { + Forward msg = new Forward(MY_SOURCE, MY_PROTO, MY_TOPIC, MY_PAYLOAD, MY_REQID); + + msg.setCreateTimeMs(System.currentTimeMillis() - MAX_AGE_MS - 100); + + return msg; + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/FeatureEnabledCheckerTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/FeatureEnabledCheckerTest.java new file mode 100644 index 00000000..5f918f73 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/FeatureEnabledCheckerTest.java @@ -0,0 +1,74 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.onap.policy.common.utils.properties.SpecPropertyConfiguration.generalize; +import static org.onap.policy.common.utils.properties.SpecPropertyConfiguration.specialize; +import java.util.Properties; +import org.junit.Test; + +public class FeatureEnabledCheckerTest { + + private static final String PROP_NAME = "enable.{?.}it"; + + private static final String SPEC = "my.specializer"; + + @Test + public void test() { + assertFalse(check(null, null)); + assertTrue(check(null, true)); + assertFalse(check(null, false)); + + assertTrue(check(true, null)); + assertTrue(check(true, true)); + assertFalse(check(true, false)); + + assertFalse(check(false, null)); + assertTrue(check(false, true)); + assertFalse(check(false, false)); + } + + /** + * Adds properties, as specified, and checks if the feature is enabled. + * + * @param wantGen value to assign to the generalized property, or + * {@code null} to leave it unset + * @param wantSpec value to assign to the specialized property, or + * {@code null} to leave it unset + * @return {@code true} if the feature is enabled, {@code false} otherwise + */ + public boolean check(Boolean wantGen, Boolean wantSpec) { + Properties props = new Properties(); + + if (wantGen != null) { + props.setProperty(generalize(PROP_NAME), wantGen.toString()); + } + + if (wantSpec != null) { + props.setProperty(specialize(PROP_NAME, SPEC), wantSpec.toString()); + } + + return FeatureEnabledChecker.isFeatureEnabled(props, SPEC, PROP_NAME); + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureExceptionTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureExceptionTest.java new file mode 100644 index 00000000..5b423d4b --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureExceptionTest.java @@ -0,0 +1,42 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling; + +import static org.junit.Assert.*; +import org.junit.Test; +import org.onap.policy.common.utils.test.ExceptionsTester; + +public class PoolingFeatureExceptionTest extends ExceptionsTester { + + @Test + public void test() { + assertEquals(5, test(PoolingFeatureException.class)); + } + + @Test + public void testToRuntimeException() { + PoolingFeatureException plainExc = new PoolingFeatureException("hello"); + PoolingFeatureRtException runtimeExc = plainExc.toRuntimeException(); + + assertTrue(plainExc == runtimeExc.getCause()); + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureRtExceptionTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureRtExceptionTest.java new file mode 100644 index 00000000..cbb24421 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureRtExceptionTest.java @@ -0,0 +1,35 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling; + +import static org.junit.Assert.*; +import org.junit.Test; +import org.onap.policy.common.utils.test.ExceptionsTester; +import org.onap.policy.drools.pooling.PoolingFeatureRtException; + +public class PoolingFeatureRtExceptionTest extends ExceptionsTester { + + @Test + public void test() { + assertEquals(5, test(PoolingFeatureRtException.class)); + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureTest.java new file mode 100644 index 00000000..cd1aea09 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureTest.java @@ -0,0 +1,495 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.util.LinkedList; +import java.util.List; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.onap.policy.drools.controller.DroolsController; +import org.onap.policy.drools.event.comm.Topic.CommInfrastructure; +import org.onap.policy.drools.pooling.PoolingFeature.Factory; +import org.onap.policy.drools.system.PolicyController; +import org.onap.policy.drools.utils.Pair; + +public class PoolingFeatureTest { + + private static final String CONFIG_DIR = "src/test/java/org/onap/policy/drools/pooling"; + + private static final String CONTROLLER1 = "controllerA"; + private static final String CONTROLLER2 = "controllerB"; + private static final String CONTROLLER_DISABLED = "controllerDisabled"; + private static final String CONTROLLER_EX = "controllerException"; + private static final String CONTROLLER_UNKNOWN = "controllerUnknown"; + + private static final String TOPIC1 = "topic.one"; + private static final String TOPIC2 = "topic.two"; + + private static final String EVENT1 = "event.one"; + private static final String EVENT2 = "event.two"; + + private static final Object OBJECT1 = new Object(); + private static final Object OBJECT2 = new Object(); + + /** + * Saved from PoolingFeature and restored on exit from this test class. + */ + private static Factory saveFactory; + + private PolicyController controller1; + private PolicyController controller2; + private PolicyController controllerDisabled; + private PolicyController controllerException; + private PolicyController controllerUnknown; + private DroolsController drools1; + private DroolsController drools2; + private DroolsController droolsDisabled; + private List<Pair<PoolingManagerImpl, PoolingProperties>> managers; + private PoolingManagerImpl mgr1; + private PoolingManagerImpl mgr2; + private Factory factory; + + private PoolingFeature pool; + + + @BeforeClass + public static void setUpBeforeClass() { + saveFactory = PoolingFeature.getFactory(); + } + + @AfterClass + public static void tearDownAfterClass() { + PoolingFeature.setFactory(saveFactory); + } + + @Before + public void setUp() throws Exception { + factory = mock(Factory.class); + controller1 = mock(PolicyController.class); + controller2 = mock(PolicyController.class); + controllerDisabled = mock(PolicyController.class); + controllerException = mock(PolicyController.class); + controllerUnknown = mock(PolicyController.class); + drools1 = mock(DroolsController.class); + drools2 = mock(DroolsController.class); + droolsDisabled = mock(DroolsController.class); + managers = new LinkedList<>(); + + PoolingFeature.setFactory(factory); + + when(controller1.getName()).thenReturn(CONTROLLER1); + when(controller2.getName()).thenReturn(CONTROLLER2); + when(controllerDisabled.getName()).thenReturn(CONTROLLER_DISABLED); + when(controllerException.getName()).thenReturn(CONTROLLER_EX); + when(controllerUnknown.getName()).thenReturn(CONTROLLER_UNKNOWN); + + when(factory.getController(drools1)).thenReturn(controller1); + when(factory.getController(drools2)).thenReturn(controller2); + when(factory.getController(droolsDisabled)).thenReturn(controllerDisabled); + + when(factory.makeManager(any(), any())).thenAnswer(args -> { + PoolingProperties props = args.getArgumentAt(1, PoolingProperties.class); + + PoolingManagerImpl mgr = mock(PoolingManagerImpl.class); + + managers.add(new Pair<>(mgr, props)); + + return mgr; + }); + + pool = new PoolingFeature(); + + pool.globalInit(null, CONFIG_DIR); + + pool.afterCreate(controller1); + pool.afterCreate(controller2); + + mgr1 = managers.get(0).first(); + mgr2 = managers.get(1).first(); + } + + @Test + public void test() { + assertEquals(2, managers.size()); + } + + @Test + public void testGetSequenceNumber() { + assertEquals(0, pool.getSequenceNumber()); + } + + @Test + public void testGlobalInit() { + pool = new PoolingFeature(); + + pool.globalInit(null, CONFIG_DIR); + } + + @Test(expected = PoolingFeatureRtException.class) + public void testGlobalInit_NotFound() { + pool = new PoolingFeature(); + + pool.globalInit(null, CONFIG_DIR + "/unknown"); + } + + @Test + public void testAfterCreate() { + managers.clear(); + pool = new PoolingFeature(); + pool.globalInit(null, CONFIG_DIR); + + assertFalse(pool.afterCreate(controller1)); + assertEquals(1, managers.size()); + + // duplicate + assertFalse(pool.afterCreate(controller1)); + assertEquals(1, managers.size()); + + // second controller + assertFalse(pool.afterCreate(controller2)); + assertEquals(2, managers.size()); + } + + @Test + public void testAfterCreate_NotEnabled() { + managers.clear(); + pool = new PoolingFeature(); + pool.globalInit(null, CONFIG_DIR); + + assertFalse(pool.afterCreate(controllerDisabled)); + assertTrue(managers.isEmpty()); + } + + @Test(expected = PoolingFeatureRtException.class) + public void testAfterCreate_PropertyEx() { + managers.clear(); + pool = new PoolingFeature(); + pool.globalInit(null, CONFIG_DIR); + + pool.afterCreate(controllerException); + } + + @Test(expected = PoolingFeatureRtException.class) + public void testAfterCreate_NoProps() { + pool = new PoolingFeature(); + + // did not perform globalInit, which is an error + + pool.afterCreate(controller1); + } + + @Test + public void testAfterCreate_NoFeatProps() { + managers.clear(); + pool = new PoolingFeature(); + pool.globalInit(null, CONFIG_DIR); + + assertFalse(pool.afterCreate(controllerUnknown)); + assertTrue(managers.isEmpty()); + } + + @Test + public void testBeforeStart() throws Exception { + assertFalse(pool.beforeStart(controller1)); + verify(mgr1).beforeStart(); + + // ensure it's still in the map by re-invoking + assertFalse(pool.beforeStart(controller1)); + verify(mgr1, times(2)).beforeStart(); + + assertFalse(pool.beforeStart(controllerDisabled)); + } + + @Test + public void testAfterStart() { + assertFalse(pool.afterStart(controller1)); + verify(mgr1).afterStart(); + + // ensure it's still in the map by re-invoking + assertFalse(pool.afterStart(controller1)); + verify(mgr1, times(2)).afterStart(); + + assertFalse(pool.afterStart(controllerDisabled)); + } + + @Test + public void testBeforeStop() { + assertFalse(pool.beforeStop(controller1)); + verify(mgr1).beforeStop(); + + // ensure it's still in the map by re-invoking + assertFalse(pool.beforeStop(controller1)); + verify(mgr1, times(2)).beforeStop(); + + assertFalse(pool.beforeStop(controllerDisabled)); + } + + @Test + public void testAfterStop() { + assertFalse(pool.afterStop(controller1)); + verify(mgr1).afterStop(); + + // ensure it has been removed from the map by re-invoking + assertFalse(pool.afterStop(controller1)); + + // count should be unchanged + verify(mgr1).afterStop(); + + assertFalse(pool.afterStop(controllerDisabled)); + } + + @Test + public void testBeforeLock() { + assertFalse(pool.beforeLock(controller1)); + verify(mgr1).beforeLock(); + + // ensure it's still in the map by re-invoking + assertFalse(pool.beforeLock(controller1)); + verify(mgr1, times(2)).beforeLock(); + + assertFalse(pool.beforeLock(controllerDisabled)); + } + + @Test + public void testAfterUnlock() { + assertFalse(pool.afterUnlock(controller1)); + verify(mgr1).afterUnlock(); + + // ensure it's still in the map by re-invoking + assertFalse(pool.afterUnlock(controller1)); + verify(mgr1, times(2)).afterUnlock(); + + assertFalse(pool.afterUnlock(controllerDisabled)); + } + + @Test + public void testBeforeOffer() { + assertFalse(pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC1, EVENT1)); + verify(mgr1).beforeOffer(CommInfrastructure.UEB, TOPIC1, EVENT1); + + // ensure that the args were captured + pool.beforeInsert(drools1, OBJECT1); + verify(mgr1).beforeInsert(CommInfrastructure.UEB, TOPIC1, EVENT1, OBJECT1); + + + // ensure it's still in the map by re-invoking + assertFalse(pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC2, EVENT2)); + verify(mgr1).beforeOffer(CommInfrastructure.UEB, TOPIC2, EVENT2); + + // ensure that the new args were captured + pool.beforeInsert(drools1, OBJECT2); + verify(mgr1).beforeInsert(CommInfrastructure.UEB, TOPIC2, EVENT2, OBJECT2); + + + assertFalse(pool.beforeOffer(controllerDisabled, CommInfrastructure.UEB, TOPIC1, EVENT1)); + } + + @Test + public void testBeforeOffer_NotFound() { + assertFalse(pool.beforeOffer(controllerDisabled, CommInfrastructure.UEB, TOPIC1, EVENT1)); + } + + @Test + public void testBeforeOffer_MgrTrue() { + + // manager will return true + when(mgr1.beforeOffer(any(), any(), any())).thenReturn(true); + + assertTrue(pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC1, EVENT1)); + verify(mgr1).beforeOffer(CommInfrastructure.UEB, TOPIC1, EVENT1); + + // ensure it's still in the map by re-invoking + assertTrue(pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC2, EVENT2)); + verify(mgr1).beforeOffer(CommInfrastructure.UEB, TOPIC2, EVENT2); + + assertFalse(pool.beforeOffer(controllerDisabled, CommInfrastructure.UEB, TOPIC1, EVENT1)); + } + + @Test + public void testBeforeInsert() { + pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC1, EVENT1); + assertFalse(pool.beforeInsert(drools1, OBJECT1)); + verify(mgr1).beforeInsert(CommInfrastructure.UEB, TOPIC1, EVENT1, OBJECT1); + + // ensure it's still in the map by re-invoking + pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC2, EVENT2); + assertFalse(pool.beforeInsert(drools1, OBJECT2)); + verify(mgr1).beforeInsert(CommInfrastructure.UEB, TOPIC2, EVENT2, OBJECT2); + + pool.beforeOffer(controllerDisabled, CommInfrastructure.UEB, TOPIC2, EVENT2); + assertFalse(pool.beforeInsert(droolsDisabled, OBJECT1)); + } + + @Test + public void testBeforeInsert_NoArgs() { + + // call beforeInsert without beforeOffer + assertFalse(pool.beforeInsert(drools1, OBJECT1)); + verify(mgr1, never()).beforeInsert(any(), any(), any(), any()); + + assertFalse(pool.beforeInsert(droolsDisabled, OBJECT1)); + verify(mgr1, never()).beforeInsert(any(), any(), any(), any()); + } + + @Test + public void testBeforeInsert_ArgEx() { + + // generate exception + doThrow(new IllegalArgumentException()).when(factory).getController(any()); + + pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC1, EVENT1); + assertFalse(pool.beforeInsert(drools1, OBJECT1)); + verify(mgr1, never()).beforeInsert(any(), any(), any(), any()); + } + + @Test + public void testBeforeInsert_StateEx() { + + // generate exception + doThrow(new IllegalStateException()).when(factory).getController(any()); + + pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC1, EVENT1); + assertFalse(pool.beforeInsert(drools1, OBJECT1)); + verify(mgr1, never()).beforeInsert(any(), any(), any(), any()); + } + + @Test + public void testBeforeInsert_NullController() { + + // return null controller + when(factory.getController(any())).thenReturn(null); + + pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC1, EVENT1); + assertFalse(pool.beforeInsert(drools1, OBJECT1)); + verify(mgr1, never()).beforeInsert(any(), any(), any(), any()); + } + + @Test + public void testBeforeInsert_NotFound() { + + pool.beforeOffer(controllerDisabled, CommInfrastructure.UEB, TOPIC2, EVENT2); + assertFalse(pool.beforeInsert(droolsDisabled, OBJECT1)); + } + + @Test + public void testAfterOffer() { + // this will create OfferArgs + pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC1, EVENT1); + + // this should clear them + assertFalse(pool.afterOffer(controller1, CommInfrastructure.UEB, TOPIC2, EVENT2, true)); + + assertFalse(pool.beforeInsert(drools1, OBJECT1)); + verify(mgr1, never()).beforeInsert(any(), any(), any(), any()); + + + assertFalse(pool.beforeInsert(droolsDisabled, OBJECT1)); + } + + @Test + public void testDoManager() throws Exception { + assertFalse(pool.beforeStart(controller1)); + verify(mgr1).beforeStart(); + + // ensure it's still in the map by re-invoking + assertFalse(pool.beforeStart(controller1)); + verify(mgr1, times(2)).beforeStart(); + + + // different controller + assertFalse(pool.beforeStart(controller2)); + verify(mgr2).beforeStart(); + + // ensure it's still in the map by re-invoking + assertFalse(pool.beforeStart(controller2)); + verify(mgr2, times(2)).beforeStart(); + + + assertFalse(pool.beforeStart(controllerDisabled)); + } + + @Test + public void testDoManager_NotFound() { + assertFalse(pool.beforeStart(controllerDisabled)); + } + + @Test(expected = PoolingFeatureRtException.class) + public void testDoManager_Ex() throws Exception { + + // generate exception + doThrow(new PoolingFeatureException()).when(mgr1).beforeStart(); + + pool.beforeStart(controller1); + } + + @Test + public void testDoDeleteManager() { + assertFalse(pool.afterStop(controller1)); + verify(mgr1).afterStop(); + + // ensure it has been removed from the map by re-invoking + assertFalse(pool.afterStop(controller1)); + + // count should be unchanged + verify(mgr1).afterStop(); + + + // different controller + assertFalse(pool.afterStop(controller2)); + verify(mgr2).afterStop(); + + // ensure it has been removed from the map by re-invoking + assertFalse(pool.afterStop(controller2)); + + // count should be unchanged + verify(mgr2).afterStop(); + + + assertFalse(pool.afterStop(controllerDisabled)); + } + + @Test + public void testDoDeleteManager_NotFound() { + assertFalse(pool.afterStop(controllerDisabled)); + } + + @Test(expected = PoolingFeatureRtException.class) + public void testDoDeleteManager_Ex() { + + // generate exception + doThrow(new PoolingFeatureRtException()).when(mgr1).afterStop(); + + pool.afterStop(controller1); + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingManagerImplTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingManagerImplTest.java new file mode 100644 index 00000000..01ee61ef --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingManagerImplTest.java @@ -0,0 +1,1342 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.util.LinkedList; +import java.util.Properties; +import java.util.Queue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.onap.policy.drools.controller.DroolsController; +import org.onap.policy.drools.event.comm.Topic.CommInfrastructure; +import org.onap.policy.drools.event.comm.TopicListener; +import org.onap.policy.drools.pooling.PoolingManagerImpl.Factory; +import org.onap.policy.drools.pooling.extractor.ClassExtractors; +import org.onap.policy.drools.pooling.message.BucketAssignments; +import org.onap.policy.drools.pooling.message.Forward; +import org.onap.policy.drools.pooling.message.Heartbeat; +import org.onap.policy.drools.pooling.message.Message; +import org.onap.policy.drools.pooling.message.Offline; +import org.onap.policy.drools.pooling.state.ActiveState; +import org.onap.policy.drools.pooling.state.IdleState; +import org.onap.policy.drools.pooling.state.InactiveState; +import org.onap.policy.drools.pooling.state.QueryState; +import org.onap.policy.drools.pooling.state.StartState; +import org.onap.policy.drools.pooling.state.State; +import org.onap.policy.drools.system.PolicyController; + +public class PoolingManagerImplTest { + + protected static final long STD_HEARTBEAT_WAIT_MS = 10; + protected static final long STD_REACTIVATE_WAIT_MS = STD_HEARTBEAT_WAIT_MS + 1; + protected static final long STD_IDENTIFICATION_MS = STD_REACTIVATE_WAIT_MS + 1; + protected static final long STD_ACTIVE_HEARTBEAT_MS = STD_IDENTIFICATION_MS + 1; + protected static final long STD_INTER_HEARTBEAT_MS = STD_ACTIVE_HEARTBEAT_MS + 1; + + private static final String HOST2 = "other.host"; + + private static final String MY_CONTROLLER = "my.controller"; + private static final String MY_TOPIC = "my.topic"; + + private static final String TOPIC2 = "topic.two"; + + private static final String THE_EVENT = "the event"; + + private static final Object DECODED_EVENT = new Object(); + private static final String REQUEST_ID = "my.request.id"; + + /** + * Number of dmaap.publish() invocations that should be issued when the + * manager is started. + */ + private static final int START_PUB = 1; + + /** + * Saved from PoolingManagerImpl and restored on exit from this test class. + */ + private static Factory saveFactory; + + /** + * Futures that have been allocated due to calls to scheduleXxx(). + */ + private Queue<ScheduledFuture<?>> futures; + + private Properties plainProps; + private PoolingProperties poolProps; + private ListeningController controller; + private EventQueue eventQueue; + private ClassExtractors extractors; + private DmaapManager dmaap; + private ScheduledThreadPoolExecutor sched; + private DroolsController drools; + private Serializer ser; + private Factory factory; + + private PoolingManagerImpl mgr; + + @BeforeClass + public static void setUpBeforeClass() { + saveFactory = PoolingManagerImpl.getFactory(); + } + + @AfterClass + public static void tearDownAfterClass() { + PoolingManagerImpl.setFactory(saveFactory); + } + + @Before + public void setUp() throws Exception { + plainProps = new Properties(); + + poolProps = mock(PoolingProperties.class); + when(poolProps.getSource()).thenReturn(plainProps); + when(poolProps.getPoolingTopic()).thenReturn(MY_TOPIC); + when(poolProps.getStartHeartbeatMs()).thenReturn(STD_HEARTBEAT_WAIT_MS); + when(poolProps.getReactivateMs()).thenReturn(STD_REACTIVATE_WAIT_MS); + when(poolProps.getIdentificationMs()).thenReturn(STD_IDENTIFICATION_MS); + when(poolProps.getActiveHeartbeatMs()).thenReturn(STD_ACTIVE_HEARTBEAT_MS); + when(poolProps.getInterHeartbeatMs()).thenReturn(STD_INTER_HEARTBEAT_MS); + + futures = new LinkedList<>(); + ser = new Serializer(); + + factory = mock(Factory.class); + eventQueue = mock(EventQueue.class); + extractors = mock(ClassExtractors.class); + dmaap = mock(DmaapManager.class); + controller = mock(ListeningController.class); + sched = mock(ScheduledThreadPoolExecutor.class); + drools = mock(DroolsController.class); + + when(factory.makeEventQueue(any())).thenReturn(eventQueue); + when(factory.makeClassExtractors(any())).thenReturn(extractors); + when(factory.makeDmaapManager(any())).thenReturn(dmaap); + when(factory.makeScheduler()).thenReturn(sched); + when(factory.canDecodeEvent(drools, TOPIC2)).thenReturn(true); + when(factory.decodeEvent(drools, TOPIC2, THE_EVENT)).thenReturn(DECODED_EVENT); + + when(extractors.extract(DECODED_EVENT)).thenReturn(REQUEST_ID); + + when(controller.getName()).thenReturn(MY_CONTROLLER); + when(controller.getDrools()).thenReturn(drools); + when(controller.isAlive()).thenReturn(true); + + when(sched.schedule(any(Runnable.class), any(Long.class), any(TimeUnit.class))).thenAnswer(args -> { + ScheduledFuture<?> fut = mock(ScheduledFuture.class); + futures.add(fut); + + return fut; + }); + + when(sched.scheduleWithFixedDelay(any(Runnable.class), any(Long.class), any(Long.class), any(TimeUnit.class))) + .thenAnswer(args -> { + ScheduledFuture<?> fut = mock(ScheduledFuture.class); + futures.add(fut); + + return fut; + }); + + PoolingManagerImpl.setFactory(factory); + + mgr = new PoolingManagerImpl(controller, poolProps); + } + + @After + public void tearDown() throws Exception { + + } + + @Test + public void testPoolingManagerImpl() { + mgr = new PoolingManagerImpl(controller, poolProps); + + State st = mgr.getCurrent(); + assertTrue(st instanceof IdleState); + + // ensure the state is attached to the manager + assertEquals(mgr.getHost(), st.getHost()); + } + + @Test + public void testPoolingManagerImpl_ClassEx() { + /* + * this controller does not implement TopicListener, which should cause + * a ClassCastException + */ + PolicyController ctlr = mock(PolicyController.class); + + PoolingFeatureRtException ex = expectException(PoolingFeatureRtException.class, + xxx -> new PoolingManagerImpl(ctlr, poolProps)); + assertNotNull(ex.getCause()); + assertTrue(ex.getCause() instanceof ClassCastException); + } + + @Test + public void testPoolingManagerImpl_PoolEx() throws PoolingFeatureException { + // throw an exception when we try to create the dmaap manager + PoolingFeatureException ex = new PoolingFeatureException(); + when(factory.makeDmaapManager(any())).thenThrow(ex); + + PoolingFeatureRtException ex2 = expectException(PoolingFeatureRtException.class, + xxx -> new PoolingManagerImpl(controller, poolProps)); + assertEquals(ex, ex2.getCause()); + } + + @Test + public void testGetHost() { + String host = mgr.getHost(); + assertNotNull(host); + + // create another manager and ensure it generates a different host + mgr = new PoolingManagerImpl(controller, poolProps); + + assertNotNull(mgr.getHost()); + assertFalse(host.equals(mgr.getHost())); + } + + @Test + public void testGetTopic() { + assertEquals(MY_TOPIC, mgr.getTopic()); + } + + @Test + public void testGetProperties() { + assertEquals(poolProps, mgr.getProperties()); + } + + @Test + public void testBeforeStart() throws Exception { + // not running yet + mgr.beforeStart(); + + verify(dmaap).startPublisher(); + + verify(factory).makeScheduler(); + verify(sched).setMaximumPoolSize(1); + verify(sched).setContinueExistingPeriodicTasksAfterShutdownPolicy(false); + + + // try again - nothing should happen + mgr.beforeStart(); + + verify(dmaap).startPublisher(); + + verify(factory).makeScheduler(); + verify(sched).setMaximumPoolSize(1); + verify(sched).setContinueExistingPeriodicTasksAfterShutdownPolicy(false); + } + + @Test + public void testBeforeStart_DmaapEx() throws Exception { + // generate an exception + PoolingFeatureException ex = new PoolingFeatureException(); + doThrow(ex).when(dmaap).startPublisher(); + + PoolingFeatureException ex2 = expectException(PoolingFeatureException.class, xxx -> mgr.beforeStart()); + assertEquals(ex, ex2); + + // should never start the scheduler + verify(factory, never()).makeScheduler(); + } + + @Test + public void testAfterStart() throws Exception { + startMgr(); + + verify(dmaap).startConsumer(mgr); + + State st = mgr.getCurrent(); + assertTrue(st instanceof StartState); + + // ensure the state is attached to the manager + assertEquals(mgr.getHost(), st.getHost()); + + ArgumentCaptor<Long> timeCap = ArgumentCaptor.forClass(Long.class); + ArgumentCaptor<TimeUnit> unitCap = ArgumentCaptor.forClass(TimeUnit.class); + verify(sched).schedule(any(Runnable.class), timeCap.capture(), unitCap.capture()); + + assertEquals(STD_HEARTBEAT_WAIT_MS, timeCap.getValue().longValue()); + assertEquals(TimeUnit.MILLISECONDS, unitCap.getValue()); + + + // already started - nothing else happens + mgr.afterStart(); + + verify(dmaap).startConsumer(mgr); + + assertTrue(mgr.getCurrent() instanceof StartState); + + verify(sched).schedule(any(Runnable.class), any(Long.class), any(TimeUnit.class)); + } + + @Test + public void testBeforeStop() throws Exception { + startMgr(); + + mgr.beforeStop(); + + verify(dmaap).stopConsumer(mgr); + verify(sched).shutdownNow(); + + assertTrue(mgr.getCurrent() instanceof IdleState); + } + + @Test + public void testBeforeStop_NotRunning() throws Exception { + State st = mgr.getCurrent(); + + mgr.beforeStop(); + + verify(dmaap, never()).stopConsumer(any()); + verify(sched, never()).shutdownNow(); + + // hasn't changed states either + assertEquals(st, mgr.getCurrent()); + } + + @Test + public void testBeforeStop_AfterPartialStart() throws Exception { + // call beforeStart but not afterStart + mgr.beforeStart(); + + State st = mgr.getCurrent(); + + mgr.beforeStop(); + + // should still shut the scheduler down + verify(sched).shutdownNow(); + + verify(dmaap, never()).stopConsumer(any()); + + // hasn't changed states + assertEquals(st, mgr.getCurrent()); + } + + @Test + public void testAfterStop() throws Exception { + startMgr(); + mgr.beforeStop(); + + when(eventQueue.isEmpty()).thenReturn(false); + when(eventQueue.size()).thenReturn(3); + + mgr.afterStop(); + + verify(eventQueue).clear(); + verify(dmaap).stopPublisher(); + } + + @Test + public void testAfterStop_EmptyQueue() throws Exception { + startMgr(); + mgr.beforeStop(); + + when(eventQueue.isEmpty()).thenReturn(true); + when(eventQueue.size()).thenReturn(0); + + mgr.afterStop(); + + verify(eventQueue, never()).clear(); + verify(dmaap).stopPublisher(); + } + + @Test + public void testBeforeLock() throws Exception { + startMgr(); + + mgr.beforeLock(); + + assertTrue(mgr.getCurrent() instanceof IdleState); + } + + @Test + public void testAfterUnlock_AliveIdle() throws Exception { + // this really shouldn't happen + + lockMgr(); + + mgr.afterUnlock(); + + // stays in idle state, because it has no scheduler + assertTrue(mgr.getCurrent() instanceof IdleState); + } + + @Test + public void testAfterUnlock_AliveStarted() throws Exception { + startMgr(); + lockMgr(); + + mgr.afterUnlock(); + + assertTrue(mgr.getCurrent() instanceof StartState); + } + + @Test + public void testAfterUnlock_StoppedIdle() throws Exception { + startMgr(); + lockMgr(); + + // controller is stopped + when(controller.isAlive()).thenReturn(false); + + mgr.afterUnlock(); + + assertTrue(mgr.getCurrent() instanceof IdleState); + } + + @Test + public void testAfterUnlock_StoppedStarted() throws Exception { + startMgr(); + + // Note: don't lockMgr() + + // controller is stopped + when(controller.isAlive()).thenReturn(false); + + mgr.afterUnlock(); + + assertTrue(mgr.getCurrent() instanceof StartState); + } + + @Test + public void testChangeState() throws Exception { + // start should invoke changeState() + startMgr(); + + int ntimes = 0; + + // should have set the filter for the StartState + verify(dmaap, times(++ntimes)).setFilter(any()); + + /* + * now go offline while it's locked + */ + lockMgr(); + + // should have set the new filter + verify(dmaap, times(++ntimes)).setFilter(any()); + + // should have cancelled the timer + assertEquals(1, futures.size()); + verify(futures.poll()).cancel(false); + + /* + * now go back online + */ + unlockMgr(); + + // should have set the new filter + verify(dmaap, times(++ntimes)).setFilter(any()); + + // timer should still be active + assertEquals(1, futures.size()); + verify(futures.poll(), never()).cancel(false); + } + + @Test + public void testSetFilter() throws Exception { + // start should cause a filter to be set + startMgr(); + + verify(dmaap).setFilter(any()); + } + + @Test + public void testSetFilter_DmaapEx() throws Exception { + + // generate an exception + doThrow(new PoolingFeatureException()).when(dmaap).setFilter(any()); + + // start should invoke setFilter() + startMgr(); + + // no exception, means success + } + + @Test + public void testInternalTopicFailed() throws Exception { + startMgr(); + + CountDownLatch latch = mgr.internalTopicFailed(); + + // wait for the thread to complete + assertTrue(latch.await(2, TimeUnit.SECONDS)); + + verify(controller).stop(); + } + + @Test + public void testSchedule() throws Exception { + // must start the scheduler + startMgr(); + + CountDownLatch latch = new CountDownLatch(1); + + mgr.schedule(STD_ACTIVE_HEARTBEAT_MS, xxx -> { + latch.countDown(); + return null; + }); + + // capture the task + ArgumentCaptor<Runnable> taskCap = ArgumentCaptor.forClass(Runnable.class); + ArgumentCaptor<Long> timeCap = ArgumentCaptor.forClass(Long.class); + ArgumentCaptor<TimeUnit> unitCap = ArgumentCaptor.forClass(TimeUnit.class); + + verify(sched, times(2)).schedule(taskCap.capture(), timeCap.capture(), unitCap.capture()); + + assertEquals(STD_ACTIVE_HEARTBEAT_MS, timeCap.getValue().longValue()); + assertEquals(TimeUnit.MILLISECONDS, unitCap.getValue()); + + // execute it + taskCap.getValue().run(); + + assertEquals(0, latch.getCount()); + } + + @Test + public void testScheduleWithFixedDelay() throws Exception { + // must start the scheduler + startMgr(); + + CountDownLatch latch = new CountDownLatch(1); + + mgr.scheduleWithFixedDelay(STD_HEARTBEAT_WAIT_MS, STD_ACTIVE_HEARTBEAT_MS, xxx -> { + latch.countDown(); + return null; + }); + + // capture the task + ArgumentCaptor<Runnable> taskCap = ArgumentCaptor.forClass(Runnable.class); + ArgumentCaptor<Long> initCap = ArgumentCaptor.forClass(Long.class); + ArgumentCaptor<Long> timeCap = ArgumentCaptor.forClass(Long.class); + ArgumentCaptor<TimeUnit> unitCap = ArgumentCaptor.forClass(TimeUnit.class); + + verify(sched).scheduleWithFixedDelay(taskCap.capture(), initCap.capture(), timeCap.capture(), + unitCap.capture()); + + assertEquals(STD_HEARTBEAT_WAIT_MS, initCap.getValue().longValue()); + assertEquals(STD_ACTIVE_HEARTBEAT_MS, timeCap.getValue().longValue()); + assertEquals(TimeUnit.MILLISECONDS, unitCap.getValue()); + + // execute it + taskCap.getValue().run(); + + assertEquals(0, latch.getCount()); + } + + @Test + public void testPublishAdmin() throws Exception { + Offline msg = new Offline(mgr.getHost()); + mgr.publishAdmin(msg); + + assertEquals(Message.ADMIN, msg.getChannel()); + + verify(dmaap).publish(any()); + } + + @Test + public void testPublish() throws Exception { + Offline msg = new Offline(mgr.getHost()); + mgr.publish("my.channel", msg); + + assertEquals("my.channel", msg.getChannel()); + + verify(dmaap).publish(any()); + } + + @Test + public void testPublish_InvalidMsg() throws Exception { + // message is missing data + mgr.publish(Message.ADMIN, new Offline()); + + // should not have attempted to publish it + verify(dmaap, never()).publish(any()); + } + + @Test + public void testPublish_DmaapEx() throws Exception { + + // generate exception + doThrow(new PoolingFeatureException()).when(dmaap).publish(any()); + + mgr.publish(Message.ADMIN, new Offline(mgr.getHost())); + } + + @Test + public void testOnTopicEvent() throws Exception { + startMgr(); + + StartState st = (StartState) mgr.getCurrent(); + + /* + * give it its heart beat, that should cause it to transition to the + * Query state. + */ + Heartbeat hb = new Heartbeat(mgr.getHost(), st.getHbTimestampMs()); + hb.setChannel(Message.ADMIN); + + String msg = ser.encodeMsg(hb); + + mgr.onTopicEvent(CommInfrastructure.UEB, MY_TOPIC, msg); + + assertTrue(mgr.getCurrent() instanceof QueryState); + } + + @Test + public void testOnTopicEvent_NullEvent() throws Exception { + startMgr(); + + mgr.onTopicEvent(CommInfrastructure.UEB, TOPIC2, null); + } + + @Test + public void testBeforeOffer_Unlocked_NoIntercept() throws Exception { + startMgr(); + + assertFalse(mgr.beforeOffer(CommInfrastructure.UEB, TOPIC2, THE_EVENT)); + } + + @Test + public void testBeforeOffer_Locked_NoIntercept() throws Exception { + startMgr(); + + lockMgr(); + + assertFalse(mgr.beforeOffer(CommInfrastructure.UEB, TOPIC2, THE_EVENT)); + } + + @Test + public void testBeforeOffer_Locked_Intercept() throws Exception { + startMgr(); + lockMgr(); + + // route the message to this host + mgr.startDistributing(makeAssignments(true)); + + CountDownLatch latch = catchRecursion(false); + + Forward msg = new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID); + mgr.handle(msg); + + verify(dmaap, times(START_PUB)).publish(any()); + verify(controller).onTopicEvent(CommInfrastructure.UEB, TOPIC2, THE_EVENT); + + // ensure we made it past both beforeXxx() methods + assertEquals(0, latch.getCount()); + } + + @Test + public void testBeforeInsert_Intercept() throws Exception { + startMgr(); + lockMgr(); + + // route the message to this host + mgr.startDistributing(makeAssignments(true)); + + CountDownLatch latch = catchRecursion(true); + + Forward msg = new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID); + mgr.handle(msg); + + verify(dmaap, times(START_PUB)).publish(any()); + verify(controller).onTopicEvent(CommInfrastructure.UEB, TOPIC2, THE_EVENT); + + // ensure we made it past both beforeXxx() methods + assertEquals(0, latch.getCount()); + } + + @Test + public void testBeforeInsert_NoIntercept() throws Exception { + startMgr(); + + long tbegin = System.currentTimeMillis(); + + assertTrue(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT)); + + ArgumentCaptor<Forward> msgCap = ArgumentCaptor.forClass(Forward.class); + verify(eventQueue).add(msgCap.capture()); + + validateMessageContent(tbegin, msgCap.getValue()); + } + + @Test + public void testHandleExternalCommInfrastructureStringStringString_NullReqId() throws Exception { + startMgr(); + + when(extractors.extract(any())).thenReturn(null); + + assertFalse(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT)); + } + + @Test + public void testHandleExternalCommInfrastructureStringStringString_EmptyReqId() throws Exception { + startMgr(); + + when(extractors.extract(any())).thenReturn(""); + + assertFalse(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT)); + } + + @Test + public void testHandleExternalCommInfrastructureStringStringString_InvalidMsg() throws Exception { + startMgr(); + + assertTrue(mgr.beforeInsert(null, TOPIC2, THE_EVENT, DECODED_EVENT)); + + // should not have tried to enqueue a message + verify(eventQueue, never()).add(any()); + } + + @Test + public void testHandleExternalCommInfrastructureStringStringString() throws Exception { + startMgr(); + + long tbegin = System.currentTimeMillis(); + + assertTrue(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT)); + + ArgumentCaptor<Forward> msgCap = ArgumentCaptor.forClass(Forward.class); + verify(eventQueue).add(msgCap.capture()); + + validateMessageContent(tbegin, msgCap.getValue()); + } + + @Test + public void testHandleExternalForward_NoAssignments() throws Exception { + startMgr(); + + long tbegin = System.currentTimeMillis(); + + assertTrue(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT)); + + ArgumentCaptor<Forward> msgCap = ArgumentCaptor.forClass(Forward.class); + verify(eventQueue).add(msgCap.capture()); + + validateMessageContent(tbegin, msgCap.getValue()); + } + + @Test + public void testHandleExternalForward() throws Exception { + startMgr(); + + // route the message to this host + mgr.startDistributing(makeAssignments(true)); + + assertFalse(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT)); + } + + @Test + public void testHandleEvent_NullTarget() throws Exception { + startMgr(); + + // buckets have null targets + mgr.startDistributing(new BucketAssignments(new String[] {null, null})); + + assertTrue(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT)); + + verify(dmaap, times(START_PUB)).publish(any()); + } + + @Test + public void testHandleEvent_SameHost() throws Exception { + startMgr(); + + // route the message to this host + mgr.startDistributing(makeAssignments(true)); + + assertFalse(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT)); + + verify(dmaap, times(START_PUB)).publish(any()); + } + + @Test + public void testHandleEvent_DiffHost_TooManyHops() throws Exception { + startMgr(); + + // route the message to this host + mgr.startDistributing(makeAssignments(false)); + + Forward msg = new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID); + msg.setNumHops(PoolingManagerImpl.MAX_HOPS + 1); + mgr.handle(msg); + + // shouldn't publish + verify(dmaap, times(START_PUB)).publish(any()); + verify(controller, never()).onTopicEvent(CommInfrastructure.UEB, TOPIC2, THE_EVENT); + } + + @Test + public void testHandleEvent_DiffHost_Forward() throws Exception { + startMgr(); + + // route the message to the *OTHER* host + mgr.startDistributing(makeAssignments(false)); + + assertTrue(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT)); + + verify(dmaap, times(START_PUB + 1)).publish(any()); + } + + @Test + public void testExtractRequestId_NullEvent() throws Exception { + startMgr(); + + assertFalse(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, null)); + } + + @Test + public void testExtractRequestId_NullReqId() throws Exception { + startMgr(); + + when(extractors.extract(any())).thenReturn(null); + + assertFalse(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT)); + } + + @Test + public void testExtractRequestId() throws Exception { + startMgr(); + + // route the message to the *OTHER* host + mgr.startDistributing(makeAssignments(false)); + + assertTrue(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT)); + } + + @Test + public void testDecodeEvent_CannotDecode() throws Exception { + startMgr(); + + when(controller.isLocked()).thenReturn(true); + + // create assignments, though they are irrelevant + mgr.startDistributing(makeAssignments(false)); + + when(factory.canDecodeEvent(drools, TOPIC2)).thenReturn(false); + + assertFalse(mgr.beforeOffer(CommInfrastructure.UEB, TOPIC2, THE_EVENT)); + } + + @Test + public void testDecodeEvent_UnsuppEx() throws Exception { + startMgr(); + + when(controller.isLocked()).thenReturn(true); + + // create assignments, though they are irrelevant + mgr.startDistributing(makeAssignments(false)); + + // generate exception + doThrow(new UnsupportedOperationException()).when(factory).decodeEvent(drools, TOPIC2, THE_EVENT); + + assertFalse(mgr.beforeOffer(CommInfrastructure.UEB, TOPIC2, THE_EVENT)); + } + + @Test + public void testDecodeEvent_ArgEx() throws Exception { + startMgr(); + + when(controller.isLocked()).thenReturn(true); + + // create assignments, though they are irrelevant + mgr.startDistributing(makeAssignments(false)); + + // generate exception + doThrow(new IllegalArgumentException()).when(factory).decodeEvent(drools, TOPIC2, THE_EVENT); + + assertFalse(mgr.beforeOffer(CommInfrastructure.UEB, TOPIC2, THE_EVENT)); + } + + @Test + public void testDecodeEvent_StateEx() throws Exception { + startMgr(); + + when(controller.isLocked()).thenReturn(true); + + // create assignments, though they are irrelevant + mgr.startDistributing(makeAssignments(false)); + + // generate exception + doThrow(new IllegalStateException()).when(factory).decodeEvent(drools, TOPIC2, THE_EVENT); + + assertFalse(mgr.beforeOffer(CommInfrastructure.UEB, TOPIC2, THE_EVENT)); + } + + @Test + public void testDecodeEvent() throws Exception { + startMgr(); + + when(controller.isLocked()).thenReturn(true); + + // route to another host + mgr.startDistributing(makeAssignments(false)); + + assertTrue(mgr.beforeOffer(CommInfrastructure.UEB, TOPIC2, THE_EVENT)); + } + + @Test + public void testMakeForward() throws Exception { + startMgr(); + + long tbegin = System.currentTimeMillis(); + + assertTrue(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT)); + + ArgumentCaptor<Forward> msgCap = ArgumentCaptor.forClass(Forward.class); + verify(eventQueue).add(msgCap.capture()); + + validateMessageContent(tbegin, msgCap.getValue()); + } + + @Test + public void testMakeForward_InvalidMsg() throws Exception { + startMgr(); + + assertTrue(mgr.beforeInsert(null, TOPIC2, THE_EVENT, DECODED_EVENT)); + + // should not have tried to enqueue a message + verify(eventQueue, never()).add(any()); + } + + @Test + public void testHandle_SameHost() throws Exception { + startMgr(); + + // route the message to this host + mgr.startDistributing(makeAssignments(true)); + + Forward msg = new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID); + mgr.handle(msg); + + verify(dmaap, times(START_PUB)).publish(any()); + verify(controller).onTopicEvent(CommInfrastructure.UEB, TOPIC2, THE_EVENT); + } + + @Test + public void testHandle_DiffHost() throws Exception { + startMgr(); + + // route the message to this host + mgr.startDistributing(makeAssignments(false)); + + Forward msg = new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID); + mgr.handle(msg); + + verify(dmaap, times(START_PUB + 1)).publish(any()); + verify(controller, never()).onTopicEvent(CommInfrastructure.UEB, TOPIC2, THE_EVENT); + } + + @Test + public void testInject() throws Exception { + startMgr(); + + // route the message to this host + mgr.startDistributing(makeAssignments(true)); + + CountDownLatch latch = catchRecursion(true); + + Forward msg = new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID); + mgr.handle(msg); + + verify(dmaap, times(START_PUB)).publish(any()); + verify(controller).onTopicEvent(CommInfrastructure.UEB, TOPIC2, THE_EVENT); + + // ensure we made it past both beforeXxx() methods + assertEquals(0, latch.getCount()); + } + + @Test + public void testHandleInternal() throws Exception { + startMgr(); + + StartState st = (StartState) mgr.getCurrent(); + + /* + * give it its heart beat, that should cause it to transition to the + * Query state. + */ + Heartbeat hb = new Heartbeat(mgr.getHost(), st.getHbTimestampMs()); + hb.setChannel(Message.ADMIN); + + String msg = ser.encodeMsg(hb); + + mgr.onTopicEvent(CommInfrastructure.UEB, MY_TOPIC, msg); + + assertTrue(mgr.getCurrent() instanceof QueryState); + } + + @Test + public void testHandleInternal_IOEx() throws Exception { + startMgr(); + + mgr.onTopicEvent(CommInfrastructure.UEB, MY_TOPIC, "invalid message"); + + assertTrue(mgr.getCurrent() instanceof StartState); + } + + @Test + public void testHandleInternal_PoolEx() throws Exception { + startMgr(); + + StartState st = (StartState) mgr.getCurrent(); + + Heartbeat hb = new Heartbeat(mgr.getHost(), st.getHbTimestampMs()); + + /* + * do NOT set the channel - this will cause the message to be invalid, + * triggering an exception + */ + + String msg = ser.encodeMsg(hb); + + mgr.onTopicEvent(CommInfrastructure.UEB, MY_TOPIC, msg); + + assertTrue(mgr.getCurrent() instanceof StartState); + } + + @Test + public void testStartDistributing() throws Exception { + startMgr(); + + // route the message to this host + mgr.startDistributing(makeAssignments(true)); + + assertFalse(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT)); + + + // null assignments should be ignored + mgr.startDistributing(null); + + assertFalse(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT)); + + + // route the message to the other host + mgr.startDistributing(makeAssignments(false)); + + assertTrue(mgr.beforeInsert(CommInfrastructure.UEB, TOPIC2, THE_EVENT, DECODED_EVENT)); + } + + @Test + public void testStartDistributing_EventsInQueue_ProcessLocally() throws Exception { + startMgr(); + + // put items in the queue + LinkedList<Forward> lst = new LinkedList<>(); + lst.add(new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID)); + lst.add(new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID)); + lst.add(new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID)); + + when(eventQueue.poll()).thenAnswer(args -> lst.poll()); + + // route the messages to this host + mgr.startDistributing(makeAssignments(true)); + + // all of the events should have been processed locally + verify(dmaap, times(START_PUB)).publish(any()); + verify(controller, times(3)).onTopicEvent(CommInfrastructure.UEB, TOPIC2, THE_EVENT); + } + + @Test + public void testStartDistributing_EventsInQueue_Forward() throws Exception { + startMgr(); + + // put items in the queue + LinkedList<Forward> lst = new LinkedList<>(); + lst.add(new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID)); + lst.add(new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID)); + lst.add(new Forward(mgr.getHost(), CommInfrastructure.UEB, TOPIC2, THE_EVENT, REQUEST_ID)); + + when(eventQueue.poll()).thenAnswer(args -> lst.poll()); + + // route the messages to the OTHER host + mgr.startDistributing(makeAssignments(false)); + + // all of the events should have been forwarded + verify(dmaap, times(4)).publish(any()); + verify(controller, never()).onTopicEvent(CommInfrastructure.UEB, TOPIC2, THE_EVENT); + } + + @Test + public void testGoStart() { + State st = mgr.goStart(); + assertTrue(st instanceof StartState); + assertEquals(mgr.getHost(), st.getHost()); + } + + @Test + public void testGoQuery() { + BucketAssignments asgn = new BucketAssignments(new String[] {HOST2}); + mgr.startDistributing(asgn); + + State st = mgr.goQuery(); + + assertTrue(st instanceof QueryState); + assertEquals(mgr.getHost(), st.getHost()); + assertEquals(asgn, mgr.getAssignments()); + } + + @Test + public void testGoActive() { + BucketAssignments asgn = new BucketAssignments(new String[] {HOST2}); + mgr.startDistributing(asgn); + + State st = mgr.goActive(); + + assertTrue(st instanceof ActiveState); + assertEquals(mgr.getHost(), st.getHost()); + assertEquals(asgn, mgr.getAssignments()); + } + + @Test + public void testGoInactive() { + State st = mgr.goInactive(); + assertTrue(st instanceof InactiveState); + assertEquals(mgr.getHost(), st.getHost()); + } + + @Test + public void testTimerActionRun() throws Exception { + // must start the scheduler + startMgr(); + + CountDownLatch latch = new CountDownLatch(1); + + mgr.schedule(STD_ACTIVE_HEARTBEAT_MS, xxx -> { + latch.countDown(); + return null; + }); + + // capture the task + ArgumentCaptor<Runnable> taskCap = ArgumentCaptor.forClass(Runnable.class); + + verify(sched, times(2)).schedule(taskCap.capture(), any(Long.class), any(TimeUnit.class)); + + // execute it + taskCap.getValue().run(); + + assertEquals(0, latch.getCount()); + } + + @Test + public void testTimerActionRun_DiffState() throws Exception { + // must start the scheduler + startMgr(); + + CountDownLatch latch = new CountDownLatch(1); + + mgr.schedule(STD_ACTIVE_HEARTBEAT_MS, xxx -> { + latch.countDown(); + return null; + }); + + // capture the task + ArgumentCaptor<Runnable> taskCap = ArgumentCaptor.forClass(Runnable.class); + + verify(sched, times(2)).schedule(taskCap.capture(), any(Long.class), any(TimeUnit.class)); + + // give it a heartbeat so that it transitions to the query state + StartState st = (StartState) mgr.getCurrent(); + Heartbeat hb = new Heartbeat(mgr.getHost(), st.getHbTimestampMs()); + hb.setChannel(Message.ADMIN); + + String msg = ser.encodeMsg(hb); + + mgr.onTopicEvent(CommInfrastructure.UEB, MY_TOPIC, msg); + + assertTrue(mgr.getCurrent() instanceof QueryState); + + // execute it + taskCap.getValue().run(); + + // it should NOT have counted down + assertEquals(1, latch.getCount()); + } + + /** + * Validates the message content. + * + * @param tbegin creation time stamp must be no less than this + * @param msg message to be validated + */ + private void validateMessageContent(long tbegin, Forward msg) { + assertEquals(0, msg.getNumHops()); + assertTrue(msg.getCreateTimeMs() >= tbegin); + assertEquals(mgr.getHost(), msg.getSource()); + assertEquals(CommInfrastructure.UEB, msg.getProtocol()); + assertEquals(TOPIC2, msg.getTopic()); + assertEquals(THE_EVENT, msg.getPayload()); + assertEquals(REQUEST_ID, msg.getRequestId()); + } + + /** + * Configure the mock controller to act like a real controller, invoking + * beforeOffer and then beforeInsert, so we can make sure they pass through. + * We'll keep count to ensure we don't get into infinite recursion. + * + * @param invokeBeforeInsert {@code true} if beforeInsert() should be + * invoked, {@code false} if it should be skipped + * + * @return a latch that will be counted down if both beforeXxx() methods + * return false + */ + private CountDownLatch catchRecursion(boolean invokeBeforeInsert) { + CountDownLatch recursion = new CountDownLatch(3); + CountDownLatch latch = new CountDownLatch(1); + + doAnswer(args -> { + + recursion.countDown(); + if (recursion.getCount() == 0) { + fail("recursive calls to onTopicEvent"); + } + + int iarg = 0; + CommInfrastructure proto = args.getArgumentAt(iarg++, CommInfrastructure.class); + String topic = args.getArgumentAt(iarg++, String.class); + String event = args.getArgumentAt(iarg++, String.class); + + if (mgr.beforeOffer(proto, topic, event)) { + return null; + } + + if (invokeBeforeInsert && mgr.beforeInsert(proto, topic, event, DECODED_EVENT)) { + return null; + } + + latch.countDown(); + + return null; + }).when(controller).onTopicEvent(any(), any(), any()); + + return latch; + } + + /** + * Makes an assignment with two buckets. + * + * @param sameHost {@code true} if the {@link #REQUEST_ID} should has to the + * manager's bucket, {@code false} if it should hash to the other + * host's bucket + * @return a new bucket assignment + */ + private BucketAssignments makeAssignments(boolean sameHost) { + int slot = REQUEST_ID.hashCode() % 2; + + // slot numbers are 0 and 1 - reverse them if it's for a different host + if (!sameHost) { + slot = 1 - slot; + } + + String[] asgn = new String[2]; + asgn[slot] = mgr.getHost(); + asgn[1 - slot] = HOST2; + + return new BucketAssignments(asgn); + } + + /** + * Invokes methods necessary to start the manager. + * + * @throws PoolingFeatureException if an error occurs + */ + private void startMgr() throws PoolingFeatureException { + mgr.beforeStart(); + mgr.afterStart(); + } + + /** + * Invokes methods necessary to lock the manager. + */ + private void lockMgr() { + mgr.beforeLock(); + } + + /** + * Invokes methods necessary to unlock the manager. + */ + private void unlockMgr() { + mgr.afterUnlock(); + } + + /** + * Used to create a mock object that implements both super interfaces. + */ + private static interface ListeningController extends TopicListener, PolicyController { + + } + + /** + * Invokes a method that is expected to throw an exception. + * + * @param exClass class of exception that is expected + * @param func function to invoke + * @return the exception that was thrown + * @throws AssertionError if no exception was thrown + */ + private <T extends Exception> T expectException(Class<T> exClass, ExFunction<T> func) { + try { + func.apply(null); + throw new AssertionError("missing exception"); + + } catch (Exception e) { + return exClass.cast(e); + } + } + + /** + * Function that is expected to throw an exception. + * + * @param <T> type of exception the function is expected to throw + */ + @FunctionalInterface + private static interface ExFunction<T extends Exception> { + + /** + * Invokes the function. + * + * @param arg always {@code null} + * @throws T if an error occurs + */ + public void apply(Void arg) throws T; + + } +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingPropertiesTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingPropertiesTest.java new file mode 100644 index 00000000..63eb59d4 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/PoolingPropertiesTest.java @@ -0,0 +1,178 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.onap.policy.common.utils.properties.SpecPropertyConfiguration.generalize; +import static org.onap.policy.common.utils.properties.SpecPropertyConfiguration.specialize; +import static org.onap.policy.drools.pooling.PoolingProperties.ACTIVE_HEARTBEAT_MS; +import static org.onap.policy.drools.pooling.PoolingProperties.FEATURE_ENABLED; +import static org.onap.policy.drools.pooling.PoolingProperties.IDENTIFICATION_MS; +import static org.onap.policy.drools.pooling.PoolingProperties.INTER_HEARTBEAT_MS; +import static org.onap.policy.drools.pooling.PoolingProperties.OFFLINE_AGE_MS; +import static org.onap.policy.drools.pooling.PoolingProperties.OFFLINE_LIMIT; +import static org.onap.policy.drools.pooling.PoolingProperties.POOLING_TOPIC; +import static org.onap.policy.drools.pooling.PoolingProperties.REACTIVATE_MS; +import static org.onap.policy.drools.pooling.PoolingProperties.START_HEARTBEAT_MS; +import java.util.Properties; +import java.util.function.Function; +import org.junit.Before; +import org.junit.Test; +import org.onap.policy.common.utils.properties.exception.PropertyException; + +public class PoolingPropertiesTest { + + private static final String CONTROLLER = "a.controller"; + + private static final String STD_POOLING_TOPIC = "my.topic"; + public static final boolean STD_FEATURE_ENABLED = true; + public static final int STD_OFFLINE_LIMIT = 10; + public static final long STD_OFFLINE_AGE_MS = 1000L; + public static final long STD_START_HEARTBEAT_MS = 2000L; + public static final long STD_REACTIVATE_MS = 3000L; + public static final long STD_IDENTIFICATION_MS = 4000L; + public static final long STD_LEADER_MS = 5000L; + public static final long STD_ACTIVE_HEARTBEAT_MS = 6000L; + public static final long STD_INTER_HEARTBEAT_MS = 7000L; + + private Properties plain; + private PoolingProperties pooling; + + @Before + public void setUp() throws Exception { + plain = makeProperties(); + + pooling = new PoolingProperties(CONTROLLER, plain); + } + + @Test + public void testPoolingProperties() throws PropertyException { + // ensure no exceptions + new PoolingProperties(CONTROLLER, plain); + } + + @Test + public void testGetSource() { + assertEquals(plain, pooling.getSource()); + } + + @Test + public void testGetPoolingTopic() { + assertEquals(STD_POOLING_TOPIC, pooling.getPoolingTopic()); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetPoolingTopic_Generalize() { + // shouldn't be able to generalize the topic + generalize(POOLING_TOPIC); + } + + @Test + public void testGetOfflineLimit() throws PropertyException { + doTest(OFFLINE_LIMIT, STD_OFFLINE_LIMIT, 1000, xxx -> pooling.getOfflineLimit()); + } + + @Test + public void testGetOfflineAgeMs() throws PropertyException { + doTest(OFFLINE_AGE_MS, STD_OFFLINE_AGE_MS, 60000L, xxx -> pooling.getOfflineAgeMs()); + } + + @Test + public void testGetStartHeartbeatMs() throws PropertyException { + doTest(START_HEARTBEAT_MS, STD_START_HEARTBEAT_MS, 50000L, xxx -> pooling.getStartHeartbeatMs()); + } + + @Test + public void testGetReactivateMs() throws PropertyException { + doTest(REACTIVATE_MS, STD_REACTIVATE_MS, 50000L, xxx -> pooling.getReactivateMs()); + } + + @Test + public void testGetIdentificationMs() throws PropertyException { + doTest(IDENTIFICATION_MS, STD_IDENTIFICATION_MS, 50000L, xxx -> pooling.getIdentificationMs()); + } + + @Test + public void testGetActiveHeartbeatMs() throws PropertyException { + doTest(ACTIVE_HEARTBEAT_MS, STD_ACTIVE_HEARTBEAT_MS, 50000L, xxx -> pooling.getActiveHeartbeatMs()); + } + + @Test + public void testGetInterHeartbeatMs() throws PropertyException { + doTest(INTER_HEARTBEAT_MS, STD_INTER_HEARTBEAT_MS, 15000L, xxx -> pooling.getInterHeartbeatMs()); + } + + /** + * Tests a particular property. Verifies that the correct value is returned + * if the specialized property has a value or the property has no value. + * Also verifies that the property name can be generalized. + * + * @param propnm name of the property of interest + * @param specValue expected specialized value + * @param dfltValue expected default value + * @param func function to get the field + * @throws PropertyException if an error occurs + */ + private <T> void doTest(String propnm, T specValue, T dfltValue, Function<Void, T> func) throws PropertyException { + /* + * With specialized property + */ + pooling = new PoolingProperties(CONTROLLER, plain); + assertEquals("special " + propnm, specValue, func.apply(null)); + + /* + * Ensure the property supports generalization - this will throw an + * exception if it does not. + */ + assertFalse(propnm.equals(generalize(propnm))); + + /* + * Without the property - should use the default value. + */ + plain.remove(specialize(propnm, CONTROLLER)); + plain.remove(generalize(propnm)); + pooling = new PoolingProperties(CONTROLLER, plain); + assertEquals("default " + propnm, dfltValue, func.apply(null)); + } + + /** + * Makes a set of properties, where all of the properties are specialized + * for the controller. + * + * @return a new property set + */ + private Properties makeProperties() { + Properties props = new Properties(); + + props.setProperty(specialize(POOLING_TOPIC, CONTROLLER), STD_POOLING_TOPIC); + props.setProperty(specialize(FEATURE_ENABLED, CONTROLLER), "" + STD_FEATURE_ENABLED); + props.setProperty(specialize(OFFLINE_LIMIT, CONTROLLER), "" + STD_OFFLINE_LIMIT); + props.setProperty(specialize(OFFLINE_AGE_MS, CONTROLLER), "" + STD_OFFLINE_AGE_MS); + props.setProperty(specialize(START_HEARTBEAT_MS, CONTROLLER), "" + STD_START_HEARTBEAT_MS); + props.setProperty(specialize(REACTIVATE_MS, CONTROLLER), "" + STD_REACTIVATE_MS); + props.setProperty(specialize(IDENTIFICATION_MS, CONTROLLER), "" + STD_IDENTIFICATION_MS); + props.setProperty(specialize(ACTIVE_HEARTBEAT_MS, CONTROLLER), "" + STD_ACTIVE_HEARTBEAT_MS); + props.setProperty(specialize(INTER_HEARTBEAT_MS, CONTROLLER), "" + STD_INTER_HEARTBEAT_MS); + + return props; + } +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/SerializerTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/SerializerTest.java new file mode 100644 index 00000000..4206a836 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/SerializerTest.java @@ -0,0 +1,96 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.onap.policy.drools.pooling.state.FilterUtils.makeAnd; +import static org.onap.policy.drools.pooling.state.FilterUtils.makeEquals; +import static org.onap.policy.drools.pooling.state.FilterUtils.makeOr; +import java.util.Map; +import java.util.TreeMap; +import org.junit.Test; +import org.onap.policy.drools.pooling.message.Message; +import org.onap.policy.drools.pooling.message.Query; + +public class SerializerTest { + + @Test + public void testSerializer() { + new Serializer(); + } + + @Test + @SuppressWarnings("unchecked") + public void testEncodeFilter() throws Exception { + Serializer ser = new Serializer(); + + /* + * Ensure raw maps serialize as expected. Use a TreeMap so the field + * order is predictable. + */ + Map<String, Object> top = new TreeMap<>(); + Map<String, Object> inner = new TreeMap<>(); + top.put("abc", 20); + top.put("def", inner); + top.put("ghi", true); + inner.put("xyz", 30); + assertEquals("{'abc':20,'def':{'xyz':30},'ghi':true}".replace('\'', '"'), ser.encodeFilter(top)); + + /* + * Ensure we can encode a complicated filter without throwing an + * exception + */ + Map<String, Object> complexFilter = makeAnd(makeEquals("fieldC", "valueC"), + makeOr(makeEquals("fieldA", "valueA"), makeEquals("fieldB", "valueB"))); + String val = ser.encodeFilter(complexFilter); + assertFalse(val.isEmpty()); + } + + @Test + public void testEncodeMsg_testDecodeMsg() throws Exception { + Serializer ser = new Serializer(); + + Query msg = new Query("hostA"); + msg.setChannel("channelB"); + + String encoded = ser.encodeMsg(msg); + assertNotNull(encoded); + + Message decoded = ser.decodeMsg(encoded); + assertEquals(Query.class, decoded.getClass()); + + assertEquals(msg.getSource(), decoded.getSource()); + assertEquals(msg.getChannel(), decoded.getChannel()); + + // should work a second time, too + encoded = ser.encodeMsg(msg); + assertNotNull(encoded); + + decoded = ser.decodeMsg(encoded); + assertEquals(Query.class, decoded.getClass()); + + assertEquals(msg.getSource(), decoded.getSource()); + assertEquals(msg.getChannel(), decoded.getChannel()); + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/SpecPropertiesTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/SpecPropertiesTest.java new file mode 100644 index 00000000..8b495099 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/SpecPropertiesTest.java @@ -0,0 +1,186 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import java.util.Properties; +import org.junit.Before; +import org.junit.Test; + +public class SpecPropertiesTest { + + /** + * Property prefix of interest. + */ + private static final String MY_PREFIX = "my.prefix"; + + /** + * Specialization, which follows the prefix. + */ + private static final String MY_SPEC = "my.spec"; + + /** + * Generalized prefix (i.e., without the spec). + */ + private static final String PREFIX_GEN = MY_PREFIX + "."; + + /** + * Specialized prefix (i.e., with the spec). + */ + private static final String PREFIX_SPEC = PREFIX_GEN + MY_SPEC + "."; + + /** + * Suffix to add to property names to generate names of properties that are + * not populated. + */ + private static final String SUFFIX = ".suffix"; + + /** + * Property name without a prefix. + */ + private static final String PROP_NO_PREFIX = "other"; + + /** + * Generalized property name (i.e., without the spec). + */ + private static final String PROP_GEN = PREFIX_GEN + "generalized"; + + // property names that include the spec + private static final String PROP_SPEC = PREFIX_SPEC + "specialized"; + private static final String PROP_UNKNOWN = PREFIX_SPEC + "unknown"; + + // property values + private static final String VAL_NO_PREFIX = "no-prefix"; + private static final String VAL_GEN = "gen"; + private static final String VAL_SPEC = "spec"; + + private static final String VAL_DEFAULT = "default value"; + + private Properties supportingProps; + private SpecProperties props; + + @Before + public void setUp() { + supportingProps = new Properties(); + + supportingProps.setProperty(PROP_NO_PREFIX, VAL_NO_PREFIX); + supportingProps.setProperty(PROP_GEN, VAL_GEN); + supportingProps.setProperty(PROP_SPEC, VAL_SPEC); + + props = new SpecProperties(MY_PREFIX, MY_SPEC); + + props.putAll(supportingProps); + } + + @Test + public void testSpecPropertiesStringString() { + + // no supporting properties + props = new SpecProperties(MY_PREFIX, MY_SPEC); + + assertEquals(PREFIX_GEN, props.getPrefix()); + assertEquals(PREFIX_SPEC, props.getSpecPrefix()); + + // everything is null + assertNull(props.getProperty(gen(PROP_NO_PREFIX))); + assertNull(props.getProperty(gen(PROP_GEN))); + assertNull(props.getProperty(gen(PROP_SPEC))); + assertNull(props.getProperty(gen(PROP_UNKNOWN))); + } + + @Test + public void testSpecPropertiesStringStringProperties() { + + // use supportingProps as default properties + props = new SpecProperties(MY_PREFIX, MY_SPEC, supportingProps); + + assertEquals(PREFIX_GEN, props.getPrefix()); + assertEquals(PREFIX_SPEC, props.getSpecPrefix()); + + assertEquals(VAL_NO_PREFIX, props.getProperty(gen(PROP_NO_PREFIX))); + assertEquals(VAL_GEN, props.getProperty(gen(PROP_GEN))); + assertEquals(VAL_SPEC, props.getProperty(gen(PROP_SPEC))); + assertNull(props.getProperty(gen(PROP_UNKNOWN))); + } + + @Test + public void testWithTrailingDot() { + // neither has trailing dot + assertEquals(PREFIX_GEN, props.getPrefix()); + assertEquals(PREFIX_SPEC, props.getSpecPrefix()); + + // both have trailing dot + props = new SpecProperties(PREFIX_GEN, MY_SPEC + "."); + assertEquals(PREFIX_GEN, props.getPrefix()); + assertEquals(PREFIX_SPEC, props.getSpecPrefix()); + } + + @Test + public void testGetPropertyString() { + // the key does contain the prefix + assertEquals(VAL_NO_PREFIX, props.getProperty(gen(PROP_NO_PREFIX))); + assertNull(props.getProperty(gen(PROP_NO_PREFIX + SUFFIX))); + + // specialized value exists + assertEquals(VAL_GEN, props.getProperty(gen(PROP_GEN))); + assertNull(props.getProperty(gen(PROP_GEN + SUFFIX))); + + // generalized value exists + assertEquals(VAL_SPEC, props.getProperty(gen(PROP_SPEC))); + assertNull(props.getProperty(gen(PROP_SPEC + SUFFIX))); + + // not found + assertNull(props.getProperty(gen(PROP_UNKNOWN))); + assertNull(props.getProperty(gen(PROP_UNKNOWN + SUFFIX))); + } + + @Test + public void testGetPropertyStringString() { + // the key does contain the prefix + assertEquals(VAL_NO_PREFIX, props.getProperty(gen(PROP_NO_PREFIX), VAL_DEFAULT)); + assertEquals(VAL_DEFAULT, props.getProperty(gen(PROP_NO_PREFIX + SUFFIX), VAL_DEFAULT)); + + // specialized value exists + assertEquals(VAL_GEN, props.getProperty(gen(PROP_GEN), VAL_DEFAULT)); + assertEquals(VAL_DEFAULT, props.getProperty(gen(PROP_GEN + SUFFIX), VAL_DEFAULT)); + + // generalized value exists + assertEquals(VAL_SPEC, props.getProperty(gen(PROP_SPEC), VAL_DEFAULT)); + assertEquals(VAL_DEFAULT, props.getProperty(gen(PROP_SPEC + SUFFIX), VAL_DEFAULT)); + + // not found + assertEquals(VAL_DEFAULT, props.getProperty(gen(PROP_UNKNOWN), VAL_DEFAULT)); + assertEquals(VAL_DEFAULT, props.getProperty(gen(PROP_UNKNOWN + SUFFIX), VAL_DEFAULT)); + + // can return null + assertNull(props.getProperty(gen(PROP_UNKNOWN), null)); + } + + private String gen(String propnm) { + if (propnm.startsWith(PREFIX_SPEC)) { + return PREFIX_GEN + propnm.substring(PREFIX_SPEC.length()); + } + + return propnm; + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ClassExtractorsTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ClassExtractorsTest.java new file mode 100644 index 00000000..e9246430 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ClassExtractorsTest.java @@ -0,0 +1,440 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.extractor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import java.util.Map; +import java.util.Properties; +import java.util.TreeMap; +import java.util.function.Function; +import org.junit.Before; +import org.junit.Test; + +public class ClassExtractorsTest { + + private static final int NTIMES = 5; + + private static final String MY_TYPE = "theType"; + private static final String PROP_PREFIX = "extractor." + MY_TYPE + "."; + + private static final String VALUE = "a value"; + private static final Integer INT_VALUE = 10; + private static final Integer INT_VALUE2 = 20; + + private Properties props; + private ClassExtractors map; + + @Before + public void setUp() { + props = new Properties(); + + props.setProperty(PROP_PREFIX + Simple.class.getName(), "${intValue}"); + props.setProperty(PROP_PREFIX + WithString.class.getName(), "${strValue}"); + + map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE); + } + + @Test + public void testExtract() { + Simple obj = new Simple(); + assertEquals(INT_VALUE, map.extract(obj)); + + // string value + assertEquals(VALUE, tryIt(Simple.class, "${strValue}", xxx -> new Simple())); + + // null object + assertNull(map.extract(null)); + + // values from two different kinds of objects + props = new Properties(); + props.setProperty(PROP_PREFIX + Simple.class.getName(), "${intValue}"); + props.setProperty(PROP_PREFIX + WithString.class.getName(), "${strValue}"); + map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE); + + assertEquals(INT_VALUE, map.extract(new Simple())); + assertEquals(VALUE, map.extract(new Sub())); + + // values from a superclass method, but property defined for subclass + props = new Properties(); + props.setProperty(PROP_PREFIX + Sub.class.getName(), "${strValue}"); + map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE); + + assertEquals(VALUE, map.extract(new Sub())); + + // values from a superclass field, but property defined for subclass + props = new Properties(); + props.setProperty(PROP_PREFIX + Sub.class.getName(), "${intValue}"); + map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE); + + assertEquals(INT_VALUE, map.extract(new Sub())); + + + // prefix includes trailing "." + props = new Properties(); + props.setProperty(PROP_PREFIX + Simple.class.getName(), "${intValue}"); + map = new ClassExtractors(props, PROP_PREFIX.substring(0, PROP_PREFIX.length() - 1), MY_TYPE); + assertEquals(INT_VALUE, map.extract(new Simple())); + + + // values from an class in a different file + props = new Properties(); + props.setProperty(PROP_PREFIX + ClassExtractorsTestSupport.class.getName(), "${nested.theValue}"); + map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE); + + assertEquals(ClassExtractorsTestSupport2.NESTED_VALUE, map.extract(new ClassExtractorsTestSupport())); + } + + @Test + public void testGetExtractor() { + Simple obj = new Simple(); + + // repeat - shouldn't re-create the extractor + for (int x = 0; x < NTIMES; ++x) { + assertEquals("x=" + x, INT_VALUE, map.extract(obj)); + assertEquals("x=" + x, 1, map.size()); + } + } + + @Test + public void testBuildExtractorClass_TopLevel() { + // extractor defined for top-level class + props = new Properties(); + props.setProperty(PROP_PREFIX + Sub.class.getName(), "${strValue}"); + + map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE); + assertEquals(VALUE, map.extract(new Sub())); + + // one extractor for top-level class + assertEquals(1, map.size()); + } + + @Test + public void testBuildExtractorClass_SuperClass() { + // extractor defined for superclass (interface) + assertEquals(VALUE, map.extract(new Sub())); + + // one extractor for top-level class and one for interface + assertEquals(2, map.size()); + } + + @Test + public void testBuildExtractorClass_NotDefined() { + // no extractor defined for "this" class + assertNull(map.extract(this)); + + // one NULL extractor for top-level class + assertEquals(1, map.size()); + } + + @Test + public void testBuildExtractorClassString() { + // no leading "${" + assertNull(tryIt(Simple.class, "intValue}", xxx -> new Simple())); + + // no trailing "}" + assertNull(tryIt(Simple.class, "${intValue", xxx -> new Simple())); + + // leading "." + assertNull(tryIt(Sub.class, "${.simple.strValue}", xxx -> new Sub())); + + // trailing "." + assertNull(tryIt(Sub.class, "${simple.strValue.}", xxx -> new Sub())); + + // one component + assertEquals(VALUE, tryIt(Sub.class, "${strValue}", xxx -> new Sub())); + + // two components + assertEquals(VALUE, tryIt(Sub.class, "${simple.strValue}", xxx -> new Sub())); + + // invalid component + assertNull(tryIt(Sub.class, "${unknown}", xxx -> new Sub())); + } + + @Test + public void testGetClassExtractor_InSuper() { + // field in the superclass + assertEquals(INT_VALUE, tryIt(Super.class, "${intValue}", xxx -> new Sub())); + } + + @Test + public void testGetClassExtractor_InInterface() { + // defined in the interface + assertEquals(VALUE, map.extract(new Sub())); + } + + @Test + public void testNullExtractorExtract() { + // empty properties - should only create NullExtractor + map = new ClassExtractors(new Properties(), PROP_PREFIX, MY_TYPE); + + Simple obj = new Simple(); + + // repeat - shouldn't re-create the extractor + for (int x = 0; x < NTIMES; ++x) { + assertNull("x=" + x, map.extract(obj)); + assertEquals("x=" + x, 1, map.size()); + } + } + + @Test + public void testComponetizedExtractor() { + // one component + assertEquals(VALUE, tryIt(Sub.class, "${strValue}", xxx -> new Sub())); + + // three components + assertEquals(VALUE, tryIt(Sub.class, "${cont.data.strValue}", xxx -> new Sub())); + } + + @Test + public void testComponetizedExtractorBuildExtractor_Method() { + assertEquals(INT_VALUE, tryIt(Simple.class, "${intValue}", xxx -> new Simple())); + } + + @Test + public void testComponetizedExtractorBuildExtractor_Field() { + assertEquals(VALUE, tryIt(Simple.class, "${strValue}", xxx -> new Simple())); + } + + @Test + public void testComponetizedExtractorBuildExtractor_Map() { + Map<String, Object> inner = new TreeMap<>(); + inner.put("inner1", "abc1"); + inner.put("inner2", "abc2"); + + Map<String, Object> outer = new TreeMap<>(); + outer.put("outer1", "def1"); + outer.put("outer2", inner); + + Simple obj = new Simple(); + + props.setProperty(PROP_PREFIX + Simple.class.getName(), "${mapValue}"); + map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE); + assertEquals(null, map.extract(obj)); + + obj.mapValue = outer; + props.setProperty(PROP_PREFIX + Simple.class.getName(), "${mapValue.outer2.inner2}"); + map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE); + assertEquals("abc2", map.extract(obj)); + } + + @Test + public void testComponetizedExtractorBuildExtractor_Unknown() { + assertNull(tryIt(Simple.class, "${unknown2}", xxx -> new Simple())); + } + + @Test + public void testComponetizedExtractorExtract_MiddleNull() { + // data component is null + assertEquals(null, tryIt(Sub.class, "${cont.data.strValue}", xxx -> { + Sub obj = new Sub(); + obj.cont.simpleValue = null; + return obj; + })); + } + + @Test + public void testComponetizedExtractorGetMethodExtractor_VoidMethod() { + // tell it to use getVoidValue() + props.setProperty(PROP_PREFIX + Simple.class.getName(), "${voidValue}"); + map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE); + + Simple obj = new Simple(); + assertNull(map.extract(obj)); + + assertFalse(obj.voidInvoked); + } + + @Test + public void testComponetizedExtractorGetMethodExtractor() { + assertEquals(INT_VALUE, map.extract(new Simple())); + } + + @Test + public void testComponetizedExtractorGetFieldExtractor() { + // use a field + assertEquals(VALUE, tryIt(Simple.class, "${strValue}", xxx -> new Simple())); + } + + @Test + public void testComponetizedExtractorGetMapExtractor() { + Map<String, Object> inner = new TreeMap<>(); + inner.put("inner1", "abc1"); + inner.put("inner2", "abc2"); + + Map<String, Object> outer = new TreeMap<>(); + outer.put("outer1", "def1"); + outer.put("outer2", inner); + + Simple obj = new Simple(); + + obj.mapValue = outer; + props.setProperty(PROP_PREFIX + Simple.class.getName(), "${mapValue.outer2.inner2}"); + map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE); + assertEquals("abc2", map.extract(obj)); + } + + @Test + public void testComponetizedExtractorGetMapExtractor_MapSubclass() { + Map<String, Object> inner = new TreeMap<>(); + inner.put("inner1", "abc1"); + inner.put("inner2", "abc2"); + + MapSubclass outer = new MapSubclass(); + outer.put("outer1", "def1"); + outer.put("outer2", inner); + + Simple obj = new Simple(); + + props.setProperty(PROP_PREFIX + Simple.class.getName(), "${mapValue}"); + map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE); + assertEquals(null, map.extract(obj)); + + obj.mapValue = outer; + props.setProperty(PROP_PREFIX + Simple.class.getName(), "${mapValue.outer2.inner2}"); + map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE); + assertEquals("abc2", map.extract(obj)); + } + + /** + * Sets a property for the given class, makes an object, and then returns + * the value extracted. + * + * @param clazz class whose property is to be set + * @param propval value to which to set the property + * @param makeObj function to create the object whose data is to be + * extracted + * @return the extracted data, or {@code null} if nothing was extracted + */ + private Object tryIt(Class<?> clazz, String propval, Function<Void, Object> makeObj) { + Properties props = new Properties(); + props.setProperty(PROP_PREFIX + clazz.getName(), propval); + + map = new ClassExtractors(props, PROP_PREFIX, MY_TYPE); + + return map.extract(makeObj.apply(null)); + } + + /** + * A Map subclass, used to verify that getMapExtractor() still handles it. + */ + private static class MapSubclass extends TreeMap<String, Object> { + private static final long serialVersionUID = 1L; + + } + + /** + * A simple class. + */ + private static class Simple { + + /** + * This will not be used because getIntValue() will override it. + */ + @SuppressWarnings("unused") + private int intValue = INT_VALUE2; + + /** + * Used to verify retrieval via a field name. + */ + @SuppressWarnings("unused") + private String strValue = VALUE; + + /** + * Used to verify retrieval within maps. + */ + @SuppressWarnings("unused") + private Map<String, Object> mapValue = null; + + /** + * {@code True} if {@link #getVoidValue()} was invoked, {@code false} + * otherwise. + */ + private boolean voidInvoked = false; + + /** + * This function will supercede the value in the "intValue" field. + * + * @return INT_VALUE + */ + @SuppressWarnings("unused") + public Integer getIntValue() { + return INT_VALUE; + } + + /** + * Used to verify that void functions are not invoked. + */ + @SuppressWarnings("unused") + public void getVoidValue() { + voidInvoked = true; + } + } + + /** + * Used to verify multi-component retrieval. + */ + private static class Container { + private Simple simpleValue = new Simple(); + + @SuppressWarnings("unused") + public Simple getData() { + return simpleValue; + } + } + + /** + * Used to verify extraction when the property refers to an interface. + */ + private static interface WithString { + + public String getStrValue(); + } + + /** + * Used to verify retrieval within a superclass. + */ + private static class Super implements WithString { + + @SuppressWarnings("unused") + private int intValue = INT_VALUE; + + @Override + public String getStrValue() { + return VALUE; + } + } + + /** + * Used to verify retrieval within a subclass. + */ + private static class Sub extends Super { + + @SuppressWarnings("unused") + private Simple simple = new Simple(); + + /** + * Used to verify multi-component retrieval. + */ + private Container cont = new Container(); + } +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ClassExtractorsTestSupport.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ClassExtractorsTestSupport.java new file mode 100644 index 00000000..be8d6c26 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ClassExtractorsTestSupport.java @@ -0,0 +1,40 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.extractor; + +/** + * Used to test extractors. + */ +public class ClassExtractorsTestSupport { + + private ClassExtractorsTestSupport2 nested = new ClassExtractorsTestSupport2(); + + /** + * + */ + public ClassExtractorsTestSupport() { + super(); + } + + protected ClassExtractorsTestSupport2 getNested() { + return nested; + } +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ClassExtractorsTestSupport2.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ClassExtractorsTestSupport2.java new file mode 100644 index 00000000..6941d033 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ClassExtractorsTestSupport2.java @@ -0,0 +1,32 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.extractor; + +/** + * Used to test extractors. + */ +public class ClassExtractorsTestSupport2 { + + public static final int NESTED_VALUE = 30; + + @SuppressWarnings("unused") + private int theValue = NESTED_VALUE; +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ExtractorExceptionTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ExtractorExceptionTest.java new file mode 100644 index 00000000..d1458de7 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/ExtractorExceptionTest.java @@ -0,0 +1,34 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.extractor; + +import static org.junit.Assert.assertEquals; +import org.junit.Test; +import org.onap.policy.common.utils.test.ExceptionsTester; + +public class ExtractorExceptionTest extends ExceptionsTester { + + @Test + public void test() { + assertEquals(5, test(ExtractorException.class)); + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/FieldExtractorTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/FieldExtractorTest.java new file mode 100644 index 00000000..6fc2e20e --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/FieldExtractorTest.java @@ -0,0 +1,77 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.extractor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import java.lang.reflect.Field; +import org.junit.Before; +import org.junit.Test; + +public class FieldExtractorTest { + + private static final String VALUE = "the value"; + private static final Integer INT_VALUE = 10; + + private Field field; + private FieldExtractor ext; + + @Before + public void setUp() throws Exception { + field = MyClass.class.getDeclaredField("value"); + ext = new FieldExtractor(field); + } + + @Test + public void testExtract() throws Exception { + assertEquals(VALUE, ext.extract(new MyClass())); + + // repeat + assertEquals(VALUE, ext.extract(new MyClass())); + + // null value + MyClass obj = new MyClass(); + obj.value = null; + assertEquals(null, ext.extract(obj)); + + obj.value = VALUE + "X"; + assertEquals(VALUE + "X", ext.extract(obj)); + + // different value type + field = MyClass.class.getDeclaredField("value2"); + ext = new FieldExtractor(field); + assertEquals(INT_VALUE, ext.extract(new MyClass())); + } + + @Test + public void testExtract_ArgEx() { + // pass it the wrong class type + assertNull(ext.extract(this)); + } + + private static class MyClass { + @SuppressWarnings("unused") + private String value = VALUE; + + @SuppressWarnings("unused") + private int value2 = INT_VALUE; + } +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/MapExtractorTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/MapExtractorTest.java new file mode 100644 index 00000000..48985bf3 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/MapExtractorTest.java @@ -0,0 +1,72 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.extractor; + +import static org.junit.Assert.*; +import java.util.HashMap; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; + +public class MapExtractorTest { + private static final String KEY = "a.key"; + private static final String VALUE = "a.value"; + + private MapExtractor ext; + + @Before + public void setUp() { + ext = new MapExtractor(KEY); + } + + @Test + public void testExtract_NotAMap() { + + // object is not a map (i.e., it's a String) + assertNull(ext.extract(KEY)); + } + + @Test + public void testExtract_MissingValue() { + + Map<String,Object> map = new HashMap<>(); + map.put(KEY+"x", VALUE+"x"); + + // object is a map, but doesn't have the key + assertNull(ext.extract(map)); + } + + @Test + public void testExtract() { + + Map<String,Object> map = new HashMap<>(); + map.put(KEY+"x", VALUE+"x"); + map.put(KEY, VALUE); + + // object is a map and contains the key + assertEquals(VALUE, ext.extract(map)); + + // change to value to a different type + map.put(KEY, 20); + assertEquals(20, ext.extract(map)); + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/MethodExtractorTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/MethodExtractorTest.java new file mode 100644 index 00000000..ae5858e7 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/extractor/MethodExtractorTest.java @@ -0,0 +1,99 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.extractor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import java.lang.reflect.Method; +import org.junit.Before; +import org.junit.Test; + +public class MethodExtractorTest { + + private static final String VALUE = "the value"; + private static final Integer INT_VALUE = 10; + + private Method meth; + private MethodExtractor ext; + + @Before + public void setUp() throws Exception { + meth = MyClass.class.getMethod("getValue"); + ext = new MethodExtractor(meth); + } + + @Test + public void testExtract() throws Exception { + assertEquals(VALUE, ext.extract(new MyClass())); + + // repeat + assertEquals(VALUE, ext.extract(new MyClass())); + + // null value + MyClass obj = new MyClass(); + meth = MyClass.class.getMethod("getNullValue"); + ext = new MethodExtractor(meth); + assertEquals(null, ext.extract(obj)); + + // different value type + meth = MyClass.class.getMethod("getIntValue"); + ext = new MethodExtractor(meth); + assertEquals(INT_VALUE, ext.extract(new MyClass())); + } + + @Test + public void testExtract_ArgEx() { + // pass it the wrong class type + assertNull(ext.extract(this)); + } + + @Test + public void testExtract_InvokeEx() throws Exception { + // invoke method that throws an exception + meth = MyClass.class.getMethod("throwException"); + ext = new MethodExtractor(meth); + assertEquals(null, ext.extract(new MyClass())); + } + + private static class MyClass { + + @SuppressWarnings("unused") + public String getValue() { + return VALUE; + } + + @SuppressWarnings("unused") + public int getIntValue() { + return INT_VALUE; + } + + @SuppressWarnings("unused") + public String getNullValue() { + return null; + } + + @SuppressWarnings("unused") + public String throwException() { + throw new IllegalStateException("expected"); + } + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/feature-pooling-dmaap.properties b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/feature-pooling-dmaap.properties new file mode 100644 index 00000000..a4b5bc76 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/feature-pooling-dmaap.properties @@ -0,0 +1,33 @@ + +pooling.controllerA.topic = topic.A +pooling.controllerA.enabled = true +pooling.controllerA.offline.queue.limit = 5 +pooling.controllerA.offline.queue.age.milliseconds = 100 +pooling.controllerA.start.heartbeat.milliseconds = 10 +pooling.controllerA.reactivate.milliseconds = 20 +pooling.controllerA.identification.milliseconds = 30 +pooling.controllerA.active.heartbeat.milliseconds = 40 +pooling.controllerA.inter.heartbeat.milliseconds = 50 + +pooling.controllerB.topic = topic.B +pooling.controllerB.enabled = true +pooling.controllerB.offline.queue.limit = 6 +pooling.controllerB.offline.queue.age.milliseconds = 101 +pooling.controllerB.start.heartbeat.milliseconds = 11 +pooling.controllerB.reactivate.milliseconds = 21 +pooling.controllerB.identification.milliseconds = 31 +pooling.controllerB.active.heartbeat.milliseconds = 41 +pooling.controllerB.inter.heartbeat.milliseconds = 51 + +pooling.controllerDisabled.enabled = false + +# this has an invalid property +pooling.controllerException.topic = topic.B +pooling.controllerException.enabled = true +pooling.controllerException.offline.queue.limit = INVALID NUMBER +pooling.controllerException.offline.queue.age.milliseconds = 101 +pooling.controllerException.start.heartbeat.milliseconds = 11 +pooling.controllerException.reactivate.milliseconds = 21 +pooling.controllerException.identification.milliseconds = 31 +pooling.controllerException.active.heartbeat.milliseconds = 41 +pooling.controllerException.inter.heartbeat.milliseconds = 51 diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/BasicMessageTester.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/BasicMessageTester.java new file mode 100644 index 00000000..69d7e67c --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/BasicMessageTester.java @@ -0,0 +1,245 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.message; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import org.junit.Test; +import org.onap.policy.drools.pooling.PoolingFeatureException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Superclass used to test subclasses of {@link Message}. + * + * @param <T> type of {@link Message} subclass that this tests + */ +public abstract class BasicMessageTester<T extends Message> { + // values set by makeValidMessage() + public static final String VALID_HOST_PREDECESSOR = "hostA"; + public static final String VALID_HOST = "hostB"; + public static final String VALID_CHANNEL = "channelC"; + + /** + * Used to perform JSON serialization and de-serialization. + */ + public final ObjectMapper mapper = new ObjectMapper(); + + /** + * The subclass of the type of Message being tested. + */ + private final Class<T> subclazz; + + /** + * + * @param subclazz subclass of {@link Message} being tested + */ + public BasicMessageTester(Class<T> subclazz) { + this.subclazz = subclazz; + } + + /** + * Creates a default Message and verifies that the source and channel are + * {@code null}. + * + * @return the default Message + */ + @Test + public final void testDefaultConstructor() { + testDefaultConstructorFields(makeDefaultMessage()); + } + + /** + * Tests that the Message has the correct source, and that the channel is + * {@code null}. + * + * @param msg message to be checked + * @param expectedSource what the source is expected to be + */ + @Test + public final void testConstructorWithArgs() { + testValidFields(makeValidMessage()); + } + + /** + * Makes a valid message and then verifies that it can be serialized and + * de-serialized. Verifies that the de-serialized message is of the same + * type, and has the same content, as the original. + * + * @throws Exception if an error occurs + */ + @Test + public final void testJsonEncodeDecode() throws Exception { + T originalMsg = makeValidMessage(); + + Message msg = mapper.readValue(mapper.writeValueAsString(originalMsg), Message.class); + assertEquals(subclazz, msg.getClass()); + + msg.checkValidity(); + + testValidFields(subclazz.cast(msg)); + } + + /** + * Creates a valid Message and verifies that checkValidity() passes. + * + * @throws PoolingFeatureException if an error occurs + */ + @Test + public final void testCheckValidity_Ok() throws PoolingFeatureException { + T msg = makeValidMessage(); + msg.checkValidity(); + + testValidFields(subclazz.cast(msg)); + } + + /** + * Creates a default Message and verifies that checkValidity() fails. Does + * not throw an exception. + */ + @Test + public final void testCheckValidity_DefaultConstructor() { + try { + makeDefaultMessage().checkValidity(); + fail("missing exception"); + + } catch (PoolingFeatureException expected) { + // success + } + } + + /** + * Creates a message via {@link #makeValidMessage()}, updates it via the + * given function, and then invokes the checkValidity() method on it. It is + * expected that the checkValidity() will throw an exception. + * + * @param func function to update the message prior to invoking + * checkValidity() + */ + public void expectCheckValidityFailure(MessageUpdateFunction<T> func) { + try { + T msg = makeValidMessage(); + func.update(msg); + + msg.checkValidity(); + + fail("missing exception"); + + } catch (PoolingFeatureException expected) { + // success + } + } + + /** + * Creates a message via {@link #makeValidMessage()}, updates one of its + * fields via the given function, and then invokes the checkValidity() + * method on it. It is expected that the checkValidity() will throw an + * exception. It checks both the case when the message's field is set to + * {@code null}, and when it is set to empty (i.e., ""). + * + * @param func function to update the message's field prior to invoking + * checkValidity() + */ + public void expectCheckValidityFailure_NullOrEmpty(MessageFieldUpdateFunction<T> func) { + expectCheckValidityFailure(msg -> func.update(msg, null)); + expectCheckValidityFailure(msg -> func.update(msg, "")); + } + + /** + * Makes a message using the default constructor. + * + * @return a new Message + */ + public final T makeDefaultMessage() { + try { + return subclazz.getConstructor().newInstance(); + + } catch (Exception e) { + throw new AssertionError(e); + } + } + + + // the remaining methods will typically be overridden + + /** + * Makes a message that will pass the validity check. Note: this should use + * the non-default constructor, and the source and channel should be set to + * {@link VALID_HOST} and {@link VALID_CHANNEL}, respectively. + * + * @return a valid Message + */ + public abstract T makeValidMessage(); + + /** + * Verifies that fields are set as expected by + * {@link #makeDefaultMessage()}. + * + * @param msg the default Message + */ + public void testDefaultConstructorFields(T msg) { + assertNull(msg.getSource()); + assertNull(msg.getChannel()); + } + + /** + * Verifies that fields are set as expected by {@link #makeValidMessage()}. + * + * @param msg message whose fields are to be validated + */ + public void testValidFields(T msg) { + assertEquals(VALID_HOST, msg.getSource()); + assertEquals(VALID_CHANNEL, msg.getChannel()); + } + + /** + * Function that updates a message. + * + * @param <T> type of Message the function updates + */ + @FunctionalInterface + public static interface MessageUpdateFunction<T extends Message> { + + /** + * Updates a message. + * + * @param msg message to be updated + */ + public void update(T msg); + } + + /** + * Function that updates a single field within a message. + * + * @param <T> type of Message the function updates + */ + @FunctionalInterface + public static interface MessageFieldUpdateFunction<T extends Message> { + + /** + * Updates a field within a message. + * + * @param msg message to be updated + * @param newValue new field value + */ + public void update(T msg, String newValue); + } +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/BucketAssignmentsTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/BucketAssignmentsTest.java new file mode 100644 index 00000000..ef03d4d6 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/BucketAssignmentsTest.java @@ -0,0 +1,351 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.message; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import java.util.SortedSet; +import java.util.TreeSet; +import org.junit.Test; +import org.onap.policy.drools.pooling.PoolingFeatureException; + +public class BucketAssignmentsTest { + + @Test + public void testBucketAssignments() { + new BucketAssignments(); + } + + @Test + public void testBucketAssignmentsStringArray() { + String arr[] = {"abc", "def"}; + BucketAssignments asgn = new BucketAssignments(arr); + + assertNotNull(asgn.getHostArray()); + assertEquals(arr.toString(), asgn.getHostArray().toString()); + } + + @Test + public void testGetHostArray_testSetHostArray() { + + String arr[] = {"abc", "def"}; + BucketAssignments asgn = new BucketAssignments(arr); + + assertNotNull(asgn.getHostArray()); + assertEquals(arr.toString(), asgn.getHostArray().toString()); + + String arr2[] = {"xyz"}; + asgn.setHostArray(arr2); + + assertNotNull(asgn.getHostArray()); + assertEquals(arr2.toString(), asgn.getHostArray().toString()); + } + + @Test + public void testGetLeader() { + // host array is null + BucketAssignments asgn = new BucketAssignments(); + assertNull(asgn.getLeader()); + + // array is non-null, but empty + asgn.setHostArray(new String[0]); + assertNull(asgn.getLeader()); + + // all entries are null + asgn.setHostArray(new String[5]); + assertNull(asgn.getLeader()); + + // some entries are null + asgn.setHostArray(new String[] {null, "abc", null}); + assertEquals("abc", asgn.getLeader()); + + // only one entry + asgn.setHostArray(new String[] {"abc"}); + assertEquals("abc", asgn.getLeader()); + + // first is least + asgn.setHostArray(new String[] {"Ahost", "Bhost", "Chost"}); + assertEquals("Ahost", asgn.getLeader()); + + // middle is least + asgn.setHostArray(new String[] {"Xhost", "Bhost", "Chost"}); + assertEquals("Bhost", asgn.getLeader()); + + // last is least + asgn.setHostArray(new String[] {"Xhost", "Yhost", "Chost"}); + assertEquals("Chost", asgn.getLeader()); + + // multiple entries + asgn.setHostArray(new String[] {"Xhost", "Bhost", "Chost", "Bhost", "Xhost", "Chost"}); + assertEquals("Bhost", asgn.getLeader()); + } + + @Test + public void testHasAssignment() { + // host array is null + BucketAssignments asgn = new BucketAssignments(); + assertFalse(asgn.hasAssignment("abc")); + + // array is non-null, but empty + asgn.setHostArray(new String[0]); + assertFalse(asgn.hasAssignment("abc")); + + // all entries are null + asgn.setHostArray(new String[5]); + assertFalse(asgn.hasAssignment("abc")); + + // some entries are null + asgn.setHostArray(new String[] {null, "abc", null}); + assertTrue(asgn.hasAssignment("abc")); + + // only one entry + asgn.setHostArray(new String[] {"abc"}); + assertTrue(asgn.hasAssignment("abc")); + + // appears as first entry + asgn.setHostArray(new String[] {"abc", "Bhost", "Chost"}); + assertTrue(asgn.hasAssignment("abc")); + + // appears in middle + asgn.setHostArray(new String[] {"Xhost", "abc", "Chost"}); + assertTrue(asgn.hasAssignment("abc")); + + // appears last + asgn.setHostArray(new String[] {"Xhost", "Yhost", "abc"}); + assertTrue(asgn.hasAssignment("abc")); + + // appears repeatedly + asgn.setHostArray(new String[] {"Xhost", "Bhost", "Chost", "Bhost", "Xhost", "Chost"}); + assertTrue(asgn.hasAssignment("Bhost")); + } + + @Test + public void testGetAllHosts() { + // host array is null + BucketAssignments asgn = new BucketAssignments(); + assertEquals("[]", getSortedHosts(asgn).toString()); + + // array is non-null, but empty + asgn.setHostArray(new String[0]); + assertEquals("[]", getSortedHosts(asgn).toString()); + + // all entries are null + asgn.setHostArray(new String[5]); + assertEquals("[]", getSortedHosts(asgn).toString()); + + // some entries are null + asgn.setHostArray(new String[] {null, "abc", null}); + assertEquals("[abc]", getSortedHosts(asgn).toString()); + + // only one entry + asgn.setHostArray(new String[] {"abc"}); + assertEquals("[abc]", getSortedHosts(asgn).toString()); + + // multiple, repeated entries + asgn.setHostArray(new String[] {"def", "abc", "def", "ghi", "def", "def", "xyz"}); + assertEquals("[abc, def, ghi, xyz]", getSortedHosts(asgn).toString()); + } + + /** + * Gets the hosts, sorted, so that the order is predictable. + * + * @param asgn assignment whose hosts are to be retrieved + * @return a new, sorted set of hosts + */ + private SortedSet<String> getSortedHosts(BucketAssignments asgn) { + return new TreeSet<>(asgn.getAllHosts()); + } + + @Test + public void testGetAssignedHost() { + // host array is null + BucketAssignments asgn = new BucketAssignments(); + assertNull(asgn.getAssignedHost(3)); + + // array is non-null, but empty + asgn.setHostArray(new String[0]); + assertNull(asgn.getAssignedHost(3)); + + // all entries are null + asgn.setHostArray(new String[5]); + assertNull(asgn.getAssignedHost(3)); + + // multiple, repeated entries + String[] arr = {"def", "abc", "def", "ghi", "def", "def", "xyz"}; + asgn.setHostArray(arr); + + for (int x = 0; x < arr.length; ++x) { + assertEquals("x=" + x, arr[x], asgn.getAssignedHost(x)); + } + + // negative + assertNull(asgn.getAssignedHost(-1)); + + // beyond end + assertNull(asgn.getAssignedHost(arr.length)); + assertNull(asgn.getAssignedHost(arr.length + 1)); + } + + @Test + public void testSize() { + // host array is null + BucketAssignments asgn = new BucketAssignments(); + assertEquals(0, asgn.size()); + + // array is non-null, but empty + asgn.setHostArray(new String[0]); + assertEquals(0, asgn.size()); + + // all entries are null + asgn.setHostArray(new String[5]); + assertEquals(5, asgn.size()); + + // multiple, repeated entries + String[] arr = {"def", "abc", "def", "ghi", "def", "def", "xyz"}; + asgn.setHostArray(arr); + assertEquals(arr.length, asgn.size()); + } + + @Test + public void testCheckValidity() throws Exception { + // host array is null + BucketAssignments asgn = new BucketAssignments(); + expectException(asgn); + + // array is non-null, but empty + asgn.setHostArray(new String[0]); + expectException(asgn); + + // array is too big + asgn.setHostArray(new String[BucketAssignments.MAX_BUCKETS + 1]); + expectException(asgn); + + // all entries are null + asgn.setHostArray(new String[5]); + expectException(asgn); + + // null at the beginning + asgn.setHostArray(new String[] {null, "Bhost", "Chost"}); + expectException(asgn); + + // null in the middle + asgn.setHostArray(new String[] {"Ahost", null, "Chost"}); + expectException(asgn); + + // null at the end + asgn.setHostArray(new String[] {"Ahost", "Bhost", null}); + expectException(asgn); + + // only one entry + asgn.setHostArray(new String[] {"abc"}); + asgn.checkValidity(); + + // multiple entries + asgn.setHostArray(new String[] {"Ahost", "Bhost", "Chost"}); + asgn.checkValidity(); + } + + @Test + public void testHashCode() { + // with null assignments + BucketAssignments asgn = new BucketAssignments(); + asgn.hashCode(); + + // with empty array + asgn = new BucketAssignments(new String[0]); + asgn.hashCode(); + + // with null items + asgn = new BucketAssignments(new String[] {"abc", null, "def"}); + asgn.hashCode(); + + // same assignments + asgn = new BucketAssignments(new String[] {"abc", null, "def"}); + int code = asgn.hashCode(); + + asgn = new BucketAssignments(new String[] {"abc", null, "def"}); + assertEquals(code, asgn.hashCode()); + + // slightly different values (i.e., changed "def" to "eef") + asgn = new BucketAssignments(new String[] {"abc", null, "eef"}); + assertTrue(code != asgn.hashCode()); + } + + @Test + public void testEquals() { + // null object + BucketAssignments asgn = new BucketAssignments(); + assertFalse(asgn.equals(null)); + + // same object + asgn = new BucketAssignments(); + assertTrue(asgn.equals(asgn)); + + // different class of object + asgn = new BucketAssignments(); + assertFalse(asgn.equals("not an assignment object")); + + // with null assignments + asgn = new BucketAssignments(); + assertTrue(asgn.equals(new BucketAssignments())); + + assertFalse(asgn.equals(new BucketAssignments(new String[] {"abc"}))); + + // with empty array + asgn = new BucketAssignments(new String[0]); + assertTrue(asgn.equals(asgn)); + + assertFalse(asgn.equals(new BucketAssignments())); + assertFalse(asgn.equals(new BucketAssignments(new String[] {"abc"}))); + + // with null items + String[] arr = {"abc", null, "def"}; + asgn = new BucketAssignments(arr); + assertTrue(asgn.equals(asgn)); + assertTrue(asgn.equals(new BucketAssignments(arr))); + assertTrue(asgn.equals(new BucketAssignments(new String[] {"abc", null, "def"}))); + + assertFalse(asgn.equals(new BucketAssignments())); + assertFalse(asgn.equals(new BucketAssignments(new String[] {"abc", null, "XYZ"}))); + + assertFalse(asgn.equals(new BucketAssignments())); + } + + /** + * Expects an exception when checkValidity() is called. + * + * @param asgn assignments to be checked + */ + private void expectException(BucketAssignments asgn) { + try { + asgn.checkValidity(); + fail("missing exception"); + + } catch (PoolingFeatureException expected) { + // success + } + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/ForwardTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/ForwardTest.java new file mode 100644 index 00000000..c56caca8 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/ForwardTest.java @@ -0,0 +1,217 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.message; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import org.onap.policy.drools.event.comm.Topic.CommInfrastructure; + +public class ForwardTest extends BasicMessageTester<Forward> { + // values set by makeValidMessage() + public static final CommInfrastructure VALID_PROTOCOL = CommInfrastructure.UEB; + public static final int VALID_HOPS = 0; + public static final String VALID_TOPIC = "topicA"; + public static final String VALID_PAYLOAD = "payloadA"; + public static final String VALID_REQUEST_ID = "requestIdA"; + + /** + * Time, in milliseconds, after which the most recent message was created. + */ + private static long tcreateMs; + + public ForwardTest() { + super(Forward.class); + } + + @Test + public void testBumpNumHops() { + Forward msg = makeValidMessage(); + + for (int x = 0; x < 3; ++x) { + assertEquals("x=" + x, x, msg.getNumHops()); + msg.bumpNumHops(); + } + } + + @Test + public void testGetNumHops_testSetNumHops() { + Forward msg = makeValidMessage(); + + // from constructor + assertEquals(VALID_HOPS, msg.getNumHops()); + + msg.setNumHops(5); + assertEquals(5, msg.getNumHops()); + + msg.setNumHops(7); + assertEquals(7, msg.getNumHops()); + } + + @Test + public void testGetCreateTimeMs_testSetCreateTimeMs() { + Forward msg = makeValidMessage(); + + // from constructor + assertTrue(msg.getCreateTimeMs() >= tcreateMs); + + msg.setCreateTimeMs(1000L); + assertEquals(1000L, msg.getCreateTimeMs()); + + msg.setCreateTimeMs(2000L); + assertEquals(2000L, msg.getCreateTimeMs()); + } + + @Test + public void testGetProtocol_testSetProtocol() { + Forward msg = makeValidMessage(); + + // from constructor + assertEquals(CommInfrastructure.UEB, msg.getProtocol()); + + msg.setProtocol(CommInfrastructure.DMAAP); + assertEquals(CommInfrastructure.DMAAP, msg.getProtocol()); + + msg.setProtocol(CommInfrastructure.UEB); + assertEquals(CommInfrastructure.UEB, msg.getProtocol()); + } + + @Test + public void testGetTopic_testSetTopic() { + Forward msg = makeValidMessage(); + + // from constructor + assertEquals(VALID_TOPIC, msg.getTopic()); + + msg.setTopic("topicX"); + assertEquals("topicX", msg.getTopic()); + + msg.setTopic("topicY"); + assertEquals("topicY", msg.getTopic()); + } + + @Test + public void testGetPayload_testSetPayload() { + Forward msg = makeValidMessage(); + + // from constructor + assertEquals(VALID_PAYLOAD, msg.getPayload()); + + msg.setPayload("payloadX"); + assertEquals("payloadX", msg.getPayload()); + + msg.setPayload("payloadY"); + assertEquals("payloadY", msg.getPayload()); + } + + @Test + public void testGetRequestId_testSetRequestId() { + Forward msg = makeValidMessage(); + + // from constructor + assertEquals(VALID_REQUEST_ID, msg.getRequestId()); + + msg.setRequestId("reqX"); + assertEquals("reqX", msg.getRequestId()); + + msg.setRequestId("reqY"); + assertEquals("reqY", msg.getRequestId()); + } + + @Test + public void testIsExpired() { + Forward msg = makeValidMessage(); + + long tcreate = msg.getCreateTimeMs(); + assertTrue(msg.isExpired(tcreate + 1)); + assertTrue(msg.isExpired(tcreate + 10)); + + assertFalse(msg.isExpired(tcreate)); + assertFalse(msg.isExpired(tcreate - 1)); + assertFalse(msg.isExpired(tcreate - 10)); + } + + @Test + public void testCheckValidity_InvalidFields() throws Exception { + // null source (i.e., superclass field) + expectCheckValidityFailure(msg -> msg.setSource(null)); + + // null protocol + expectCheckValidityFailure(msg -> msg.setProtocol(null)); + + // null or empty topic + expectCheckValidityFailure_NullOrEmpty((msg, value) -> msg.setTopic(value)); + + // null payload + expectCheckValidityFailure(msg -> msg.setPayload(null)); + + // empty payload should NOT throw an exception + Forward forward = makeValidMessage(); + forward.setPayload(""); + forward.checkValidity(); + + // null or empty requestId + expectCheckValidityFailure_NullOrEmpty((msg, value) -> msg.setRequestId(value)); + + // invalid hop count + expectCheckValidityFailure(msg -> msg.setNumHops(-1)); + } + + /** + * Makes a message that will pass the validity check. + * + * @return a valid Message + */ + public Forward makeValidMessage() { + tcreateMs = System.currentTimeMillis(); + + Forward msg = new Forward(VALID_HOST, VALID_PROTOCOL, VALID_TOPIC, VALID_PAYLOAD, VALID_REQUEST_ID); + msg.setChannel(VALID_CHANNEL); + + return msg; + } + + @Override + public void testDefaultConstructorFields(Forward msg) { + super.testDefaultConstructorFields(msg); + + assertEquals(VALID_HOPS, msg.getNumHops()); + assertEquals(0, msg.getCreateTimeMs()); + assertNull(msg.getPayload()); + assertNull(msg.getProtocol()); + assertNull(msg.getRequestId()); + assertNull(msg.getTopic()); + } + + @Override + public void testValidFields(Forward msg) { + super.testValidFields(msg); + + assertEquals(VALID_HOPS, msg.getNumHops()); + assertTrue(msg.getCreateTimeMs() >= tcreateMs); + assertEquals(VALID_PAYLOAD, msg.getPayload()); + assertEquals(VALID_PROTOCOL, msg.getProtocol()); + assertEquals(VALID_REQUEST_ID, msg.getRequestId()); + assertEquals(VALID_TOPIC, msg.getTopic()); + } +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/HeartbeatTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/HeartbeatTest.java new file mode 100644 index 00000000..da78dbe3 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/HeartbeatTest.java @@ -0,0 +1,62 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.message; + +import static org.junit.Assert.assertEquals; + +public class HeartbeatTest extends BasicMessageTester<Heartbeat> { + + /** + * Sequence number to validate time stamps within the heart beat. + */ + private long sequence = 0; + + public HeartbeatTest() { + super(Heartbeat.class); + } + + /** + * Makes a message that will pass the validity check. + * + * @return a valid Message + */ + public Heartbeat makeValidMessage() { + Heartbeat msg = new Heartbeat(VALID_HOST, ++sequence); + msg.setChannel(VALID_CHANNEL); + + return msg; + } + + @Override + public void testDefaultConstructorFields(Heartbeat msg) { + super.testDefaultConstructorFields(msg); + + assertEquals(sequence, msg.getTimestampMs()); + } + + @Override + public void testValidFields(Heartbeat msg) { + super.testValidFields(msg); + + assertEquals(sequence, msg.getTimestampMs()); + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/IdentificationTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/IdentificationTest.java new file mode 100644 index 00000000..8255034f --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/IdentificationTest.java @@ -0,0 +1,77 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.message; + +import org.junit.Before; +import org.junit.Test; + +public class IdentificationTest extends MessageWithAssignmentsTester<Identification> { + + public IdentificationTest() { + super(Identification.class); + } + + @Before + public void setUp() { + setNullAssignments(false); + } + + /** + * The superclass will already invoke testJsonEncodeDecode() to verify that + * things work with a fully populated message. This verifies that it also + * works if the assignments are null. + * + * @throws Exception if an error occurs + */ + @Test + public final void testJsonEncodeDecode_WithNullAssignments() throws Exception { + setNullAssignments(true); + testJsonEncodeDecode(); + } + + /** + * The superclass will already invoke testCheckValidity() to + * verify that things work with a fully populated message. This verifies + * that it also works if the assignments are null. + * + * @throws Exception if an error occurs + */ + @Test + public void testCheckValidity_NullAssignments() throws Exception { + // null assignments are OK + Identification msg = makeValidMessage(); + msg.setAssignments(null); + msg.checkValidity(); + } + + /** + * Makes a message that will pass the validity check. + * + * @return a valid Message + */ + public Identification makeValidMessage() { + Identification msg = new Identification(VALID_HOST, (isNullAssignments() ? null : VALID_ASGN)); + msg.setChannel(VALID_CHANNEL); + + return msg; + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/LeaderTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/LeaderTest.java new file mode 100644 index 00000000..0f58e224 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/LeaderTest.java @@ -0,0 +1,77 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.message; + +import org.junit.Before; +import org.junit.Test; + +public class LeaderTest extends MessageWithAssignmentsTester<Leader> { + + public LeaderTest() { + super(Leader.class); + } + + @Before + public void setUp() { + setNullAssignments(false); + } + + /** + * The superclass will already invoke testCheckValidity_InvalidFields() to + * verify that things work with a fully populated message. This verifies + * that it also works if the assignments are null. + * + * @throws Exception if an error occurs + */ + @Test + public void testCheckValidity_InvalidFields_NullAssignments() throws Exception { + // null assignments are invalid + expectCheckValidityFailure(msg -> msg.setAssignments(null)); + } + + @Test + public void testCheckValidity_SourceIsNotLeader() throws Exception { + Leader ldr = makeValidMessage(); + + ldr.setSource("xyz"); + + // the source does not have an assignment + BucketAssignments asgnUnassigned = new BucketAssignments(new String[] {"abc", "def"}); + expectCheckValidityFailure(msg -> msg.setAssignments(asgnUnassigned)); + + // the source is not the smallest UUID in this assignment + BucketAssignments asgnNotSmallest = new BucketAssignments(new String[] {VALID_HOST_PREDECESSOR, VALID_HOST}); + expectCheckValidityFailure(msg -> msg.setAssignments(asgnNotSmallest)); + } + + /** + * Makes a message that will pass the validity check. + * + * @return a valid Message + */ + public Leader makeValidMessage() { + Leader msg = new Leader(VALID_HOST, (isNullAssignments() ? null : VALID_ASGN)); + msg.setChannel(VALID_CHANNEL); + + return msg; + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/MessageTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/MessageTest.java new file mode 100644 index 00000000..432dcc3c --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/MessageTest.java @@ -0,0 +1,80 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.message; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import org.junit.Test; + +public class MessageTest extends BasicMessageTester<Message> { + + public MessageTest() { + super(Message.class); + } + + @Test + public void testGetSource_testSetSource() { + Message msg = new Message(); + + msg.setSource("hello"); + assertEquals("hello", msg.getSource()); + assertNull(msg.getChannel()); + + msg.setSource("world"); + assertEquals("world", msg.getSource()); + assertNull(msg.getChannel()); + } + + @Test + public void testGetChannel_testSetChannel() { + Message msg = new Message(); + + msg.setChannel("hello"); + assertEquals("hello", msg.getChannel()); + assertNull(msg.getSource()); + + msg.setChannel("world"); + assertEquals("world", msg.getChannel()); + assertNull(msg.getSource()); + } + + @Test + public void testCheckValidity_InvalidFields() { + // null or empty source + expectCheckValidityFailure_NullOrEmpty((msg, value) -> msg.setSource(value)); + + // null or empty channel + expectCheckValidityFailure_NullOrEmpty((msg, value) -> msg.setChannel(value)); + } + + /** + * Makes a message that will pass the validity check. + * + * @return a valid Message + */ + public Message makeValidMessage() { + Message msg = new Message(VALID_HOST); + msg.setChannel(VALID_CHANNEL); + + return msg; + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/MessageWithAssignmentsTester.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/MessageWithAssignmentsTester.java new file mode 100644 index 00000000..2b670dcc --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/MessageWithAssignmentsTester.java @@ -0,0 +1,110 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.message; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import org.junit.Test; + +/** + * Superclass used to test subclasses of {@link MessageWithAssignments}. + * + * @param <T> type of {@link MessageWithAssignments} subclass that this tests + */ +public abstract class MessageWithAssignmentsTester<T extends MessageWithAssignments> extends BasicMessageTester<T> { + // values set by makeValidMessage() + public static final String[] VALID_ARRAY = {VALID_HOST, VALID_HOST+"_xxx"}; + public static final BucketAssignments VALID_ASGN = new BucketAssignments(VALID_ARRAY); + + /** + * {@code True} if {@code null} assignments are allowed, {@code false} + * otherwise. + */ + private boolean nullAssignments; + + /** + * + * @param subclazz subclass of {@link MessageWithAssignments} being tested + */ + public MessageWithAssignmentsTester(Class<T> subclazz) { + super(subclazz); + } + + /** + * Indicates whether or not {@code null} assignments should be used for the + * remaining tests. + * + * @param nullAssignments {@code true} to use {@code null} assignments, + * {@code false} otherwise + */ + public void setNullAssignments(boolean nullAssignments) { + this.nullAssignments = nullAssignments; + } + + public boolean isNullAssignments() { + return nullAssignments; + } + + @Test + public void testCheckValidity_InvalidFields() throws Exception { + // null source (i.e., superclass field) + expectCheckValidityFailure(msg -> msg.setSource(null)); + + // empty assignments + expectCheckValidityFailure(msg -> msg.setAssignments(new BucketAssignments(new String[0]))); + + // invalid assignment + String[] invalidAssignment = {"abc", null}; + expectCheckValidityFailure(msg -> msg.setAssignments(new BucketAssignments(invalidAssignment))); + } + + @Test + public void testGetAssignments_testSetAssignments() { + MessageWithAssignments msg = makeValidMessage(); + + // from constructor + assertEquals(VALID_ASGN, msg.getAssignments()); + + BucketAssignments asgn = new BucketAssignments(); + msg.setAssignments(asgn); + assertEquals(asgn, msg.getAssignments()); + } + + @Override + public void testDefaultConstructorFields(T msg) { + super.testDefaultConstructorFields(msg); + + assertNull(msg.getAssignments()); + } + + @Override + public void testValidFields(T msg) { + super.testValidFields(msg); + + if (nullAssignments) { + assertNull(msg.getAssignments()); + + } else { + assertEquals(VALID_ASGN, msg.getAssignments()); + } + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/OfflineTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/OfflineTest.java new file mode 100644 index 00000000..8d0f4a6f --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/OfflineTest.java @@ -0,0 +1,41 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.message; + +public class OfflineTest extends BasicMessageTester<Offline> { + + public OfflineTest() { + super(Offline.class); + } + + /** + * Makes a message that will pass the validity check. + * + * @return a valid Message + */ + public Offline makeValidMessage() { + Offline msg = new Offline(VALID_HOST); + msg.setChannel(VALID_CHANNEL); + + return msg; + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/QueryTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/QueryTest.java new file mode 100644 index 00000000..0b2a986d --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/QueryTest.java @@ -0,0 +1,41 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.message; + +public class QueryTest extends BasicMessageTester<Query> { + + public QueryTest() { + super(Query.class); + } + + /** + * Makes a message that will pass the validity check. + * + * @return a valid Message + */ + public Query makeValidMessage() { + Query msg = new Query(VALID_HOST); + msg.setChannel(VALID_CHANNEL); + + return msg; + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/Trial.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/Trial.java new file mode 100644 index 00000000..428b5853 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/message/Trial.java @@ -0,0 +1,41 @@ +/* + * ============LICENSE_START======================================================= + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.message; + +import org.junit.Test; +import org.onap.policy.drools.event.comm.Topic.CommInfrastructure; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class Trial { + + @Test + public void test() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + + Message msg = new Forward("me", CommInfrastructure.DMAAP, "my topic", "a message", "my req"); + + String enc = mapper.writeValueAsString(msg); + System.out.println("enc=" + enc); + + Message msg2 = mapper.readValue(enc, Message.class); + System.out.println("class=" + msg2.getClass()); + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/ActiveStateTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/ActiveStateTest.java new file mode 100644 index 00000000..7997a4ee --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/ActiveStateTest.java @@ -0,0 +1,441 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.state; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.util.Arrays; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.onap.policy.drools.pooling.message.BucketAssignments; +import org.onap.policy.drools.pooling.message.Heartbeat; +import org.onap.policy.drools.pooling.message.Identification; +import org.onap.policy.drools.pooling.message.Leader; +import org.onap.policy.drools.pooling.message.Message; +import org.onap.policy.drools.pooling.message.Offline; +import org.onap.policy.drools.pooling.message.Query; + +public class ActiveStateTest extends BasicStateTester { + + private ActiveState state; + + @Before + public void setUp() throws Exception { + super.setUp(); + + state = new ActiveState(mgr); + } + + @Test + public void testStart() { + state.start(); + + // ensure the timers were created + verify(mgr, atLeast(1)).scheduleWithFixedDelay(anyLong(), anyLong(), any(StateTimerTask.class)); + + // ensure a heart beat was generated + Pair<String, Heartbeat> msg = capturePublishedMessage(Heartbeat.class); + assertEquals(MY_HOST, msg.second.getSource()); + } + + @Test + public void testGetFilter() { + Map<String, Object> filter = state.getFilter(); + + FilterUtilsTest utils = new FilterUtilsTest(); + + utils.checkArray(FilterUtils.CLASS_OR, 2, filter); + utils.checkEquals(FilterUtils.MSG_CHANNEL, Message.ADMIN, utils.getItem(filter, 0)); + utils.checkEquals(FilterUtils.MSG_CHANNEL, MY_HOST, utils.getItem(filter, 1)); + } + + @Test + public void testProcessHeartbeat_NullHost() { + assertNull(state.process(new Heartbeat())); + + assertFalse(state.isMyHeartbeatSeen()); + assertFalse(state.isPredHeartbeatSeen()); + + verify(mgr, never()).goInactive(); + verify(mgr, never()).goQuery(); + } + + @Test + public void testProcessHeartbeat_MyHost() { + assertNull(state.process(new Heartbeat(MY_HOST, 0L))); + + assertTrue(state.isMyHeartbeatSeen()); + assertFalse(state.isPredHeartbeatSeen()); + + verify(mgr, never()).goInactive(); + verify(mgr, never()).goQuery(); + } + + @Test + public void testProcessHeartbeat_Predecessor() { + assertNull(state.process(new Heartbeat(HOST2, 0L))); + + assertFalse(state.isMyHeartbeatSeen()); + assertTrue(state.isPredHeartbeatSeen()); + + verify(mgr, never()).goInactive(); + verify(mgr, never()).goQuery(); + } + + @Test + public void testProcessHeartbeat_OtherHost() { + assertNull(state.process(new Heartbeat(HOST3, 0L))); + + assertFalse(state.isMyHeartbeatSeen()); + assertFalse(state.isPredHeartbeatSeen()); + + verify(mgr, never()).goInactive(); + verify(mgr, never()).goQuery(); + } + + @Test + public void testProcessOffline_NullHost() { + // should be ignored + assertNull(state.process(new Offline())); + } + + @Test + public void testProcessOffline_UnassignedHost() { + // HOST4 is not in the assignment list - should be ignored + assertNull(state.process(new Offline(HOST4))); + } + + @Test + public void testProcessOffline_IAmLeader() { + // configure the next state + State next = mock(State.class); + when(mgr.goActive()).thenReturn(next); + + // one of the assigned hosts went offline + assertEquals(next, state.process(new Offline(HOST1))); + + // should have sent a new Leader message + Leader msg = captureAdminMessage(Leader.class); + + assertEquals(MY_HOST, msg.getSource()); + + // check new bucket assignments + assertEquals(Arrays.asList(MY_HOST, MY_HOST, HOST2), Arrays.asList(msg.getAssignments().getHostArray())); + } + + @Test + public void testProcessOffline_PredecessorIsLeaderNowOffline() { + // configure the next state + State next = mock(State.class); + when(mgr.goActive()).thenReturn(next); + + // I am not the leader, but my predecessor was + mgr.startDistributing(new BucketAssignments(new String[] {PREV_HOST, MY_HOST, HOST1})); + state = new ActiveState(mgr); + + // my predecessor went offline + assertEquals(next, state.process(new Offline(PREV_HOST))); + + // should have sent a new Leader message + Leader msg = captureAdminMessage(Leader.class); + + assertEquals(MY_HOST, msg.getSource()); + + // check new bucket assignments + assertEquals(Arrays.asList(MY_HOST, MY_HOST, HOST1), Arrays.asList(msg.getAssignments().getHostArray())); + } + + @Test + public void testProcessOffline__PredecessorIsNotLeaderNowOffline() { + // I am not the leader, and neither is my predecessor + mgr.startDistributing(new BucketAssignments(new String[] {PREV_HOST, MY_HOST, PREV_HOST2})); + state = new ActiveState(mgr); + + /* + * + * PREV_HOST2 has buckets and is my predecessor, but it isn't the leader + * thus should be ignored. + */ + assertNull(state.process(new Offline(PREV_HOST2))); + } + + @Test + public void testProcessOffline_OtherAssignedHostOffline() { + // I am not the leader + mgr.startDistributing(new BucketAssignments(new String[] {PREV_HOST, MY_HOST, HOST1})); + state = new ActiveState(mgr); + + /* + * HOST1 has buckets, but it isn't the leader and it isn't my + * predecessor, thus should be ignored. + */ + assertNull(state.process(new Offline(HOST1))); + } + + @Test + public void testProcessQuery() { + State next = mock(State.class); + when(mgr.goQuery()).thenReturn(next); + + assertEquals(next, state.process(new Query())); + + Identification ident = captureAdminMessage(Identification.class); + assertEquals(MY_HOST, ident.getSource()); + assertEquals(ASGN3, ident.getAssignments()); + } + + @Test + public void testActiveState() { + assertEquals(MY_HOST, state.getLeader()); + assertEquals(ASGN3, state.getAssignments()); + + // verify that it determined its neighbors + assertEquals(HOST1, state.getSuccHost()); + assertEquals(HOST2, state.getPredHost()); + } + + @Test + public void testDetmNeighbors() { + // if only one host (i.e., itself) + mgr.startDistributing(new BucketAssignments(new String[] {MY_HOST, MY_HOST})); + state = new ActiveState(mgr); + assertEquals(null, state.getSuccHost()); + assertEquals("", state.getPredHost()); + + // two hosts + mgr.startDistributing(new BucketAssignments(new String[] {MY_HOST, HOST2})); + state = new ActiveState(mgr); + assertEquals(HOST2, state.getSuccHost()); + assertEquals(HOST2, state.getPredHost()); + + // three hosts + mgr.startDistributing(new BucketAssignments(new String[] {HOST3, MY_HOST, HOST2})); + state = new ActiveState(mgr); + assertEquals(HOST2, state.getSuccHost()); + assertEquals(HOST3, state.getPredHost()); + + // more hosts + mgr.startDistributing(new BucketAssignments(new String[] {HOST3, MY_HOST, HOST2, HOST4})); + state = new ActiveState(mgr); + assertEquals(HOST2, state.getSuccHost()); + assertEquals(HOST4, state.getPredHost()); + } + + @Test + public void testAddTimers_WithPredecessor() { + // invoke start() to add the timers + state.start(); + + assertEquals(3, repeatedFutures.size()); + + Triple<Long, Long, StateTimerTask> timer; + + // heart beat generator + timer = repeatedTasks.remove(); + assertEquals(STD_ACTIVE_HEARTBEAT_MS, timer.first.longValue()); + assertEquals(STD_ACTIVE_HEARTBEAT_MS, timer.second.longValue()); + + // my heart beat checker + timer = repeatedTasks.remove(); + assertEquals(STD_INTER_HEARTBEAT_MS, timer.first.longValue()); + assertEquals(STD_INTER_HEARTBEAT_MS, timer.second.longValue()); + + // predecessor's heart beat checker + timer = repeatedTasks.remove(); + assertEquals(STD_INTER_HEARTBEAT_MS, timer.first.longValue()); + assertEquals(STD_INTER_HEARTBEAT_MS, timer.second.longValue()); + } + + @Test + public void testAddTimers_SansPredecessor() { + // only one host, thus no predecessor + mgr.startDistributing(new BucketAssignments(new String[] {MY_HOST, MY_HOST})); + state = new ActiveState(mgr); + + // invoke start() to add the timers + state.start(); + + assertEquals(2, repeatedFutures.size()); + + Triple<Long, Long, StateTimerTask> timer; + + // heart beat generator + timer = repeatedTasks.remove(); + assertEquals(STD_ACTIVE_HEARTBEAT_MS, timer.first.longValue()); + assertEquals(STD_ACTIVE_HEARTBEAT_MS, timer.second.longValue()); + + // my heart beat checker + timer = repeatedTasks.remove(); + assertEquals(STD_INTER_HEARTBEAT_MS, timer.first.longValue()); + assertEquals(STD_INTER_HEARTBEAT_MS, timer.second.longValue()); + } + + @Test + public void testAddTimers_HeartbeatGenerator() { + // only one host so we only have to look at one heart beat at a time + mgr.startDistributing(new BucketAssignments(new String[] {MY_HOST})); + state = new ActiveState(mgr); + + // invoke start() to add the timers + state.start(); + + Triple<Long, Long, StateTimerTask> task = repeatedTasks.remove(); + + verify(mgr).publish(anyString(), any(Heartbeat.class)); + + // fire the task + assertNull(task.third.fire(null)); + + // should have generated a second pair of heart beats + verify(mgr, times(2)).publish(anyString(), any(Heartbeat.class)); + + Pair<String, Heartbeat> msg = capturePublishedMessage(Heartbeat.class); + assertEquals(MY_HOST, msg.first); + assertEquals(MY_HOST, msg.second.getSource()); + } + + @Test + public void testAddTimers_MyHeartbeatSeen() { + // invoke start() to add the timers + state.start(); + + Triple<Long, Long, StateTimerTask> task = repeatedTasks.get(1); + + // indicate that this host is still alive + state.process(new Heartbeat(MY_HOST, 0L)); + + // set up next state + State next = mock(State.class); + when(mgr.goInactive()).thenReturn(next); + + // fire the task - should not transition + assertNull(task.third.fire(null)); + + verify(mgr, never()).publishAdmin(any(Query.class)); + } + + @Test + public void testAddTimers_MyHeartbeatMissed() { + // invoke start() to add the timers + state.start(); + + Triple<Long, Long, StateTimerTask> task = repeatedTasks.get(1); + + // set up next state + State next = mock(State.class); + when(mgr.goInactive()).thenReturn(next); + + // fire the task - should transition + assertEquals(next, task.third.fire(null)); + + // should indicate failure + verify(mgr).internalTopicFailed(); + + // should publish an offline message + Offline msg = captureAdminMessage(Offline.class); + assertEquals(MY_HOST, msg.getSource()); + } + + @Test + public void testAddTimers_PredecessorHeartbeatSeen() { + // invoke start() to add the timers + state.start(); + + Triple<Long, Long, StateTimerTask> task = repeatedTasks.get(2); + + // indicate that the predecessor is still alive + state.process(new Heartbeat(HOST2, 0L)); + + // set up next state, just in case + State next = mock(State.class); + when(mgr.goQuery()).thenReturn(next); + + // fire the task - should NOT transition + assertNull(task.third.fire(null)); + + verify(mgr, never()).publishAdmin(any(Query.class)); + } + + @Test + public void testAddTimers_PredecessorHeartbeatMissed() { + // invoke start() to add the timers + state.start(); + + Triple<Long, Long, StateTimerTask> task = repeatedTasks.get(2); + + // set up next state + State next = mock(State.class); + when(mgr.goQuery()).thenReturn(next); + + // fire the task - should transition + assertEquals(next, task.third.fire(null)); + + verify(mgr).publishAdmin(any(Query.class)); + } + + @Test + public void testGenHeartbeat_OneHost() { + // only one host (i.e., itself) + mgr.startDistributing(new BucketAssignments(new String[] {MY_HOST})); + state = new ActiveState(mgr); + + state.start(); + + verify(mgr, times(1)).publish(any(), any()); + + Pair<String, Heartbeat> msg = capturePublishedMessage(Heartbeat.class); + assertEquals(MY_HOST, msg.first); + assertEquals(MY_HOST, msg.second.getSource()); + } + + @Test + public void testGenHeartbeat_MultipleHosts() { + state.start(); + + verify(mgr, times(2)).publish(any(), any()); + + Pair<String, Heartbeat> msg; + int index = 0; + + // this message should go to itself + msg = capturePublishedMessage(Heartbeat.class, index++); + assertEquals(MY_HOST, msg.first); + assertEquals(MY_HOST, msg.second.getSource()); + + // this message should go to its successor + msg = capturePublishedMessage(Heartbeat.class, index++); + assertEquals(HOST1, msg.first); + assertEquals(MY_HOST, msg.second.getSource()); + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/BasicStateTester.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/BasicStateTester.java new file mode 100644 index 00000000..e48742f7 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/BasicStateTester.java @@ -0,0 +1,318 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.state; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.atomic.AtomicReference; +import org.onap.policy.drools.pooling.PoolingManager; +import org.onap.policy.drools.pooling.PoolingProperties; +import org.onap.policy.drools.pooling.message.BucketAssignments; +import org.onap.policy.drools.pooling.message.Leader; +import org.onap.policy.drools.pooling.message.Message; + +/** + * Superclass used to test subclasses of {@link Message}. + */ +public class BasicStateTester { + + protected static final long STD_HEARTBEAT_WAIT_MS = 10; + protected static final long STD_REACTIVATE_WAIT_MS = STD_HEARTBEAT_WAIT_MS + 1; + protected static final long STD_IDENTIFICATION_MS = STD_REACTIVATE_WAIT_MS + 1; + protected static final long STD_ACTIVE_HEARTBEAT_MS = STD_IDENTIFICATION_MS + 1; + protected static final long STD_INTER_HEARTBEAT_MS = STD_ACTIVE_HEARTBEAT_MS + 1; + + protected static final String MY_TOPIC = "myTopic"; + + protected static final String PREV_HOST = "prevHost"; + protected static final String PREV_HOST2 = PREV_HOST + "A"; + + // this follows PREV_HOST, alphabetically + protected static final String MY_HOST = PREV_HOST + "X"; + + // these follow MY_HOST, alphabetically + protected static final String HOST1 = MY_HOST + "1"; + protected static final String HOST2 = MY_HOST + "2"; + protected static final String HOST3 = MY_HOST + "3"; + protected static final String HOST4 = MY_HOST + "4"; + + protected static final String LEADER = HOST1; + + protected static final String[] HOST_ARR3 = {HOST1, MY_HOST, HOST2}; + + protected static final BucketAssignments EMPTY_ASGN = new BucketAssignments(); + protected static final BucketAssignments ASGN3 = new BucketAssignments(HOST_ARR3); + + /** + * Futures returned by schedule(). + */ + protected LinkedList<ScheduledFuture<?>> onceFutures; + + /** + * Tasks captured via schedule(). + */ + protected LinkedList<Pair<Long, StateTimerTask>> onceTasks; + + /** + * Futures returned by scheduleWithFixedDelay(). + */ + protected LinkedList<ScheduledFuture<?>> repeatedFutures; + + /** + * Tasks captured via scheduleWithFixedDelay(). + */ + protected LinkedList<Triple<Long, Long, StateTimerTask>> repeatedTasks; + + /** + * Messages captured via publish(). + */ + protected LinkedList<Pair<String, Message>> published; + + /** + * Messages captured via publishAdmin(). + */ + protected LinkedList<Message> admin; + + protected PoolingManager mgr; + protected PoolingProperties props; + protected State prevState; + + public BasicStateTester() { + super(); + } + + public void setUp() throws Exception { + onceFutures = new LinkedList<>(); + onceTasks = new LinkedList<>(); + + repeatedFutures = new LinkedList<>(); + repeatedTasks = new LinkedList<>(); + + published = new LinkedList<>(); + admin = new LinkedList<>(); + + mgr = mock(PoolingManager.class); + props = mock(PoolingProperties.class); + + when(mgr.getHost()).thenReturn(MY_HOST); + when(mgr.getTopic()).thenReturn(MY_TOPIC); + when(mgr.getProperties()).thenReturn(props); + + when(props.getStartHeartbeatMs()).thenReturn(STD_HEARTBEAT_WAIT_MS); + when(props.getReactivateMs()).thenReturn(STD_REACTIVATE_WAIT_MS); + when(props.getIdentificationMs()).thenReturn(STD_IDENTIFICATION_MS); + when(props.getActiveHeartbeatMs()).thenReturn(STD_ACTIVE_HEARTBEAT_MS); + when(props.getInterHeartbeatMs()).thenReturn(STD_INTER_HEARTBEAT_MS); + + prevState = new State(mgr) { + @Override + public Map<String, Object> getFilter() { + throw new UnsupportedOperationException("cannot filter"); + } + }; + + // capture publish() arguments + doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + published.add(new Pair<>((String) args[0], (Message) args[1])); + + return null; + }).when(mgr).publish(anyString(), any(Message.class)); + + // capture publishAdmin() arguments + doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + admin.add((Message) args[0]); + + return null; + }).when(mgr).publishAdmin(any(Message.class)); + + // capture schedule() arguments, and return a new future + when(mgr.schedule(anyLong(), any(StateTimerTask.class))).thenAnswer(invocation -> { + Object[] args = invocation.getArguments(); + onceTasks.add(new Pair<>((Long) args[0], (StateTimerTask) args[1])); + + ScheduledFuture<?> fut = mock(ScheduledFuture.class); + onceFutures.add(fut); + return fut; + }); + + // capture scheduleWithFixedDelay() arguments, and return a new future + when(mgr.scheduleWithFixedDelay(anyLong(), anyLong(), any(StateTimerTask.class))).thenAnswer(invocation -> { + Object[] args = invocation.getArguments(); + repeatedTasks.add(new Triple<>((Long) args[0], (Long) args[1], (StateTimerTask) args[2])); + + ScheduledFuture<?> fut = mock(ScheduledFuture.class); + repeatedFutures.add(fut); + return fut; + }); + + // get/set assignments in the manager + AtomicReference<BucketAssignments> asgn = new AtomicReference<>(ASGN3); + + when(mgr.getAssignments()).thenAnswer(args -> asgn.get()); + + doAnswer(args -> { + asgn.set(args.getArgumentAt(0, BucketAssignments.class)); + return null; + }).when(mgr).startDistributing(any()); + } + + /** + * Makes a sorted set of hosts. + * + * @param hosts the hosts to be sorted + * @return the set of hosts, sorted + */ + protected SortedSet<String> sortHosts(String... hosts) { + return new TreeSet<>(Arrays.asList(hosts)); + } + + /** + * Captures the host array from the Leader message published to the admin + * channel. + * + * @return the host array, as a list + */ + protected List<String> captureHostList() { + return Arrays.asList(captureHostArray()); + } + + /** + * Captures the host array from the Leader message published to the admin + * channel. + * + * @return the host array + */ + protected String[] captureHostArray() { + BucketAssignments asgn = captureAssignments(); + + String[] arr = asgn.getHostArray(); + assertNotNull(arr); + + return arr; + } + + /** + * Captures the assignments from the Leader message published to the admin + * channel. + * + * @return the bucket assignments + */ + protected BucketAssignments captureAssignments() { + Leader msg = captureAdminMessage(Leader.class); + + BucketAssignments asgn = msg.getAssignments(); + assertNotNull(asgn); + return asgn; + } + + /** + * Captures the message published to the admin channel. + * + * @param clazz type of {@link Message} to capture + * @return the message that was published + */ + protected <T extends Message> T captureAdminMessage(Class<T> clazz) { + return captureAdminMessage(clazz, 0); + } + + /** + * Captures the message published to the admin channel. + * + * @param clazz type of {@link Message} to capture + * @param index index of the item to be captured + * @return the message that was published + */ + protected <T extends Message> T captureAdminMessage(Class<T> clazz, int index) { + return clazz.cast(admin.get(index)); + } + + /** + * Captures the message published to the non-admin channels. + * + * @param clazz type of {@link Message} to capture + * @return the (channel,message) pair that was published + */ + protected <T extends Message> Pair<String, T> capturePublishedMessage(Class<T> clazz) { + return capturePublishedMessage(clazz, 0); + } + + /** + * Captures the message published to the non-admin channels. + * + * @param clazz type of {@link Message} to capture + * @param index index of the item to be captured + * @return the (channel,message) pair that was published + */ + protected <T extends Message> Pair<String, T> capturePublishedMessage(Class<T> clazz, int index) { + Pair<String, Message> msg = published.get(index); + return new Pair<>(msg.first, clazz.cast(msg.second)); + } + + /** + * Pair of values. + * + * @param <F> first value's type + * @param <S> second value's type + */ + public static class Pair<F, S> { + public final F first; + public final S second; + + public Pair(F first, S second) { + this.first = first; + this.second = second; + } + } + + /** + * Pair of values. + * + * @param <F> first value's type + * @param <S> second value's type + * @param <T> third value's type + */ + public static class Triple<F, S, T> { + public final F first; + public final S second; + public final T third; + + public Triple(F first, S second, T third) { + this.first = first; + this.second = second; + this.third = third; + } + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/FilterUtilsTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/FilterUtilsTest.java new file mode 100644 index 00000000..ba517194 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/FilterUtilsTest.java @@ -0,0 +1,109 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.state; + +import static org.junit.Assert.assertEquals; +import static org.onap.policy.drools.pooling.state.FilterUtils.CLASS_AND; +import static org.onap.policy.drools.pooling.state.FilterUtils.CLASS_EQUALS; +import static org.onap.policy.drools.pooling.state.FilterUtils.CLASS_OR; +import static org.onap.policy.drools.pooling.state.FilterUtils.JSON_CLASS; +import static org.onap.policy.drools.pooling.state.FilterUtils.JSON_FIELD; +import static org.onap.policy.drools.pooling.state.FilterUtils.JSON_FILTERS; +import static org.onap.policy.drools.pooling.state.FilterUtils.JSON_VALUE; +import static org.onap.policy.drools.pooling.state.FilterUtils.makeAnd; +import static org.onap.policy.drools.pooling.state.FilterUtils.makeEquals; +import static org.onap.policy.drools.pooling.state.FilterUtils.makeOr; +import java.util.Map; +import org.junit.Test; + +public class FilterUtilsTest { + + @Test + public void testMakeEquals() { + checkEquals("abc", "def", makeEquals("abc", "def")); + } + + @Test + public void testMakeAnd() { + @SuppressWarnings("unchecked") + Map<String, Object> filter = + makeAnd(makeEquals("an1", "av1"), makeEquals("an2", "av2"), makeEquals("an3", "av3")); + + checkArray(CLASS_AND, 3, filter); + checkEquals("an1", "av1", getItem(filter, 0)); + checkEquals("an2", "av2", getItem(filter, 1)); + checkEquals("an3", "av3", getItem(filter, 2)); + } + + @Test + public void testMakeOr() { + @SuppressWarnings("unchecked") + Map<String, Object> filter = + makeOr(makeEquals("on1", "ov1"), makeEquals("on2", "ov2"), makeEquals("on3", "ov3")); + + checkArray(CLASS_OR, 3, filter); + checkEquals("on1", "ov1", getItem(filter, 0)); + checkEquals("on2", "ov2", getItem(filter, 1)); + checkEquals("on3", "ov3", getItem(filter, 2)); + } + + /** + * Checks that the filter contains an array. + * + * @param expectedClassName type of filter this should represent + * @param expectedCount number of items expected in the array + * @param filter filter to be examined + */ + protected void checkArray(String expectedClassName, int expectedCount, Map<String, Object> filter) { + assertEquals(expectedClassName, filter.get(JSON_CLASS)); + + Object[] val = (Object[]) filter.get(JSON_FILTERS); + assertEquals(expectedCount, val.length); + } + + /** + * Checks that a map represents an "equals". + * + * @param name name of the field on the left side of the equals + * @param value value on the right side of the equals + * @param map map whose content is to be examined + */ + protected void checkEquals(String name, String value, Map<String, Object> map) { + assertEquals(CLASS_EQUALS, map.get(JSON_CLASS)); + assertEquals(name, map.get(JSON_FIELD)); + assertEquals(value, map.get(JSON_VALUE)); + } + + /** + * Gets a particular sub-filter from the array contained within a filter. + * + * @param filter containing filter + * @param index index of the sub-filter of interest + * @return the sub-filter with the given index + */ + @SuppressWarnings("unchecked") + protected Map<String, Object> getItem(Map<String, Object> filter, int index) { + Object[] val = (Object[]) filter.get(JSON_FILTERS); + + return (Map<String, Object>) val[index]; + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/IdleStateTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/IdleStateTest.java new file mode 100644 index 00000000..96c59719 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/IdleStateTest.java @@ -0,0 +1,121 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.state; + +import static org.junit.Assert.assertNull; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.onap.policy.drools.pooling.message.BucketAssignments; +import org.onap.policy.drools.pooling.message.Forward; +import org.onap.policy.drools.pooling.message.Heartbeat; +import org.onap.policy.drools.pooling.message.Identification; +import org.onap.policy.drools.pooling.message.Leader; +import org.onap.policy.drools.pooling.message.Message; +import org.onap.policy.drools.pooling.message.Offline; +import org.onap.policy.drools.pooling.message.Query; + +public class IdleStateTest extends BasicStateTester { + + private IdleState state; + + @Before + public void setUp() throws Exception { + super.setUp(); + + state = new IdleState(mgr); + } + + @Test + public void testGetFilter() { + Map<String, Object> filter = state.getFilter(); + + FilterUtilsTest utils = new FilterUtilsTest(); + + utils.checkArray(FilterUtils.CLASS_OR, 2, filter); + utils.checkEquals(FilterUtils.MSG_CHANNEL, Message.ADMIN, utils.getItem(filter, 0)); + utils.checkEquals(FilterUtils.MSG_CHANNEL, MY_HOST, utils.getItem(filter, 1)); + } + + @Test + public void testStop() { + state.stop(); + verifyNothingPublished(); + } + + @Test + public void testProcessForward() { + Forward msg = new Forward(); + assertNull(state.process(msg)); + + verify(mgr).handle(msg); + } + + @Test + public void testProcessHeartbeat() { + assertNull(state.process(new Heartbeat(PREV_HOST, 0L))); + verifyNothingPublished(); + } + + @Test + public void testProcessIdentification() { + assertNull(state.process(new Identification(PREV_HOST, null))); + verifyNothingPublished(); + } + + @Test + public void testProcessLeader() { + BucketAssignments asgn = new BucketAssignments(new String[] {HOST2, PREV_HOST, MY_HOST}); + Leader msg = new Leader(PREV_HOST, asgn); + + State next = mock(State.class); + when(mgr.goActive()).thenReturn(next); + + // should stay in current state, but start distributing + assertNull(state.process(msg)); + verify(mgr).startDistributing(asgn); + } + + @Test + public void testProcessOffline() { + assertNull(state.process(new Offline(PREV_HOST))); + verifyNothingPublished(); + } + + @Test + public void testProcessQuery() { + assertNull(state.process(new Query())); + verifyNothingPublished(); + } + + /** + * Verifies that nothing was published on either channel. + */ + private void verifyNothingPublished() { + verify(mgr, never()).publish(any(), any()); + verify(mgr, never()).publishAdmin(any()); + } +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/InactiveStateTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/InactiveStateTest.java new file mode 100644 index 00000000..48d5b1ed --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/InactiveStateTest.java @@ -0,0 +1,83 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.state; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.onap.policy.drools.pooling.message.Message; + +public class InactiveStateTest extends BasicStateTester { + + private InactiveState state; + + @Before + public void setUp() throws Exception { + super.setUp(); + + state = new InactiveState(mgr); + } + + @Test + public void testGetFilter() { + Map<String, Object> filter = state.getFilter(); + + FilterUtilsTest utils = new FilterUtilsTest(); + + utils.checkArray(FilterUtils.CLASS_OR, 2, filter); + utils.checkEquals(FilterUtils.MSG_CHANNEL, Message.ADMIN, utils.getItem(filter, 0)); + utils.checkEquals(FilterUtils.MSG_CHANNEL, MY_HOST, utils.getItem(filter, 1)); + } + + @Test + public void testGoInatcive() { + assertNull(state.goInactive()); + } + + @Test + public void testStart() { + state.start(); + + Pair<Long, StateTimerTask> timer = onceTasks.remove(); + + assertEquals(STD_REACTIVATE_WAIT_MS, timer.first.longValue()); + + // invoke the task - it should go to the state returned by the mgr + State next = mock(State.class); + when(mgr.goStart()).thenReturn(next); + + assertEquals(next, timer.second.fire(null)); + } + + @Test + public void testInactiveState() { + /* + * Prove the state is attached to the manager by invoking getHost(), + * which delegates to the manager. + */ + assertEquals(MY_HOST, state.getHost()); + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/ProcessingStateTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/ProcessingStateTest.java new file mode 100644 index 00000000..d60ad2ea --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/ProcessingStateTest.java @@ -0,0 +1,328 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.state; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.util.Arrays; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.onap.policy.drools.pooling.message.BucketAssignments; +import org.onap.policy.drools.pooling.message.Identification; +import org.onap.policy.drools.pooling.message.Leader; +import org.onap.policy.drools.pooling.message.Message; +import org.onap.policy.drools.pooling.message.Query; + +public class ProcessingStateTest extends BasicStateTester { + + private ProcessingState state; + + @Before + public void setUp() throws Exception { + super.setUp(); + + state = new ProcessingState(mgr, MY_HOST); + } + + @Test + public void testGetFilter() { + Map<String, Object> filter = state.getFilter(); + + FilterUtilsTest utils = new FilterUtilsTest(); + + utils.checkArray(FilterUtils.CLASS_OR, 2, filter); + utils.checkEquals(FilterUtils.MSG_CHANNEL, Message.ADMIN, utils.getItem(filter, 0)); + utils.checkEquals(FilterUtils.MSG_CHANNEL, MY_HOST, utils.getItem(filter, 1)); + } + + @Test + public void testProcessQuery() { + State next = mock(State.class); + when(mgr.goQuery()).thenReturn(next); + + assertEquals(next, state.process(new Query())); + + Identification ident = captureAdminMessage(Identification.class); + assertEquals(MY_HOST, ident.getSource()); + assertEquals(ASGN3, ident.getAssignments()); + } + + @Test + public void testProcessingState() { + /* + * Null assignments should be OK. + */ + when(mgr.getAssignments()).thenReturn(null); + state = new ProcessingState(mgr, LEADER); + + /* + * Empty assignments should be OK. + */ + when(mgr.getAssignments()).thenReturn(EMPTY_ASGN); + state = new ProcessingState(mgr, LEADER); + assertEquals(MY_HOST, state.getHost()); + assertEquals(LEADER, state.getLeader()); + assertEquals(EMPTY_ASGN, state.getAssignments()); + + /* + * Now try something with assignments. + */ + when(mgr.getAssignments()).thenReturn(ASGN3); + state = new ProcessingState(mgr, LEADER); + + /* + * Prove the state is attached to the manager by invoking getHost(), + * which delegates to the manager. + */ + assertEquals(MY_HOST, state.getHost()); + + assertEquals(LEADER, state.getLeader()); + assertEquals(ASGN3, state.getAssignments()); + } + + @Test(expected = IllegalArgumentException.class) + public void testProcessingState_NullLeader() { + when(mgr.getAssignments()).thenReturn(EMPTY_ASGN); + state = new ProcessingState(mgr, null); + } + + @Test(expected = IllegalArgumentException.class) + public void testProcessingState_ZeroLengthHostArray() { + when(mgr.getAssignments()).thenReturn(new BucketAssignments(new String[] {})); + state = new ProcessingState(mgr, LEADER); + } + + @Test + public void testMakeIdentification() { + Identification ident = state.makeIdentification(); + assertEquals(MY_HOST, ident.getSource()); + assertEquals(ASGN3, ident.getAssignments()); + } + + @Test + public void testGetAssignments() { + // assignments from constructor + assertEquals(ASGN3, state.getAssignments()); + + // null assignments - no effect + state.setAssignments(null); + assertEquals(ASGN3, state.getAssignments()); + + // empty assignments + state.setAssignments(EMPTY_ASGN); + assertEquals(EMPTY_ASGN, state.getAssignments()); + + // non-empty assignments + state.setAssignments(ASGN3); + assertEquals(ASGN3, state.getAssignments()); + } + + @Test + public void testSetAssignments() { + state.setAssignments(null); + verify(mgr, never()).startDistributing(any()); + + state.setAssignments(ASGN3); + verify(mgr).startDistributing(ASGN3); + } + + @Test + public void testGetLeader() { + // check value from constructor + assertEquals(MY_HOST, state.getLeader()); + + state.setLeader(HOST2); + assertEquals(HOST2, state.getLeader()); + + state.setLeader(HOST3); + assertEquals(HOST3, state.getLeader()); + } + + @Test + public void testSetLeader() { + state.setLeader(MY_HOST); + assertEquals(MY_HOST, state.getLeader()); + } + + @Test(expected = IllegalArgumentException.class) + public void testSetLeader_Null() { + state.setLeader(null); + } + + @Test + public void testIsLeader() { + state.setLeader(MY_HOST); + assertTrue(state.isLeader()); + + state.setLeader(HOST2); + assertFalse(state.isLeader()); + } + + @Test + public void testBecomeLeader() { + State next = mock(State.class); + when(mgr.goActive()).thenReturn(next); + + assertEquals(next, state.becomeLeader(sortHosts(MY_HOST, HOST2))); + + Leader msg = captureAdminMessage(Leader.class); + + verify(mgr).startDistributing(msg.getAssignments()); + verify(mgr).goActive(); + } + + @Test(expected = IllegalArgumentException.class) + public void testBecomeLeader_NotFirstAlive() { + // alive list contains something before my host name + state.becomeLeader(sortHosts(PREV_HOST, MY_HOST)); + } + + @Test + public void testMakeLeader() throws Exception { + state.becomeLeader(sortHosts(MY_HOST, HOST2)); + + Leader msg = captureAdminMessage(Leader.class); + + // need a channel before invoking checkValidity() + msg.setChannel(Message.ADMIN); + + msg.checkValidity(); + + assertEquals(MY_HOST, msg.getSource()); + assertNotNull(msg.getAssignments()); + assertTrue(msg.getAssignments().hasAssignment(MY_HOST)); + assertTrue(msg.getAssignments().hasAssignment(HOST2)); + + // this one wasn't in the list of hosts, so it should have been removed + assertFalse(msg.getAssignments().hasAssignment(HOST1)); + } + + @Test + public void testMakeAssignments() throws Exception { + state.becomeLeader(sortHosts(MY_HOST, HOST2)); + + captureAssignments().checkValidity(); + } + + @Test + public void testMakeBucketArray_NullAssignments() { + when(mgr.getAssignments()).thenReturn(null); + state = new ProcessingState(mgr, MY_HOST); + state.becomeLeader(sortHosts(MY_HOST)); + + String[] arr = captureHostArray(); + + assertEquals(BucketAssignments.MAX_BUCKETS, arr.length); + + assertTrue(Arrays.asList(arr).stream().allMatch(host -> MY_HOST.equals(host))); + } + + @Test + public void testMakeBucketArray_ZeroAssignments() { + // bucket assignment with a zero-length array + state.setAssignments(new BucketAssignments(new String[0])); + + state.becomeLeader(sortHosts(MY_HOST)); + + String[] arr = captureHostArray(); + + assertEquals(BucketAssignments.MAX_BUCKETS, arr.length); + + assertTrue(Arrays.asList(arr).stream().allMatch(host -> MY_HOST.equals(host))); + } + + @Test + public void testMakeBucketArray() { + /* + * All hosts are still alive, so it should have the exact same + * assignments as it had to start. + */ + state.setAssignments(ASGN3); + state.becomeLeader(sortHosts(HOST_ARR3)); + + String[] arr = captureHostArray(); + + assertTrue(arr != HOST_ARR3); + assertEquals(Arrays.asList(HOST_ARR3), Arrays.asList(arr)); + } + + @Test + public void testRemoveExcessHosts() { + /** + * All hosts are still alive, plus some others. + */ + state.setAssignments(ASGN3); + state.becomeLeader(sortHosts(MY_HOST, HOST1, HOST2, HOST3, HOST4)); + + // assignments should be unchanged + assertEquals(Arrays.asList(HOST_ARR3), captureHostList()); + } + + @Test + public void testAddIndicesToHostBuckets() { + // some are null, some hosts are no longer alive + String[] asgn = {null, MY_HOST, HOST3, null, HOST4, HOST1, HOST2}; + + state.setAssignments(new BucketAssignments(asgn)); + state.becomeLeader(sortHosts(MY_HOST, HOST1, HOST2)); + + // every bucket should be assigned to one of the three hosts + String[] expected = {MY_HOST, MY_HOST, HOST1, HOST2, MY_HOST, HOST1, HOST2}; + assertEquals(Arrays.asList(expected), captureHostList()); + } + + @Test + public void testAssignNullBuckets() { + /* + * Ensure buckets are assigned to the host with the fewest buckets. + */ + String[] asgn = {MY_HOST, HOST1, MY_HOST, null, null, null, null, null, MY_HOST}; + + state.setAssignments(new BucketAssignments(asgn)); + state.becomeLeader(sortHosts(MY_HOST, HOST1, HOST2)); + + String[] expected = {MY_HOST, HOST1, MY_HOST, HOST2, HOST1, HOST2, HOST1, HOST2, MY_HOST}; + assertEquals(Arrays.asList(expected), captureHostList()); + } + + @Test + public void testRebalanceBuckets() { + /** + * Some are very lopsided. + */ + String[] asgn = {MY_HOST, HOST1, MY_HOST, MY_HOST, MY_HOST, MY_HOST, HOST1, HOST2, HOST1, HOST3}; + + state.setAssignments(new BucketAssignments(asgn)); + state.becomeLeader(sortHosts(MY_HOST, HOST1, HOST2, HOST3)); + + String[] expected = {HOST2, HOST1, HOST3, MY_HOST, MY_HOST, MY_HOST, HOST1, HOST2, HOST1, HOST3}; + assertEquals(Arrays.asList(expected), captureHostList()); + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/QueryStateTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/QueryStateTest.java new file mode 100644 index 00000000..d714d5cc --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/QueryStateTest.java @@ -0,0 +1,462 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.state; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.onap.policy.drools.pooling.message.BucketAssignments; +import org.onap.policy.drools.pooling.message.Identification; +import org.onap.policy.drools.pooling.message.Leader; +import org.onap.policy.drools.pooling.message.Message; +import org.onap.policy.drools.pooling.message.Offline; +import org.onap.policy.drools.pooling.message.Query; + +public class QueryStateTest extends BasicStateTester { + + private QueryState state; + + @Before + public void setUp() throws Exception { + super.setUp(); + + state = new QueryState(mgr); + } + + @Test + public void testGetFilter() { + Map<String, Object> filter = state.getFilter(); + + FilterUtilsTest utils = new FilterUtilsTest(); + + utils.checkArray(FilterUtils.CLASS_OR, 2, filter); + utils.checkEquals(FilterUtils.MSG_CHANNEL, Message.ADMIN, utils.getItem(filter, 0)); + utils.checkEquals(FilterUtils.MSG_CHANNEL, MY_HOST, utils.getItem(filter, 1)); + } + + @Test + public void testStart() { + state.start(); + + Pair<Long, StateTimerTask> timer = onceTasks.remove(); + + assertEquals(STD_IDENTIFICATION_MS, timer.first.longValue()); + assertNotNull(timer.second); + } + + @Test + public void testGoQuery() { + assertNull(state.process(new Query())); + assertEquals(ASGN3, state.getAssignments()); + } + + @Test + public void testProcessIdentification_NullSource() { + assertNull(state.process(new Identification())); + + assertEquals(MY_HOST, state.getLeader()); + } + + @Test + public void testProcessIdentification_NewLeader() { + assertNull(state.process(new Identification(PREV_HOST, null))); + + assertEquals(PREV_HOST, state.getLeader()); + } + + @Test + public void testProcessIdentification_NotNewLeader() { + assertNull(state.process(new Identification(HOST2, null))); + + assertEquals(MY_HOST, state.getLeader()); + } + + @Test + public void testProcessLeader_NullAssignment() { + Leader msg = new Leader(PREV_HOST, null); + + // should stay in the same state, and not start distributing + assertNull(state.process(msg)); + verify(mgr, never()).startDistributing(any()); + verify(mgr, never()).goActive(); + verify(mgr, never()).goInactive(); + + // info should be unchanged + assertEquals(MY_HOST, state.getLeader()); + assertEquals(ASGN3, state.getAssignments()); + } + + @Test + public void testProcessLeader_NullSource() { + String[] arr = {HOST2, PREV_HOST, MY_HOST}; + BucketAssignments asgn = new BucketAssignments(arr); + Leader msg = new Leader(null, asgn); + + // should stay in the same state, and not start distributing + assertNull(state.process(msg)); + verify(mgr, never()).startDistributing(any()); + verify(mgr, never()).goActive(); + verify(mgr, never()).goInactive(); + + // info should be unchanged + assertEquals(MY_HOST, state.getLeader()); + assertEquals(ASGN3, state.getAssignments()); + } + + @Test + public void testProcessLeader_SourceIsNotAssignmentLeader() { + String[] arr = {HOST2, PREV_HOST, MY_HOST}; + BucketAssignments asgn = new BucketAssignments(arr); + Leader msg = new Leader(HOST2, asgn); + + // should stay in the same state, and not start distributing + assertNull(state.process(msg)); + verify(mgr, never()).startDistributing(any()); + verify(mgr, never()).goActive(); + verify(mgr, never()).goInactive(); + + // info should be unchanged + assertEquals(MY_HOST, state.getLeader()); + assertEquals(ASGN3, state.getAssignments()); + } + + @Test + public void testProcessLeader_EmptyAssignment() { + Leader msg = new Leader(PREV_HOST, new BucketAssignments()); + + // should stay in the same state, and not start distributing + assertNull(state.process(msg)); + verify(mgr, never()).startDistributing(any()); + verify(mgr, never()).goActive(); + verify(mgr, never()).goInactive(); + + // info should be unchanged + assertEquals(MY_HOST, state.getLeader()); + assertEquals(ASGN3, state.getAssignments()); + } + + @Test + public void testProcessLeader_BetterLeader() { + String[] arr = {HOST2, PREV_HOST, MY_HOST}; + BucketAssignments asgn = new BucketAssignments(arr); + Leader msg = new Leader(PREV_HOST, asgn); + + State next = mock(State.class); + when(mgr.goActive()).thenReturn(next); + + // should go Active and start distributing + assertEquals(next, state.process(msg)); + verify(mgr).startDistributing(asgn); + verify(mgr, never()).goInactive(); + } + + @Test + public void testProcessLeader_NotABetterLeader() { + // no assignments yet + mgr.startDistributing(null); + state = new QueryState(mgr); + + BucketAssignments asgn = new BucketAssignments(new String[] {HOST1, HOST2}); + Leader msg = new Leader(HOST1, asgn); + + State next = mock(State.class); + when(mgr.goInactive()).thenReturn(next); + + // should stay in the same state + assertNull(state.process(msg)); + verify(mgr, never()).goActive(); + verify(mgr, never()).goInactive(); + + // should have started distributing + verify(mgr).startDistributing(asgn); + + // this host should still be the leader + assertEquals(MY_HOST, state.getLeader()); + + // new assignments + assertEquals(asgn, state.getAssignments()); + } + + @Test + public void testProcessOffline_NullHost() { + assertNull(state.process(new Offline())); + assertEquals(MY_HOST, state.getLeader()); + } + + @Test + public void testProcessOffline_SameHost() { + assertNull(state.process(new Offline(MY_HOST))); + assertEquals(MY_HOST, state.getLeader()); + } + + @Test + public void testProcessOffline_DiffHost() { + BucketAssignments asgn = new BucketAssignments(new String[] {PREV_HOST, HOST1}); + mgr.startDistributing(asgn); + state = new QueryState(mgr); + + // tell it that the hosts are alive + state.process(new Identification(PREV_HOST, asgn)); + state.process(new Identification(HOST1, asgn)); + + // #2 goes offline + assertNull(state.process(new Offline(HOST1))); + + // #1 should still be the leader + assertEquals(PREV_HOST, state.getLeader()); + + // #1 goes offline + assertNull(state.process(new Offline(PREV_HOST))); + + // this should still be the leader now + assertEquals(MY_HOST, state.getLeader()); + } + + @Test + public void testProcessQuery() { + BucketAssignments asgn = new BucketAssignments(new String[] {HOST1, HOST2}); + mgr.startDistributing(asgn); + state = new QueryState(mgr); + + State next = mock(State.class); + when(mgr.goQuery()).thenReturn(next); + + assertEquals(null, state.process(new Query())); + + verify(mgr).publishAdmin(any(Identification.class)); + } + + @Test + public void testQueryState() { + /* + * Prove the state is attached to the manager by invoking getHost(), + * which delegates to the manager. + */ + assertEquals(MY_HOST, state.getHost()); + } + + @Test + public void testAwaitIdentification_Leader() { + state.start(); + + Pair<Long, StateTimerTask> timer = onceTasks.remove(); + + assertEquals(STD_IDENTIFICATION_MS, timer.first.longValue()); + assertNotNull(timer.second); + + State next = mock(State.class); + when(mgr.goActive()).thenReturn(next); + + assertEquals(next, timer.second.fire(null)); + + // should have published a Leader message + Leader msg = captureAdminMessage(Leader.class); + assertEquals(MY_HOST, msg.getSource()); + assertTrue(msg.getAssignments().hasAssignment(MY_HOST)); + } + + @Test + public void testAwaitIdentification_HasAssignment() { + // not the leader, but has an assignment + BucketAssignments asgn = new BucketAssignments(new String[] {PREV_HOST, MY_HOST, HOST2}); + mgr.startDistributing(asgn); + state = new QueryState(mgr); + + state.start(); + + // tell it the leader is still active + state.process(new Identification(PREV_HOST, asgn)); + + Pair<Long, StateTimerTask> timer = onceTasks.remove(); + + assertEquals(STD_IDENTIFICATION_MS, timer.first.longValue()); + assertNotNull(timer.second); + + // set up active state, as that's what it should return + State next = mock(State.class); + when(mgr.goActive()).thenReturn(next); + + assertEquals(next, timer.second.fire(null)); + + // should NOT have published a Leader message + assertTrue(admin.isEmpty()); + + // should have gone active with the current assignments + verify(mgr).goActive(); + } + + @Test + public void testAwaitIdentification_NoAssignment() { + // not the leader and no assignment + BucketAssignments asgn = new BucketAssignments(new String[] {HOST1, HOST2}); + mgr.startDistributing(asgn); + state = new QueryState(mgr); + + state.start(); + + // tell it the leader is still active + state.process(new Identification(PREV_HOST, asgn)); + + Pair<Long, StateTimerTask> timer = onceTasks.remove(); + + assertEquals(STD_IDENTIFICATION_MS, timer.first.longValue()); + assertNotNull(timer.second); + + // set up inactive state, as that's what it should return + State next = mock(State.class); + when(mgr.goInactive()).thenReturn(next); + + assertEquals(next, timer.second.fire(null)); + + // should NOT have published a Leader message + assertTrue(admin.isEmpty()); + } + + @Test + public void testHasAssignment() { + // null assignment + mgr.startDistributing(null); + assertFalse(state.hasAssignment()); + + // not in assignments + state.setAssignments(new BucketAssignments(new String[] {HOST3})); + assertFalse(state.hasAssignment()); + + // it IS in the assignments + state.setAssignments(new BucketAssignments(new String[] {MY_HOST})); + assertTrue(state.hasAssignment()); + } + + @Test + public void testRecordInfo_NullSource() { + state.setAssignments(ASGN3); + state.setLeader(MY_HOST); + + BucketAssignments asgn = new BucketAssignments(new String[] {PREV_HOST, MY_HOST, HOST2}); + state.process(new Identification(null, asgn)); + + // leader unchanged + assertEquals(MY_HOST, state.getLeader()); + + // assignments still updated + assertEquals(asgn, state.getAssignments()); + } + + @Test + public void testRecordInfo_SourcePreceedsMyHost() { + state.setAssignments(ASGN3); + state.setLeader(MY_HOST); + + BucketAssignments asgn = new BucketAssignments(new String[] {PREV_HOST, MY_HOST, HOST2}); + state.process(new Identification(PREV_HOST, asgn)); + + // new leader + assertEquals(PREV_HOST, state.getLeader()); + + // assignments still updated + assertEquals(asgn, state.getAssignments()); + } + + @Test + public void testRecordInfo_SourceFollowsMyHost() { + mgr.startDistributing(null); + state.setLeader(MY_HOST); + + BucketAssignments asgn = new BucketAssignments(new String[] {HOST1, HOST2}); + state.process(new Identification(HOST1, asgn)); + + // leader unchanged + assertEquals(MY_HOST, state.getLeader()); + + // assignments still updated + assertEquals(asgn, state.getAssignments()); + } + + @Test + public void testRecordInfo_NewIsNull() { + state.setAssignments(ASGN3); + state.process(new Identification(HOST1, null)); + + assertEquals(ASGN3, state.getAssignments()); + } + + @Test + public void testRecordInfo_NewIsEmpty() { + state.setAssignments(ASGN3); + state.process(new Identification(PREV_HOST, new BucketAssignments())); + + assertEquals(ASGN3, state.getAssignments()); + } + + @Test + public void testRecordInfo_OldIsNull() { + mgr.startDistributing(null); + + BucketAssignments asgn = new BucketAssignments(new String[] {HOST1, HOST2}); + state.process(new Identification(HOST1, asgn)); + + assertEquals(asgn, state.getAssignments()); + } + + @Test + public void testRecordInfo_OldIsEmpty() { + state.setAssignments(new BucketAssignments()); + + BucketAssignments asgn = new BucketAssignments(new String[] {HOST1, HOST2}); + state.process(new Identification(HOST1, asgn)); + + assertEquals(asgn, state.getAssignments()); + } + + @Test + public void testRecordInfo_NewLeaderPreceedsOld() { + state.setAssignments(ASGN3); + state.setLeader(MY_HOST); + + BucketAssignments asgn = new BucketAssignments(new String[] {PREV_HOST, MY_HOST, HOST2}); + state.process(new Identification(HOST3, asgn)); + + assertEquals(asgn, state.getAssignments()); + } + + @Test + public void testRecordInfo_NewLeaderSucceedsOld() { + state.setAssignments(ASGN3); + state.setLeader(MY_HOST); + + BucketAssignments asgn = new BucketAssignments(new String[] {HOST2, HOST3}); + state.process(new Identification(HOST3, asgn)); + + // should be unchanged + assertEquals(ASGN3, state.getAssignments()); + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/StartStateTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/StartStateTest.java new file mode 100644 index 00000000..f29d2348 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/StartStateTest.java @@ -0,0 +1,180 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.state; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.onap.policy.drools.pooling.message.Forward; +import org.onap.policy.drools.pooling.message.Heartbeat; +import org.onap.policy.drools.pooling.message.Identification; +import org.onap.policy.drools.pooling.message.Leader; +import org.onap.policy.drools.pooling.message.Message; +import org.onap.policy.drools.pooling.message.Offline; +import org.onap.policy.drools.pooling.message.Query; + +public class StartStateTest extends BasicStateTester { + + private StartState state; + + @Before + public void setUp() throws Exception { + super.setUp(); + + state = new StartState(mgr); + } + + @Test + public void testGetFilter() { + Map<String, Object> filter = state.getFilter(); + + FilterUtilsTest utils = new FilterUtilsTest(); + + utils.checkArray(FilterUtils.CLASS_OR, 2, filter); + utils.checkEquals(FilterUtils.MSG_CHANNEL, Message.ADMIN, utils.getItem(filter, 0)); + + // get the sub-filter + filter = utils.getItem(filter, 1); + + utils.checkArray(FilterUtils.CLASS_AND, 2, filter); + utils.checkEquals(FilterUtils.MSG_CHANNEL, MY_HOST, utils.getItem(filter, 0)); + utils.checkEquals(FilterUtils.MSG_TIMESTAMP, String.valueOf(state.getHbTimestampMs()), + utils.getItem(filter, 1)); + } + + @Test + public void testStart() { + state.start(); + + Pair<String, Heartbeat> msg = capturePublishedMessage(Heartbeat.class); + + assertEquals(MY_HOST, msg.first); + assertEquals(state.getHbTimestampMs(), msg.second.getTimestampMs()); + + Pair<Long, StateTimerTask> timer = onceTasks.removeFirst(); + + assertEquals(STD_HEARTBEAT_WAIT_MS, timer.first.longValue()); + + // invoke the task - it should go to the state returned by the mgr + State next = mock(State.class); + when(mgr.goInactive()).thenReturn(next); + + assertEquals(next, timer.second.fire(null)); + + verify(mgr).internalTopicFailed(); + } + + @Test + public void testStartStatePoolingManager() { + /* + * Prove the state is attached to the manager by invoking getHost(), + * which delegates to the manager. + */ + assertEquals(MY_HOST, state.getHost()); + } + + @Test + public void testStartStateState() { + // create a new state from the current state + state = new StartState(mgr); + + /* + * Prove the state is attached to the manager by invoking getHost(), + * which delegates to the manager. + */ + assertEquals(MY_HOST, state.getHost()); + } + + @Test + public void testProcessForward() { + assertNull(state.process(new Forward())); + } + + @Test + public void testProcessHeartbeat() { + Heartbeat msg = new Heartbeat(); + + // no matching data in heart beat + assertNull(state.process(msg)); + verify(mgr, never()).publishAdmin(any()); + + // same source, different time stamp + msg.setSource(MY_HOST); + msg.setTimestampMs(state.getHbTimestampMs() - 1); + assertNull(state.process(msg)); + verify(mgr, never()).publishAdmin(any()); + + // same time stamp, different source + msg.setSource("unknown"); + msg.setTimestampMs(state.getHbTimestampMs()); + assertNull(state.process(msg)); + verify(mgr, never()).publishAdmin(any()); + + // matching heart beat + msg.setSource(MY_HOST); + msg.setTimestampMs(state.getHbTimestampMs()); + + State next = mock(State.class); + when(mgr.goQuery()).thenReturn(next); + + assertEquals(next, state.process(msg)); + + verify(mgr).publishAdmin(any(Query.class)); + } + + @Test + public void testProcessIdentification() { + assertNull(state.process(new Identification(MY_HOST, null))); + } + + @Test + public void testProcessLeader() { + assertNull(state.process(new Leader(MY_HOST, null))); + } + + @Test + public void testProcessOffline() { + assertNull(state.process(new Offline(HOST1))); + } + + @Test + public void testProcessQuery() { + assertNull(state.process(new Query())); + } + + @Test + public void testGetHbTimestampMs() { + long tcurrent = System.currentTimeMillis(); + assertTrue(new StartState(mgr).getHbTimestampMs() >= tcurrent); + + tcurrent = System.currentTimeMillis(); + assertTrue(new StartState(mgr).getHbTimestampMs() >= tcurrent); + } + +} diff --git a/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/StateTest.java b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/StateTest.java new file mode 100644 index 00000000..1be48e21 --- /dev/null +++ b/feature-pooling-dmaap/src/test/java/org/onap/policy/drools/pooling/state/StateTest.java @@ -0,0 +1,440 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.drools.pooling.state; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import org.junit.Before; +import org.junit.Test; +import org.onap.policy.drools.pooling.PoolingManager; +import org.onap.policy.drools.pooling.message.BucketAssignments; +import org.onap.policy.drools.pooling.message.Forward; +import org.onap.policy.drools.pooling.message.Heartbeat; +import org.onap.policy.drools.pooling.message.Identification; +import org.onap.policy.drools.pooling.message.Leader; +import org.onap.policy.drools.pooling.message.Message; +import org.onap.policy.drools.pooling.message.Offline; +import org.onap.policy.drools.pooling.message.Query; + +public class StateTest extends BasicStateTester { + + private State state; + + @Before + public void setUp() throws Exception { + super.setUp(); + + state = new MyState(mgr); + } + + @Test + public void testStatePoolingManager() { + /* + * Prove the state is attached to the manager by invoking getHost(), + * which delegates to the manager. + */ + assertEquals(MY_HOST, state.getHost()); + } + + @Test + public void testStateState() { + // allocate a new state, copying from the old state + state = new MyState(mgr); + + /* + * Prove the state is attached to the manager by invoking getHost(), + * which delegates to the manager. + */ + assertEquals(MY_HOST, state.getHost()); + } + + @Test + public void testCancelTimers() { + int delay = 100; + int initDelay = 200; + + /* + * Create three tasks tasks. + */ + + StateTimerTask task1 = mock(StateTimerTask.class); + StateTimerTask task2 = mock(StateTimerTask.class); + StateTimerTask task3 = mock(StateTimerTask.class); + + // two tasks via schedule() + state.schedule(delay, task1); + state.schedule(delay, task2); + + // one task via scheduleWithFixedDelay() + state.scheduleWithFixedDelay(initDelay, delay, task3); + + // ensure all were scheduled, but not yet canceled + verify(mgr).schedule(delay, task1); + verify(mgr).schedule(delay, task2); + verify(mgr).scheduleWithFixedDelay(initDelay, delay, task3); + + ScheduledFuture<?> fut1 = onceFutures.removeFirst(); + ScheduledFuture<?> fut2 = onceFutures.removeFirst(); + ScheduledFuture<?> fut3 = repeatedFutures.removeFirst(); + + verify(fut1, never()).cancel(false); + verify(fut2, never()).cancel(false); + verify(fut3, never()).cancel(false); + + /* + * Cancel the timers. + */ + state.cancelTimers(); + + // verify that all were cancelled + verify(fut1).cancel(false); + verify(fut2).cancel(false); + verify(fut3).cancel(false); + } + + @Test + public void testGetFilter() { + Map<String, Object> filter = state.getFilter(); + + FilterUtilsTest utils = new FilterUtilsTest(); + + utils.checkArray(FilterUtils.CLASS_OR, 2, filter); + utils.checkEquals(FilterUtils.MSG_CHANNEL, Message.ADMIN, utils.getItem(filter, 0)); + utils.checkEquals(FilterUtils.MSG_CHANNEL, MY_HOST, utils.getItem(filter, 1)); + } + + @Test + public void testStart() { + state.start(); + } + + @Test + public void testStop() { + state.stop(); + + assertEquals(MY_HOST, captureAdminMessage(Offline.class).getSource()); + } + + @Test + public void testGoStart() { + State next = mock(State.class); + when(mgr.goStart()).thenReturn(next); + + State next2 = state.goStart(); + assertEquals(next, next2); + } + + @Test + public void testGoQuery() { + State next = mock(State.class); + when(mgr.goQuery()).thenReturn(next); + + State next2 = state.goQuery(); + assertEquals(next, next2); + } + + @Test + public void testGoActive() { + State next = mock(State.class); + when(mgr.goActive()).thenReturn(next); + + State next2 = state.goActive(); + assertEquals(next, next2); + } + + @Test + public void testGoInactive() { + State next = mock(State.class); + when(mgr.goInactive()).thenReturn(next); + + State next2 = state.goInactive(); + assertEquals(next, next2); + } + + @Test + public void testProcessForward() { + Forward msg = new Forward(); + assertNull(state.process(msg)); + + verify(mgr).handle(msg); + } + + @Test + public void testProcessHeartbeat() { + assertNull(state.process(new Heartbeat())); + } + + @Test + public void testProcessIdentification() { + assertNull(state.process(new Identification())); + } + + @Test + public void testProcessLeader_NullAssignment() { + Leader msg = new Leader(PREV_HOST, null); + + // should stay in the same state, and not start distributing + assertNull(state.process(msg)); + verify(mgr, never()).startDistributing(any()); + verify(mgr, never()).goActive(); + verify(mgr, never()).goInactive(); + } + + @Test + public void testProcessLeader_NullSource() { + String[] arr = {HOST2, PREV_HOST, MY_HOST}; + BucketAssignments asgn = new BucketAssignments(arr); + Leader msg = new Leader(null, asgn); + + // should stay in the same state, and not start distributing + assertNull(state.process(msg)); + verify(mgr, never()).startDistributing(any()); + verify(mgr, never()).goActive(); + verify(mgr, never()).goInactive(); + } + + @Test + public void testProcessLeader_EmptyAssignment() { + Leader msg = new Leader(PREV_HOST, new BucketAssignments()); + + // should stay in the same state, and not start distributing + assertNull(state.process(msg)); + verify(mgr, never()).startDistributing(any()); + verify(mgr, never()).goActive(); + verify(mgr, never()).goInactive(); + } + + @Test + public void testProcessLeader_MyHostAssigned() { + String[] arr = {HOST2, PREV_HOST, MY_HOST}; + BucketAssignments asgn = new BucketAssignments(arr); + Leader msg = new Leader(PREV_HOST, asgn); + + State next = mock(State.class); + when(mgr.goActive()).thenReturn(next); + + // should go Active and start distributing + assertEquals(next, state.process(msg)); + verify(mgr).startDistributing(asgn); + verify(mgr, never()).goInactive(); + } + + @Test + public void testProcessLeader_MyHostUnassigned() { + String[] arr = {HOST2, HOST1}; + BucketAssignments asgn = new BucketAssignments(arr); + Leader msg = new Leader(HOST1, asgn); + + State next = mock(State.class); + when(mgr.goInactive()).thenReturn(next); + + // should go Inactive and start distributing + assertEquals(next, state.process(msg)); + verify(mgr).startDistributing(asgn); + verify(mgr, never()).goActive(); + } + + @Test + public void testProcessOffline() { + assertNull(state.process(new Offline())); + } + + @Test + public void testProcessQuery() { + assertNull(state.process(new Query())); + } + + @Test + public void testPublishIdentification() { + Identification msg = new Identification(); + state.publish(msg); + + verify(mgr).publishAdmin(msg); + } + + @Test + public void testPublishLeader() { + Leader msg = new Leader(); + state.publish(msg); + + verify(mgr).publishAdmin(msg); + } + + @Test + public void testPublishOffline() { + Offline msg = new Offline(); + state.publish(msg); + + verify(mgr).publishAdmin(msg); + } + + @Test + public void testPublishQuery() { + Query msg = new Query(); + state.publish(msg); + + verify(mgr).publishAdmin(msg); + } + + @Test + public void testPublishStringForward() { + String chnl = "channelF"; + Forward msg = new Forward(); + + state.publish(chnl, msg); + + verify(mgr).publish(chnl, msg); + } + + @Test + public void testPublishStringHeartbeat() { + String chnl = "channelH"; + Heartbeat msg = new Heartbeat(); + + state.publish(chnl, msg); + + verify(mgr).publish(chnl, msg); + } + + @Test + public void testStartDistributing() { + BucketAssignments asgn = new BucketAssignments(); + state.startDistributing(asgn); + + verify(mgr).startDistributing(asgn); + } + + @Test + public void testStartDistributing_NullAssignments() { + state.startDistributing(null); + + verify(mgr, never()).startDistributing(any()); + } + + @Test + public void testSchedule() { + int delay = 100; + + StateTimerTask task = mock(StateTimerTask.class); + + state.schedule(delay, task); + + ScheduledFuture<?> fut = onceFutures.removeFirst(); + + // scheduled, but not canceled yet + verify(mgr).schedule(delay, task); + verify(fut, never()).cancel(false); + + /* + * Ensure the state added the timer to its list by telling it to cancel + * its timers and then seeing if this timer was canceled. + */ + state.cancelTimers(); + verify(fut).cancel(false); + } + + @Test + public void testScheduleWithFixedDelay() { + int initdel = 100; + int delay = 200; + + StateTimerTask task = mock(StateTimerTask.class); + + state.scheduleWithFixedDelay(initdel, delay, task); + + ScheduledFuture<?> fut = repeatedFutures.removeFirst(); + + // scheduled, but not canceled yet + verify(mgr).scheduleWithFixedDelay(initdel, delay, task); + verify(fut, never()).cancel(false); + + /* + * Ensure the state added the timer to its list by telling it to cancel + * its timers and then seeing if this timer was canceled. + */ + state.cancelTimers(); + verify(fut).cancel(false); + } + + @Test + public void testInternalTopicFailed() { + State next = mock(State.class); + when(mgr.goInactive()).thenReturn(next); + + State next2 = state.internalTopicFailed(); + assertEquals(next, next2); + + verify(mgr).internalTopicFailed(); + + Offline msg = captureAdminMessage(Offline.class); + assertEquals(MY_HOST, msg.getSource()); + } + + @Test + public void testMakeHeartbeat() { + long timestamp = 30000L; + Heartbeat msg = state.makeHeartbeat(timestamp); + + assertEquals(MY_HOST, msg.getSource()); + assertEquals(timestamp, msg.getTimestampMs()); + } + + @Test + public void testMakeOffline() { + Offline msg = state.makeOffline(); + + assertEquals(MY_HOST, msg.getSource()); + } + + @Test + public void testMakeQuery() { + Query msg = state.makeQuery(); + + assertEquals(MY_HOST, msg.getSource()); + } + + @Test + public void testGetHost() { + assertEquals(MY_HOST, state.getHost()); + } + + @Test + public void testGetTopic() { + assertEquals(MY_TOPIC, state.getTopic()); + } + + /** + * State used for testing purposes, with abstract methods implemented. + */ + private class MyState extends State { + + public MyState(PoolingManager mgr) { + super(mgr); + } + } +} diff --git a/feature-pooling-dmaap/src/test/resources/logback-test.xml b/feature-pooling-dmaap/src/test/resources/logback-test.xml new file mode 100644 index 00000000..6f745157 --- /dev/null +++ b/feature-pooling-dmaap/src/test/resources/logback-test.xml @@ -0,0 +1,17 @@ +<configuration> + + <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> + <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> + <Pattern> + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36}.%M\(%line\) - %msg%n + </Pattern> + </encoder> + </appender> + + <logger name="org.onap.policy.drools.http.server.test" level="INFO"/> + + <root level="WARN"> + <appender-ref ref="STDOUT"/> + </root> + +</configuration>
\ No newline at end of file |