diff options
author | adheli.tavares <adheli.tavares@est.tech> | 2024-01-26 15:28:56 +0000 |
---|---|---|
committer | Adheli Tavares <adheli.tavares@est.tech> | 2024-02-09 10:31:15 +0000 |
commit | cc1d3d352771d1fa35d297e90663539e34b022f6 (patch) | |
tree | b1b00d678b0157742b91be1262fbf614a62063b1 /feature-pooling-messages/src | |
parent | cce79248655f484e007769ba81b092148f940d27 (diff) |
Removing deprecated DMAAP library
Issue-ID: POLICY-4402
Change-Id: I4ce5b303cb3a775010683d3c7102b0bd065280ea
Signed-off-by: adheli.tavares <adheli.tavares@est.tech>
Diffstat (limited to 'feature-pooling-messages/src')
58 files changed, 11764 insertions, 0 deletions
diff --git a/feature-pooling-messages/src/assembly/assemble_zip.xml b/feature-pooling-messages/src/assembly/assemble_zip.xml new file mode 100644 index 00000000..67424116 --- /dev/null +++ b/feature-pooling-messages/src/assembly/assemble_zip.xml @@ -0,0 +1,76 @@ +<!-- + ============LICENSE_START======================================================= + feature-pooling-messages + ================================================================================ + 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-messages</id> + <formats> + <format>zip</format> + </formats> + + <!-- we want "system" and related files right at the root level as this + file is supposed to be unzipped on top of a karaf distro. --> + <includeBaseDirectory>false</includeBaseDirectory> + + <fileSets> + <fileSet> + <directory>target</directory> + <outputDirectory>lib/feature</outputDirectory> + <includes> + <include>feature-pooling-messages-${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-messages/src/main/feature/config/feature-pooling-messages.properties b/feature-pooling-messages/src/main/feature/config/feature-pooling-messages.properties new file mode 100644 index 00000000..925d1698 --- /dev/null +++ b/feature-pooling-messages/src/main/feature/config/feature-pooling-messages.properties @@ -0,0 +1,89 @@ +### +# ============LICENSE_START======================================================= +# feature-pooling-messages +# ================================================================================ +# Copyright (C) 2018-2020 AT&T Intellectual Property. All rights reserved. +# Modifications Copyright (C) 2024 Nordix Foundation. +# ================================================================================ +# 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========================================================= +### + +# In general, the feature-specific properties begin with "pooling", +# and they may be made specific to a controller by prepending with +# "pooling.<controller-name>", instead. +# +# The available properties and their default values are shown below. + +# Whether the feature is enabled. +#pooling.enabled=false + +# The internal kafka topic used by a controller. Note: the controller +# name is required for this property. +#pooling.<controller-name>.topic = + +# Maximum number of events to retain in the queue while a new host waits +# to be assigned work. +#pooling.offline.queue.limit=1000 + +# Maximum age, in milliseconds, of events to be retained in the queue. +# Events older than this are discarded. +#pooling.offline.queue.age.milliseconds=60000 + +# Time, in milliseconds, to wait for an "Offline" message to be published +# to topic manager before the connection may be closed. +#pooling.offline.publish.wait.milliseconds=3000 + +# Time, in milliseconds, to wait for this host's initial heart beat. This +# is used to verify connectivity to the internal topic. +#pooling.start.heartbeat.milliseconds=100000 + +# Time, in milliseconds, to wait before attempting to reactivate this +# host when it was not assigned any work. +#pooling.reactivate.milliseconds=50000 + +# Time, in milliseconds, to wait for other hosts to identify themselves +# when this host is started. +#pooling.identification.milliseconds=50000 + +# Time, in milliseconds, to wait for heart beats from this host, or its +# predecessor, during the active state. +#pooling.active.heartbeat.milliseconds=50000 + +# Time, in milliseconds, to wait between heart beat generations. +#pooling.inter.heartbeat.milliseconds=15000 + +# Topic used for inter-host communication for a particular controller +# pooling.<controller-name>.topic=XXX + +# Each controller that is enabled should have its own topic and the +# corresponding ${topicManager}.xxx properties (using kafka as default). +# However, for now, just assume that the usecases features will not both +# be enabled at the same time. + +pooling.usecases.enabled=true +pooling.usecases.topic=${env:POOLING_TOPIC} + +# the list of sources and sinks should be identical +kafka.source.topics=POOLING_TOPIC +kafka.sink.topics=POOLING_TOPIC + +kafka.source.topics.POOLING_TOPIC.servers=${env:KAFKA_SERVERS} +kafka.source.topics.POOLING_TOPIC.effectiveTopic=${env:POOLING_TOPIC} +kafka.source.topics.POOLING_TOPIC.apiKey= +kafka.source.topics.POOLING_TOPIC.apiSecret= + +kafka.sink.topics.POOLING_TOPIC.servers=${env:kafka_SERVERS} +kafka.sink.topics.POOLING_TOPIC.effectiveTopic=${env:POOLING_TOPIC} +kafka.sink.topics.POOLING_TOPIC.apiKey= +kafka.sink.topics.POOLING_TOPIC.apiSecret= diff --git a/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/CancellableScheduledTask.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/CancellableScheduledTask.java new file mode 100644 index 00000000..4e9112e6 --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/CancellableScheduledTask.java @@ -0,0 +1,34 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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 scheduled task that can be cancelled. + */ +@FunctionalInterface +public interface CancellableScheduledTask { + + /** + * Cancels the scheduled task. + */ + void cancel(); +} diff --git a/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/PoolingFeature.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/PoolingFeature.java new file mode 100644 index 00000000..6411dd81 --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/PoolingFeature.java @@ -0,0 +1,397 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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 java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import lombok.AccessLevel; +import lombok.Getter; +import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure; +import org.onap.policy.common.endpoints.event.comm.TopicEndpointManager; +import org.onap.policy.common.endpoints.event.comm.TopicSink; +import org.onap.policy.common.endpoints.event.comm.TopicSource; +import org.onap.policy.common.utils.properties.SpecProperties; +import org.onap.policy.common.utils.properties.exception.PropertyException; +import org.onap.policy.drools.controller.DroolsController; +import org.onap.policy.drools.features.DroolsControllerFeatureApi; +import org.onap.policy.drools.features.PolicyControllerFeatureApi; +import org.onap.policy.drools.features.PolicyEngineFeatureApi; +import org.onap.policy.drools.persistence.SystemPersistenceConstants; +import org.onap.policy.drools.system.PolicyController; +import org.onap.policy.drools.system.PolicyControllerConstants; +import org.onap.policy.drools.system.PolicyEngine; +import org.onap.policy.drools.util.FeatureEnabledChecker; +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 PolicyEngineFeatureApi, PolicyControllerFeatureApi, DroolsControllerFeatureApi { + + private static final Logger logger = LoggerFactory.getLogger(PoolingFeature.class); + + /** + * ID of this host. + */ + @Getter + private final String host; + + /** + * Entire set of feature properties, including those specific to various controllers. + */ + private Properties featProps = null; + + /** + * Maps a controller name to its associated manager. + */ + private final ConcurrentHashMap<String, PoolingManagerImpl> ctlr2pool = new ConcurrentHashMap<>(107); + + /** + * Decremented each time a manager enters the Active state. Used by junit tests. + */ + @Getter(AccessLevel.PROTECTED) + private final CountDownLatch activeLatch = new CountDownLatch(1); + + /** + * Topic names 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<String> offerTopics = new ThreadLocal<>(); + + /** + * Constructor. + */ + public PoolingFeature() { + super(); + + this.host = UUID.randomUUID().toString(); + } + + @Override + public int getSequenceNumber() { + return 0; + } + + @Override + public boolean beforeStart(PolicyEngine engine) { + logger.info("initializing {}", PoolingProperties.FEATURE_NAME); + featProps = getProperties(PoolingProperties.FEATURE_NAME); + + // remove any generic pooling topic - always use controller-specific property + featProps.remove(PoolingProperties.POOLING_TOPIC); + + initTopicSources(featProps); + initTopicSinks(featProps); + + return false; + } + + @Override + public boolean beforeStart(PolicyController controller) { + return doManager(controller, mgr -> { + mgr.beforeStart(); + return false; + }); + } + + /** + * 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(); + + var specProps = new SpecProperties(PoolingProperties.PREFIX, name, featProps); + + if (FeatureEnabledChecker.isFeatureEnabled(specProps, PoolingProperties.FEATURE_ENABLED)) { + try { + // get & validate the properties + var props = new PoolingProperties(name, featProps); + + logger.info("pooling enabled for {}", name); + ctlr2pool.computeIfAbsent(name, xxx -> makeManager(host, controller, props, activeLatch)); + + } catch (PropertyException e) { + logger.error("pooling disabled due to exception for {}", name); + throw new PoolingFeatureRtException(e); + } + + } else { + logger.info("pooling disabled for {}", name); + } + + + 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) { + return doManager(controller, mgr -> { + mgr.afterStop(); + return false; + }); + } + + @Override + public boolean afterShutdown(PolicyController controller) { + return commonShutdown(controller); + } + + @Override + public boolean afterHalt(PolicyController controller) { + return commonShutdown(controller); + } + + private boolean commonShutdown(PolicyController controller) { + deleteManager(controller); + 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(topic2, event)) { + return true; + } + + offerTopics.set(topic2); + return false; + } + + @Override + public boolean beforeInsert(DroolsController droolsController, Object fact) { + + String topic = offerTopics.get(); + if (topic == null) { + logger.warn("missing arguments for feature-pooling-messages in beforeInsert"); + return false; + } + + PolicyController controller; + try { + controller = getController(droolsController); + + } catch (IllegalArgumentException | IllegalStateException e) { + logger.warn("cannot get controller for {} {}", droolsController.getGroupId(), + droolsController.getArtifactId(), e); + return false; + } + + + if (controller == null) { + logger.warn("cannot determine controller for {} {}", droolsController.getGroupId(), + droolsController.getArtifactId()); + 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(topic, fact); + } + + @Override + public boolean afterOffer(PolicyController controller, CommInfrastructure protocol, String topic, String event, + boolean success) { + + // clear any stored arguments + offerTopics.remove(); + + 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 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 new PoolingFeatureRtException(e); + } + } + + /** + * Deletes the manager associated with a controller. + * + * @param controller controller + * @throws PoolingFeatureRtException if an error occurs + */ + private void deleteManager(PolicyController controller) { + + String name = controller.getName(); + logger.info("remove feature-pooling-messages manager for {}", name); + + ctlr2pool.remove(name); + } + + /** + * Function that operates on a manager. + */ + @FunctionalInterface + private static interface MgrFunc { + + /** + * Apply. + * + * @param mgr manager + * @return {@code true} if the request was handled by the manager, {@code false} otherwise + * @throws PoolingFeatureException feature exception + */ + boolean apply(PoolingManagerImpl mgr) throws PoolingFeatureException; + } + + /* + * The remaining methods may be overridden by junit tests. + */ + + /** + * Get properties. + * + * @param featName feature name + * @return the properties for the specified feature + */ + protected Properties getProperties(String featName) { + return SystemPersistenceConstants.getManager().getProperties(featName); + } + + /** + * Makes a pooling manager for a controller. + * + * @param host name/uuid of this host + * @param controller controller + * @param props properties to use to configure the manager + * @param activeLatch decremented when the manager goes Active + * @return a new pooling manager + */ + protected PoolingManagerImpl makeManager(String host, PolicyController controller, PoolingProperties props, + CountDownLatch activeLatch) { + return new PoolingManagerImpl(host, controller, props, activeLatch); + } + + /** + * Gets the policy controller associated with a drools controller. + * + * @param droolsController drools controller + * @return the policy controller associated with a drools controller + */ + protected PolicyController getController(DroolsController droolsController) { + return PolicyControllerConstants.getFactory().get(droolsController); + } + + /** + * Initializes the topic sources. + * + * @param props properties used to configure the topics + * @return the topic sources + */ + protected List<TopicSource> initTopicSources(Properties props) { + return TopicEndpointManager.getManager().addTopicSources(props); + } + + /** + * Initializes the topic sinks. + * + * @param props properties used to configure the topics + * @return the topic sinks + */ + protected List<TopicSink> initTopicSinks(Properties props) { + return TopicEndpointManager.getManager().addTopicSinks(props); + } +} diff --git a/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/PoolingFeatureException.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/PoolingFeatureException.java new file mode 100644 index 00000000..5d7b9f76 --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/PoolingFeatureException.java @@ -0,0 +1,54 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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.Serial; + +/** + * Exception thrown by the pooling feature. + */ +public class PoolingFeatureException extends Exception { + @Serial + 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); + } + +} diff --git a/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/PoolingFeatureRtException.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/PoolingFeatureRtException.java new file mode 100644 index 00000000..5d0a2755 --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/PoolingFeatureRtException.java @@ -0,0 +1,54 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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.Serial; + +/** + * A runtime exception thrown by the pooling feature. + */ +public class PoolingFeatureRtException extends RuntimeException { + @Serial + 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-messages/src/main/java/org/onap/policy/drools/pooling/PoolingManager.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/PoolingManager.java new file mode 100644 index 00000000..5e358e61 --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/PoolingManager.java @@ -0,0 +1,133 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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 org.onap.policy.drools.pooling.message.BucketAssignments; +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 pooling properties + */ + PoolingProperties getProperties(); + + /** + * Gets the host id. + * + * @return the host id + */ + 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 + */ + String getTopic(); + + /** + * Starts distributing requests according to the given bucket assignments. + * + * @param assignments must <i>not</i> be {@code null} + */ + void startDistributing(BucketAssignments assignments); + + /** + * Gets the current bucket assignments. + * + * @return the current bucket assignments, or {@code null} if no assignments have been + * made + */ + BucketAssignments getAssignments(); + + /** + * Publishes a message to the internal topic on the administrative channel. + * + * @param msg message to be published + */ + 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 + */ + void publish(String channel, Message msg); + + /** + * Schedules a timer to fire after a delay. + * + * @param delayMs delay, in milliseconds + * @param task task + * @return a new scheduled task + */ + CancellableScheduledTask schedule(long delayMs, StateTimerTask task); + + /** + * Schedules a timer to fire repeatedly. + * + * @param initialDelayMs initial delay, in milliseconds + * @param delayMs delay, in milliseconds + * @param task task + * @return a new scheduled task + */ + CancellableScheduledTask scheduleWithFixedDelay(long initialDelayMs, long delayMs, StateTimerTask task); + + /** + * Transitions to the "start" state. + * + * @return the new state + */ + State goStart(); + + /** + * Transitions to the "query" state. + * + * @return the new state + */ + State goQuery(); + + /** + * Transitions to the "active" state. + * + * @return the new state + */ + State goActive(); + + /** + * Transitions to the "inactive" state. + * + * @return the new state + */ + State goInactive(); + +} diff --git a/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/PoolingManagerImpl.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/PoolingManagerImpl.java new file mode 100644 index 00000000..7c0436eb --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/PoolingManagerImpl.java @@ -0,0 +1,647 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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 com.google.gson.JsonParseException; +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import lombok.Getter; +import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure; +import org.onap.policy.common.endpoints.event.comm.TopicListener; +import org.onap.policy.drools.controller.DroolsController; +import org.onap.policy.drools.pooling.message.BucketAssignments; +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.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.EventProtocolCoderConstants; +import org.onap.policy.drools.system.PolicyController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 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); + + /** + * Maximum number of times a message can be forwarded. + */ + public static final int MAX_HOPS = 5; + + /** + * ID of this host. + */ + @Getter + private final String host; + + /** + * Properties with which this was configured. + */ + @Getter + private final PoolingProperties properties; + + /** + * Associated controller. + */ + private final PolicyController controller; + + /** + * Decremented each time the manager enters the Active state. Used by junit tests. + */ + private final CountDownLatch activeLatch; + + /** + * Used to encode & decode request objects received from & sent to a rule engine. + */ + private final Serializer serializer; + + /** + * Internal DMaaP topic used by this controller. + */ + @Getter + private final String topic; + + /** + * Manager for the internal DMaaP topic. + */ + private final TopicMessageManager topicMessageManager; + + /** + * 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}. + */ + @Getter + private BucketAssignments assignments = null; + + /** + * Pool used to execute timers. + */ + private ScheduledThreadPoolExecutor scheduler = null; + + /** + * Constructs the manager, initializing all the data structures. + * + * @param host name/uuid of this host + * @param controller controller with which this is associated + * @param props feature properties specific to the controller + * @param activeLatch latch to be decremented each time the manager enters the Active + * state + */ + public PoolingManagerImpl(String host, PolicyController controller, PoolingProperties props, + CountDownLatch activeLatch) { + this.host = host; + this.controller = controller; + this.properties = props; + this.activeLatch = activeLatch; + + try { + this.serializer = new Serializer(); + this.topic = props.getPoolingTopic(); + this.topicMessageManager = makeTopicMessagesManager(props.getPoolingTopic()); + 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 new PoolingFeatureRtException(e); + } + } + + /** + * Should only be used by junit tests. + * + * @return the current state + */ + protected State getCurrent() { + synchronized (curLocker) { + return current; + } + } + + /** + * Indicates that the controller is about to start. Starts the publisher for the + * internal topic, and creates a thread pool for the timers. + */ + public void beforeStart() { + synchronized (curLocker) { + if (scheduler == null) { + topicMessageManager.startPublisher(); + + logger.debug("make scheduler thread for topic {}", getTopic()); + scheduler = 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) { + topicMessageManager.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)) { + changeState(new IdleState(this)); + topicMessageManager.stopConsumer(this); + publishAdmin(new Offline(getHost())); + } + + assignments = null; + } + + if (sched != null) { + logger.debug("stop scheduler for topic {}", getTopic()); + 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) { + /* + * stop the publisher, but allow time for any Offline message to be + * transmitted + */ + topicMessageManager.stopPublisher(properties.getOfflinePubWaitMs()); + } + } + + /** + * 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() { + logger.info("locking manager for topic {}", getTopic()); + + 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() { + logger.info("unlocking manager for topic {}", getTopic()); + + 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; + + newState.start(); + } + } + + @Override + public CancellableScheduledTask schedule(long delayMs, StateTimerTask task) { + // wrap the task in a TimerAction and schedule it + ScheduledFuture<?> fut = scheduler.schedule(new TimerAction(task), delayMs, TimeUnit.MILLISECONDS); + + // wrap the future in a "CancellableScheduledTask" + return () -> fut.cancel(false); + } + + @Override + public CancellableScheduledTask scheduleWithFixedDelay(long initialDelayMs, long delayMs, StateTimerTask task) { + // wrap the task in a TimerAction and schedule it + ScheduledFuture<?> fut = scheduler.scheduleWithFixedDelay(new TimerAction(task), initialDelayMs, delayMs, + TimeUnit.MILLISECONDS); + + // wrap the future in a "CancellableScheduledTask" + return () -> fut.cancel(false); + } + + @Override + public void publishAdmin(Message msg) { + publish(Message.ADMIN, msg); + } + + @Override + public void publish(String channel, Message msg) { + logger.info("publish {} to {} on topic {}", msg.getClass().getSimpleName(), channel, getTopic()); + + msg.setChannel(channel); + + try { + // ensure it's valid before we send it + msg.checkValidity(); + + String txt = serializer.encodeMsg(msg); + topicMessageManager.publish(txt); + + } catch (JsonParseException 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 commType comm infrastructure + * @param topic2 topic + * @param event event + */ + @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(String, Object) beforeInsert()} handle + * it instead, as it already has the decoded message. + * + * @param topic2 topic + * @param event 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(String topic2, String event) { + + if (!controller.isLocked()) { + // we should NOT intercept this message - let the invoker handle it + return false; + } + + return handleExternal(topic2, decodeEvent(topic2, event)); + } + + /** + * Called by the DroolsController before it inserts the event into the rule engine. + * + * @param topic2 topic + * @param event 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(String topic2, Object event) { + return handleExternal(topic2, event); + } + + /** + * Handles an event from an external topic. + * + * @param topic2 topic + * @param event event, as an object, or {@code null} if it cannot be decoded + * @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(String topic2, Object event) { + if (event == null) { + // no event - let the invoker handle it + return false; + } + + synchronized (curLocker) { + return handleExternal(topic2, event, event.hashCode()); + } + } + + /** + * Handles an event from an external topic. + * + * @param topic2 topic + * @param event event, as an object + * @param eventHashCode event's hash code + * @return {@code true} if the event was handled, {@code false} if the invoker should + * handle it + */ + private boolean handleExternal(String topic2, Object event, int eventHashCode) { + if (assignments == null) { + // no bucket assignments yet - handle locally + logger.info("handle event locally for request {}", event); + + // we did NOT consume the event + return false; + + } else { + return handleEvent(topic2, event, eventHashCode); + } + } + + /** + * Handles a {@link Forward} event, possibly forwarding it again. + * + * @param topic2 topic + * @param event event, as an object + * @param eventHashCode event's hash code + * @return {@code true} if the event was handled, {@code false} if the invoker should + * handle it + */ + private boolean handleEvent(String topic2, Object event, int eventHashCode) { + String target = assignments.getAssignedHost(eventHashCode); + + if (target == null) { + /* + * This bucket has no assignment - just discard the event + */ + logger.warn("discarded event for unassigned bucket from topic {}", topic2); + return true; + } + + if (target.equals(host)) { + /* + * Message belongs to this host - allow the controller to handle it. + */ + logger.info("handle local event for request {} from topic {}", event, topic2); + return false; + } + + // not our message, consume the event + logger.warn("discarded event for host {} from topic {}", target, topic2); + return true; + } + + /** + * Decodes an event from a String into an event Object. + * + * @param topic2 topic + * @param event 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 (!canDecodeEvent(drools, topic2)) { + + logger.warn("{}: DECODING-UNSUPPORTED {}:{}:{}", drools, topic2, drools.getGroupId(), + drools.getArtifactId()); + return null; + } + + // decode + + try { + return decodeEventWrapper(drools, topic2, event); + + } catch (UnsupportedOperationException | IllegalStateException | IllegalArgumentException e) { + logger.debug("{}: DECODE FAILED: {} <- {} because of {}", drools, topic2, event, e.getMessage(), e); + return null; + } + } + + /** + * 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(); + + var meth = current.getClass().getMethod("process", msg.getClass()); + changeState((State) meth.invoke(current, msg)); + + } catch (JsonParseException 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 + | PoolingFeatureException e) { + logger.error("failed to process message {} for topic {}", clazz, topic, e); + } + } + + @Override + public void startDistributing(BucketAssignments asgn) { + synchronized (curLocker) { + int sz = (asgn == null ? 0 : asgn.getAllHosts().size()); + logger.info("new assignments for {} hosts on topic {}", sz, getTopic()); + assignments = asgn; + } + } + + @Override + public State goStart() { + return new StartState(this); + } + + @Override + public State goQuery() { + return new QueryState(this); + } + + @Override + public State goActive() { + activeLatch.countDown(); + 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; + + /** + * Constructor. + * + * @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()); + } + } + } + } + + /** + * Creates a DMaaP manager. + * + * @param topic name of the internal DMaaP topic + * @return a new topic messages manager + * @throws PoolingFeatureException if an error occurs + */ + protected TopicMessageManager makeTopicMessagesManager(String topic) throws PoolingFeatureException { + return new TopicMessageManager(topic); + } + + /** + * Creates a scheduled thread pool. + * + * @return a new scheduled thread pool + */ + protected 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 + */ + protected boolean canDecodeEvent(DroolsController drools, String topic) { + return EventProtocolCoderConstants.getManager().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 illegal argument + * @throws UnsupportedOperationException unsupported operation + * @throws IllegalStateException illegal state + */ + protected Object decodeEventWrapper(DroolsController drools, String topic, String event) { + return EventProtocolCoderConstants.getManager().decode(drools.getGroupId(), drools.getArtifactId(), topic, + event); + } +} diff --git a/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/PoolingProperties.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/PoolingProperties.java new file mode 100644 index 00000000..fd1c3d3d --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/PoolingProperties.java @@ -0,0 +1,150 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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 lombok.Getter; +import lombok.Setter; +import org.onap.policy.common.utils.properties.BeanConfigurator; +import org.onap.policy.common.utils.properties.Property; +import org.onap.policy.common.utils.properties.SpecProperties; +import org.onap.policy.common.utils.properties.exception.PropertyException; + +/** + * Properties used by the pooling feature, specific to a controller. + */ +@Getter +@Setter +public class PoolingProperties { + + /** + * The feature name, used to retrieve properties. + */ + public static final String FEATURE_NAME = "feature-pooling-messages"; + + /** + * Feature properties all begin with this prefix. + */ + public static final String PREFIX = "pooling."; + + public static final String FEATURE_ENABLED = PREFIX + "enabled"; + public static final String POOLING_TOPIC = PREFIX + "topic"; + 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 OFFLINE_PUB_WAIT_MS = PREFIX + "offline.publish.wait.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"; + + /** + * Type of item that the extractors will be extracting. + */ + public static final String EXTRACTOR_TYPE = "requestId"; + + /** + * Prefix for extractor properties. + */ + public static final String PROP_EXTRACTOR_PREFIX = "extractor." + EXTRACTOR_TYPE; + + /** + * 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 an "Offline" message to be published + * to topic. + */ + @Property(name = OFFLINE_PUB_WAIT_MS, defaultValue = "3000") + private long offlinePubWaitMs; + + /** + * Time, in milliseconds, to wait for this host's heart beat during the + * start-up state. + */ + @Property(name = START_HEARTBEAT_MS, defaultValue = "100000") + private long startHeartbeatMs; + + /** + * Time, in milliseconds, to wait before attempting to reactivate 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 and start-up states. + */ + @Property(name = INTER_HEARTBEAT_MS, defaultValue = "15000") + private long interHeartbeatMs; + + /** + * Constructor. + * + * @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 { + source = props; + + new BeanConfigurator().configureFromProperties(this, new SpecProperties(PREFIX, controllerName, props)); + } +} diff --git a/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/Serializer.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/Serializer.java new file mode 100644 index 00000000..2894b1da --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/Serializer.java @@ -0,0 +1,124 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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 com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import java.util.HashMap; +import java.util.Map; +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; + +/** + * Serialization helper functions. + */ +public class Serializer { + + /** + * The message type is stored in fields of this name within the JSON. + */ + private static final String TYPE_FIELD = "type"; + + /** + * Used to encode & decode JSON messages sent & received, respectively, on the + * internal topic. + */ + private final Gson gson = new Gson(); + + /** + * Maps a message subclass to its type. + */ + private static final Map<Class<? extends Message>, String> class2type = new HashMap<>(); + + /** + * Maps a message type to the appropriate subclass. + */ + private static final Map<String, Class<? extends Message>> type2class = new HashMap<>(); + + static { + class2type.put(Heartbeat.class, "heartbeat"); + class2type.put(Identification.class, "identification"); + class2type.put(Leader.class, "leader"); + class2type.put(Offline.class, "offline"); + class2type.put(Query.class, "query"); + + class2type.forEach((clazz, type) -> type2class.put(type, clazz)); + } + + /** + * Encodes a filter. + * + * @param filter filter to be encoded + * @return the filter, serialized as a JSON string + */ + public String encodeFilter(Map<String, Object> filter) { + return gson.toJson(filter); + } + + /** + * Encodes a message. + * + * @param msg message to be encoded + * @return the message, serialized as a JSON string + */ + public String encodeMsg(Message msg) { + JsonElement jsonEl = gson.toJsonTree(msg); + + String type = class2type.get(msg.getClass()); + if (type == null) { + throw new JsonParseException("cannot serialize " + msg.getClass()); + } + + jsonEl.getAsJsonObject().addProperty(TYPE_FIELD, type); + + return gson.toJson(jsonEl); + } + + /** + * Decodes a JSON string into a Message. + * + * @param msg JSON string representing the message + * @return the message + */ + public Message decodeMsg(String msg) { + JsonElement jsonEl = gson.fromJson(msg, JsonElement.class); + + JsonElement typeEl = jsonEl.getAsJsonObject().get(TYPE_FIELD); + if (typeEl == null) { + throw new JsonParseException("cannot deserialize " + Message.class + + " because it does not contain a field named " + TYPE_FIELD); + + } + + Class<? extends Message> clazz = type2class.get(typeEl.getAsString()); + if (clazz == null) { + throw new JsonParseException("cannot deserialize " + typeEl); + } + + return gson.fromJson(jsonEl, clazz); + } +} diff --git a/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/TopicMessageManager.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/TopicMessageManager.java new file mode 100644 index 00000000..4c9b3f34 --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/TopicMessageManager.java @@ -0,0 +1,233 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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 lombok.Getter; +import org.onap.policy.common.endpoints.event.comm.TopicEndpoint; +import org.onap.policy.common.endpoints.event.comm.TopicEndpointManager; +import org.onap.policy.common.endpoints.event.comm.TopicListener; +import org.onap.policy.common.endpoints.event.comm.TopicSink; +import org.onap.policy.common.endpoints.event.comm.TopicSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages the internal topic. Assumes all topics are managed by + * {@link TopicEndpoint}. + */ +public class TopicMessageManager { + + private static final Logger logger = LoggerFactory.getLogger(TopicMessageManager.class); + + /** + * Name of the topic. + */ + @Getter + private final String topic; + + /** + * Topic source whose filter is to be manipulated. + */ + private final TopicSource topicSource; + + /** + * Where to publish messages. + */ + private final TopicSink topicSink; + + /** + * {@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 topic + * @throws PoolingFeatureException if an error occurs + */ + public TopicMessageManager(String topic) throws PoolingFeatureException { + + logger.info("initializing bus for topic {}", topic); + + try { + this.topic = topic; + + this.topicSource = findTopicSource(); + this.topicSink = findTopicSink(); + + } catch (IllegalArgumentException e) { + logger.error("failed to attach to topic {}", topic); + throw new PoolingFeatureException(e); + } + } + + /** + * Finds the topic source associated with the internal topic. + * + * @return the topic source + * @throws PoolingFeatureException if the source doesn't exist or is not filterable + */ + private TopicSource findTopicSource() throws PoolingFeatureException { + for (TopicSource src : getTopicSources()) { + if (topic.equals(src.getTopic())) { + return src; + } + } + + throw new PoolingFeatureException("missing topic source " + topic); + } + + /** + * Finds the topic sink associated with the internal topic. + * + * @return the topic sink + * @throws PoolingFeatureException if the sink doesn't exist + */ + private TopicSink findTopicSink() throws PoolingFeatureException { + for (TopicSink sink : getTopicSinks()) { + if (topic.equals(sink.getTopic())) { + return sink; + } + } + + throw new PoolingFeatureException("missing topic sink " + topic); + } + + /** + * Starts the publisher, if it isn't already running. + */ + public void startPublisher() { + if (publishing) { + return; + } + + logger.info("start publishing to topic {}", topic); + publishing = true; + } + + /** + * Stops the publisher. + * + * @param waitMs time, in milliseconds, to wait for the sink to transmit any queued messages and + * close + */ + public void stopPublisher(long waitMs) { + if (!publishing) { + return; + } + + /* + * Give the sink a chance to transmit messages in the queue. It would be better if "waitMs" + * could be passed to sink.stop(), but that isn't an option at this time. + */ + try { + Thread.sleep(waitMs); + + } catch (InterruptedException e) { + logger.warn("message transmission stopped due to {}", e.getMessage()); + Thread.currentThread().interrupt(); + } + + logger.info("stop publishing to topic {}", topic); + publishing = false; + } + + /** + * 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; + } + + logger.info("start consuming from topic {}", topic); + topicSource.register(listener); + consuming = true; + } + + /** + * Stops the consumer. + * + * @param listener listener to unregister with the source + */ + public void stopConsumer(TopicListener listener) { + if (!consuming) { + return; + } + + logger.info("stop consuming from topic {}", topic); + consuming = false; + topicSource.unregister(listener); + } + + /** + * 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); + } + } + + /* + * The remaining methods may be overridden by junit tests. + */ + + /** + * Get topic source. + * + * @return the topic sources + */ + protected List<TopicSource> getTopicSources() { + return TopicEndpointManager.getManager().getTopicSources(); + } + + /** + * Get topic sinks. + * + * @return the topic sinks + */ + protected List<TopicSink> getTopicSinks() { + return TopicEndpointManager.getManager().getTopicSinks(); + } +} diff --git a/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/message/BucketAssignments.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/message/BucketAssignments.java new file mode 100644 index 00000000..485a9d2e --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/message/BucketAssignments.java @@ -0,0 +1,205 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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 lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.onap.policy.drools.pooling.PoolingFeatureException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Bucket assignments, which is simply an array of host names. + */ +@Getter +@Setter +@NoArgsConstructor +public class BucketAssignments { + + private static final Logger logger = LoggerFactory.getLogger(BucketAssignments.class); + + /** + * The number of bits in the maximum number of buckets. + */ + private static final int MAX_BUCKET_BITS = 10; + + /** + * Maximum number of buckets. Must be a power of two. + */ + public static final int MAX_BUCKETS = 1 << MAX_BUCKET_BITS; + + /** + * Used to ensure that a hash code is not negative. + */ + private static final int MAX_BUCKETS_MASK = MAX_BUCKETS - 1; + + /** + * Identifies the host serving a particular bucket. + */ + private String[] hostArray = null; + + + /** + * Constructor. + * + * @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; + } + + /** + * Gets the leader, which is the host with the minimum UUID. + * + * @return the assignment leader + */ + 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 + */ + public boolean hasAssignment(String host) { + if (hostArray == null) { + return false; + } + + for (String host2 : hostArray) { + if (host.equals(host2)) { + return true; + } + } + + return false; + } + + /** + * Gets all the hosts that have an assignment. + * + * @return all the hosts that have an assignment + */ + 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 hashCode hash code of the item whose assignment is desired + * @return the assigned host, or {@code null} if the item has no assigned host + */ + public String getAssignedHost(int hashCode) { + if (hostArray == null || hostArray.length == 0) { + logger.error("no buckets have been assigned"); + return null; + } + + return hostArray[(Math.abs(hashCode) & MAX_BUCKETS_MASK) % hostArray.length]; + } + + /** + * Gets the number of buckets. + * + * @return the number of buckets + */ + 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 + */ + 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 (var x = 0; x < hostArray.length; ++x) { + if (hostArray[x] == null) { + throw new PoolingFeatureException("bucket " + x + " has no assignment"); + } + } + } + + @Override + public int hashCode() { + final var prime = 31; + var 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; + return Arrays.equals(hostArray, other.hostArray); + } +} diff --git a/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/message/Heartbeat.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/message/Heartbeat.java new file mode 100644 index 00000000..4a8bdc3b --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/message/Heartbeat.java @@ -0,0 +1,52 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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 lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * Heart beat message sent to self, or to the succeeding host. + */ +@Getter +@Setter +@NoArgsConstructor +public class Heartbeat extends Message { + + /** + * Time, in milliseconds, when this was created. + */ + private long timestampMs; + + /** + * Constructor. + * + * @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; + } +} diff --git a/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/message/Identification.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/message/Identification.java new file mode 100644 index 00000000..b8fd8414 --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/message/Identification.java @@ -0,0 +1,41 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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 lombok.NoArgsConstructor; + +/** + * Identifies the source host and the bucket assignments which it knows about. + */ +@NoArgsConstructor +public class Identification extends MessageWithAssignments { + + /** + * Constructor. + * + * @param source host on which the message originated + * @param assignments assignments + */ + public Identification(String source, BucketAssignments assignments) { + super(source, assignments); + } +} diff --git a/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/message/Leader.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/message/Leader.java new file mode 100644 index 00000000..10c33382 --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/message/Leader.java @@ -0,0 +1,70 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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 lombok.NoArgsConstructor; +import org.onap.policy.drools.pooling.PoolingFeatureException; + +/** + * Indicates that the "source" of this message is now the "lead" host. + */ +@NoArgsConstructor +public class Leader extends MessageWithAssignments { + + /** + * Constructor. + * + * @param source host on which the message originated + * @param assignments 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 + 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-messages/src/main/java/org/onap/policy/drools/pooling/message/Message.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/message/Message.java new file mode 100644 index 00000000..bb973142 --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/message/Message.java @@ -0,0 +1,79 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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 lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.onap.policy.drools.pooling.PoolingFeatureException; + +/** + * Messages sent on the internal topic. + */ +@Getter +@Setter +@NoArgsConstructor +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; + + + /** + * Constructor. + * + * @param source host on which the message originated + */ + public Message(String source) { + this.source = source; + } + + /** + * Checks the validity of the message, including verifying that required + * fields are not missing. + * + * @throws PoolingFeatureException if the message is invalid + */ + 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-messages/src/main/java/org/onap/policy/drools/pooling/message/MessageWithAssignments.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/message/MessageWithAssignments.java new file mode 100644 index 00000000..b8521dd8 --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/message/MessageWithAssignments.java @@ -0,0 +1,68 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2019, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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 lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.onap.policy.drools.pooling.PoolingFeatureException; + +/** + * A Message that includes bucket assignments. + */ +@Setter +@Getter +@NoArgsConstructor +public class MessageWithAssignments extends Message { + + /** + * Bucket assignments, as known by the source host. + */ + private BucketAssignments assignments; + + + /** + * Constructor. + * + * @param source host on which the message originated + * @param assignments assignments + */ + public MessageWithAssignments(String source, BucketAssignments assignments) { + super(source); + + this.assignments = assignments; + } + + /** + * If there are any assignments, it verifies there validity. + */ + @Override + public void checkValidity() throws PoolingFeatureException { + + super.checkValidity(); + + if (assignments != null) { + assignments.checkValidity(); + } + } + +} diff --git a/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/message/Offline.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/message/Offline.java new file mode 100644 index 00000000..3ff7bf1e --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/message/Offline.java @@ -0,0 +1,41 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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 lombok.NoArgsConstructor; + +/** + * Indicates that the source host is going offline and will be unable to process + * any further requests. + */ +@NoArgsConstructor +public class Offline extends Message { + + /** + * Constructor. + * + * @param source host on which the message originated + */ + public Offline(String source) { + super(source); + } +} diff --git a/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/message/Query.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/message/Query.java new file mode 100644 index 00000000..4c856bb7 --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/message/Query.java @@ -0,0 +1,40 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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 lombok.NoArgsConstructor; + +/** + * Query the other hosts for their identification. + */ +@NoArgsConstructor +public class Query extends Message { + + /** + * Constructor. + * + * @param source host on which the message originated + */ + public Query(String source) { + super(source); + } +} diff --git a/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/ActiveState.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/ActiveState.java new file mode 100644 index 00000000..cafcb45b --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/ActiveState.java @@ -0,0 +1,270 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018, 2020-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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 lombok.AccessLevel; +import lombok.Getter; +import org.onap.policy.drools.pooling.PoolingManager; +import org.onap.policy.drools.pooling.message.Heartbeat; +import org.onap.policy.drools.pooling.message.Leader; +import org.onap.policy.drools.pooling.message.Offline; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The active state. In this state, this host has one more bucket assignments and + * processes any events associated with one of its buckets. Other events are forwarded to + * appropriate target hosts. + */ +@Getter(AccessLevel.PROTECTED) +public class ActiveState extends ProcessingState { + + private static final Logger logger = LoggerFactory.getLogger(ActiveState.class); + + /** + * Set of hosts that have been assigned a bucket. + */ + @Getter(AccessLevel.NONE) + 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; + + + /** + * Constructor. + * + * @param mgr pooling manager + */ + 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) { + logger.info("this host has no neighbors on topic {}", getTopic()); + /* + * 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(); + } + logger.info("this host's successor is {} on topic {}", succHost, getTopic()); + + if ((predHost = assigned.lower(getHost())) == null) { + // wrapped around - predecessor is the last host in the set + predHost = assigned.last(); + } + logger.info("this host's predecessor is {} on topic {}", predHost, getTopic()); + } + + @Override + public void start() { + super.start(); + addTimers(); + genHeartbeat(); + } + + /** + * Adds the timers. + */ + private void addTimers() { + logger.info("add timers"); + + /* + * heart beat generator + */ + long genMs = getProperties().getInterHeartbeatMs(); + + scheduleWithFixedDelay(genMs, genMs, () -> { + genHeartbeat(); + return null; + }); + + /* + * my heart beat checker + */ + long waitMs = getProperties().getActiveHeartbeatMs(); + + scheduleWithFixedDelay(waitMs, waitMs, () -> { + if (myHeartbeatSeen) { + myHeartbeatSeen = false; + return null; + } + + // missed my heart beat + logger.error("missed my heartbeat on topic {}", getTopic()); + + return missedHeartbeat(); + }); + + /* + * predecessor heart beat checker + */ + if (!predHost.isEmpty()) { + + scheduleWithFixedDelay(waitMs, waitMs, () -> { + if (predHeartbeatSeen) { + predHeartbeatSeen = false; + return null; + } + + // missed the predecessor's heart beat + logger.warn("missed predecessor's heartbeat on topic {}", getTopic()); + + publish(makeQuery()); + + return goQuery(); + }); + } + } + + /** + * Generates a heart beat for this host and its successor. + */ + private void genHeartbeat() { + var 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) { + logger.warn("Heartbeat message has no source on topic {}", getTopic()); + + } else if (src.equals(getHost())) { + logger.info("saw my heartbeat on topic {}", getTopic()); + myHeartbeatSeen = true; + + } else if (src.equals(predHost)) { + logger.info("saw heartbeat from {} on topic {}", src, getTopic()); + predHeartbeatSeen = true; + + } else { + logger.info("ignored heartbeat message from {} on topic {}", src, getTopic()); + } + + return null; + } + + @Override + public State process(Leader msg) { + if (!isValid(msg)) { + return null; + } + + String src = msg.getSource(); + + if (getHost().compareTo(src) < 0) { + // our host would be a better leader - find out what's up + logger.warn("unexpected Leader message from {} on topic {}", src, getTopic()); + return goQuery(); + } + + logger.info("have a new leader {} on topic {}", src, getTopic()); + + return goActive(msg.getAssignments()); + } + + @Override + public State process(Offline msg) { + String src = msg.getSource(); + + if (src == null) { + logger.warn("Offline message has no source on topic {}", getTopic()); + return null; + + } else if (!assigned.contains(src)) { + /* + * the offline host wasn't assigned any buckets, so just ignore the message + */ + logger.info("ignore Offline message from unassigned source {} on topic {}", src, getTopic()); + 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. + */ + + logger.info("Offline message from source {} on topic {}", src, getTopic()); + + 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. + */ + logger.info("ignore Offline message from source {} on topic {}", src, getTopic()); + return null; + } + } +} diff --git a/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/IdleState.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/IdleState.java new file mode 100644 index 00000000..06418280 --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/IdleState.java @@ -0,0 +1,34 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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; + +/** + * Idle state, used when offline. + */ +public class IdleState extends State { + + public IdleState(PoolingManager mgr) { + super(mgr); + } +} diff --git a/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/InactiveState.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/InactiveState.java new file mode 100644 index 00000000..7fc220a0 --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/InactiveState.java @@ -0,0 +1,81 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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.Leader; +import org.onap.policy.drools.pooling.message.Query; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 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 { + + private static final Logger logger = LoggerFactory.getLogger(InactiveState.class); + + /** + * Constructor. + * + * @param mgr pooling manager + */ + public InactiveState(PoolingManager mgr) { + super(mgr); + } + + @Override + public void start() { + super.start(); + schedule(getProperties().getReactivateMs(), this::goStart); + } + + @Override + public State process(Leader msg) { + if (isValid(msg)) { + logger.info("received Leader message from {} on topic {}", msg.getSource(), getTopic()); + return goActive(msg.getAssignments()); + } + + return null; + } + + /** + * Generates an Identification message and goes to the query state. + */ + @Override + public State process(Query msg) { + logger.info("received Query message on topic {}", getTopic()); + publish(makeIdentification()); + return goQuery(); + } + + /** + * Remains in this state, without resetting any timers. + */ + @Override + protected State goInactive() { + return null; + } + +} diff --git a/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/ProcessingState.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/ProcessingState.java new file mode 100644 index 00000000..76914b75 --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/ProcessingState.java @@ -0,0 +1,398 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018, 2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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 lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import org.onap.policy.drools.pooling.PoolingManager; +import org.onap.policy.drools.pooling.message.BucketAssignments; +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. + */ +@Setter +@Getter +public class ProcessingState extends State { + + private static final Logger logger = LoggerFactory.getLogger(ProcessingState.class); + + /** + * Current known leader, never {@code null}. + */ + @NonNull + private String leader; + + /** + * Constructor. + * + * @param mgr pooling manager + * @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, @NonNull String leader) { + super(mgr); + + 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 goes to the query state. + */ + @Override + public State process(Query msg) { + logger.info("received Query message on topic {}", getTopic()); + publish(makeIdentification()); + return goQuery(); + } + + /** + * Sets the assignments. + * + * @param assignments new assignments, or {@code null} + */ + protected final void setAssignments(BucketAssignments assignments) { + if (assignments != null) { + startDistributing(assignments); + } + } + + /** + * 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 newLeader = getHost(); + + if (!newLeader.equals(alive.first())) { + throw new IllegalArgumentException(newLeader + " cannot replace " + alive.first()); + } + + var msg = makeLeader(alive); + logger.info("{}/{} hosts have an assignment", msg.getAssignments().getAllHosts().size(), alive.size()); + + publish(msg); + + return goActive(msg.getAssignments()); + } + + /** + * 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]; + } + + var 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<>(); + + for (var 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 buckets that still need to be assigned to 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(); + assert newhb != null; + 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(); + + assert larger != null && smaller != null; + 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 bucket = larger.remove(); + smaller.add(bucket); + + bucket2host[bucket] = 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. + */ + protected 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<>(); + + /** + * Constructor. + * + * @param host 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); + } + + /** + * Size. + * + * @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 diff = buckets.size() - other.buckets.size(); + if (diff == 0) { + diff = host.compareTo(other.host); + } + return diff; + } + + @Override + public final int hashCode() { + throw new UnsupportedOperationException("HostBucket cannot be hashed"); + } + + @Override + public final boolean equals(Object obj) { + throw new UnsupportedOperationException("cannot compare HostBuckets"); + } + } +} diff --git a/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/QueryState.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/QueryState.java new file mode 100644 index 00000000..ef401dcb --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/QueryState.java @@ -0,0 +1,204 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 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 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 { + + private static final Logger logger = LoggerFactory.getLogger(QueryState.class); + + /** + * Hosts that have sent an "Identification" message. Always includes this host. + */ + private final TreeSet<String> alive = new TreeSet<>(); + + /** + * {@code True} if we saw our own Identification method, {@code false} otherwise. + */ + private boolean sawSelfIdent = false; + + /** + * Constructor. + * + * @param mgr manager + */ + 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(), () -> { + + if (!sawSelfIdent) { + // didn't see our identification + logger.error("missed our own Ident message on topic {}", getTopic()); + return missedHeartbeat(); + + } else if (isLeader()) { + // "this" host is the new leader + logger.info("this host is the new leader for topic {}", getTopic()); + return becomeLeader(alive); + + } else { + // not the leader - return to previous state + logger.info("no new leader on topic {}", getTopic()); + return goActive(getAssignments()); + } + }); + } + + @Override + public State goQuery() { + return null; + } + + @Override + public State process(Identification msg) { + + if (getHost().equals(msg.getSource())) { + logger.info("saw our own Ident message on topic {}", getTopic()); + sawSelfIdent = true; + + } else { + logger.info("received Ident message from {} on topic {}", msg.getSource(), getTopic()); + 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) { + if (!isValid(msg)) { + return null; + } + + String source = msg.getSource(); + BucketAssignments asgn = msg.getAssignments(); + + // go active, if this has a leader that's the same or better than the one we have + if (source.compareTo(getLeader()) <= 0) { + logger.warn("leader with {} on topic {}", source, getTopic()); + return goActive(asgn); + } + + /* + * The message does not have an acceptable leader, but we'll still record its + * info. + */ + logger.info("record leader info from {} on topic {}", source, getTopic()); + recordInfo(source, asgn); + + return null; + } + + @Override + public State process(Offline msg) { + String host = msg.getSource(); + + if (host != null && !host.equals(getHost())) { + logger.warn("host {} offline on topic {}", host, getTopic()); + alive.remove(host); + setLeader(alive.first()); + + } else { + logger.info("ignored offline message from {} on topic {}", msg.getSource(), getTopic()); + } + + 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) { + logger.info("received initial assignments on topic {}", getTopic()); + setAssignments(assignments); + return; + } + + /* + * Record assignments, if the new assignments have a better (i.e., lesser) leader. + */ + String curldr = current.getLeader(); + if (curldr == null || assignments.getLeader().compareTo(curldr) < 0) { + logger.info("use new assignments from {} on topic {}", source, getTopic()); + setAssignments(assignments); + } + } +} diff --git a/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/StartState.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/StartState.java new file mode 100644 index 00000000..73717d7c --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/StartState.java @@ -0,0 +1,99 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018, 2020-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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 lombok.Getter; +import org.onap.policy.drools.pooling.PoolingManager; +import org.onap.policy.drools.pooling.message.Heartbeat; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 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}. + */ +@Getter +public class StartState extends State { + + private static final Logger logger = LoggerFactory.getLogger(StartState.class); + + /** + * Time stamp inserted into the heart beat message. + */ + private long hbTimestampMs = System.currentTimeMillis(); + + /** + * Constructor. + * + * @param mgr pooling manager + */ + public StartState(PoolingManager mgr) { + super(mgr); + } + + @Override + public void start() { + + super.start(); + + var hb = makeHeartbeat(hbTimestampMs); + publish(getHost(), hb); + + /* + * heart beat generator + */ + long genMs = getProperties().getInterHeartbeatMs(); + + scheduleWithFixedDelay(genMs, genMs, () -> { + publish(getHost(), hb); + return null; + }); + + /* + * my heart beat checker + */ + schedule(getProperties().getStartHeartbeatMs(), () -> { + logger.error("missed heartbeat on topic {}", getTopic()); + return 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 + logger.info("saw our own heartbeat on topic {}", getTopic()); + publish(makeQuery()); + return goQuery(); + + } else { + logger.info("ignored old heartbeat message from {} on topic {}", msg.getSource(), getTopic()); + } + + return null; + } +} diff --git a/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/State.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/State.java new file mode 100644 index 00000000..e2cf9586 --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/State.java @@ -0,0 +1,373 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018, 2020-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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.LinkedList; +import java.util.List; +import org.onap.policy.drools.pooling.CancellableScheduledTask; +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.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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 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 { + + private static final Logger logger = LoggerFactory.getLogger(State.class); + + /** + * Host pool manager. + */ + private final PoolingManager mgr; + + /** + * Timers added by this state. + */ + private final List<CancellableScheduledTask> timers = new LinkedList<>(); + + /** + * Constructor. + * + * @param mgr pooling manager + */ + protected State(PoolingManager mgr) { + this.mgr = mgr; + } + + /** + * Cancels the timers added by this state. + */ + public final void cancelTimers() { + timers.forEach(CancellableScheduledTask::cancel); + } + + /** + * Starts the state. The default method simply logs a message and returns. + */ + public void start() { + logger.info("entered {} for topic {}", getClass().getSimpleName(), getTopic()); + } + + /** + * Transitions to the "start" state. + * + * @return the new state + */ + public final State goStart() { + return mgr.goStart(); + } + + /** + * Transitions to the "query" state. + * + * @return the new state + */ + public State goQuery() { + return mgr.goQuery(); + } + + /** + * Goes active with a new set of assignments. + * + * @param asgn new assignments + * @return the new state, either Active or Inactive, depending on whether or not this + * host has an assignment + */ + protected State goActive(BucketAssignments asgn) { + startDistributing(asgn); + + if (asgn != null && asgn.hasAssignment(getHost())) { + return mgr.goActive(); + + } else { + return goInactive(); + } + } + + /** + * Transitions to the "inactive" state. + * + * @return the new state + */ + protected State goInactive() { + return mgr.goInactive(); + } + + /** + * 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) { + logger.info("ignored heartbeat message from {} on topic {}", msg.getSource(), getTopic()); + 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) { + logger.info("ignored ident message from {} on topic {}", msg.getSource(), getTopic()); + return null; + } + + /** + * Processes a message. The default method copies the assignments and then returns + * {@code null}. + * + * @param msg message to be processed + * @return the new state, or {@code null} if the state is unchanged + */ + public State process(Leader msg) { + if (isValid(msg)) { + logger.info("extract assignments from Leader message from {} on topic {}", msg.getSource(), getTopic()); + startDistributing(msg.getAssignments()); + } + + 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) { + logger.info("ignored offline message from {} on topic {}", msg.getSource(), getTopic()); + 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) { + logger.info("ignored Query message from {} on topic {}", msg.getSource(), getTopic()); + return null; + } + + /** + * Determines if a message is valid and did not originate from this host. + * + * @param msg message to be validated + * @return {@code true} if the message is valid, {@code false} otherwise + */ + protected boolean isValid(Leader msg) { + BucketAssignments asgn = msg.getAssignments(); + if (asgn == null) { + logger.warn("Leader message from {} has no assignments for topic {}", msg.getSource(), getTopic()); + return false; + } + + // ignore Leader messages from ourself + String source = msg.getSource(); + if (source == null || source.equals(getHost())) { + logger.debug("ignore Leader message from {} for topic {}", msg.getSource(), getTopic()); + return false; + } + + // the new leader must equal the source + boolean result = source.equals(asgn.getLeader()); + + if (!result) { + logger.warn("Leader message from {} has an invalid assignment for topic {}", msg.getSource(), getTopic()); + } + + return result; + } + + /** + * Publishes a message. + * + * @param msg message to be published + */ + protected final void publish(Identification msg) { + mgr.publishAdmin(msg); + } + + /** + * Publishes a message. + * + * @param msg message to be published + */ + protected final void publish(Leader msg) { + mgr.publishAdmin(msg); + } + + /** + * Publishes a message. + * + * @param msg message to be published + */ + protected final void publish(Offline msg) { + mgr.publishAdmin(msg); + } + + /** + * Publishes a message. + * + * @param msg message to be published + */ + protected final void publish(Query msg) { + mgr.publishAdmin(msg); + } + + /** + * Publishes a message on the specified channel. + * + * @param channel channel + * @param msg message to be published + */ + protected final void publish(String channel, Heartbeat msg) { + mgr.publish(channel, msg); + } + + /** + * Starts distributing messages using the specified bucket assignments. + * + * @param assignments assignments + */ + protected final void startDistributing(BucketAssignments assignments) { + if (assignments != null) { + mgr.startDistributing(assignments); + } + } + + /** + * Schedules a timer to fire after a delay. + * + * @param delayMs delay in ms + * @param task task + */ + protected final void schedule(long delayMs, StateTimerTask task) { + timers.add(mgr.schedule(delayMs, task)); + } + + /** + * Schedules a timer to fire repeatedly. + * + * @param initialDelayMs initial delay ms + * @param delayMs delay ms + * @param task task + */ + protected final void scheduleWithFixedDelay(long initialDelayMs, long delayMs, StateTimerTask task) { + timers.add(mgr.scheduleWithFixedDelay(initialDelayMs, delayMs, task)); + } + + /** + * Indicates that we failed to see our own heartbeat; must be a problem with the + * internal topic. Assumes the problem is temporary and continues to use the current + * bucket assignments. + * + * @return a new {@link StartState} + */ + protected final State missedHeartbeat() { + publish(makeOffline()); + + return mgr.goStart(); + } + + /** + * Indicates that the internal topic failed; this should only be invoked from the + * StartState. Discards bucket assignments and begins processing everything locally. + * + * @return a new {@link InactiveState} + */ + protected final State internalTopicFailed() { + publish(makeOffline()); + mgr.startDistributing(null); + + return mgr.goInactive(); + } + + /** + * Makes a heart beat message. + * + * @param timestampMs time, in milliseconds, associated with the message + * + * @return a new message + */ + protected final Heartbeat makeHeartbeat(long timestampMs) { + return new Heartbeat(getHost(), timestampMs); + } + + /** + * Makes an Identification message. + * + * @return a new message + */ + protected Identification makeIdentification() { + return new Identification(getHost(), getAssignments()); + } + + /** + * Makes an "offline" message. + * + * @return a new message + */ + protected final Offline makeOffline() { + return new Offline(getHost()); + } + + /** + * Makes a query message. + * + * @return a new message + */ + protected final 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-messages/src/main/java/org/onap/policy/drools/pooling/state/StateTimerTask.java b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/StateTimerTask.java new file mode 100644 index 00000000..892a1767 --- /dev/null +++ b/feature-pooling-messages/src/main/java/org/onap/policy/drools/pooling/state/StateTimerTask.java @@ -0,0 +1,37 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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. + * + * @return the new state, or {@code null} if the state is unchanged + */ + State fire(); + +} diff --git a/feature-pooling-messages/src/main/resources/META-INF/services/org.onap.policy.drools.features.DroolsControllerFeatureApi b/feature-pooling-messages/src/main/resources/META-INF/services/org.onap.policy.drools.features.DroolsControllerFeatureApi new file mode 100644 index 00000000..cd59e469 --- /dev/null +++ b/feature-pooling-messages/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-messages/src/main/resources/META-INF/services/org.onap.policy.drools.features.PolicyControllerFeatureApi b/feature-pooling-messages/src/main/resources/META-INF/services/org.onap.policy.drools.features.PolicyControllerFeatureApi new file mode 100644 index 00000000..cd59e469 --- /dev/null +++ b/feature-pooling-messages/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-messages/src/main/resources/META-INF/services/org.onap.policy.drools.features.PolicyEngineFeatureApi b/feature-pooling-messages/src/main/resources/META-INF/services/org.onap.policy.drools.features.PolicyEngineFeatureApi new file mode 100644 index 00000000..cd59e469 --- /dev/null +++ b/feature-pooling-messages/src/main/resources/META-INF/services/org.onap.policy.drools.features.PolicyEngineFeatureApi @@ -0,0 +1 @@ +org.onap.policy.drools.pooling.PoolingFeature diff --git a/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/EndToEndFeatureTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/EndToEndFeatureTest.java new file mode 100644 index 00000000..31ad207c --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/EndToEndFeatureTest.java @@ -0,0 +1,810 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.onap.policy.drools.pooling.PoolingProperties.PREFIX; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import java.util.Arrays; +import java.util.Deque; +import java.util.IdentityHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import java.util.TreeMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure; +import org.onap.policy.common.endpoints.event.comm.TopicEndpointManager; +import org.onap.policy.common.endpoints.event.comm.TopicListener; +import org.onap.policy.common.endpoints.event.comm.TopicSink; +import org.onap.policy.common.endpoints.event.comm.TopicSource; +import org.onap.policy.common.endpoints.properties.PolicyEndPointProperties; +import org.onap.policy.drools.controller.DroolsController; +import org.onap.policy.drools.system.PolicyController; +import org.onap.policy.drools.system.PolicyEngine; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * End-to-end tests of the pooling feature. Launches one or more "hosts", each one having its own + * feature object. Uses real feature objects, as well as real DMaaP sources and sinks. However, the + * following are not: <dl> <dt>PolicyEngine, PolicyController, DroolsController</dt> <dd>mocked</dd> + * </dl> + * + * <p>The following fields must be set before executing this: <ul> <li>UEB_SERVERS</li> + * <li>INTERNAL_TOPIC</li> <li>EXTERNAL_TOPIC</li> </ul> + */ +public class EndToEndFeatureTest { + + private static final Logger logger = LoggerFactory.getLogger(EndToEndFeatureTest.class); + + /** + * UEB servers for both internal & external topics. + */ + private static final String UEB_SERVERS = "ueb-server"; + + /** + * Name of the topic used for inter-host communication. + */ + private static final String INTERNAL_TOPIC = "internal-topic"; + + /** + * Name of the topic from which "external" events "arrive". + */ + private static final String EXTERNAL_TOPIC = "external-topic"; + + /** + * Consumer group to use when polling the external topic. + */ + private static final String EXTERNAL_GROUP = EndToEndFeatureTest.class.getName(); + + /** + * Name of the controller. + */ + private static final String CONTROLLER1 = "controller.one"; + + /** + * Maximum number of items to fetch from DMaaP in a single poll. + */ + private static final String FETCH_LIMIT = "5"; + + private static final long STD_REACTIVATE_WAIT_MS = 10000; + private static final long STD_IDENTIFICATION_MS = 10000; + private static final long STD_START_HEARTBEAT_MS = 15000; + private static final long STD_ACTIVE_HEARTBEAT_MS = 12000; + private static final long STD_INTER_HEARTBEAT_MS = 5000; + private static final long STD_OFFLINE_PUB_WAIT_MS = 2; + private static final long EVENT_WAIT_SEC = 15; + + /** + * Used to decode events from the external topic. + */ + private static final Gson mapper = new Gson(); + + /** + * Used to identify the current host. + */ + private static final ThreadLocal<Host> currentHost = new ThreadLocal<Host>(); + + /** + * Sink for external DMaaP topic. + */ + private static TopicSink externalSink; + + /** + * Sink for internal DMaaP topic. + */ + private static TopicSink internalSink; + + /** + * Context for the current test case. + */ + private Context ctx; + + /** + * Setup before class. + * + */ + @BeforeAll + public static void setUpBeforeClass() { + externalSink = TopicEndpointManager.getManager().addTopicSinks(makeSinkProperties(EXTERNAL_TOPIC)).get(0); + externalSink.start(); + + internalSink = TopicEndpointManager.getManager().addTopicSinks(makeSinkProperties(INTERNAL_TOPIC)).get(0); + internalSink.start(); + } + + /** + * Tear down after class. + * + */ + @AfterAll + public static void tearDownAfterClass() { + externalSink.stop(); + internalSink.stop(); + } + + /** + * Setup. + */ + @BeforeEach + public void setUp() { + ctx = null; + } + + /** + * Tear down. + */ + @AfterEach + public void tearDown() { + if (ctx != null) { + ctx.destroy(); + } + } + + /* + * This test should only be run manually, after configuring all the fields, + * thus it is ignored. + */ + @Disabled + @Test + public void test_SingleHost() throws Exception { // NOSONAR + run(70, 1); + } + + /* + * This test should only be run manually, after configuring all the fields, + * thus it is ignored. + */ + @Disabled + @Test + public void test_TwoHosts() throws Exception { // NOSONAR + run(200, 2); + } + + /* + * This test should only be run manually, after configuring all the fields, + * thus it is ignored. + */ + @Disabled + @Test + public void test_ThreeHosts() throws Exception { // NOSONAR + run(200, 3); + } + + private void run(int nmessages, int nhosts) throws Exception { + ctx = new Context(nmessages); + + for (int x = 0; x < nhosts; ++x) { + ctx.addHost(); + } + + ctx.startHosts(); + ctx.awaitAllActive(STD_IDENTIFICATION_MS * 2); + + for (int x = 0; x < nmessages; ++x) { + ctx.offerExternal(makeMessage(x)); + } + + ctx.awaitEvents(EVENT_WAIT_SEC, TimeUnit.SECONDS); + + assertEquals(0, ctx.getDecodeErrors()); + assertEquals(0, ctx.getRemainingEvents()); + ctx.checkAllSawAMsg(); + } + + private String makeMessage(int reqnum) { + return "{\"reqid\":\"req" + reqnum + "\", \"data\":\"hello " + reqnum + "\"}"; + } + + private static Properties makeSinkProperties(String topic) { + Properties props = new Properties(); + + props.setProperty(PolicyEndPointProperties.PROPERTY_UEB_SINK_TOPICS, topic); + + props.setProperty(PolicyEndPointProperties.PROPERTY_UEB_SINK_TOPICS + "." + topic + + PolicyEndPointProperties.PROPERTY_TOPIC_SERVERS_SUFFIX, UEB_SERVERS); + props.setProperty(PolicyEndPointProperties.PROPERTY_UEB_SINK_TOPICS + "." + topic + + PolicyEndPointProperties.PROPERTY_TOPIC_SINK_PARTITION_KEY_SUFFIX, "0"); + props.setProperty(PolicyEndPointProperties.PROPERTY_UEB_SINK_TOPICS + "." + topic + + PolicyEndPointProperties.PROPERTY_MANAGED_SUFFIX, "false"); + + return props; + } + + private static Properties makeSourceProperties(String topic) { + Properties props = new Properties(); + + props.setProperty(PolicyEndPointProperties.PROPERTY_UEB_SOURCE_TOPICS, topic); + + props.setProperty(PolicyEndPointProperties.PROPERTY_UEB_SOURCE_TOPICS + "." + topic + + PolicyEndPointProperties.PROPERTY_TOPIC_SERVERS_SUFFIX, UEB_SERVERS); + props.setProperty(PolicyEndPointProperties.PROPERTY_UEB_SOURCE_TOPICS + "." + topic + + PolicyEndPointProperties.PROPERTY_TOPIC_SOURCE_FETCH_LIMIT_SUFFIX, FETCH_LIMIT); + props.setProperty(PolicyEndPointProperties.PROPERTY_UEB_SOURCE_TOPICS + "." + topic + + PolicyEndPointProperties.PROPERTY_MANAGED_SUFFIX, "false"); + + if (EXTERNAL_TOPIC.equals(topic)) { + // consumer group is a constant + props.setProperty(PolicyEndPointProperties.PROPERTY_UEB_SOURCE_TOPICS + "." + topic + + PolicyEndPointProperties.PROPERTY_TOPIC_SOURCE_CONSUMER_GROUP_SUFFIX, EXTERNAL_GROUP); + + // consumer instance is generated by the BusConsumer code + } + + // else internal topic: feature populates info for internal topic + + return props; + } + + /** + * Decodes an event. + * + * @param event event + * @return the decoded event, or {@code null} if it cannot be decoded + */ + private static Object decodeEvent(String event) { + try { + return mapper.fromJson(event, TreeMap.class); + + } catch (JsonParseException e) { + logger.warn("cannot decode external event", e); + return null; + } + } + + /** + * Context used for a single test case. + */ + private static class Context { + + /** + * Hosts that have been added to this context. + */ + private final Deque<Host> hosts = new LinkedList<>(); + + /** + * Maps a drools controller to its policy controller. + */ + private final IdentityHashMap<DroolsController, PolicyController> drools2policy = new IdentityHashMap<>(); + + /** + * Counts the number of decode errors. + */ + private final AtomicInteger decodeErrors = new AtomicInteger(0); + + /** + * Number of events we're still waiting to receive. + */ + private final CountDownLatch eventCounter; + + /** + * Constructor. + * + * @param events number of events to be processed + */ + public Context(int events) { + eventCounter = new CountDownLatch(events); + } + + /** + * Destroys the context, stopping any hosts that remain. + */ + public void destroy() { + stopHosts(); + hosts.clear(); + } + + /** + * Creates and adds a new host to the context. + * + * @return the new Host + */ + public Host addHost() { + Host host = new Host(this); + hosts.add(host); + + return host; + } + + /** + * Starts the hosts. + */ + public void startHosts() { + hosts.forEach(host -> host.start()); + } + + /** + * Stops the hosts. + */ + public void stopHosts() { + hosts.forEach(host -> host.stop()); + } + + /** + * Verifies that all hosts processed at least one message. + */ + public void checkAllSawAMsg() { + int msgs = 0; + for (Host host : hosts) { + assertTrue(host.messageSeen(), "msgs=" + msgs); + ++msgs; + } + } + + /** + * Offers an event to the external topic. + * + * @param event event + */ + public void offerExternal(String event) { + externalSink.send(event); + } + + /** + * Associates a controller with its drools controller. + * + * @param controller controller + * @param droolsController drools controller + */ + public void addController(PolicyController controller, DroolsController droolsController) { + drools2policy.put(droolsController, controller); + } + + /** + * Get controller. + * + * @param droolsController drools controller + * @return the controller associated with a drools controller, or {@code null} if it has no + * associated controller + */ + public PolicyController getController(DroolsController droolsController) { + return drools2policy.get(droolsController); + } + + /** + * Get decode errors. + * + * @return the number of decode errors so far + */ + public int getDecodeErrors() { + return decodeErrors.get(); + } + + /** + * Increments the count of decode errors. + */ + public void bumpDecodeErrors() { + decodeErrors.incrementAndGet(); + } + + /** + * Get remaining events. + * + * @return the number of events that haven't been processed + */ + public long getRemainingEvents() { + return eventCounter.getCount(); + } + + /** + * Adds an event to the counter. + */ + public void addEvent() { + eventCounter.countDown(); + } + + /** + * Waits, for a period of time, for all events to be processed. + * + * @param time time + * @param units units + * @return {@code true} if all events have been processed, {@code false} otherwise + * @throws InterruptedException throws interrupted exception + */ + public boolean awaitEvents(long time, TimeUnit units) throws InterruptedException { + return eventCounter.await(time, units); + } + + /** + * Waits, for a period of time, for all hosts to enter the Active state. + * + * @param timeMs maximum time to wait, in milliseconds + * @throws InterruptedException throws interrupted exception + */ + public void awaitAllActive(long timeMs) throws InterruptedException { + long tend = timeMs + System.currentTimeMillis(); + + for (Host host : hosts) { + long tremain = Math.max(0, tend - System.currentTimeMillis()); + assertTrue(host.awaitActive(tremain)); + } + } + } + + /** + * Simulates a single "host". + */ + private static class Host { + + private final PoolingFeature feature; + + /** + * {@code True} if this host has processed a message, {@code false} otherwise. + */ + private final AtomicBoolean sawMsg = new AtomicBoolean(false); + + private final TopicSource externalSource; + private final TopicSource internalSource; + + // mock objects + private final PolicyEngine engine = mock(PolicyEngine.class); + private final ListenerController controller = mock(ListenerController.class); + private final DroolsController drools = mock(DroolsController.class); + + /** + * Constructor. + * + * @param context context + */ + public Host(Context context) { + + when(controller.getName()).thenReturn(CONTROLLER1); + when(controller.getDrools()).thenReturn(drools); + + externalSource = TopicEndpointManager.getManager().addTopicSources(makeSourceProperties(EXTERNAL_TOPIC)) + .get(0); + internalSource = TopicEndpointManager.getManager().addTopicSources(makeSourceProperties(INTERNAL_TOPIC)) + .get(0); + + // stop consuming events if the controller stops + when(controller.stop()).thenAnswer(args -> { + externalSource.unregister(controller); + return true; + }); + + doAnswer(new MyExternalTopicListener(context, this)).when(controller).onTopicEvent(any(), any(), any()); + + context.addController(controller, drools); + + feature = new PoolingFeatureImpl(context, this); + } + + /** + * Waits, for a period of time, for the host to enter the Active state. + * + * @param timeMs time to wait, in milliseconds + * @return {@code true} if the host entered the Active state within the given amount of + * time, {@code false} otherwise + * @throws InterruptedException throws interrupted exception + */ + public boolean awaitActive(long timeMs) throws InterruptedException { + return feature.getActiveLatch().await(timeMs, TimeUnit.MILLISECONDS); + } + + /** + * Starts threads for the host so that it begins consuming from both the external "DMaaP" + * topic and its own internal "DMaaP" topic. + */ + public void start() { + feature.beforeStart(engine); + feature.afterCreate(controller); + + feature.beforeStart(controller); + + // start consuming events from the external topic + externalSource.register(controller); + + feature.afterStart(controller); + } + + /** + * Stops the host's threads. + */ + public void stop() { + feature.beforeStop(controller); + externalSource.unregister(controller); + feature.afterStop(controller); + } + + /** + * Offers an event to the feature, before the policy controller handles it. + * + * @param protocol protocol + * @param topic2 topic + * @param event event + * @return {@code true} if the event was handled, {@code false} otherwise + */ + public boolean beforeOffer(CommInfrastructure protocol, String topic2, String event) { + return feature.beforeOffer(controller, protocol, topic2, event); + } + + /** + * Offers an event to the feature, after the policy controller handles it. + * + * @param protocol protocol + * @param topic topic + * @param event event + * @param success success + * @return {@code true} if the event was handled, {@code false} otherwise + */ + public boolean afterOffer(CommInfrastructure protocol, String topic, String event, boolean success) { + + return feature.afterOffer(controller, protocol, topic, event, success); + } + + /** + * Offers an event to the feature, before the drools controller handles it. + * + * @param fact fact + * @return {@code true} if the event was handled, {@code false} otherwise + */ + public boolean beforeInsert(Object fact) { + return feature.beforeInsert(drools, fact); + } + + /** + * Offers an event to the feature, after the drools controller handles it. + * + * @param fact fact + * @param successInsert {@code true} if it was successfully inserted by the drools + * controller, {@code false} otherwise + * @return {@code true} if the event was handled, {@code false} otherwise + */ + public boolean afterInsert(Object fact, boolean successInsert) { + return feature.afterInsert(drools, fact, successInsert); + } + + /** + * Indicates that a message was seen for this host. + */ + public void sawMessage() { + sawMsg.set(true); + } + + /** + * Message seen. + * + * @return {@code true} if a message was seen for this host, {@code false} otherwise + */ + public boolean messageSeen() { + return sawMsg.get(); + } + } + + /** + * Listener for the external topic. Simulates the actions taken by + * <i>AggregatedPolicyController.onTopicEvent</i>. + */ + private static class MyExternalTopicListener implements Answer<Void> { + + private final Context context; + private final Host host; + + public MyExternalTopicListener(Context context, Host host) { + this.context = context; + this.host = host; + } + + @Override + public Void answer(InvocationOnMock args) throws Throwable { + int index = 0; + CommInfrastructure commType = args.getArgument(index++); + String topic = args.getArgument(index++); + String event = args.getArgument(index++); + + if (host.beforeOffer(commType, topic, event)) { + return null; + } + + boolean result; + Object fact = decodeEvent(event); + + if (fact == null) { + result = false; + context.bumpDecodeErrors(); + + } else { + result = true; + + if (!host.beforeInsert(fact)) { + // feature did not handle it so we handle it here + host.afterInsert(fact, result); + + host.sawMessage(); + context.addEvent(); + } + } + + host.afterOffer(commType, topic, event, result); + return null; + } + } + + /** + * Feature with overrides. + */ + private static class PoolingFeatureImpl extends PoolingFeature { + + private final Context context; + private final Host host; + + /** + * Constructor. + * + * @param context context + */ + public PoolingFeatureImpl(Context context, Host host) { + this.context = context; + this.host = host; + + /* + * Note: do NOT extract anything from "context" at this point, because it hasn't been + * fully initialized yet + */ + } + + @Override + public Properties getProperties(String featName) { + Properties props = new Properties(); + + props.setProperty(PoolingProperties.PROP_EXTRACTOR_PREFIX + ".java.util.Map", "${reqid}"); + + props.setProperty(specialize(PoolingProperties.FEATURE_ENABLED, CONTROLLER1), "true"); + props.setProperty(specialize(PoolingProperties.POOLING_TOPIC, CONTROLLER1), INTERNAL_TOPIC); + props.setProperty(specialize(PoolingProperties.OFFLINE_LIMIT, CONTROLLER1), "10000"); + props.setProperty(specialize(PoolingProperties.OFFLINE_AGE_MS, CONTROLLER1), "1000000"); + props.setProperty(specialize(PoolingProperties.OFFLINE_PUB_WAIT_MS, CONTROLLER1), + "" + STD_OFFLINE_PUB_WAIT_MS); + props.setProperty(specialize(PoolingProperties.START_HEARTBEAT_MS, CONTROLLER1), + "" + STD_START_HEARTBEAT_MS); + props.setProperty(specialize(PoolingProperties.REACTIVATE_MS, CONTROLLER1), "" + STD_REACTIVATE_WAIT_MS); + props.setProperty(specialize(PoolingProperties.IDENTIFICATION_MS, CONTROLLER1), "" + STD_IDENTIFICATION_MS); + props.setProperty(specialize(PoolingProperties.ACTIVE_HEARTBEAT_MS, CONTROLLER1), + "" + STD_ACTIVE_HEARTBEAT_MS); + props.setProperty(specialize(PoolingProperties.INTER_HEARTBEAT_MS, CONTROLLER1), + "" + STD_INTER_HEARTBEAT_MS); + + props.putAll(makeSinkProperties(INTERNAL_TOPIC)); + props.putAll(makeSourceProperties(INTERNAL_TOPIC)); + + return props; + } + + @Override + public PolicyController getController(DroolsController droolsController) { + return context.getController(droolsController); + } + + /** + * Embeds a specializer within a property name, after the prefix. + * + * @param propnm property name into which it should be embedded + * @param spec specializer to be embedded + * @return the property name, with the specializer embedded within it + */ + private String specialize(String propnm, String spec) { + String suffix = propnm.substring(PREFIX.length()); + return PREFIX + spec + "." + suffix; + } + + @Override + protected PoolingManagerImpl makeManager(String hostName, PolicyController controller, PoolingProperties props, + CountDownLatch activeLatch) { + + /* + * Set this before creating the test, because the test's superclass + * constructor uses it before the test object has a chance to store it. + */ + currentHost.set(host); + + return new PoolingManagerTest(hostName, controller, props, activeLatch); + } + } + + /** + * Pooling Manager with overrides. + */ + private static class PoolingManagerTest extends PoolingManagerImpl { + + /** + * Constructor. + * + * @param hostName the host + * @param controller the controller + * @param props the properties + * @param activeLatch the latch + */ + public PoolingManagerTest(String hostName, PolicyController controller, + PoolingProperties props, CountDownLatch activeLatch) { + + super(hostName, controller, props, activeLatch); + } + + @Override + protected TopicMessageManager makeTopicMessagesManager(String topic) throws PoolingFeatureException { + return new TopicMessageManagerImpl(topic); + } + + @Override + protected boolean canDecodeEvent(DroolsController drools, String topic) { + return true; + } + + @Override + protected Object decodeEventWrapper(DroolsController drools, String topic, String event) { + return decodeEvent(event); + } + } + + /** + * DMaaP Manager with overrides. + */ + private static class TopicMessageManagerImpl extends TopicMessageManager { + + /** + * Constructor. + * + * @param topic the topic + * @throws PoolingFeatureException if an error occurs + */ + public TopicMessageManagerImpl(String topic) throws PoolingFeatureException { + super(topic); + } + + @Override + protected List<TopicSource> getTopicSources() { + Host host = currentHost.get(); + return Arrays.asList(host.internalSource, host.externalSource); + } + + @Override + protected List<TopicSink> getTopicSinks() { + return Arrays.asList(internalSink, externalSink); + } + } + + /** + * Controller that also implements the {@link TopicListener} interface. + */ + private static interface ListenerController extends PolicyController, TopicListener { + + } +} diff --git a/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/FeatureTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/FeatureTest.java new file mode 100644 index 00000000..e9bd3cb5 --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/FeatureTest.java @@ -0,0 +1,1033 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2020, 2024 Nordix Foundation + * ================================================================================ + * 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.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.onap.policy.drools.pooling.PoolingProperties.PREFIX; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import java.util.Deque; +import java.util.IdentityHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import java.util.TreeMap; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import lombok.Getter; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.onap.policy.common.endpoints.event.comm.Topic; +import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure; +import org.onap.policy.common.endpoints.event.comm.TopicListener; +import org.onap.policy.common.endpoints.event.comm.TopicSink; +import org.onap.policy.common.endpoints.event.comm.TopicSource; +import org.onap.policy.drools.controller.DroolsController; +import org.onap.policy.drools.system.PolicyController; +import org.onap.policy.drools.system.PolicyEngine; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * End-to-end tests of the pooling feature. Launches one or more "hosts", each one having + * its own feature object. Uses real feature objects. However, the following are not: + * <dl> + * <dt>DMaaP sources and sinks</dt> + * <dd>simulated using queues. There is one queue for the external topic, and one queue + * for each host's internal topic. Messages published to the "admin" channel are simply + * sent to all of the hosts' internal topic queues</dd> + * <dt>PolicyEngine, PolicyController, DroolsController</dt> + * <dd>mocked</dd> + * </dl> + * + * <p>Invoke {@link #runSlow()}, before the test, to slow things down. + */ + +class FeatureTest { + private static final Logger logger = LoggerFactory.getLogger(FeatureTest.class); + /** + * Name of the topic used for inter-host communication. + */ + private static final String INTERNAL_TOPIC = "my.internal.topic"; + /** + * Name of the topic from which "external" events "arrive". + */ + private static final String EXTERNAL_TOPIC = "my.external.topic"; + /** + * Name of the controller. + */ + private static final String CONTROLLER1 = "controller.one"; + private static long stdReactivateWaitMs = 200; + private static long stdIdentificationMs = 60; + private static long stdStartHeartbeatMs = 60; + private static long stdActiveHeartbeatMs = 50; + private static long stdInterHeartbeatMs = 5; + private static long stdOfflinePubWaitMs = 2; + private static long stdPollMs = 2; + private static long stdInterPollMs = 2; + private static long stdEventWaitSec = 10; + /** + * Used to decode events from the external topic. + */ + private static final Gson mapper = new Gson(); + /** + * Used to identify the current context. + */ + private static final ThreadLocal<Context> currentContext = new ThreadLocal<Context>(); + /** + * Context for the current test case. + */ + private Context ctx; + + /** + * Setup. + */ + + @BeforeEach + public void setUp() { + ctx = null; + } + + /** + * Tear down. + */ + + @AfterEach + public void tearDown() { + if (ctx != null) { + ctx.destroy(); + } + } + + @Test + void test_SingleHost() throws Exception { + run(70, 1); + } + + @Test + void test_TwoHosts() throws Exception { + run(200, 2); + } + + @Test + void test_ThreeHosts() throws Exception { + run(200, 3); + } + + private void run(int nmessages, int nhosts) throws Exception { + ctx = new Context(nmessages); + for (int x = 0; x < nhosts; ++x) { + ctx.addHost(); + } + ctx.startHosts(); + for (int x = 0; x < nmessages; ++x) { + ctx.offerExternal(makeMessage(x)); + } + ctx.awaitEvents(stdEventWaitSec, TimeUnit.SECONDS); + assertEquals(0, ctx.getDecodeErrors()); + assertEquals(0, ctx.getRemainingEvents()); + ctx.checkAllSawAMsg(); + } + + private String makeMessage(int reqnum) { + return "{\"reqid\":\"req" + reqnum + "\", \"data\":\"hello " + reqnum + "\"}"; + } + + /** + * Invoke this to slow the timers down. + */ + + protected static void runSlow() { + stdReactivateWaitMs = 10000; + stdIdentificationMs = 10000; + stdStartHeartbeatMs = 15000; + stdActiveHeartbeatMs = 12000; + stdInterHeartbeatMs = 5000; + stdOfflinePubWaitMs = 2; + stdPollMs = 2; + stdInterPollMs = 2000; + stdEventWaitSec = 1000; + } + + /** + * Decodes an event. + * + * @param event event + * @return the decoded event, or {@code null} if it cannot be decoded + */ + + private static Object decodeEvent(String event) { + try { + return mapper.fromJson(event, TreeMap.class); + } catch (JsonParseException e) { + logger.warn("cannot decode external event", e); + return null; + } + } + + /** + * Context used for a single test case. + */ + + private static class Context { + /** + * Hosts that have been added to this context. + */ + private final Deque<Host> hosts = new LinkedList<>(); + /** + * Maps a drools controller to its policy controller. + */ + private final IdentityHashMap<DroolsController, PolicyController> drools2policy = new IdentityHashMap<>(); + /** + * Maps a channel to its queue. Does <i>not</i> include the "admin" channel. + */ + private final ConcurrentMap<String, BlockingQueue<String>> channel2queue = new ConcurrentHashMap<>(7); + /** + * Counts the number of decode errors. + */ + private final AtomicInteger numDecodeErrors = new AtomicInteger(0); + /** + * Number of events we're still waiting to receive. + */ + private final CountDownLatch eventCounter; + + /** + * The current host. Set by {@link #withHost(Host, VoidFunction)} and used by + * {@link #getCurrentHost()}. + */ + @Getter + private Host currentHost = null; + + /** + * Constructor. + * + * @param events number of events to be processed + */ + + public Context(int events) { + eventCounter = new CountDownLatch(events); + } + + /** + * Destroys the context, stopping any hosts that remain. + */ + + public void destroy() { + stopHosts(); + hosts.clear(); + } + + /** + * Creates and adds a new host to the context. + * + * @return the new Host + */ + + public Host addHost() { + Host host = new Host(this); + hosts.add(host); + return host; + } + + /** + * Starts the hosts. + */ + + public void startHosts() { + hosts.forEach(host -> host.start()); + } + + /** + * Stops the hosts. + */ + + public void stopHosts() { + hosts.forEach(host -> host.stop()); + } + + /** + * Verifies that all hosts processed at least one message. + */ + + public void checkAllSawAMsg() { + int msgs = 0; + for (Host host : hosts) { + assertTrue(host.messageSeen(), "msgs=" + msgs); + ++msgs; + } + } + + /** + * Sets {@link #currentHost} to the specified host, and then invokes the given + * function. Resets {@link #currentHost} to {@code null} before returning. + * + * @param host host + * @param func function to invoke + */ + + public void withHost(Host host, VoidFunction func) { + currentHost = host; + func.apply(); + currentHost = null; + } + + /** + * Offers an event to the external topic. As each host needs a copy, it is posted + * to each Host's queue. + * + * @param event event + */ + + public void offerExternal(String event) { + for (Host host : hosts) { + host.getExternalTopic().offer(event); + } + } + + /** + * Adds an internal channel to the set of channels. + * + * @param channel channel + * @param queue the channel's queue + */ + + public void addInternal(String channel, BlockingQueue<String> queue) { + channel2queue.put(channel, queue); + } + + /** + * Offers a message to all internal channels. + * + * @param message message + */ + + public void offerInternal(String message) { + channel2queue.values().forEach(queue -> queue.offer(message)); + } + + /** + * Associates a controller with its drools controller. + * + * @param controller controller + * @param droolsController drools controller + */ + + public void addController(PolicyController controller, DroolsController droolsController) { + drools2policy.put(droolsController, controller); + } + + /** + * Get controller. + * + * @param droolsController drools controller + * @return the controller associated with a drools controller, or {@code null} if + * it has no associated controller + */ + + public PolicyController getController(DroolsController droolsController) { + return drools2policy.get(droolsController); + } + + /** + * Get decode errors. + * + * @return the number of decode errors so far + */ + + public int getDecodeErrors() { + return numDecodeErrors.get(); + } + + /** + * Increments the count of decode errors. + */ + + public void bumpDecodeErrors() { + numDecodeErrors.incrementAndGet(); + } + + /** + * Get remaining events. + * + * @return the number of events that haven't been processed + */ + + public long getRemainingEvents() { + return eventCounter.getCount(); + } + + /** + * Adds an event to the counter. + */ + + public void addEvent() { + eventCounter.countDown(); + } + + /** + * Waits, for a period of time, for all events to be processed. + * + * @param time time + * @param units units + * @return {@code true} if all events have been processed, {@code false} otherwise + * @throws InterruptedException throws interrupted + */ + + public boolean awaitEvents(long time, TimeUnit units) throws InterruptedException { + return eventCounter.await(time, units); + } + + } + + /** + * Simulates a single "host". + */ + + private static class Host { + private final Context context; + private final PoolingFeature feature; + + /** + * {@code True} if this host has processed a message, {@code false} otherwise. + */ + + private final AtomicBoolean sawMsg = new AtomicBoolean(false); + + /** + * This host's internal "DMaaP" topic. + */ + + private final BlockingQueue<String> msgQueue = new LinkedBlockingQueue<>(); + + /** + * Queue for the external "DMaaP" topic. + */ + @Getter + private final BlockingQueue<String> externalTopic = new LinkedBlockingQueue<String>(); + + /** + * Source that reads from the external topic and posts to the listener. + */ + + private TopicSource externalSource; + + // mock objects + private final PolicyEngine engine = mock(PolicyEngine.class); + private final ListenerController controller = mock(ListenerController.class); + private final DroolsController drools = mock(DroolsController.class); + + /** + * Constructor. + * + * @param context context + */ + + public Host(Context context) { + this.context = context; + when(controller.getName()).thenReturn(CONTROLLER1); + when(controller.getDrools()).thenReturn(drools); + // stop consuming events if the controller stops + when(controller.stop()).thenAnswer(args -> { + externalSource.unregister(controller); + return true; + }); + doAnswer(new MyExternalTopicListener(context, this)).when(controller).onTopicEvent(any(), any(), any()); + context.addController(controller, drools); + // arrange to read from the external topic + externalSource = new TopicSourceImpl(EXTERNAL_TOPIC, externalTopic); + feature = new PoolingFeatureImpl(context); + } + + /** + * Get name. + * + * @return the host name + */ + + public String getName() { + return feature.getHost(); + } + + /** + * Starts threads for the host so that it begins consuming from both the external + * "DMaaP" topic and its own internal "DMaaP" topic. + */ + + public void start() { + context.withHost(this, () -> { + feature.beforeStart(engine); + feature.afterCreate(controller); + // assign the queue for this host's internal topic + context.addInternal(getName(), msgQueue); + feature.beforeStart(controller); + // start consuming events from the external topic + externalSource.register(controller); + feature.afterStart(controller); + }); + } + + /** + * Stops the host's threads. + */ + + public void stop() { + feature.beforeStop(controller); + externalSource.unregister(controller); + feature.afterStop(controller); + } + + /** + * Offers an event to the feature, before the policy controller handles it. + * + * @param protocol protocol + * @param topic2 topic + * @param event event + * @return {@code true} if the event was handled, {@code false} otherwise + */ + + public boolean beforeOffer(CommInfrastructure protocol, String topic2, String event) { + return feature.beforeOffer(controller, protocol, topic2, event); + } + + /** + * Offers an event to the feature, after the policy controller handles it. + * + * @param protocol protocol + * @param topic topic + * @param event event + * @param success success + * @return {@code true} if the event was handled, {@code false} otherwise + */ + + public boolean afterOffer(CommInfrastructure protocol, String topic, String event, boolean success) { + return feature.afterOffer(controller, protocol, topic, event, success); + } + + /** + * Offers an event to the feature, before the drools controller handles it. + * + * @param fact fact + * @return {@code true} if the event was handled, {@code false} otherwise + */ + + public boolean beforeInsert(Object fact) { + return feature.beforeInsert(drools, fact); + } + + /** + * Offers an event to the feature, after the drools controller handles it. + * + * @param fact fact + * @param successInsert {@code true} if it was successfully inserted by the drools + * controller, {@code false} otherwise + * @return {@code true} if the event was handled, {@code false} otherwise + */ + + public boolean afterInsert(Object fact, boolean successInsert) { + return feature.afterInsert(drools, fact, successInsert); + } + + /** + * Indicates that a message was seen for this host. + */ + + public void sawMessage() { + sawMsg.set(true); + } + + /** + * Message seen. + * + * @return {@code true} if a message was seen for this host, {@code false} otherwise + */ + + public boolean messageSeen() { + return sawMsg.get(); + } + + /** + * Get internal queue. + * + * @return the queue associated with this host's internal topic + */ + + public BlockingQueue<String> getInternalQueue() { + return msgQueue; + } + } + + /** + * Listener for the external topic. Simulates the actions taken by + * <i>AggregatedPolicyController.onTopicEvent</i>. + */ + + private static class MyExternalTopicListener implements Answer<Void> { + private final Context context; + private final Host host; + + public MyExternalTopicListener(Context context, Host host) { + this.context = context; + this.host = host; + } + + @Override + public Void answer(InvocationOnMock args) throws Throwable { + int index = 0; + CommInfrastructure commType = args.getArgument(index++); + String topic = args.getArgument(index++); + String event = args.getArgument(index++); + if (host.beforeOffer(commType, topic, event)) { + return null; + } + boolean result; + Object fact = decodeEvent(event); + if (fact == null) { + result = false; + context.bumpDecodeErrors(); + } else { + result = true; + if (!host.beforeInsert(fact)) { + // feature did not handle it so we handle it here + host.afterInsert(fact, result); + host.sawMessage(); + context.addEvent(); + } + } + host.afterOffer(commType, topic, event, result); + return null; + } + } + + /** + * Sink implementation that puts a message on the queue specified by the + * <i>channel</i> embedded within the message. If it's the "admin" channel, then the + * message is placed on all queues. + */ + + private static class TopicSinkImpl extends TopicImpl implements TopicSink { + private final Context context; + + /** + * Constructor. + * + * @param context context + */ + + public TopicSinkImpl(Context context) { + this.context = context; + } + + @Override + public synchronized boolean send(String message) { + if (!isAlive()) { + return false; + } + try { + context.offerInternal(message); + return true; + } catch (JsonParseException e) { + logger.warn("could not decode message: {}", message); + context.bumpDecodeErrors(); + return false; + } + } + } + + /** + * Source implementation that reads from a queue associated with a topic. + */ + + private static class TopicSourceImpl extends TopicImpl implements TopicSource { + + private final String topic; + /** + * Queue from which to retrieve messages. + */ + private final BlockingQueue<String> queue; + /** + * Manages the current consumer thread. The "first" item is used as a trigger to + * tell the thread to stop processing, while the "second" item is triggered <i>by + * the thread</i> when it completes. + */ + private AtomicReference<Pair<CountDownLatch, CountDownLatch>> pair = new AtomicReference<>(null); + + /** + * Constructor. + * + * @param type topic type + * @param queue topic from which to read + */ + + public TopicSourceImpl(String type, BlockingQueue<String> queue) { + this.topic = type; + this.queue = queue; + } + + @Override + public String getTopic() { + return topic; + } + + @Override + public boolean offer(String event) { + throw new UnsupportedOperationException("offer topic source"); + } + + /** + * Starts a thread that takes messages from the queue and gives them to the + * listener. Stops the thread of any previously registered listener. + */ + + @Override + public void register(TopicListener listener) { + Pair<CountDownLatch, CountDownLatch> newPair = Pair.of(new CountDownLatch(1), new CountDownLatch(1)); + reregister(newPair); + Thread thread = new Thread(() -> { + try { + do { + processMessages(newPair.getLeft(), listener); + } while (!newPair.getLeft().await(stdInterPollMs, TimeUnit.MILLISECONDS)); + logger.info("topic source thread completed"); + } catch (InterruptedException e) { + logger.warn("topic source thread aborted", e); + Thread.currentThread().interrupt(); + } catch (RuntimeException e) { + logger.warn("topic source thread aborted", e); + } + newPair.getRight().countDown(); + }); + thread.setDaemon(true); + thread.start(); + } + + /** + * Stops the thread of <i>any</i> currently registered listener. + */ + + @Override + public void unregister(TopicListener listener) { + reregister(null); + } + + /** + * Registers a new "pair" with this source, stopping the consumer associated with + * any previous registration. + * + * @param newPair the new "pair", or {@code null} to unregister + */ + + private void reregister(Pair<CountDownLatch, CountDownLatch> newPair) { + try { + Pair<CountDownLatch, CountDownLatch> oldPair = pair.getAndSet(newPair); + if (oldPair == null) { + if (newPair == null) { + // unregister was invoked twice in a row + logger.warn("re-unregister for topic source"); + } + // no previous thread to stop + return; + } + // need to stop the previous thread + // tell it to stop + oldPair.getLeft().countDown(); + // wait for it to stop + if (!oldPair.getRight().await(2, TimeUnit.SECONDS)) { + logger.warn("old topic registration is still running"); + } + } catch (InterruptedException e) { + logger.warn("old topic registration may still be running", e); + Thread.currentThread().interrupt(); + } + if (newPair != null) { + // register was invoked twice in a row + logger.warn("re-register for topic source"); + } + } + + /** + * Polls for messages from the topic and offers them to the listener. + * + * @param stopped triggered if processing should stop + * @param listener listener + * @throws InterruptedException throws interrupted exception + */ + + private void processMessages(CountDownLatch stopped, TopicListener listener) throws InterruptedException { + for (int x = 0; x < 5 && stopped.getCount() > 0; ++x) { + String msg = queue.poll(stdPollMs, TimeUnit.MILLISECONDS); + if (msg == null) { + return; + } + listener.onTopicEvent(CommInfrastructure.UEB, topic, msg); + } + } + } + + /** + * Topic implementation. Most methods just throw + * {@link UnsupportedOperationException}. + */ + + private static class TopicImpl implements Topic { + + /** + * Constructor. + */ + + public TopicImpl() { + super(); + } + + @Override + public String getTopic() { + return INTERNAL_TOPIC; + } + + @Override + public String getEffectiveTopic() { + return INTERNAL_TOPIC; + } + + @Override + public CommInfrastructure getTopicCommInfrastructure() { + throw new UnsupportedOperationException("topic protocol"); + } + + @Override + public List<String> getServers() { + throw new UnsupportedOperationException("topic servers"); + } + + @Override + public String[] getRecentEvents() { + throw new UnsupportedOperationException("topic events"); + } + + @Override + public void register(TopicListener topicListener) { + throw new UnsupportedOperationException("register topic"); + } + + @Override + public void unregister(TopicListener topicListener) { + throw new UnsupportedOperationException("unregister topic"); + } + + @Override + public synchronized boolean start() { + return true; + } + + @Override + public synchronized boolean stop() { + return true; + } + + @Override + public synchronized void shutdown() { + // do nothing + } + + @Override + public synchronized boolean isAlive() { + return true; + } + + @Override + public boolean lock() { + throw new UnsupportedOperationException("lock topicink"); + } + + @Override + public boolean unlock() { + throw new UnsupportedOperationException("unlock topic"); + } + + @Override + public boolean isLocked() { + throw new UnsupportedOperationException("topic isLocked"); + } + } + + /** + * Feature with overrides. + */ + + private static class PoolingFeatureImpl extends PoolingFeature { + private final Context context; + + /** + * Constructor. + * + * @param context context + */ + + public PoolingFeatureImpl(Context context) { + this.context = context; + /* + * Note: do NOT extract anything from "context" at this point, because it + * hasn't been fully initialized yet + */ + } + + @Override + public Properties getProperties(String featName) { + Properties props = new Properties(); + props.setProperty(PoolingProperties.PROP_EXTRACTOR_PREFIX + ".java.util.Map", "${reqid}"); + props.setProperty(specialize(PoolingProperties.FEATURE_ENABLED, CONTROLLER1), "true"); + props.setProperty(specialize(PoolingProperties.POOLING_TOPIC, CONTROLLER1), INTERNAL_TOPIC); + props.setProperty(specialize(PoolingProperties.OFFLINE_LIMIT, CONTROLLER1), "10000"); + props.setProperty(specialize(PoolingProperties.OFFLINE_AGE_MS, CONTROLLER1), "1000000"); + props.setProperty(specialize(PoolingProperties.OFFLINE_PUB_WAIT_MS, CONTROLLER1), "" + stdOfflinePubWaitMs); + props.setProperty(specialize(PoolingProperties.START_HEARTBEAT_MS, CONTROLLER1), "" + stdStartHeartbeatMs); + props.setProperty(specialize(PoolingProperties.REACTIVATE_MS, CONTROLLER1), "" + stdReactivateWaitMs); + props.setProperty(specialize(PoolingProperties.IDENTIFICATION_MS, CONTROLLER1), "" + stdIdentificationMs); + props.setProperty(specialize(PoolingProperties.ACTIVE_HEARTBEAT_MS, CONTROLLER1), + "" + stdActiveHeartbeatMs); + props.setProperty(specialize(PoolingProperties.INTER_HEARTBEAT_MS, CONTROLLER1), "" + stdInterHeartbeatMs); + return props; + } + + @Override + public PolicyController getController(DroolsController droolsController) { + return context.getController(droolsController); + } + + /** + * Embeds a specializer within a property name, after the prefix. + * + * @param propnm property name into which it should be embedded + * @param spec specializer to be embedded + * @return the property name, with the specializer embedded within it + */ + + private String specialize(String propnm, String spec) { + String suffix = propnm.substring(PREFIX.length()); + return PREFIX + spec + "." + suffix; + } + + @Override + protected PoolingManagerImpl makeManager(String host, PolicyController controller, PoolingProperties props, + CountDownLatch activeLatch) { + currentContext.set(context); + return new PoolingManagerTest(host, controller, props, activeLatch); + } + } + + /** + * Pooling Manager with overrides. + */ + + private static class PoolingManagerTest extends PoolingManagerImpl { + + /** + * Constructor. + * + * @param host the host + * @param controller the controller + * @param props the properties + * @param activeLatch the latch + */ + + public PoolingManagerTest(String host, PolicyController controller, PoolingProperties props, + CountDownLatch activeLatch) { + super(host, controller, props, activeLatch); + } + + @Override + protected TopicMessageManager makeTopicMessagesManager(String topic) throws PoolingFeatureException { + return new TopicMessageManagerImpl(topic); + } + + @Override + protected boolean canDecodeEvent(DroolsController drools, String topic) { + return true; + } + + @Override + protected Object decodeEventWrapper(DroolsController drools, String topic, String event) { + return decodeEvent(event); + } + } + + /** + * DMaaP Manager with overrides. + */ + + private static class TopicMessageManagerImpl extends TopicMessageManager { + + /** + * Constructor. + * + * @param topic the topic + * @throws PoolingFeatureException if an error occurs + */ + + public TopicMessageManagerImpl(String topic) throws PoolingFeatureException { + super(topic); + } + + @Override + protected List<TopicSource> getTopicSources() { + return List.of( + new TopicSourceImpl(INTERNAL_TOPIC, currentContext.get().getCurrentHost().getInternalQueue())); + } + + @Override + protected List<TopicSink> getTopicSinks() { + return List.of(new TopicSinkImpl(currentContext.get())); + } + } + + /** + * Controller that also implements the {@link TopicListener} interface. + */ + + private static interface ListenerController extends PolicyController, TopicListener { + } + + /** + * Simple function that takes no arguments and returns nothing. + */ + + @FunctionalInterface + private static interface VoidFunction { + void apply(); + } +} diff --git a/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureExceptionTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureExceptionTest.java new file mode 100644 index 00000000..1bfba19c --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureExceptionTest.java @@ -0,0 +1,36 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.onap.policy.common.utils.test.ExceptionsTester; + +class PoolingFeatureExceptionTest extends ExceptionsTester { + + @Test + void test() { + assertEquals(5, test(PoolingFeatureException.class)); + } + +} diff --git a/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureRtExceptionTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureRtExceptionTest.java new file mode 100644 index 00000000..a4305a7f --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureRtExceptionTest.java @@ -0,0 +1,36 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.onap.policy.common.utils.test.ExceptionsTester; + +class PoolingFeatureRtExceptionTest extends ExceptionsTester { + + @Test + void test() { + assertEquals(5, test(PoolingFeatureRtException.class)); + } + +} diff --git a/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureTest.java new file mode 100644 index 00000000..1b05e021 --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/PoolingFeatureTest.java @@ -0,0 +1,548 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018, 2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2020, 2024 Nordix Foundation + * ================================================================================ + * 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.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.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.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure; +import org.onap.policy.common.endpoints.event.comm.TopicSink; +import org.onap.policy.common.endpoints.event.comm.TopicSource; +import org.onap.policy.drools.controller.DroolsController; +import org.onap.policy.drools.system.PolicyController; +import org.onap.policy.drools.system.PolicyEngine; + +class PoolingFeatureTest { + + 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(); + + private Properties props; + private PolicyEngine engine; + 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 PoolingFeature pool; + + /** + * Setup. + * + * @throws Exception exception + */ + @BeforeEach + public void setUp() throws Exception { + props = initProperties(); + engine = mock(PolicyEngine.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<>(); + + 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); + + pool = new PoolingFeatureImpl(); + + pool.beforeStart(engine); + + pool.afterCreate(controller1); + pool.afterCreate(controller2); + + mgr1 = managers.get(0).getLeft(); + mgr2 = managers.get(1).getLeft(); + } + + @Test + void test() { + assertEquals(2, managers.size()); + } + + @Test + void testGetHost() { + String host = pool.getHost(); + assertNotNull(host); + + // create another and ensure it generates another host name + pool = new PoolingFeatureImpl(); + String host2 = pool.getHost(); + assertNotNull(host2); + + assertNotEquals(host, host2); + } + + @Test + void testGetSequenceNumber() { + assertEquals(0, pool.getSequenceNumber()); + } + + @Test + void testBeforeStartEngine() { + pool = new PoolingFeatureImpl(); + + assertFalse(pool.beforeStart(engine)); + } + + @Test + void testAfterCreate() { + managers.clear(); + pool = new PoolingFeatureImpl(); + pool.beforeStart(engine); + + 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 + void testAfterCreate_NotEnabled() { + managers.clear(); + pool = new PoolingFeatureImpl(); + pool.beforeStart(engine); + + assertFalse(pool.afterCreate(controllerDisabled)); + assertTrue(managers.isEmpty()); + } + + @Test + void testAfterCreate_PropertyEx() { + managers.clear(); + pool = new PoolingFeatureImpl(); + pool.beforeStart(engine); + + assertThrows(PoolingFeatureRtException.class, () -> pool.afterCreate(controllerException)); + } + + @Test + void testAfterCreate_NoProps() { + pool = new PoolingFeatureImpl(); + + // did not perform globalInit, which is an error + + assertThrows(PoolingFeatureRtException.class, () -> pool.afterCreate(controller1)); + } + + @Test + void testAfterCreate_NoFeatProps() { + managers.clear(); + pool = new PoolingFeatureImpl(); + pool.beforeStart(engine); + + assertFalse(pool.afterCreate(controllerUnknown)); + assertTrue(managers.isEmpty()); + } + + @Test + 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 + 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 + 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 + void testAfterStop() { + assertFalse(pool.afterStop(controller1)); + verify(mgr1).afterStop(); + + assertFalse(pool.afterStop(controllerDisabled)); + + // count should be unchanged + verify(mgr1).afterStop(); + } + + @Test + void testAfterHalt() { + assertFalse(pool.afterHalt(controller1)); + assertFalse(pool.afterHalt(controller1)); + + verify(mgr1, never()).afterStop(); + + assertFalse(pool.afterStop(controllerDisabled)); + } + + @Test + void testAfterShutdown() { + assertFalse(pool.afterShutdown(controller1)); + assertFalse(pool.afterShutdown(controller1)); + + verify(mgr1, never()).afterStop(); + + assertFalse(pool.afterStop(controllerDisabled)); + } + + @Test + 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 + 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 + void testBeforeOffer() { + assertFalse(pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC1, EVENT1)); + verify(mgr1).beforeOffer(TOPIC1, EVENT1); + + // ensure that the args were captured + pool.beforeInsert(drools1, OBJECT1); + verify(mgr1).beforeInsert(TOPIC1, OBJECT1); + + + // ensure it's still in the map by re-invoking + assertFalse(pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC2, EVENT2)); + verify(mgr1).beforeOffer(TOPIC2, EVENT2); + + // ensure that the new args were captured + pool.beforeInsert(drools1, OBJECT2); + verify(mgr1).beforeInsert(TOPIC2, OBJECT2); + + + assertFalse(pool.beforeOffer(controllerDisabled, CommInfrastructure.UEB, TOPIC1, EVENT1)); + } + + @Test + void testBeforeOffer_NotFound() { + assertFalse(pool.beforeOffer(controllerDisabled, CommInfrastructure.UEB, TOPIC1, EVENT1)); + } + + @Test + void testBeforeOffer_MgrTrue() { + + // manager will return true + when(mgr1.beforeOffer(any(), any())).thenReturn(true); + + assertTrue(pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC1, EVENT1)); + verify(mgr1).beforeOffer(TOPIC1, EVENT1); + + // ensure it's still in the map by re-invoking + assertTrue(pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC2, EVENT2)); + verify(mgr1).beforeOffer(TOPIC2, EVENT2); + + assertFalse(pool.beforeOffer(controllerDisabled, CommInfrastructure.UEB, TOPIC1, EVENT1)); + } + + @Test + void testBeforeInsert() { + pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC1, EVENT1); + assertFalse(pool.beforeInsert(drools1, OBJECT1)); + verify(mgr1).beforeInsert(TOPIC1, 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(TOPIC2, OBJECT2); + + pool.beforeOffer(controllerDisabled, CommInfrastructure.UEB, TOPIC2, EVENT2); + assertFalse(pool.beforeInsert(droolsDisabled, OBJECT1)); + } + + @Test + void testBeforeInsert_NoArgs() { + + // call beforeInsert without beforeOffer + assertFalse(pool.beforeInsert(drools1, OBJECT1)); + verify(mgr1, never()).beforeInsert(any(), any()); + + assertFalse(pool.beforeInsert(droolsDisabled, OBJECT1)); + verify(mgr1, never()).beforeInsert(any(), any()); + } + + @Test + void testBeforeInsert_ArgEx() { + // generate exception + pool = new PoolingFeatureImpl() { + @Override + protected PolicyController getController(DroolsController droolsController) { + throw new IllegalArgumentException(); + } + }; + + pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC1, EVENT1); + assertFalse(pool.beforeInsert(drools1, OBJECT1)); + verify(mgr1, never()).beforeInsert(any(), any()); + } + + @Test + void testBeforeInsert_StateEx() { + // generate exception + pool = new PoolingFeatureImpl() { + @Override + protected PolicyController getController(DroolsController droolsController) { + throw new IllegalStateException(); + } + }; + + pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC1, EVENT1); + assertFalse(pool.beforeInsert(drools1, OBJECT1)); + verify(mgr1, never()).beforeInsert(any(), any()); + } + + @Test + void testBeforeInsert_NullController() { + + // return null controller + pool = new PoolingFeatureImpl() { + @Override + protected PolicyController getController(DroolsController droolsController) { + return null; + } + }; + + pool.beforeOffer(controller1, CommInfrastructure.UEB, TOPIC1, EVENT1); + assertFalse(pool.beforeInsert(drools1, OBJECT1)); + verify(mgr1, never()).beforeInsert(any(), any()); + } + + @Test + void testBeforeInsert_NotFound() { + + pool.beforeOffer(controllerDisabled, CommInfrastructure.UEB, TOPIC2, EVENT2); + assertFalse(pool.beforeInsert(droolsDisabled, OBJECT1)); + } + + @Test + 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()); + + + assertFalse(pool.beforeInsert(droolsDisabled, OBJECT1)); + } + + @Test + void testDoManager() { + 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 + void testDoManager_NotFound() { + assertFalse(pool.beforeStart(controllerDisabled)); + } + + @Test + void testDoManager_Ex() { + + // generate exception + doThrow(new RuntimeException()).when(mgr1).beforeStart(); + + assertThrows(RuntimeException.class, () -> pool.beforeStart(controller1)); + } + + private Properties initProperties() { + Properties props = new Properties(); + + initProperties(props, "A", 0); + initProperties(props, "B", 1); + initProperties(props, "Exception", 2); + + props.setProperty("pooling.controllerDisabled.enabled", "false"); + + props.setProperty("pooling.controllerException.offline.queue.limit", "INVALID NUMBER"); + + return props; + } + + private void initProperties(Properties props, String suffix, int offset) { + props.setProperty("pooling.controller" + suffix + ".topic", "topic." + suffix); + props.setProperty("pooling.controller" + suffix + ".enabled", "true"); + props.setProperty("pooling.controller" + suffix + ".offline.queue.limit", String.valueOf(5 + offset)); + props.setProperty("pooling.controller" + suffix + ".offline.queue.age.milliseconds", + String.valueOf(100 + offset)); + props.setProperty("pooling.controller" + suffix + ".start.heartbeat.milliseconds", String.valueOf(10 + offset)); + props.setProperty("pooling.controller" + suffix + ".reactivate.milliseconds", String.valueOf(20 + offset)); + props.setProperty("pooling.controller" + suffix + ".identification.milliseconds", String.valueOf(30 + offset)); + props.setProperty("pooling.controller" + suffix + ".active.heartbeat.milliseconds", + String.valueOf(40 + offset)); + props.setProperty("pooling.controller" + suffix + ".inter.heartbeat.milliseconds", String.valueOf(50 + offset)); + } + + /** + * Feature with overrides. + */ + private class PoolingFeatureImpl extends PoolingFeature { + + @Override + protected Properties getProperties(String featName) { + if (PoolingProperties.FEATURE_NAME.equals(featName)) { + return props; + } else { + throw new IllegalArgumentException("unknown feature name"); + } + } + + @Override + protected PoolingManagerImpl makeManager(String host, PolicyController controller, PoolingProperties props, + CountDownLatch activeLatch) { + + PoolingManagerImpl mgr = mock(PoolingManagerImpl.class); + + managers.add(Pair.of(mgr, props)); + + return mgr; + } + + @Override + protected PolicyController getController(DroolsController droolsController) { + if (droolsController == drools1) { + return controller1; + } else if (droolsController == drools2) { + return controller2; + } else if (droolsController == droolsDisabled) { + return controllerDisabled; + } else { + throw new IllegalArgumentException("unknown drools controller"); + } + } + + @Override + protected List<TopicSource> initTopicSources(Properties props) { + return Collections.emptyList(); + } + + @Override + protected List<TopicSink> initTopicSinks(Properties props) { + return Collections.emptyList(); + } + } +} diff --git a/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/PoolingManagerImplTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/PoolingManagerImplTest.java new file mode 100644 index 00000000..ac60ae27 --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/PoolingManagerImplTest.java @@ -0,0 +1,995 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.contains; +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.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure; +import org.onap.policy.common.endpoints.event.comm.TopicListener; +import org.onap.policy.drools.controller.DroolsController; +import org.onap.policy.drools.pooling.message.BucketAssignments; +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; + +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; + protected static final long STD_OFFLINE_PUB_WAIT_MS = STD_INTER_HEARTBEAT_MS + 1; + + private static final String MY_HOST = "my.host"; + 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(); + + /** + * Number of publish() invocations that should be issued when the manager is + * started. + */ + private static final int START_PUB = 1; + + /** + * Futures that have been allocated due to calls to scheduleXxx(). + */ + private Queue<ScheduledFuture<?>> futures; + + private PoolingProperties poolProps; + private ListeningController controller; + private TopicMessageManager topicMessageManager; + private boolean gotManager; + private ScheduledThreadPoolExecutor sched; + private int schedCount; + private DroolsController drools; + private Serializer ser; + private CountDownLatch active; + + private PoolingManagerImpl mgr; + + /** + * Setup. + * + * @throws Exception throws exception + */ + @BeforeEach + public void setUp() throws Exception { + Properties 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); + when(poolProps.getOfflinePubWaitMs()).thenReturn(STD_OFFLINE_PUB_WAIT_MS); + + futures = new LinkedList<>(); + ser = new Serializer(); + active = new CountDownLatch(1); + + topicMessageManager = mock(TopicMessageManager.class); + gotManager = false; + controller = mock(ListeningController.class); + sched = mock(ScheduledThreadPoolExecutor.class); + schedCount = 0; + drools = mock(DroolsController.class); + + 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; + }); + + mgr = new PoolingManagerTest(MY_HOST, controller, poolProps, active); + } + + @Test + void testPoolingManagerImpl() { + assertTrue(gotManager); + + State st = mgr.getCurrent(); + assertInstanceOf(IdleState.class, st); + + // ensure the state is attached to the manager + assertEquals(mgr.getHost(), st.getHost()); + } + + @Test + void testPoolingManagerImpl_PoolEx() { + // throw an exception when we try to create the topic messages manager + PoolingFeatureException ex = new PoolingFeatureException(); + + assertThatThrownBy(() -> new PoolingManagerTest(MY_HOST, controller, poolProps, active) { + @Override + protected TopicMessageManager makeTopicMessagesManager(String topic) throws PoolingFeatureException { + throw ex; + } + }).isInstanceOf(PoolingFeatureRtException.class).hasCause(ex); + } + + @Test + void testGetCurrent() throws Exception { + assertEquals(IdleState.class, mgr.getCurrent().getClass()); + + startMgr(); + + assertEquals(StartState.class, mgr.getCurrent().getClass()); + } + + @Test + void testGetHost() { + assertEquals(MY_HOST, mgr.getHost()); + + mgr = new PoolingManagerTest(HOST2, controller, poolProps, active); + assertEquals(HOST2, mgr.getHost()); + } + + @Test + void testGetTopic() { + assertEquals(MY_TOPIC, mgr.getTopic()); + } + + @Test + void testGetProperties() { + assertEquals(poolProps, mgr.getProperties()); + } + + @Test + void testBeforeStart() { + // not running yet + mgr.beforeStart(); + + verify(topicMessageManager).startPublisher(); + + assertEquals(1, schedCount); + verify(sched).setMaximumPoolSize(1); + verify(sched).setContinueExistingPeriodicTasksAfterShutdownPolicy(false); + + + // try again - nothing should happen + mgr.beforeStart(); + + verify(topicMessageManager).startPublisher(); + + assertEquals(1, schedCount); + verify(sched).setMaximumPoolSize(1); + verify(sched).setContinueExistingPeriodicTasksAfterShutdownPolicy(false); + } + + @Test + void testAfterStart() throws Exception { + startMgr(); + + verify(topicMessageManager).startConsumer(mgr); + + State st = mgr.getCurrent(); + assertInstanceOf(StartState.class, st); + + // 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(topicMessageManager).startConsumer(mgr); + + assertInstanceOf(StartState.class, mgr.getCurrent()); + + verify(sched).schedule(any(Runnable.class), any(Long.class), any(TimeUnit.class)); + } + + @Test + void testBeforeStop() throws Exception { + startMgr(); + mgr.startDistributing(makeAssignments(false)); + + verify(topicMessageManager, times(START_PUB)).publish(any()); + + mgr.beforeStop(); + + verify(topicMessageManager).stopConsumer(mgr); + verify(sched).shutdownNow(); + verify(topicMessageManager, times(START_PUB + 1)).publish(any()); + verify(topicMessageManager).publish(contains("offline")); + + assertInstanceOf(IdleState.class, mgr.getCurrent()); + + // verify that next message is handled locally + assertFalse(mgr.beforeInsert(TOPIC2, DECODED_EVENT)); + verify(topicMessageManager, times(START_PUB + 1)).publish(any()); + } + + @Test + void testBeforeStop_NotRunning() { + final State st = mgr.getCurrent(); + + mgr.beforeStop(); + + verify(topicMessageManager, never()).stopConsumer(any()); + verify(sched, never()).shutdownNow(); + + // hasn't changed states either + assertEquals(st, mgr.getCurrent()); + } + + @Test + void testBeforeStop_AfterPartialStart() { + // call beforeStart but not afterStart + mgr.beforeStart(); + + final State st = mgr.getCurrent(); + + mgr.beforeStop(); + + // should still shut the scheduler down + verify(sched).shutdownNow(); + + verify(topicMessageManager, never()).stopConsumer(any()); + + // hasn't changed states + assertEquals(st, mgr.getCurrent()); + } + + @Test + void testAfterStop() throws Exception { + startMgr(); + mgr.beforeStop(); + + mgr.afterStop(); + + verify(topicMessageManager).stopPublisher(STD_OFFLINE_PUB_WAIT_MS); + } + + @Test + void testBeforeLock() throws Exception { + startMgr(); + + mgr.beforeLock(); + + assertInstanceOf(IdleState.class, mgr.getCurrent()); + } + + @Test + void testAfterUnlock_AliveIdle() { + // this really shouldn't happen + + lockMgr(); + + mgr.afterUnlock(); + + // stays in idle state, because it has no scheduler + assertInstanceOf(IdleState.class, mgr.getCurrent()); + } + + @Test + void testAfterUnlock_AliveStarted() throws Exception { + startMgr(); + lockMgr(); + + mgr.afterUnlock(); + + assertInstanceOf(StartState.class, mgr.getCurrent()); + } + + @Test + void testAfterUnlock_StoppedIdle() throws Exception { + startMgr(); + lockMgr(); + + // controller is stopped + when(controller.isAlive()).thenReturn(false); + + mgr.afterUnlock(); + + assertInstanceOf(IdleState.class, mgr.getCurrent()); + } + + @Test + void testAfterUnlock_StoppedStarted() throws Exception { + startMgr(); + + // Note: don't lockMgr() + + // controller is stopped + when(controller.isAlive()).thenReturn(false); + + mgr.afterUnlock(); + + assertInstanceOf(StartState.class, mgr.getCurrent()); + } + + @Test + void testChangeState() throws Exception { + // start should invoke changeState() + startMgr(); + + /* + * now go offline while it's locked + */ + lockMgr(); + + // should have cancelled the timers + assertEquals(2, futures.size()); + verify(futures.poll()).cancel(false); + verify(futures.poll()).cancel(false); + + /* + * now go back online + */ + unlockMgr(); + + // new timers should now be active + assertEquals(2, futures.size()); + verify(futures.poll(), never()).cancel(false); + verify(futures.poll(), never()).cancel(false); + } + + @Test + void testSchedule() throws Exception { + // must start the scheduler + startMgr(); + + CountDownLatch latch = new CountDownLatch(1); + + mgr.schedule(STD_ACTIVE_HEARTBEAT_MS, () -> { + 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 + void testScheduleWithFixedDelay() throws Exception { + // must start the scheduler + startMgr(); + + CountDownLatch latch = new CountDownLatch(1); + + mgr.scheduleWithFixedDelay(STD_HEARTBEAT_WAIT_MS, STD_ACTIVE_HEARTBEAT_MS, () -> { + 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, times(2)).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 + void testPublishAdmin() throws Exception { + Offline msg = new Offline(mgr.getHost()); + mgr.publishAdmin(msg); + + assertEquals(Message.ADMIN, msg.getChannel()); + + verify(topicMessageManager).publish(any()); + } + + @Test + void testPublish() throws Exception { + Offline msg = new Offline(mgr.getHost()); + mgr.publish("my.channel", msg); + + assertEquals("my.channel", msg.getChannel()); + + verify(topicMessageManager).publish(any()); + } + + @Test + void testPublish_InvalidMsg() throws Exception { + // message is missing data + mgr.publish(Message.ADMIN, new Offline()); + + // should not have attempted to publish it + verify(topicMessageManager, never()).publish(any()); + } + + @Test + void testPublish_TopicMessageMngEx() throws Exception { + + // generate exception + doThrow(new PoolingFeatureException()).when(topicMessageManager).publish(any()); + + assertThatCode(() -> mgr.publish(Message.ADMIN, new Offline(mgr.getHost()))).doesNotThrowAnyException(); + } + + @Test + 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); + + assertInstanceOf(QueryState.class, mgr.getCurrent()); + } + + @Test + void testOnTopicEvent_NullEvent() throws Exception { + startMgr(); + + assertThatCode(() -> mgr.onTopicEvent(CommInfrastructure.UEB, TOPIC2, null)).doesNotThrowAnyException(); + } + + @Test + void testBeforeOffer_Unlocked() throws Exception { + startMgr(); + + // route the message to another host + mgr.startDistributing(makeAssignments(false)); + + assertFalse(mgr.beforeOffer(TOPIC2, THE_EVENT)); + } + + @Test + void testBeforeOffer_Locked() throws Exception { + startMgr(); + lockMgr(); + + // route the message to another host + mgr.startDistributing(makeAssignments(false)); + + assertTrue(mgr.beforeOffer(TOPIC2, THE_EVENT)); + } + + @Test + void testBeforeInsert() throws Exception { + startMgr(); + lockMgr(); + + // route the message to this host + mgr.startDistributing(makeAssignments(true)); + + assertFalse(mgr.beforeInsert(TOPIC2, DECODED_EVENT)); + } + + @Test + void testHandleExternalCommInfrastructureStringStringString_NullReqId() throws Exception { + validateHandleReqId(null); + } + + @Test + void testHandleExternalCommInfrastructureStringStringString_EmptyReqId() throws Exception { + validateHandleReqId(""); + } + + @Test + void testHandleExternalCommInfrastructureStringStringString_InvalidMsg() throws Exception { + startMgr(); + + assertFalse(mgr.beforeInsert(TOPIC2, "invalid message")); + } + + @Test + void testHandleExternalCommInfrastructureStringStringString() throws Exception { + validateUnhandled(); + } + + @Test + void testHandleExternalForward_NoAssignments() throws Exception { + validateUnhandled(); + } + + @Test + void testHandleExternalForward() throws Exception { + validateNoForward(); + } + + @Test + void testHandleEvent_NullTarget() throws Exception { + // buckets have null targets + validateDiscarded(new BucketAssignments(new String[] {null, null})); + } + + @Test + void testHandleEvent_SameHost() throws Exception { + validateNoForward(); + } + + @Test + void testHandleEvent_DiffHost() throws Exception { + // route the message to the *OTHER* host + validateDiscarded(makeAssignments(false)); + } + + @Test + void testDecodeEvent_CannotDecode() throws Exception { + + mgr = new PoolingManagerTest(MY_HOST, controller, poolProps, active) { + @Override + protected boolean canDecodeEvent(DroolsController drools2, String topic2) { + return false; + } + }; + + startMgr(); + + when(controller.isLocked()).thenReturn(true); + + // create assignments, though they are irrelevant + mgr.startDistributing(makeAssignments(false)); + + assertFalse(mgr.beforeOffer(TOPIC2, THE_EVENT)); + } + + @Test + void testDecodeEvent_UnsuppEx() throws Exception { + + // generate exception + mgr = new PoolingManagerTest(MY_HOST, controller, poolProps, active) { + @Override + protected Object decodeEventWrapper(DroolsController drools2, String topic2, String event) { + throw new UnsupportedOperationException(); + } + }; + + startMgr(); + + when(controller.isLocked()).thenReturn(true); + + // create assignments, though they are irrelevant + mgr.startDistributing(makeAssignments(false)); + + assertFalse(mgr.beforeOffer(TOPIC2, THE_EVENT)); + } + + @Test + void testDecodeEvent_ArgEx() throws Exception { + // generate exception + mgr = new PoolingManagerTest(MY_HOST, controller, poolProps, active) { + @Override + protected Object decodeEventWrapper(DroolsController drools2, String topic2, String event) { + throw new IllegalArgumentException(); + } + }; + + startMgr(); + + when(controller.isLocked()).thenReturn(true); + + // create assignments, though they are irrelevant + mgr.startDistributing(makeAssignments(false)); + + assertFalse(mgr.beforeOffer(TOPIC2, THE_EVENT)); + } + + @Test + void testDecodeEvent_StateEx() throws Exception { + // generate exception + mgr = new PoolingManagerTest(MY_HOST, controller, poolProps, active) { + @Override + protected Object decodeEventWrapper(DroolsController drools2, String topic2, String event) { + throw new IllegalStateException(); + } + }; + + startMgr(); + + when(controller.isLocked()).thenReturn(true); + + // create assignments, though they are irrelevant + mgr.startDistributing(makeAssignments(false)); + + assertFalse(mgr.beforeOffer(TOPIC2, THE_EVENT)); + } + + @Test + void testDecodeEvent() throws Exception { + startMgr(); + + when(controller.isLocked()).thenReturn(true); + + // route to another host + mgr.startDistributing(makeAssignments(false)); + + assertTrue(mgr.beforeOffer(TOPIC2, THE_EVENT)); + } + + @Test + 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); + + assertInstanceOf(QueryState.class, mgr.getCurrent()); + } + + @Test + void testHandleInternal_IoEx() throws Exception { + startMgr(); + + mgr.onTopicEvent(CommInfrastructure.UEB, MY_TOPIC, "invalid message"); + + assertInstanceOf(StartState.class, mgr.getCurrent()); + } + + @Test + 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); + + assertInstanceOf(StartState.class, mgr.getCurrent()); + } + + @Test + void testStartDistributing() throws Exception { + validateNoForward(); + + + // null assignments should cause message to be processed locally + mgr.startDistributing(null); + assertFalse(mgr.beforeInsert(TOPIC2, DECODED_EVENT)); + verify(topicMessageManager, times(START_PUB)).publish(any()); + + + // message for this host + mgr.startDistributing(makeAssignments(true)); + assertFalse(mgr.beforeInsert(TOPIC2, DECODED_EVENT)); + + + // message for another host + mgr.startDistributing(makeAssignments(false)); + assertTrue(mgr.beforeInsert(TOPIC2, DECODED_EVENT)); + } + + @Test + void testGoStart() { + State st = mgr.goStart(); + assertInstanceOf(StartState.class, st); + assertEquals(mgr.getHost(), st.getHost()); + } + + @Test + void testGoQuery() { + BucketAssignments asgn = new BucketAssignments(new String[] {HOST2}); + mgr.startDistributing(asgn); + + State st = mgr.goQuery(); + + assertInstanceOf(QueryState.class, st); + assertEquals(mgr.getHost(), st.getHost()); + assertEquals(asgn, mgr.getAssignments()); + } + + @Test + void testGoActive() { + BucketAssignments asgn = new BucketAssignments(new String[] {HOST2}); + mgr.startDistributing(asgn); + + State st = mgr.goActive(); + + assertInstanceOf(ActiveState.class, st); + assertEquals(mgr.getHost(), st.getHost()); + assertEquals(asgn, mgr.getAssignments()); + assertEquals(0, active.getCount()); + } + + @Test + void testGoInactive() { + State st = mgr.goInactive(); + assertInstanceOf(InactiveState.class, st); + assertEquals(mgr.getHost(), st.getHost()); + assertEquals(1, active.getCount()); + } + + @Test + void testTimerActionRun() throws Exception { + // must start the scheduler + startMgr(); + + CountDownLatch latch = new CountDownLatch(1); + + mgr.schedule(STD_ACTIVE_HEARTBEAT_MS, () -> { + 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 + void testTimerActionRun_DiffState() throws Exception { + // must start the scheduler + startMgr(); + + CountDownLatch latch = new CountDownLatch(1); + + mgr.schedule(STD_ACTIVE_HEARTBEAT_MS, () -> { + 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); + + assertInstanceOf(QueryState.class, mgr.getCurrent()); + + // execute it + taskCap.getValue().run(); + + // it should NOT have counted down + assertEquals(1, latch.getCount()); + } + + private void validateHandleReqId(String requestId) throws PoolingFeatureException { + startMgr(); + + assertFalse(mgr.beforeInsert(TOPIC2, DECODED_EVENT)); + } + + private void validateNoForward() throws PoolingFeatureException { + startMgr(); + + // route the message to this host + mgr.startDistributing(makeAssignments(true)); + + assertFalse(mgr.beforeInsert(TOPIC2, DECODED_EVENT)); + + verify(topicMessageManager, times(START_PUB)).publish(any()); + } + + private void validateUnhandled() throws PoolingFeatureException { + startMgr(); + assertFalse(mgr.beforeInsert(TOPIC2, DECODED_EVENT)); + } + + private void validateDiscarded(BucketAssignments bucketAssignments) throws PoolingFeatureException { + startMgr(); + + // buckets have null targets + mgr.startDistributing(bucketAssignments); + + assertTrue(mgr.beforeInsert(TOPIC2, DECODED_EVENT)); + } + + /** + * Makes an assignment with two buckets. + * + * @param sameHost {@code true} if the REQUEST_ID should hash 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 = DECODED_EVENT.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. + * + */ + private void startMgr() { + mgr.beforeStart(); + mgr.afterStart(); + } + + /** + * Invokes methods necessary to lock the manager. + */ + private void lockMgr() { + mgr.beforeLock(); + when(controller.isLocked()).thenReturn(true); + } + + /** + * Invokes methods necessary to unlock the manager. + */ + private void unlockMgr() { + mgr.afterUnlock(); + when(controller.isLocked()).thenReturn(false); + } + + /** + * Used to create a mock object that implements both super interfaces. + */ + private static interface ListeningController extends TopicListener, PolicyController { + + } + + /** + * Manager with overrides. + */ + private class PoolingManagerTest extends PoolingManagerImpl { + + public PoolingManagerTest(String host, PolicyController controller, PoolingProperties props, + CountDownLatch activeLatch) { + + super(host, controller, props, activeLatch); + } + + @Override + protected TopicMessageManager makeTopicMessagesManager(String topic) throws PoolingFeatureException { + gotManager = true; + return topicMessageManager; + } + + @Override + protected ScheduledThreadPoolExecutor makeScheduler() { + ++schedCount; + return sched; + } + + @Override + protected boolean canDecodeEvent(DroolsController drools2, String topic2) { + return (drools2 == drools && TOPIC2.equals(topic2)); + } + + @Override + protected Object decodeEventWrapper(DroolsController drools2, String topic2, String event) { + if (drools2 == drools && TOPIC2.equals(topic2) && event == THE_EVENT) { + return DECODED_EVENT; + } else { + return null; + } + } + } +} diff --git a/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/PoolingPropertiesTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/PoolingPropertiesTest.java new file mode 100644 index 00000000..383e0071 --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/PoolingPropertiesTest.java @@ -0,0 +1,190 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018, 2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertEquals; +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.OFFLINE_PUB_WAIT_MS; +import static org.onap.policy.drools.pooling.PoolingProperties.POOLING_TOPIC; +import static org.onap.policy.drools.pooling.PoolingProperties.PREFIX; +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.jupiter.api.BeforeEach; +import org.junit.jupiter.api.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_OFFLINE_PUB_WAIT_MS = 2000L; + public static final long STD_START_HEARTBEAT_MS = 3000L; + public static final long STD_REACTIVATE_MS = 4000L; + public static final long STD_IDENTIFICATION_MS = 5000L; + public static final long STD_ACTIVE_HEARTBEAT_MS = 7000L; + public static final long STD_INTER_HEARTBEAT_MS = 8000L; + + private Properties plain; + private PoolingProperties pooling; + + /** + * Setup. + * + * @throws Exception throws an exception + */ + @BeforeEach + public void setUp() throws Exception { + plain = makeProperties(); + + pooling = new PoolingProperties(CONTROLLER, plain); + } + + @Test + void testPoolingProperties() { + // ensure no exceptions + assertThatCode(() -> new PoolingProperties(CONTROLLER, plain)).doesNotThrowAnyException(); + } + + @Test + void testGetSource() { + assertEquals(plain, pooling.getSource()); + } + + @Test + void testGetPoolingTopic() { + assertEquals(STD_POOLING_TOPIC, pooling.getPoolingTopic()); + } + + @Test + void testGetOfflineLimit() throws PropertyException { + doTest(OFFLINE_LIMIT, STD_OFFLINE_LIMIT, 1000, xxx -> pooling.getOfflineLimit()); + } + + @Test + void testGetOfflineAgeMs() throws PropertyException { + doTest(OFFLINE_AGE_MS, STD_OFFLINE_AGE_MS, 60000L, xxx -> pooling.getOfflineAgeMs()); + } + + @Test + void testGetOfflinePubWaitMs() throws PropertyException { + doTest(OFFLINE_PUB_WAIT_MS, STD_OFFLINE_PUB_WAIT_MS, 3000L, xxx -> pooling.getOfflinePubWaitMs()); + } + + @Test + void testGetStartHeartbeatMs() throws PropertyException { + doTest(START_HEARTBEAT_MS, STD_START_HEARTBEAT_MS, 100000L, xxx -> pooling.getStartHeartbeatMs()); + } + + @Test + void testGetReactivateMs() throws PropertyException { + doTest(REACTIVATE_MS, STD_REACTIVATE_MS, 50000L, xxx -> pooling.getReactivateMs()); + } + + @Test + void testGetIdentificationMs() throws PropertyException { + doTest(IDENTIFICATION_MS, STD_IDENTIFICATION_MS, 50000L, xxx -> pooling.getIdentificationMs()); + } + + @Test + void testGetActiveHeartbeatMs() throws PropertyException { + doTest(ACTIVE_HEARTBEAT_MS, STD_ACTIVE_HEARTBEAT_MS, 50000L, xxx -> pooling.getActiveHeartbeatMs()); + } + + @Test + 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(specValue, func.apply(null), "special " + propnm); + + /* + * Without the property - should use the default value. + */ + plain.remove(specialize(propnm)); + plain.remove(propnm); + pooling = new PoolingProperties(CONTROLLER, plain); + assertEquals(dfltValue, func.apply(null), "default " + propnm); + } + + /** + * Makes a set of properties, where all the properties are specialized for the + * controller. + * + * @return a new property set + */ + private Properties makeProperties() { + Properties props = new Properties(); + + props.setProperty(specialize(POOLING_TOPIC), STD_POOLING_TOPIC); + props.setProperty(specialize(FEATURE_ENABLED), "" + STD_FEATURE_ENABLED); + props.setProperty(specialize(OFFLINE_LIMIT), "" + STD_OFFLINE_LIMIT); + props.setProperty(specialize(OFFLINE_AGE_MS), "" + STD_OFFLINE_AGE_MS); + props.setProperty(specialize(OFFLINE_PUB_WAIT_MS), "" + STD_OFFLINE_PUB_WAIT_MS); + props.setProperty(specialize(START_HEARTBEAT_MS), "" + STD_START_HEARTBEAT_MS); + props.setProperty(specialize(REACTIVATE_MS), "" + STD_REACTIVATE_MS); + props.setProperty(specialize(IDENTIFICATION_MS), "" + STD_IDENTIFICATION_MS); + props.setProperty(specialize(ACTIVE_HEARTBEAT_MS), "" + STD_ACTIVE_HEARTBEAT_MS); + props.setProperty(specialize(INTER_HEARTBEAT_MS), "" + STD_INTER_HEARTBEAT_MS); + + return props; + } + + /** + * Embeds a specializer within a property name, after the prefix. + * + * @param propnm property name into which it should be embedded + * @return the property name, with the specializer embedded within it + */ + private String specialize(String propnm) { + String suffix = propnm.substring(PREFIX.length()); + return PREFIX + PoolingPropertiesTest.CONTROLLER + "." + suffix; + } +} diff --git a/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/SerializerTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/SerializerTest.java new file mode 100644 index 00000000..a81ea68b --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/SerializerTest.java @@ -0,0 +1,85 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.google.gson.JsonParseException; +import org.junit.jupiter.api.Test; +import org.onap.policy.drools.pooling.message.Message; +import org.onap.policy.drools.pooling.message.Query; + +class SerializerTest { + + @Test + void testSerializer() { + assertThatCode(Serializer::new).doesNotThrowAnyException(); + } + + @Test + void testEncodeMsg_testDecodeMsg() { + 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()); + + // invalid subclass when encoding + Message msg2 = new Message() {}; + assertThatThrownBy(() -> ser.encodeMsg(msg2)).isInstanceOf(JsonParseException.class) + .hasMessageContaining("cannot serialize"); + + // missing type when decoding + final String enc2 = encoded.replaceAll("type", "other-field-name"); + + assertThatThrownBy(() -> ser.decodeMsg(enc2)).isInstanceOf(JsonParseException.class) + .hasMessageContaining("does not contain a field named"); + + // invalid type + final String enc3 = encoded.replaceAll("query", "invalid-type"); + + assertThatThrownBy(() -> ser.decodeMsg(enc3)).isInstanceOf(JsonParseException.class) + .hasMessage("cannot deserialize \"invalid-type\""); + } + +} diff --git a/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/TopicMessageManagerTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/TopicMessageManagerTest.java new file mode 100644 index 00000000..74098487 --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/TopicMessageManagerTest.java @@ -0,0 +1,322 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +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.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.common.endpoints.event.comm.TopicListener; +import org.onap.policy.common.endpoints.event.comm.TopicSink; +import org.onap.policy.common.endpoints.event.comm.TopicSource; + +class TopicMessageManagerTest { + + private static final String EXPECTED = "expected"; + private static final String MY_TOPIC = "my.topic"; + private static final String MSG = "a message"; + + private TopicListener listener; + private TopicSource source; + private boolean gotSources; + private TopicSink sink; + private boolean gotSinks; + private TopicMessageManager mgr; + + /** + * Setup. + * + * @throws Exception throws an exception + */ + @BeforeEach + public void setUp() throws Exception { + listener = mock(TopicListener.class); + source = mock(TopicSource.class); + gotSources = false; + sink = mock(TopicSink.class); + gotSinks = false; + + when(source.getTopic()).thenReturn(MY_TOPIC); + + when(sink.getTopic()).thenReturn(MY_TOPIC); + when(sink.send(any())).thenReturn(true); + + mgr = new TopicMessageManagerImpl(MY_TOPIC); + } + + @Test + void testTopicMessageManager() { + // verify that the init methods were called + assertTrue(gotSources); + assertTrue(gotSinks); + } + + @Test + void testTopicMessageManager_PoolingEx() { + // force error by having no topics match + when(source.getTopic()).thenReturn(""); + + assertThrows(PoolingFeatureException.class, () -> new TopicMessageManagerImpl(MY_TOPIC)); + } + + @Test + void testTopicMessageManager_IllegalArgEx() { + // force error + assertThrows(PoolingFeatureException.class, () -> + new TopicMessageManagerImpl(MY_TOPIC) { + @Override + protected List<TopicSource> getTopicSources() { + throw new IllegalArgumentException(EXPECTED); + } + }); + } + + @Test + void testGetTopic() { + assertEquals(MY_TOPIC, mgr.getTopic()); + } + + @Test + void testFindTopicSource_NotFound() { + // one item in list, and its topic doesn't match + assertThrows(PoolingFeatureException.class, () -> new TopicMessageManagerImpl(MY_TOPIC) { + @Override + protected List<TopicSource> getTopicSources() { + return Collections.singletonList(mock(TopicSource.class)); + } + }); + } + + @Test + void testFindTopicSource_EmptyList() { + // empty list + assertThrows(PoolingFeatureException.class, () -> new TopicMessageManagerImpl(MY_TOPIC) { + @Override + protected List<TopicSource> getTopicSources() { + return Collections.emptyList(); + } + }); + } + + @Test + void testFindTopicSink_NotFound() { + // one item in list, and its topic doesn't match + assertThrows(PoolingFeatureException.class, () -> new TopicMessageManagerImpl(MY_TOPIC) { + @Override + protected List<TopicSink> getTopicSinks() { + return Collections.singletonList(mock(TopicSink.class)); + } + }); + } + + @Test + void testFindTopicSink_EmptyList() { + // empty list + assertThrows(PoolingFeatureException.class, () -> new TopicMessageManagerImpl(MY_TOPIC) { + @Override + protected List<TopicSink> getTopicSinks() { + return Collections.emptyList(); + } + }); + } + + @Test + void testStartPublisher() throws PoolingFeatureException { + + mgr.startPublisher(); + + // restart should have no effect + mgr.startPublisher(); + + // should be able to publish now + mgr.publish(MSG); + verify(sink).send(MSG); + } + + @Test + void testStopPublisher() { + // not publishing yet, so stopping should have no effect + mgr.stopPublisher(0); + + // now start it + mgr.startPublisher(); + + // this time, stop should do something + mgr.stopPublisher(0); + + // re-stopping should have no effect + assertThatCode(() -> mgr.stopPublisher(0)).doesNotThrowAnyException(); + } + + @Test + void testStopPublisher_WithDelay() { + + mgr.startPublisher(); + + long tbeg = System.currentTimeMillis(); + + mgr.stopPublisher(100L); + + assertTrue(System.currentTimeMillis() >= tbeg + 100L); + } + + @Test + void testStopPublisher_WithDelayInterrupted() throws Exception { + + mgr.startPublisher(); + + long minms = 2000L; + + // tell the publisher to stop in minms + additional time + CountDownLatch latch = new CountDownLatch(1); + Thread thread = new Thread(() -> { + latch.countDown(); + mgr.stopPublisher(minms + 3000L); + }); + thread.start(); + + // wait for the thread to start + latch.await(); + + // interrupt it - it should immediately finish its work + thread.interrupt(); + + // wait for it to stop, but only wait the minimum time + thread.join(minms); + + assertFalse(thread.isAlive()); + } + + @Test + 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 + 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 + void testPublish() throws PoolingFeatureException { + // cannot publish before starting + assertThatThrownBy(() -> mgr.publish(MSG)).as("publish,pre").isInstanceOf(PoolingFeatureException.class); + + 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(0); + assertThatThrownBy(() -> mgr.publish(MSG)).as("publish,stopped").isInstanceOf(PoolingFeatureException.class); + } + + @Test + void testPublish_SendFailed() { + mgr.startPublisher(); + + // arrange for send() to fail + when(sink.send(MSG)).thenReturn(false); + + assertThrows(PoolingFeatureException.class, () -> mgr.publish(MSG)); + } + + @Test + void testPublish_SendEx() { + mgr.startPublisher(); + + // arrange for send() to throw an exception + doThrow(new IllegalStateException(EXPECTED)).when(sink).send(MSG); + + assertThrows(PoolingFeatureException.class, () -> mgr.publish(MSG)); + } + + /** + * Manager with overrides. + */ + private class TopicMessageManagerImpl extends TopicMessageManager { + + public TopicMessageManagerImpl(String topic) throws PoolingFeatureException { + super(topic); + } + + @Override + protected List<TopicSource> getTopicSources() { + gotSources = true; + + // three sources, with the desired one in the middle + return Arrays.asList(mock(TopicSource.class), source, mock(TopicSource.class)); + } + + @Override + protected List<TopicSink> getTopicSinks() { + gotSinks = true; + + // three sinks, with the desired one in the middle + return Arrays.asList(mock(TopicSink.class), sink, mock(TopicSink.class)); + } + } +} diff --git a/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/feature-pooling-messages.properties b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/feature-pooling-messages.properties new file mode 100644 index 00000000..b89e4062 --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/feature-pooling-messages.properties @@ -0,0 +1,47 @@ +# Copyright 2018 AT&T Intellectual Property. All rights reserved +# Modifications Copyright (C) 2024 Nordix Foundation. +# +# 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. + +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-messages/src/test/java/org/onap/policy/drools/pooling/message/BucketAssignmentsTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/BucketAssignmentsTest.java new file mode 100644 index 00000000..ca47f9c1 --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/BucketAssignmentsTest.java @@ -0,0 +1,361 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018, 2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.Arrays; +import java.util.SortedSet; +import java.util.TreeSet; +import org.junit.jupiter.api.Test; +import org.onap.policy.drools.pooling.PoolingFeatureException; + +class BucketAssignmentsTest { + + @Test + void testBucketAssignments() { + assertThatCode(BucketAssignments::new).doesNotThrowAnyException(); + } + + @Test + void testBucketAssignmentsStringArray() { + String[] arr = {"abc", "def"}; + BucketAssignments asgn = new BucketAssignments(arr); + + assertNotNull(asgn.getHostArray()); + assertEquals(Arrays.toString(arr), Arrays.toString(asgn.getHostArray())); + } + + @Test + void testGetHostArray_testSetHostArray() { + + String[] arr = {"abc", "def"}; + BucketAssignments asgn = new BucketAssignments(arr); + + assertNotNull(asgn.getHostArray()); + assertEquals(Arrays.toString(arr), Arrays.toString(asgn.getHostArray())); + + String[] arr2 = {"xyz"}; + asgn.setHostArray(arr2); + + assertNotNull(asgn.getHostArray()); + assertEquals(Arrays.toString(arr2), Arrays.toString(asgn.getHostArray())); + } + + @Test + 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 + 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 + 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 + 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); + + /* + * get assignments for consecutive integers, including negative numbers and + * numbers extending past the length of the array. + * + */ + TreeSet<String> seen = new TreeSet<>(); + for (int x = -1; x < arr.length + 2; ++x) { + seen.add(asgn.getAssignedHost(x)); + } + + TreeSet<String> expected = new TreeSet<>(Arrays.asList(arr)); + assertEquals(expected, seen); + + // try a much bigger number + assertNotNull(asgn.getAssignedHost(arr.length * 1000)); + } + + @Test + 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 + 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 + 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"}); + assertNotEquals(code, asgn.hashCode()); + } + + @Test + void testEquals() { + // null object + BucketAssignments asgn = new BucketAssignments(); + assertNotEquals(null, asgn); + + // same object + asgn = new BucketAssignments(); + assertEquals(asgn, asgn); + + // different class of object + asgn = new BucketAssignments(); + assertNotEquals("not an assignment object", asgn); + + assertNotEquals(asgn, new BucketAssignments(new String[] {"abc"})); + + // with null assignments + asgn = new BucketAssignments(); + assertEquals(asgn, new BucketAssignments()); + + // with empty array + asgn = new BucketAssignments(new String[0]); + assertEquals(asgn, asgn); + + assertNotEquals(asgn, new BucketAssignments()); + assertNotEquals(asgn, new BucketAssignments(new String[] {"abc"})); + + // with null items + String[] arr = {"abc", null, "def"}; + asgn = new BucketAssignments(arr); + assertEquals(asgn, asgn); + assertEquals(asgn, new BucketAssignments(arr)); + assertEquals(asgn, new BucketAssignments(new String[] {"abc", null, "def"})); + + assertNotEquals(asgn, new BucketAssignments()); + assertNotEquals(asgn, new BucketAssignments(new String[] {"abc", null, "XYZ"})); + + assertNotEquals(asgn, 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-messages/src/test/java/org/onap/policy/drools/pooling/message/HeartbeatTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/HeartbeatTest.java new file mode 100644 index 00000000..142ebbca --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/HeartbeatTest.java @@ -0,0 +1,59 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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.jupiter.api.Assertions.assertEquals; + +public class HeartbeatTest extends SupportBasicMessageTester<Heartbeat> { + + /** + * Sequence number to validate time stamps within the heart beat. + */ + private long sequence = 0; + + public HeartbeatTest() { + super(Heartbeat.class); + } + + @Override + 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-messages/src/test/java/org/onap/policy/drools/pooling/message/IdentificationTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/IdentificationTest.java new file mode 100644 index 00000000..dced372d --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/IdentificationTest.java @@ -0,0 +1,74 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class IdentificationTest extends SupportMessageWithAssignmentsTester<Identification> { + + public IdentificationTest() { + super(Identification.class); + } + + @BeforeEach + 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 + 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 + void testCheckValidity_NullAssignments() throws Exception { + // null assignments are OK + Identification msg = makeValidMessage(); + msg.setAssignments(null); + msg.checkValidity(); + } + + @Override + public Identification makeValidMessage() { + Identification msg = new Identification(VALID_HOST, (isNullAssignments() ? null : VALID_ASGN)); + msg.setChannel(VALID_CHANNEL); + + return msg; + } + +} diff --git a/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/LeaderTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/LeaderTest.java new file mode 100644 index 00000000..15d9d338 --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/LeaderTest.java @@ -0,0 +1,73 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class LeaderTest extends SupportMessageWithAssignmentsTester<Leader> { + + public LeaderTest() { + super(Leader.class); + } + + @BeforeEach + 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. + * + */ + @Test + void testCheckValidity_InvalidFields_NullAssignments() { + // null assignments are invalid + expectCheckValidityFailure(msg -> msg.setAssignments(null)); + } + + @Test + void testCheckValidity_SourceIsNotLeader() { + 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)); + } + + @Override + public Leader makeValidMessage() { + Leader msg = new Leader(VALID_HOST, (isNullAssignments() ? null : VALID_ASGN)); + msg.setChannel(VALID_CHANNEL); + + return msg; + } + +} diff --git a/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/MessageTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/MessageTest.java new file mode 100644 index 00000000..cf2cb695 --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/MessageTest.java @@ -0,0 +1,82 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +class MessageTest extends SupportBasicMessageTester<Message> { + + public MessageTest() { + super(Message.class); + } + + @Test + 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 + 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 + 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-messages/src/test/java/org/onap/policy/drools/pooling/message/OfflineTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/OfflineTest.java new file mode 100644 index 00000000..29a7fad1 --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/OfflineTest.java @@ -0,0 +1,38 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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 SupportBasicMessageTester<Offline> { + + public OfflineTest() { + super(Offline.class); + } + + @Override + public Offline makeValidMessage() { + Offline msg = new Offline(VALID_HOST); + msg.setChannel(VALID_CHANNEL); + + return msg; + } + +} diff --git a/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/QueryTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/QueryTest.java new file mode 100644 index 00000000..737bcc19 --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/QueryTest.java @@ -0,0 +1,38 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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 SupportBasicMessageTester<Query> { + + public QueryTest() { + super(Query.class); + } + + @Override + public Query makeValidMessage() { + Query msg = new Query(VALID_HOST); + msg.setChannel(VALID_CHANNEL); + + return msg; + } + +} diff --git a/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/SupportBasicMessageTester.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/SupportBasicMessageTester.java new file mode 100644 index 00000000..2fe905a9 --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/SupportBasicMessageTester.java @@ -0,0 +1,251 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018-2019 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.fail; + +import org.junit.jupiter.api.Test; +import org.onap.policy.drools.pooling.PoolingFeatureException; +import org.onap.policy.drools.pooling.Serializer; + +/** + * Superclass used to test subclasses of {@link Message}. + * + * @param <T> type of {@link Message} subclass that this tests + */ +public abstract class SupportBasicMessageTester<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 Serializer mapper = new Serializer(); + + /** + * The subclass of the type of Message being tested. + */ + private final Class<T> subclazz; + + /** + * Constructor. + * + * @param subclazz subclass of {@link Message} being tested + */ + public SupportBasicMessageTester(Class<T> subclazz) { + this.subclazz = subclazz; + } + + /** + * Creates a default Message and verifies that the source and channel are + * {@code null}. + * + */ + @Test + public final void testDefaultConstructor() { + testDefaultConstructorFields(makeDefaultMessage()); + } + + /** + * Tests that the Message has the correct source, and that the channel is + * {@code null}. + * + */ + @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; + if (originalMsg.getClass() == Message.class) { + msg = originalMsg; + } else { + msg = mapper.decodeMsg(mapper.encodeMsg(originalMsg)); + } + + 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 + * VALID_HOST and 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 + */ + 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 + */ + void update(T msg, String newValue); + } +} diff --git a/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/SupportMessageWithAssignmentsTester.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/SupportMessageWithAssignmentsTester.java new file mode 100644 index 00000000..3814553f --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/message/SupportMessageWithAssignmentsTester.java @@ -0,0 +1,103 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import lombok.Getter; +import lombok.Setter; +import org.junit.jupiter.api.Test; + +/** + * Superclass used to test subclasses of {@link MessageWithAssignments}. + * + * @param <T> type of {@link MessageWithAssignments} subclass that this tests + */ +@Setter +@Getter +public abstract class SupportMessageWithAssignmentsTester<T extends MessageWithAssignments> + extends SupportBasicMessageTester<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; + + /** + * Constructor. + * + * @param subclazz subclass of {@link MessageWithAssignments} being tested + */ + public SupportMessageWithAssignmentsTester(Class<T> subclazz) { + super(subclazz); + } + + @Test + public void testCheckValidity_InvalidFields() { + // 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-messages/src/test/java/org/onap/policy/drools/pooling/state/ActiveStateTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/state/ActiveStateTest.java new file mode 100644 index 00000000..ce9adb9f --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/state/ActiveStateTest.java @@ -0,0 +1,470 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018, 2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2020, 2024 Nordix Foundation + * ================================================================================ + * 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.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.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 org.apache.commons.lang3.tuple.Pair; +import org.apache.commons.lang3.tuple.Triple; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.drools.pooling.message.BucketAssignments; +import org.onap.policy.drools.pooling.message.Heartbeat; +import org.onap.policy.drools.pooling.message.Leader; +import org.onap.policy.drools.pooling.message.Offline; +import org.onap.policy.drools.pooling.message.Query; + +class ActiveStateTest extends SupportBasicStateTester { + + private ActiveState state; + + /** + * Setup. + */ + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + + state = new ActiveState(mgr); + } + + @Test + 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.getRight().getSource()); + } + + @Test + void testProcessHeartbeat_NullHost() { + assertNull(state.process(new Heartbeat())); + + assertFalse(state.isMyHeartbeatSeen()); + assertFalse(state.isPredHeartbeatSeen()); + + verify(mgr, never()).goInactive(); + verify(mgr, never()).goQuery(); + } + + @Test + 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 + 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 + 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 + void testProcessOffline_NullHost() { + // should be ignored + assertNull(state.process(new Offline())); + } + + @Test + void testProcessOffline_UnassignedHost() { + // HOST4 is not in the assignment list - should be ignored + assertNull(state.process(new Offline(HOST4))); + } + + @Test + 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 + 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 + 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 + 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 + void testProcessLeader_Invalid() { + 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 + void testProcessLeader_BadLeader() { + String[] arr = {HOST2, HOST1}; + BucketAssignments asgn = new BucketAssignments(arr); + + // now send a Leader message for that leader + Leader msg = new Leader(HOST1, asgn); + + State next = mock(State.class); + when(mgr.goQuery()).thenReturn(next); + + // should go Query, but not start distributing + assertEquals(next, state.process(msg)); + verify(mgr, never()).startDistributing(asgn); + } + + @Test + void testProcessLeader_GoodLeader() { + String[] arr = {HOST2, PREV_HOST, MY_HOST}; + BucketAssignments asgn = new BucketAssignments(arr); + + // now send a Leader message for that leader + 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); + } + + @Test + 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 + void testDetmNeighbors() { + // if only one host (i.e., itself) + mgr.startDistributing(new BucketAssignments(new String[] {MY_HOST, MY_HOST})); + state = new ActiveState(mgr); + assertNull(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 + void testAddTimers_WithPredecessor() { + // invoke start() to add the timers + state.start(); + + assertEquals(3, repeatedSchedules.size()); + + Triple<Long, Long, StateTimerTask> timer; + + // heart beat generator + timer = repeatedTasks.remove(); + assertEquals(STD_INTER_HEARTBEAT_MS, timer.getLeft().longValue()); + assertEquals(STD_INTER_HEARTBEAT_MS, timer.getMiddle().longValue()); + + // my heart beat checker + timer = repeatedTasks.remove(); + assertEquals(STD_ACTIVE_HEARTBEAT_MS, timer.getLeft().longValue()); + assertEquals(STD_ACTIVE_HEARTBEAT_MS, timer.getMiddle().longValue()); + + // predecessor's heart beat checker + timer = repeatedTasks.remove(); + assertEquals(STD_ACTIVE_HEARTBEAT_MS, timer.getLeft().longValue()); + assertEquals(STD_ACTIVE_HEARTBEAT_MS, timer.getMiddle().longValue()); + } + + @Test + 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, repeatedSchedules.size()); + + Triple<Long, Long, StateTimerTask> timer; + + // heart beat generator + timer = repeatedTasks.remove(); + assertEquals(STD_INTER_HEARTBEAT_MS, timer.getLeft().longValue()); + assertEquals(STD_INTER_HEARTBEAT_MS, timer.getMiddle().longValue()); + + // my heart beat checker + timer = repeatedTasks.remove(); + assertEquals(STD_ACTIVE_HEARTBEAT_MS, timer.getLeft().longValue()); + assertEquals(STD_ACTIVE_HEARTBEAT_MS, timer.getMiddle().longValue()); + } + + @Test + 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.getRight().fire()); + + // 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.getLeft()); + assertEquals(MY_HOST, msg.getRight().getSource()); + } + + @Test + 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.getRight().fire()); + + verify(mgr, never()).publishAdmin(any(Query.class)); + } + + @Test + 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.goStart()).thenReturn(next); + + // fire the task - should transition + assertEquals(next, task.getRight().fire()); + + // should continue to distribute + verify(mgr, never()).startDistributing(null); + + // should publish an offline message + Offline msg = captureAdminMessage(Offline.class); + assertEquals(MY_HOST, msg.getSource()); + } + + @Test + 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.getRight().fire()); + + verify(mgr, never()).publishAdmin(any(Query.class)); + } + + @Test + 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.getRight().fire()); + + verify(mgr).publishAdmin(any(Query.class)); + } + + @Test + 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.getLeft()); + assertEquals(MY_HOST, msg.getRight().getSource()); + } + + @Test + 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.getLeft()); + assertEquals(MY_HOST, msg.getRight().getSource()); + + // this message should go to its successor + msg = capturePublishedMessage(Heartbeat.class, index++); + assertEquals(HOST1, msg.getLeft()); + assertEquals(MY_HOST, msg.getRight().getSource()); + } + +} diff --git a/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/state/IdleStateTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/state/IdleStateTest.java new file mode 100644 index 00000000..51e27738 --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/state/IdleStateTest.java @@ -0,0 +1,98 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018, 2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.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.Offline; +import org.onap.policy.drools.pooling.message.Query; + +class IdleStateTest extends SupportBasicStateTester { + + private IdleState state; + + /** + * Setup. + */ + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + + state = new IdleState(mgr); + } + + @Test + void testProcessHeartbeat() { + assertNull(state.process(new Heartbeat(PREV_HOST, 0L))); + verifyNothingPublished(); + } + + @Test + void testProcessIdentification() { + assertNull(state.process(new Identification(PREV_HOST, null))); + verifyNothingPublished(); + } + + @Test + 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 + void testProcessOffline() { + assertNull(state.process(new Offline(PREV_HOST))); + verifyNothingPublished(); + } + + @Test + 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-messages/src/test/java/org/onap/policy/drools/pooling/state/InactiveStateTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/state/InactiveStateTest.java new file mode 100644 index 00000000..a5ee2d06 --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/state/InactiveStateTest.java @@ -0,0 +1,121 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018, 2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2020, 2024 Nordix Foundation + * ================================================================================ + * 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.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.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 org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.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.Query; + +class InactiveStateTest extends SupportBasicStateTester { + + private InactiveState state; + + /** + * Setup. + * + */ + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + + state = new InactiveState(mgr); + } + + @Test + void testProcessLeader() { + State next = mock(State.class); + when(mgr.goActive()).thenReturn(next); + + String[] arr = {PREV_HOST, MY_HOST, HOST1}; + BucketAssignments asgn = new BucketAssignments(arr); + Leader msg = new Leader(PREV_HOST, asgn); + + assertEquals(next, state.process(msg)); + verify(mgr).startDistributing(asgn); + } + + @Test + void testProcessLeader_Invalid() { + 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 + 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 + void testGoInatcive() { + assertNull(state.goInactive()); + } + + @Test + void testStart() { + state.start(); + + Pair<Long, StateTimerTask> timer = onceTasks.remove(); + + assertEquals(STD_REACTIVATE_WAIT_MS, timer.getLeft().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.getRight().fire()); + } + + @Test + 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-messages/src/test/java/org/onap/policy/drools/pooling/state/ProcessingStateTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/state/ProcessingStateTest.java new file mode 100644 index 00000000..dbac7619 --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/state/ProcessingStateTest.java @@ -0,0 +1,396 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018, 2020-2021 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.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; +import org.onap.policy.drools.pooling.state.ProcessingState.HostBucket; + +class ProcessingStateTest extends SupportBasicStateTester { + + private ProcessingState state; + private HostBucket hostBucket; + + /** + * Setup. + */ + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + + state = new ProcessingState(mgr, MY_HOST); + hostBucket = new HostBucket(MY_HOST); + } + + @Test + 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 + 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 + void testProcessingState_NullLeader() { + when(mgr.getAssignments()).thenReturn(EMPTY_ASGN); + assertThrows(NullPointerException.class, () -> state = new ProcessingState(mgr, null)); + } + + @Test + void testProcessingState_ZeroLengthHostArray() { + when(mgr.getAssignments()).thenReturn(new BucketAssignments(new String[] {})); + assertThrows(IllegalArgumentException.class, () -> state = new ProcessingState(mgr, LEADER)); + } + + @Test + 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 + void testSetAssignments() { + state.setAssignments(null); + verify(mgr, never()).startDistributing(any()); + + state.setAssignments(ASGN3); + verify(mgr).startDistributing(ASGN3); + } + + @Test + 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 + void testSetLeader() { + state.setLeader(MY_HOST); + assertEquals(MY_HOST, state.getLeader()); + } + + @Test + void testSetLeader_Null() { + assertThrows(NullPointerException.class, () -> state.setLeader(null)); + } + + @Test + void testIsLeader() { + state.setLeader(MY_HOST); + assertTrue(state.isLeader()); + + state.setLeader(HOST2); + assertFalse(state.isLeader()); + } + + @Test + 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 + void testBecomeLeader_NotFirstAlive() { + // alive list contains something before my host name + var sortedHosts = sortHosts(PREV_HOST, MY_HOST); + assertThrows(IllegalArgumentException.class, () -> state.becomeLeader(sortedHosts)); + } + + @Test + 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 + void testMakeAssignments() throws Exception { + state.becomeLeader(sortHosts(MY_HOST, HOST2)); + + captureAssignments().checkValidity(); + } + + @Test + 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.stream(arr).allMatch(MY_HOST::equals)); + } + + @Test + 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.stream(arr).allMatch(MY_HOST::equals)); + } + + @Test + 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(); + + assertNotSame(HOST_ARR3, arr); + assertEquals(Arrays.asList(HOST_ARR3), Arrays.asList(arr)); + } + + @Test + 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 + 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 + 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 + 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()); + } + + @Test + void testHostBucketRemove_testHostBucketAdd_testHostBucketSize() { + assertEquals(0, hostBucket.size()); + + hostBucket.add(20); + hostBucket.add(30); + hostBucket.add(40); + assertEquals(3, hostBucket.size()); + + assertEquals(20, hostBucket.remove().intValue()); + assertEquals(30, hostBucket.remove().intValue()); + assertEquals(1, hostBucket.size()); + + // add more before taking the last item + hostBucket.add(50); + hostBucket.add(60); + assertEquals(3, hostBucket.size()); + + assertEquals(40, hostBucket.remove().intValue()); + assertEquals(50, hostBucket.remove().intValue()); + assertEquals(60, hostBucket.remove().intValue()); + assertEquals(0, hostBucket.size()); + + // add more AFTER taking the last item + hostBucket.add(70); + assertEquals(70, hostBucket.remove().intValue()); + assertEquals(0, hostBucket.size()); + } + + @Test + void testHostBucketCompareTo() { + HostBucket hb1 = new HostBucket(PREV_HOST); + HostBucket hb2 = new HostBucket(MY_HOST); + + assertEquals(0, hb1.compareTo(hb1)); + assertEquals(0, hb1.compareTo(new HostBucket(PREV_HOST))); + + // both empty + assertTrue(hb1.compareTo(hb2) < 0); + assertTrue(hb2.compareTo(hb1) > 0); + + // hb1 has one bucket, so it should not be larger + hb1.add(100); + assertTrue(hb1.compareTo(hb2) > 0); + assertTrue(hb2.compareTo(hb1) < 0); + + // hb1 has two buckets, so it should still be larger + hb1.add(200); + assertTrue(hb1.compareTo(hb2) > 0); + assertTrue(hb2.compareTo(hb1) < 0); + + // hb1 has two buckets, hb2 has one, so hb1 should still be larger + hb2.add(1000); + assertTrue(hb1.compareTo(hb2) > 0); + assertTrue(hb2.compareTo(hb1) < 0); + + // same number of buckets, so hb2 should now be larger + hb2.add(2000); + assertTrue(hb1.compareTo(hb2) < 0); + assertTrue(hb2.compareTo(hb1) > 0); + + // hb2 has more buckets, it should still be larger + hb2.add(3000); + assertTrue(hb1.compareTo(hb2) < 0); + assertTrue(hb2.compareTo(hb1) > 0); + } + + @Test + void testHostBucketHashCode() { + assertThrows(UnsupportedOperationException.class, () -> hostBucket.hashCode()); + } + + @Test + void testHostBucketEquals() { + assertThrows(UnsupportedOperationException.class, () -> hostBucket.equals(hostBucket)); + } +} diff --git a/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/state/QueryStateTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/state/QueryStateTest.java new file mode 100644 index 00000000..aae6e056 --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/state/QueryStateTest.java @@ -0,0 +1,444 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018, 2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2020, 2024 Nordix Foundation + * ================================================================================ + * 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.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +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 org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.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.Offline; + +class QueryStateTest extends SupportBasicStateTester { + + private QueryState state; + + /** + * Setup. + */ + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + + state = new QueryState(mgr); + } + + @Test + void testGoQuery() { + assertNull(state.goQuery()); + } + + @Test + void testStart() { + state.start(); + + Pair<Long, StateTimerTask> timer = onceTasks.remove(); + + assertEquals(STD_IDENTIFICATION_MS, timer.getLeft().longValue()); + assertNotNull(timer.getRight()); + } + + @Test + void testProcessIdentification_SameSource() { + String[] arr = {HOST2, PREV_HOST, MY_HOST}; + BucketAssignments asgn = new BucketAssignments(arr); + + assertNull(state.process(new Identification(MY_HOST, asgn))); + + // info should be unchanged + assertEquals(MY_HOST, state.getLeader()); + verify(mgr, never()).startDistributing(asgn); + } + + @Test + void testProcessIdentification_DiffSource() { + String[] arr = {HOST2, PREV_HOST, MY_HOST}; + BucketAssignments asgn = new BucketAssignments(arr); + + assertNull(state.process(new Identification(HOST2, asgn))); + + // leader should be unchanged + assertEquals(MY_HOST, state.getLeader()); + + // should have picked up the assignments + verify(mgr).startDistributing(asgn); + } + + @Test + void testProcessLeader_Invalid() { + 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 + void testProcessLeader_SameLeader() { + String[] arr = {HOST2, PREV_HOST, MY_HOST}; + BucketAssignments asgn = new BucketAssignments(arr); + + // identify a leader that's better than my host + assertNull(state.process(new Identification(PREV_HOST, asgn))); + + // now send a Leader message for that leader + 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, never()).goInactive(); + + // Ident msg + Leader msg = times(2) + verify(mgr, times(2)).startDistributing(asgn); + } + + @Test + void testProcessLeader_BetterLeaderWithAssignment() { + 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 + void testProcessLeader_BetterLeaderWithoutAssignment() { + String[] arr = {HOST2, PREV_HOST, HOST1}; + BucketAssignments asgn = new BucketAssignments(arr); + Leader msg = new Leader(PREV_HOST, asgn); + + State next = mock(State.class); + when(mgr.goInactive()).thenReturn(next); + + // should go Inactive, but start distributing + assertEquals(next, state.process(msg)); + verify(mgr).startDistributing(asgn); + verify(mgr, never()).goActive(); + } + + @Test + 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 + void testProcessOffline_NullHost() { + assertNull(state.process(new Offline())); + assertEquals(MY_HOST, state.getLeader()); + } + + @Test + void testProcessOffline_SameHost() { + assertNull(state.process(new Offline(MY_HOST))); + assertEquals(MY_HOST, state.getLeader()); + } + + @Test + 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 + void testQueryState() { + /* + * Prove the state is attached to the manager by invoking getHost(), which + * delegates to the manager. + */ + assertEquals(MY_HOST, state.getHost()); + } + + @Test + void testAwaitIdentification_MissingSelfIdent() { + state.start(); + + Pair<Long, StateTimerTask> timer = onceTasks.remove(); + + assertEquals(STD_IDENTIFICATION_MS, timer.getLeft().longValue()); + assertNotNull(timer.getRight()); + + // should published an Offline message and go inactive + + State next = mock(State.class); + when(mgr.goStart()).thenReturn(next); + + assertEquals(next, timer.getRight().fire()); + + // should continue distributing + verify(mgr, never()).startDistributing(null); + + Offline msg = captureAdminMessage(Offline.class); + assertEquals(MY_HOST, msg.getSource()); + } + + @Test + void testAwaitIdentification_Leader() { + state.start(); + state.process(new Identification(MY_HOST, null)); + + Pair<Long, StateTimerTask> timer = onceTasks.remove(); + + assertEquals(STD_IDENTIFICATION_MS, timer.getLeft().longValue()); + assertNotNull(timer.getRight()); + + State next = mock(State.class); + when(mgr.goActive()).thenReturn(next); + + assertEquals(next, timer.getRight().fire()); + + // should have published a Leader message + Leader msg = captureAdminMessage(Leader.class); + assertEquals(MY_HOST, msg.getSource()); + assertTrue(msg.getAssignments().hasAssignment(MY_HOST)); + } + + @Test + 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(); + state.process(new Identification(MY_HOST, null)); + + // 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.getLeft().longValue()); + assertNotNull(timer.getRight()); + + // set up active state, as that's what it should return + State next = mock(State.class); + when(mgr.goActive()).thenReturn(next); + + assertEquals(next, timer.getRight().fire()); + + // should NOT have published a Leader message + assertTrue(admin.isEmpty()); + + // should have gone active with the current assignments + verify(mgr).goActive(); + } + + @Test + 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(); + state.process(new Identification(MY_HOST, null)); + + // 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.getLeft().longValue()); + assertNotNull(timer.getRight()); + + // set up inactive state, as that's what it should return + State next = mock(State.class); + when(mgr.goInactive()).thenReturn(next); + + assertEquals(next, timer.getRight().fire()); + + // should NOT have published a Leader message + assertTrue(admin.isEmpty()); + } + + @Test + 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 + 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 + 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 + void testRecordInfo_NewIsNull() { + state.setAssignments(ASGN3); + state.process(new Identification(HOST1, null)); + + assertEquals(ASGN3, state.getAssignments()); + } + + @Test + void testRecordInfo_NewIsEmpty() { + state.setAssignments(ASGN3); + state.process(new Identification(PREV_HOST, new BucketAssignments())); + + assertEquals(ASGN3, state.getAssignments()); + } + + @Test + 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 + 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 + 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 + 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-messages/src/test/java/org/onap/policy/drools/pooling/state/StartStateTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/state/StartStateTest.java new file mode 100644 index 00000000..1a85304f --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/state/StartStateTest.java @@ -0,0 +1,184 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018, 2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2020, 2024 Nordix Foundation + * ================================================================================ + * 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.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +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 org.apache.commons.lang3.tuple.Pair; +import org.apache.commons.lang3.tuple.Triple; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +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; + +class StartStateTest extends SupportBasicStateTester { + + private StartState state; + + /** + * Setup. + */ + @Override + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + + state = new StartState(mgr); + } + + @Test + void testStart() { + state.start(); + + Pair<String, Heartbeat> msg = capturePublishedMessage(Heartbeat.class); + + assertEquals(MY_HOST, msg.getLeft()); + assertEquals(state.getHbTimestampMs(), msg.getRight().getTimestampMs()); + + + /* + * Verify heartbeat generator + */ + Triple<Long, Long, StateTimerTask> generator = repeatedTasks.removeFirst(); + + assertEquals(STD_INTER_HEARTBEAT_MS, generator.getLeft().longValue()); + assertEquals(STD_INTER_HEARTBEAT_MS, generator.getMiddle().longValue()); + + // invoke the task - it should generate another heartbeat + assertNull(generator.getRight().fire()); + verify(mgr, times(2)).publish(MY_HOST, msg.getRight()); + + // and again + assertNull(generator.getRight().fire()); + verify(mgr, times(3)).publish(MY_HOST, msg.getRight()); + + + /* + * Verify heartbeat checker + */ + Pair<Long, StateTimerTask> checker = onceTasks.removeFirst(); + + assertEquals(STD_HEARTBEAT_WAIT_MS, checker.getLeft().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, checker.getRight().fire()); + + verify(mgr).startDistributing(null); + } + + @Test + void testStartStatePoolingManager() { + /* + * Prove the state is attached to the manager by invoking getHost(), which + * delegates to the manager. + */ + assertEquals(MY_HOST, state.getHost()); + } + + @Test + 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 + 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 + void testProcessIdentification() { + assertNull(state.process(new Identification(MY_HOST, null))); + } + + @Test + void testProcessLeader() { + assertNull(state.process(new Leader(MY_HOST, null))); + } + + @Test + void testProcessOffline() { + assertNull(state.process(new Offline(HOST1))); + } + + @Test + void testProcessQuery() { + assertNull(state.process(new Query())); + } + + @Test + 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-messages/src/test/java/org/onap/policy/drools/pooling/state/StateTest.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/state/StateTest.java new file mode 100644 index 00000000..cfae6f3c --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/state/StateTest.java @@ -0,0 +1,466 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018, 2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * 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.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.policy.drools.pooling.CancellableScheduledTask; +import org.onap.policy.drools.pooling.PoolingManager; +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.Offline; +import org.onap.policy.drools.pooling.message.Query; + +class StateTest extends SupportBasicStateTester { + + private State state; + + /** + * Setup. + */ + @BeforeEach + public void setUp() throws Exception { + super.setUp(); + + state = new MyState(mgr); + } + + @Test + void testStatePoolingManager() { + /* + * Prove the state is attached to the manager by invoking getHost(), which + * delegates to the manager. + */ + assertEquals(MY_HOST, state.getHost()); + } + + @Test + 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 + 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); + + CancellableScheduledTask sched1 = onceSchedules.removeFirst(); + CancellableScheduledTask sched2 = onceSchedules.removeFirst(); + CancellableScheduledTask sched3 = repeatedSchedules.removeFirst(); + + verify(sched1, never()).cancel(); + verify(sched2, never()).cancel(); + verify(sched3, never()).cancel(); + + /* + * Cancel the timers. + */ + state.cancelTimers(); + + // verify that all were cancelled + verify(sched1).cancel(); + verify(sched2).cancel(); + verify(sched3).cancel(); + } + + @Test + void testStart() { + assertThatCode(() -> state.start()).doesNotThrowAnyException(); + } + + @Test + void testGoStart() { + State next = mock(State.class); + when(mgr.goStart()).thenReturn(next); + + State next2 = state.goStart(); + assertEquals(next, next2); + } + + @Test + void testGoQuery() { + State next = mock(State.class); + when(mgr.goQuery()).thenReturn(next); + + State next2 = state.goQuery(); + assertEquals(next, next2); + } + + @Test + void testGoActive_WithAssignment() { + State act = mock(State.class); + State inact = mock(State.class); + + when(mgr.goActive()).thenReturn(act); + when(mgr.goInactive()).thenReturn(inact); + + String[] arr = {HOST2, PREV_HOST, MY_HOST}; + BucketAssignments asgn = new BucketAssignments(arr); + + assertEquals(act, state.goActive(asgn)); + + verify(mgr).startDistributing(asgn); + } + + @Test + void testGoActive_WithoutAssignment() { + State act = mock(State.class); + State inact = mock(State.class); + + when(mgr.goActive()).thenReturn(act); + when(mgr.goInactive()).thenReturn(inact); + + String[] arr = {HOST2, PREV_HOST}; + BucketAssignments asgn = new BucketAssignments(arr); + + assertEquals(inact, state.goActive(asgn)); + + verify(mgr).startDistributing(asgn); + } + + @Test + void testGoActive_NullAssignment() { + State act = mock(State.class); + State inact = mock(State.class); + + when(mgr.goActive()).thenReturn(act); + when(mgr.goInactive()).thenReturn(inact); + + assertEquals(inact, state.goActive(null)); + + verify(mgr, never()).startDistributing(any()); + } + + @Test + void testGoInactive() { + State next = mock(State.class); + when(mgr.goInactive()).thenReturn(next); + + State next2 = state.goInactive(); + assertEquals(next, next2); + } + + @Test + void testProcessHeartbeat() { + assertNull(state.process(new Heartbeat())); + } + + @Test + void testProcessIdentification() { + assertNull(state.process(new Identification())); + } + + @Test + void testProcessLeader() { + String[] arr = {HOST2, HOST1}; + BucketAssignments asgn = new BucketAssignments(arr); + Leader msg = new Leader(HOST1, asgn); + + // should ignore it + assertNull(state.process(msg)); + verify(mgr).startDistributing(asgn); + } + + @Test + void testProcessLeader_Invalid() { + 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 + void testIsValidLeader_NullAssignment() { + assertFalse(state.isValid(new Leader(PREV_HOST, null))); + } + + @Test + void testIsValidLeader_NullSource() { + String[] arr = {HOST2, PREV_HOST, MY_HOST}; + BucketAssignments asgn = new BucketAssignments(arr); + assertFalse(state.isValid(new Leader(null, asgn))); + } + + @Test + void testIsValidLeader_EmptyAssignment() { + assertFalse(state.isValid(new Leader(PREV_HOST, new BucketAssignments()))); + } + + @Test + void testIsValidLeader_FromSelf() { + String[] arr = {HOST2, MY_HOST}; + BucketAssignments asgn = new BucketAssignments(arr); + + assertFalse(state.isValid(new Leader(MY_HOST, asgn))); + } + + @Test + void testIsValidLeader_WrongLeader() { + String[] arr = {HOST2, HOST3}; + BucketAssignments asgn = new BucketAssignments(arr); + + assertFalse(state.isValid(new Leader(HOST1, asgn))); + } + + @Test + void testIsValidLeader() { + String[] arr = {HOST2, HOST1}; + BucketAssignments asgn = new BucketAssignments(arr); + + assertTrue(state.isValid(new Leader(HOST1, asgn))); + } + + @Test + void testProcessOffline() { + assertNull(state.process(new Offline())); + } + + @Test + void testProcessQuery() { + assertNull(state.process(new Query())); + } + + @Test + void testPublishIdentification() { + Identification msg = new Identification(); + state.publish(msg); + + verify(mgr).publishAdmin(msg); + } + + @Test + void testPublishLeader() { + Leader msg = new Leader(); + state.publish(msg); + + verify(mgr).publishAdmin(msg); + } + + @Test + void testPublishOffline() { + Offline msg = new Offline(); + state.publish(msg); + + verify(mgr).publishAdmin(msg); + } + + @Test + void testPublishQuery() { + Query msg = new Query(); + state.publish(msg); + + verify(mgr).publishAdmin(msg); + } + + @Test + void testPublishStringHeartbeat() { + String chnl = "channelH"; + Heartbeat msg = new Heartbeat(); + + state.publish(chnl, msg); + + verify(mgr).publish(chnl, msg); + } + + @Test + void testStartDistributing() { + BucketAssignments asgn = new BucketAssignments(); + state.startDistributing(asgn); + + verify(mgr).startDistributing(asgn); + } + + @Test + void testStartDistributing_NullAssignments() { + state.startDistributing(null); + + verify(mgr, never()).startDistributing(any()); + } + + @Test + void testSchedule() { + int delay = 100; + + StateTimerTask task = mock(StateTimerTask.class); + + state.schedule(delay, task); + + CancellableScheduledTask sched = onceSchedules.removeFirst(); + + // scheduled, but not canceled yet + verify(mgr).schedule(delay, task); + verify(sched, never()).cancel(); + + /* + * 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(sched).cancel(); + } + + @Test + void testScheduleWithFixedDelay() { + int initdel = 100; + int delay = 200; + + StateTimerTask task = mock(StateTimerTask.class); + + state.scheduleWithFixedDelay(initdel, delay, task); + + CancellableScheduledTask sched = repeatedSchedules.removeFirst(); + + // scheduled, but not canceled yet + verify(mgr).scheduleWithFixedDelay(initdel, delay, task); + verify(sched, never()).cancel(); + + /* + * 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(sched).cancel(); + } + + @Test + void testMissedHeartbeat() { + State next = mock(State.class); + when(mgr.goStart()).thenReturn(next); + + State next2 = state.missedHeartbeat(); + assertEquals(next, next2); + + // should continue to distribute + verify(mgr, never()).startDistributing(null); + + Offline msg = captureAdminMessage(Offline.class); + assertEquals(MY_HOST, msg.getSource()); + } + + @Test + void testInternalTopicFailed() { + State next = mock(State.class); + when(mgr.goInactive()).thenReturn(next); + + State next2 = state.internalTopicFailed(); + assertEquals(next, next2); + + // should stop distributing + verify(mgr).startDistributing(null); + + Offline msg = captureAdminMessage(Offline.class); + assertEquals(MY_HOST, msg.getSource()); + } + + @Test + void testMakeHeartbeat() { + long timestamp = 30000L; + Heartbeat msg = state.makeHeartbeat(timestamp); + + assertEquals(MY_HOST, msg.getSource()); + assertEquals(timestamp, msg.getTimestampMs()); + } + + @Test + void testMakeIdentification() { + Identification ident = state.makeIdentification(); + assertEquals(MY_HOST, ident.getSource()); + assertEquals(ASGN3, ident.getAssignments()); + } + + @Test + void testMakeOffline() { + Offline msg = state.makeOffline(); + + assertEquals(MY_HOST, msg.getSource()); + } + + @Test + void testMakeQuery() { + Query msg = state.makeQuery(); + + assertEquals(MY_HOST, msg.getSource()); + } + + @Test + void testGetHost() { + assertEquals(MY_HOST, state.getHost()); + } + + @Test + void testGetTopic() { + assertEquals(MY_TOPIC, state.getTopic()); + } + + /** + * State used for testing purposes, with abstract methods implemented. + */ + private static class MyState extends State { + + public MyState(PoolingManager mgr) { + super(mgr); + } + } +} diff --git a/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/state/SupportBasicStateTester.java b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/state/SupportBasicStateTester.java new file mode 100644 index 00000000..8f2c9160 --- /dev/null +++ b/feature-pooling-messages/src/test/java/org/onap/policy/drools/pooling/state/SupportBasicStateTester.java @@ -0,0 +1,282 @@ +/* + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2018, 2020 AT&T Intellectual Property. All rights reserved. + * Modifications Copyright (C) 2020, 2024 Nordix Foundation + * ================================================================================ + * 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.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.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.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.commons.lang3.tuple.Triple; +import org.onap.policy.drools.pooling.CancellableScheduledTask; +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 State}. + */ +public class SupportBasicStateTester { + + 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); + + /** + * Scheduled tasks returned by schedule(). + */ + protected LinkedList<CancellableScheduledTask> onceSchedules; + + /** + * Tasks captured via schedule(). + */ + protected LinkedList<Pair<Long, StateTimerTask>> onceTasks; + + /** + * Scheduled tasks returned by scheduleWithFixedDelay(). + */ + protected LinkedList<CancellableScheduledTask> repeatedSchedules; + + /** + * 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 SupportBasicStateTester() { + super(); + } + + /** + * Setup. + * + * @throws Exception throws exception + */ + public void setUp() throws Exception { + onceSchedules = new LinkedList<>(); + onceTasks = new LinkedList<>(); + + repeatedSchedules = 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) {}; + + // capture publish() arguments + doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + published.add(Pair.of((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(Pair.of((Long) args[0], (StateTimerTask) args[1])); + + CancellableScheduledTask sched = mock(CancellableScheduledTask.class); + onceSchedules.add(sched); + return sched; + }); + + // capture scheduleWithFixedDelay() arguments, and return a new future + when(mgr.scheduleWithFixedDelay(anyLong(), anyLong(), any(StateTimerTask.class))).thenAnswer(invocation -> { + Object[] args = invocation.getArguments(); + repeatedTasks.add(Triple.of((Long) args[0], (Long) args[1], (StateTimerTask) args[2])); + + CancellableScheduledTask sched = mock(CancellableScheduledTask.class); + repeatedSchedules.add(sched); + return sched; + }); + + // get/set assignments in the manager + AtomicReference<BucketAssignments> asgn = new AtomicReference<>(ASGN3); + + when(mgr.getAssignments()).thenAnswer(args -> asgn.get()); + + doAnswer(args -> { + asgn.set(args.getArgument(0)); + 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 Pair.of(msg.getLeft(), clazz.cast(msg.getRight())); + } +} diff --git a/feature-pooling-messages/src/test/resources/logback-test.xml b/feature-pooling-messages/src/test/resources/logback-test.xml new file mode 100644 index 00000000..45d8201d --- /dev/null +++ b/feature-pooling-messages/src/test/resources/logback-test.xml @@ -0,0 +1,36 @@ +<!-- + ============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========================================================= + --> +<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 |