diff options
author | Jim Hahn <jrh3@att.com> | 2018-03-26 16:48:31 -0400 |
---|---|---|
committer | Jim Hahn <jrh3@att.com> | 2018-03-28 23:47:53 -0400 |
commit | a3fa1c69a955af57f4e9023488bac3ef67a4fc3e (patch) | |
tree | 0f5173ea23c5d40cdef0f64dffc3fc18e695cf64 | |
parent | 1d2c8346e0ac02320ca933b66c1943c7f72343c6 (diff) |
Add pooling capability
Add an optional feature that that supports session pooling, wherein
more than one host can be active at a time.
Use beforeInsert() instead of beforeOffer(), where possible.
Move request-id-extraction from policy-managment to feature-pooling.
Combined AdditionalProperties into PoolingProperties.
Finished junit tests for DmaapManager.
Adjusted filters for all XxxState classes, and added testGetFilter to
all XxxStateTest classes.
Always publish Offline message when the internal topic fails.
Remove DelayedExtractor, as it isn't needed.
Renamed ExtractorMap to ClassExtractors, and added property name prefix
to the constructor to give more control over property naming to invokers.
Remove State copy constructor.
Use class name instead of class in ClassExtractors map.
Remove BucketAssignments from ProcessingState.
Remove some TODO items.
Add META-INF for implemented feature APIs.
Fix ClassExtractor bug where it can't find a field in a superclass,
and add a test for classes defined in another file.
Add assembly and rename project directory.
Add more junit coverage.
Change-Id: I7f132f84a7b284a58ab09c9069db19b853acd7e9
Issue-ID: POLICY-577
Signed-off-by: Jim Hahn <jrh3@att.com>
80 files changed, 13640 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 @@ -82,6 +82,7 @@ <module>policy-management</module> <module>feature-healthcheck</module> <module>feature-eelf</module> + <module>feature-pooling-dmaap</module> <module>feature-session-persistence</module> <module>feature-test-transaction</module> <module>api-state-management</module> |