Add frankfurt rules for Actor redesign

Note: VcpeTest and VfwTest are not included, as they depend on updates
to the APPC and APPC-LCM Actors.
Added feature-controlloop-frankfurt.
Added HTTP client property files to feature-controlloop-management.

Updates per review comments:
- pom changes
- simplify FrankfurtBase
- rename event-svc-http.properties
- change "usescases" to "frankfurt"
- use blanks for CDS property defaults
- trailing spaces in http-client files
- add https property to http-client files

Added newlines to config files that appear to be missing them (based
on feedback from gerrit).

Issue-ID: POLICY-2385
Signed-off-by: Jim Hahn <jrh3@att.com>
Change-Id: Ib4a4d75461c734ae47309e41dc9d099e8815d55d
diff --git a/controlloop/common/controller-frankfurt/pom.xml b/controlloop/common/controller-frankfurt/pom.xml
new file mode 100644
index 0000000..9633f1f
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/pom.xml
@@ -0,0 +1,241 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ============LICENSE_START=======================================================
+  ONAP
+  ================================================================================
+  Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+  ================================================================================
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  ============LICENSE_END=========================================================
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.onap.policy.drools-applications.controlloop.common</groupId>
+        <artifactId>drools-applications-common</artifactId>
+        <version>1.6.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>controller-frankfurt</artifactId>
+    <packaging>kjar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>Frankfurt Experimental Controller</description>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.kie</groupId>
+                <artifactId>kie-maven-plugin</artifactId>
+                <extensions>true</extensions>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId>
+            <artifactId>events</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId>
+            <artifactId>aai</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId>
+            <artifactId>appc</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId>
+            <artifactId>appclcm</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId>
+            <artifactId>cds</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.drools-applications.controlloop.common</groupId>
+            <artifactId>guard</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId>
+            <artifactId>sdc</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId>
+            <artifactId>sdnc</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId>
+            <artifactId>sdnr</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId>
+            <artifactId>so</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId>
+            <artifactId>vfc</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.drools-applications.controlloop.common</groupId>
+            <artifactId>eventmanager</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId>
+            <artifactId>actorServiceProvider</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId>
+            <artifactId>actor.aai</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId>
+            <artifactId>actor.appc</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId>
+            <artifactId>actor.appclcm</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId>
+            <artifactId>actor.cds</artifactId>
+            <version>${policy.models.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId>
+            <artifactId>actor.guard</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId>
+            <artifactId>actor.sdnc</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId>
+            <artifactId>actor.sdnr</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId>
+            <artifactId>actor.so</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId>
+            <artifactId>actor.vfc</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions</groupId>
+            <artifactId>model-yaml</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.drools-pdp</groupId>
+            <artifactId>policy-management</artifactId>
+            <version>${version.policy.drools-pdp}</version>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions</groupId>
+            <artifactId>simulators</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <profiles>
+        <profile>
+            <!--This profile is used to store Eclipse m2e settings only. It has no
+                influence on the Maven build itself. -->
+            <id>only-eclipse</id>
+            <activation>
+                <property>
+                    <name>m2e.version</name>
+                </property>
+            </activation>
+            <build>
+                <pluginManagement>
+                    <plugins>
+                        <plugin>
+                            <groupId>org.eclipse.m2e</groupId>
+                            <artifactId>lifecycle-mapping</artifactId>
+                            <version>1.0.0</version>
+                            <configuration>
+                                <lifecycleMappingMetadata>
+                                    <pluginExecutions>
+                                        <pluginExecution>
+                                            <pluginExecutionFilter>
+                                                <groupId>org.kie</groupId>
+                                                <artifactId>kie-maven-plugin</artifactId>
+                                                <goals>
+                                                    <goal>build</goal>
+                                                </goals>
+                                            </pluginExecutionFilter>
+                                            <action>
+                                                <ignore />
+                                            </action>
+                                        </pluginExecution>
+                                    </pluginExecutions>
+                                </lifecycleMappingMetadata>
+                            </configuration>
+                        </plugin>
+                    </plugins>
+                </pluginManagement>
+            </build>
+        </profile>
+    </profiles>
+</project>
diff --git a/controlloop/common/controller-frankfurt/src/main/resources/META-INF/kmodule.xml b/controlloop/common/controller-frankfurt/src/main/resources/META-INF/kmodule.xml
new file mode 100644
index 0000000..b5e4e3f
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/src/main/resources/META-INF/kmodule.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ============LICENSE_START=======================================================
+  ONAP
+  ================================================================================
+  Copyright (C) 2020 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=========================================================
+  -->
+<kmodule xmlns="http://jboss.org/kie/6.0.0/kmodule">
+    <kbase name="onap.policies.controlloop.operational.common.Drools" default="false" equalsBehavior="equality"/>
+    <kbase name="onap.policies.controlloop.Operational" equalsBehavior="equality"
+           packages="org.onap.policy.controlloop" includes="onap.policies.controlloop.operational.common.Drools">
+        <ksession name="frankfurt"/>
+    </kbase>
+</kmodule>
diff --git a/controlloop/common/controller-frankfurt/src/main/resources/frankfurt.drl b/controlloop/common/controller-frankfurt/src/main/resources/frankfurt.drl
new file mode 100644
index 0000000..00c4f5f
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/src/main/resources/frankfurt.drl
@@ -0,0 +1,348 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop;
+
+import org.onap.policy.controlloop.drl.legacy.ControlLoopParams;
+import org.onap.policy.controlloop.CanonicalOnset;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+import org.onap.policy.controlloop.VirtualControlLoopNotification;
+import org.onap.policy.controlloop.ControlLoopNotificationType;
+import org.onap.policy.controlloop.policy.Policy;
+import org.onap.policy.controlloop.eventmanager.ControlLoopEventManager2;
+import org.onap.policy.controlloop.eventmanager.ControlLoopEventManager2.NewEventStatus;
+import org.onap.policy.controlloop.eventmanager.ControlLoopOperationManager2;
+import org.onap.policy.controlloop.utils.ControlLoopUtils;
+import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
+
+import org.slf4j.LoggerFactory;
+import org.slf4j.Logger;
+
+import org.onap.policy.drools.system.PolicyEngineConstants;
+
+/*
+*
+* Called when the ControlLoopParams object has been inserted into working memory from the BRMSGW.
+*
+*/
+rule "INSERT.PARAMS"
+    when
+        $params : ControlLoopParams()
+    then
+
+    Logger logger = LoggerFactory.getLogger(drools.getRule().getPackage());
+    logger.info("{}: {} : TOSCA-POLICY=[{}]", $params.getClosedLoopControlName(), $params.getPolicyName() + "."
+        + drools.getRule().getName(), $params.getToscaPolicy());
+end
+
+/*
+*
+* Called when a Tosca Policy is present.
+*
+*/
+rule "NEW.TOSCA.POLICY"
+    when
+        $policy : ToscaPolicy()
+    then
+
+    Logger logger = LoggerFactory.getLogger(drools.getRule().getPackage());
+    logger.info("{}: [{}|{}|{}|{}]: CONTENT: {}", drools.getRule().getName(),
+                $policy.getType(), $policy.getTypeVersion(), $policy.getName(),
+                $policy.getVersion(), $policy);
+
+    ControlLoopParams params = ControlLoopUtils.toControlLoopParams($policy);
+    if (params != null) {
+        insert(params);
+    }
+end
+
+/*
+ * Remove Control Loop Parameters.
+ */
+rule "REMOVE.PARAMS"
+    when
+        $params : ControlLoopParams( $policyName :  getPolicyName(), $policyVersion : getPolicyVersion() )
+        not ( ToscaPolicy( getName() == $policyName, getVersion() == $policyVersion ) )
+    then
+
+    Logger logger = LoggerFactory.getLogger(drools.getRule().getPackage());
+    logger.info("{}: [{}|{}|{}]", drools.getRule().getName(),
+                $params.getPolicyScope(), $params.getPolicyName(), $params.getPolicyVersion());
+
+    retract($params);
+end
+
+/*
+*
+* This rule responds to DCAE Events where there is no manager yet. Either it is
+* the first ONSET, or a subsequent badly formed Event (i.e. Syntax error, or is-closed-loop-disabled)
+*
+*/
+rule "EVENT"
+    when
+        $params : ControlLoopParams( $clName : getClosedLoopControlName() )
+        $event : CanonicalOnset( closedLoopControlName == $clName )
+        not ( ControlLoopEventManager2( closedLoopControlName == $event.getClosedLoopControlName(),
+            requestId == $event.getRequestId() ) )
+    then
+
+    Logger logger = LoggerFactory.getLogger(drools.getRule().getPackage());
+    logger.info("{}: {}.{}: event={}",
+                $clName, $params.getPolicyName(), drools.getRule().getName(),
+                $event);
+    //
+    // Retract the event from memory; it will be managed by the manager for now on
+    //
+    retract($event);
+
+    VirtualControlLoopNotification notification;
+
+    try {
+        //
+        // Check the event, because we need it to not be null when
+        // we create the ControlLoopEventManager2. The ControlLoopEventManager2
+        // will do extra syntax checking as well as check if the closed loop is disabled.
+        //
+        if ($event.getRequestId() == null) {
+            notification = new VirtualControlLoopNotification($event);
+            notification.setNotification(ControlLoopNotificationType.REJECTED);
+            notification.setFrom("policy");
+            notification.setMessage("Missing requestId");
+            notification.setPolicyName($params.getPolicyName() + "." + drools.getRule().getName());
+            notification.setPolicyScope($params.getPolicyScope());
+            notification.setPolicyVersion($params.getPolicyVersion());
+
+        } else {
+            ControlLoopEventManager2 manager = new ControlLoopEventManager2($params, $event, drools.getWorkingMemory());
+            insert(manager);
+            try {
+                manager.start();
+            } catch(Exception e) {
+                retract(manager);
+                throw e;
+            }
+            notification = manager.makeNotification();
+            notification.setNotification(ControlLoopNotificationType.ACTIVE);
+            notification.setPolicyName($params.getPolicyName() + "." + drools.getRule().getName());
+        }
+    } catch (Exception e) {
+        logger.warn("{}: {}.{}", $clName, $params.getPolicyName(), drools.getRule().getName(), e);
+        notification = new VirtualControlLoopNotification($event);
+        notification.setNotification(ControlLoopNotificationType.REJECTED);
+        notification.setMessage("Exception occurred: " + e.getMessage());
+        notification.setPolicyName($params.getPolicyName() + "." + drools.getRule().getName());
+        notification.setPolicyScope($params.getPolicyScope());
+        notification.setPolicyVersion($params.getPolicyVersion());
+    }
+    //
+    // Generate notification
+    //
+    try {
+        PolicyEngineConstants.getManager().deliver("POLICY-CL-MGT", notification);
+
+    } catch(RuntimeException e) {
+        logger.warn("{}: {}.{}: event={} exception generating notification",
+                $clName, $params.getPolicyName(), drools.getRule().getName(),
+                $event, e);
+    }
+end
+
+/*
+*
+* This rule happens when we get a subsequent event.
+*
+*/
+rule "EVENT.MANAGER.NEW.EVENT"
+    when
+        $event : VirtualControlLoopEvent( )
+        $manager : ControlLoopEventManager2( closedLoopControlName == $event.getClosedLoopControlName(),
+            requestId == $event.getRequestId() )
+    then
+
+    Logger logger = LoggerFactory.getLogger(drools.getRule().getPackage());
+    logger.info("{}: {}.{}: event={} manager={}",
+            $manager.getClosedLoopControlName(), $manager.getPolicyName(), drools.getRule().getName(),
+            $event, $manager);
+    //
+    // Remove the event from memory
+    //
+    retract($event);
+
+    //
+    // Check what kind of event this is
+    //
+    switch($manager.onNewEvent($event)) {
+        case SYNTAX_ERROR:
+            //
+            // Ignore any bad syntax events
+            //
+            logger.warn("{}: {}.{}: syntax error",
+                $manager.getClosedLoopControlName(), $manager.getPolicyName(), drools.getRule().getName());
+            break;
+
+        case FIRST_ABATEMENT:
+        case SUBSEQUENT_ABATEMENT:
+            //
+            // TODO: handle the abatement.  Currently, it's just discarded.
+            //
+            break;
+
+        case FIRST_ONSET:
+        case SUBSEQUENT_ONSET:
+        default:
+            //
+            // We don't care about subsequent onsets
+            //
+            logger.warn("{}: {}.{}: subsequent onset",
+                $manager.getClosedLoopControlName(), $manager.getPolicyName(), drools.getRule().getName());
+            break;
+    }
+end
+
+/*
+*
+* Step completed
+*
+*/
+rule "EVENT.MANAGER.PROCESSING"
+    when
+        $manager : ControlLoopEventManager2( isUpdated(), isActive(), $notification : getNotification() )
+    then
+
+    Logger logger = LoggerFactory.getLogger(drools.getRule().getPackage());
+    logger.info("{}: {}.{}: manager={}",
+            $manager.getClosedLoopControlName(), $manager.getPolicyName(), drools.getRule().getName(),
+            $manager);
+    //
+    // Generate notification
+    //
+    try {
+        $notification.setPolicyName($manager.getPolicyName() + "." + drools.getRule().getName());
+        PolicyEngineConstants.getManager().deliver("POLICY-CL-MGT", $notification);
+
+    } catch(RuntimeException e) {
+        logger.warn("{}: {}.{}: manager={} exception generating notification",
+                $manager.getClosedLoopControlName(), $manager.getPolicyName(), drools.getRule().getName(),
+                $manager, e);
+    }
+    //
+    // Discard this message and wait for the next response.
+    //
+    $manager.nextStep();
+    update($manager);
+end
+
+/*
+*
+* Final step completed
+*
+*/
+rule "EVENT.MANAGER.FINAL"
+    when
+        $manager : ControlLoopEventManager2( !isActive(), $notification : getNotification() )
+    then
+
+    Logger logger = LoggerFactory.getLogger(drools.getRule().getPackage());
+    logger.info("{}: {}.{}: manager={}",
+            $manager.getClosedLoopControlName(), $manager.getPolicyName(), drools.getRule().getName(),
+            $manager);
+    //
+    // Generate notification
+    //
+    try {
+        $notification.setPolicyName($manager.getPolicyName() + "." + drools.getRule().getName());
+        PolicyEngineConstants.getManager().deliver("POLICY-CL-MGT", $notification);
+
+    } catch(RuntimeException e) {
+        logger.warn("{}: {}.{}: manager={} exception generating notification",
+                $manager.getClosedLoopControlName(), $manager.getPolicyName(), drools.getRule().getName(),
+                $manager, e);
+    }
+    //
+    // Retract and destroy the manager
+    //
+    retract($manager);
+    $manager.destroy();
+end
+
+/*
+*
+* This rule will clean up any rogue events where there is no
+* ControlLoopParams object corresponding to the onset event.
+*
+*/
+rule "EVENT.CLEANUP"
+    salience -1
+    when
+        $event : VirtualControlLoopEvent( $clName: closedLoopControlName )
+    then
+
+    Logger logger = LoggerFactory.getLogger(drools.getRule().getPackage());
+    logger.info("{}: {}", $clName, drools.getRule().getName());
+    logger.debug("{}: {}: orphan event={}",
+                $clName, drools.getRule().getName(), $event);
+    //
+    // Retract the event
+    //
+    retract($event);
+end
+
+/*
+*
+* At this point, it appears that if we prevent the rules from getting messages from
+* topics, then that will also prevent the actors from getting them.  So the following
+* rules are here just to discard those messages.
+*
+* These have a higher salience so the objects are removed before the "FINAL" message
+* is processed, so that the junit test can assume things are done once they see the
+* "FINAL" message.  Otherwise, tests might fail sporadically.
+*
+*/
+rule "APPC.Response.CLEANUP"
+    salience 1
+    when
+        $msg : org.onap.policy.appc.Response( )
+    then
+        retract($msg);
+end
+
+rule "APPC.Request.CLEANUP"
+    salience 1
+    when
+        $msg : org.onap.policy.appc.Request( )
+    then
+        retract($msg);
+end
+
+rule "APPC-LCM.Response.CLEANUP"
+    salience 1
+    when
+        $msg : org.onap.policy.appclcm.AppcLcmDmaapWrapper( )
+    then
+        retract($msg);
+end
+
+rule "SDNR.Response.CLEANUP"
+    salience 1
+    when
+        $msg : org.onap.policy.sdnr.PciResponseWrapper( )
+    then
+        retract($msg);
+end
diff --git a/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/FrankfurtBase.java b/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/FrankfurtBase.java
new file mode 100644
index 0000000..6e3380e
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/FrankfurtBase.java
@@ -0,0 +1,558 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.Properties;
+import java.util.Queue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import lombok.Getter;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.kie.api.event.rule.AfterMatchFiredEvent;
+import org.kie.api.event.rule.BeforeMatchFiredEvent;
+import org.kie.api.event.rule.DefaultAgendaEventListener;
+import org.kie.api.event.rule.DefaultRuleRuntimeEventListener;
+import org.kie.api.event.rule.MatchCancelledEvent;
+import org.kie.api.event.rule.MatchCreatedEvent;
+import org.kie.api.event.rule.ObjectDeletedEvent;
+import org.kie.api.event.rule.ObjectInsertedEvent;
+import org.kie.api.event.rule.ObjectUpdatedEvent;
+import org.kie.api.event.rule.RuleRuntimeEventListener;
+import org.kie.api.runtime.KieSession;
+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.TopicEndpointManager;
+import org.onap.policy.common.endpoints.event.comm.TopicListener;
+import org.onap.policy.common.endpoints.http.client.HttpClientConfigException;
+import org.onap.policy.common.endpoints.http.client.HttpClientFactoryInstance;
+import org.onap.policy.common.endpoints.http.server.HttpServletServerFactoryInstance;
+import org.onap.policy.common.utils.coder.Coder;
+import org.onap.policy.common.utils.coder.CoderException;
+import org.onap.policy.common.utils.coder.StandardCoder;
+import org.onap.policy.common.utils.resources.ResourceUtils;
+import org.onap.policy.controlloop.drl.legacy.ControlLoopParams;
+import org.onap.policy.drools.persistence.SystemPersistence;
+import org.onap.policy.drools.persistence.SystemPersistenceConstants;
+import org.onap.policy.drools.protocol.coders.EventProtocolCoderConstants;
+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.system.PolicyEngineConstants;
+import org.onap.policy.drools.util.KieUtils;
+import org.onap.policy.drools.utils.logging.LoggerUtil;
+import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
+import org.onap.policy.models.tosca.authorative.concepts.ToscaServiceTemplate;
+import org.onap.policy.simulators.Util;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Use Cases Tests Framework.
+ */
+public abstract class FrankfurtBase {
+
+    private static final Logger logger = LoggerFactory.getLogger(FrankfurtBase.class);
+    private static final StandardCoder coder = new StandardCoder();
+
+    /**
+     * PDP-D Engine.
+     */
+    protected static final PolicyEngine pdpD = PolicyEngineConstants.getManager();
+
+    /**
+     * PDP-D Configuration Repository.
+     */
+    protected static final SystemPersistence repo = SystemPersistenceConstants.getManager();
+
+    /**
+     * Frankfurt controller and session name.
+     */
+    protected static final String CONTROLLER_NAME = "frankfurt";
+
+    /**
+     * Frankfurt controller.
+     */
+    protected static PolicyController controller;
+
+    /*
+     * Canonical Topic Names.
+     */
+    protected static final String DCAE_TOPIC = "DCAE_TOPIC";
+    protected static final String APPC_LCM_WRITE_TOPIC = "APPC-LCM-WRITE";
+    protected static final String POLICY_CL_MGT_TOPIC = "POLICY-CL-MGT";
+    protected static final String APPC_LCM_READ_TOPIC = "APPC-LCM-READ";
+    protected static final String APPC_CL_TOPIC = "APPC-CL";
+
+    protected static void initConfigDir() {
+        SystemPersistenceConstants.getManager().setConfigurationDir("src/test/resources/config");
+    }
+
+    /**
+     * Sets up overall logging.
+     */
+    protected static void setupLogging() {
+        LoggerUtil.setLevel(LoggerUtil.ROOT_LOGGER, "WARN");
+        LoggerUtil.setLevel("org.eclipse.jetty", "WARN");
+        LoggerUtil.setLevel("org.onap.policy.controlloop", "INFO");
+        LoggerUtil.setLevel("network", "INFO");
+    }
+
+    /**
+     * Sets up Drools Logging for events of interest.
+     */
+    protected static void setupDroolsLogging() {
+        KieSession session = PolicyControllerConstants.getFactory().get(CONTROLLER_NAME).getDrools().getContainer()
+                        .getPolicySession(CONTROLLER_NAME).getKieSession();
+
+        session.addEventListener(new RuleListenerLogger());
+        session.addEventListener(new AgendaListenerLogger());
+    }
+
+    /**
+     * Sets up Http Clients specified in the property file.
+     */
+    protected static void setUpHttpClients() {
+        try {
+            HttpClientFactoryInstance.getClientFactory().build(
+                            SystemPersistenceConstants.getManager().getHttpClientProperties("frankfurt"));
+        } catch (HttpClientConfigException e) {
+            throw new IllegalArgumentException("cannot initialize HTTP clients", e);
+        }
+    }
+
+    /**
+     * Sets up Simulators for use case testing.
+     */
+    protected static void setupSimulators() throws InterruptedException {
+        Util.buildAaiSim();
+        Util.buildSoSim();
+        Util.buildVfcSim();
+        Util.buildGuardSim();
+        Util.buildSdncSim();
+    }
+
+    /**
+     * Returns the runtime Control Loop Parameters associated with a Tosca Policy.
+     */
+    protected ControlLoopParams clParameters(ToscaPolicy policy) {
+        return controller.getDrools().facts(CONTROLLER_NAME, ControlLoopParams.class).stream()
+                        .filter((params) -> params.getToscaPolicy() == policy).findFirst().get();
+    }
+
+    protected ToscaPolicy getPolicyFromResource(String resourcePath, String policyName) throws CoderException {
+        String policyJson = ResourceUtils.getResourceAsString(resourcePath);
+        ToscaServiceTemplate serviceTemplate = coder.decode(policyJson, ToscaServiceTemplate.class);
+        ToscaPolicy policy = serviceTemplate.getToscaTopologyTemplate().getPolicies().get(0).get(policyName);
+        assertNotNull(policy);
+
+        /*
+         * name and version are used within a drl. api component and drools core will
+         * ensure that these are populated.
+         */
+        if (StringUtils.isBlank(policy.getName())) {
+            policy.setName(policyName);
+        }
+
+        if (StringUtils.isBlank(policy.getVersion())) {
+            policy.setVersion(policy.getTypeVersion());
+        }
+
+        return serviceTemplate.getToscaTopologyTemplate().getPolicies().get(0).get(policyName);
+    }
+
+    protected ToscaPolicy getPolicyFromFile(String policyPath) throws IOException, CoderException {
+        String rawPolicy = new String(Files.readAllBytes(Paths.get(policyPath)));
+        return coder.decode(rawPolicy, ToscaPolicy.class);
+    }
+
+    private ToscaPolicy setupPolicy(ToscaPolicy policy) throws InterruptedException {
+        final KieObjectExpectedCallback<?> policyTracker = new KieObjectInsertedExpectedCallback<>(policy);
+        final KieObjectExpectedCallback<?> paramsTracker =
+                        new KieClassInsertedExpectedCallback<>(ControlLoopParams.class);
+
+        controller.getDrools().offer(policy);
+
+        assertTrue(policyTracker.isNotified());
+        assertTrue(paramsTracker.isNotified());
+
+        assertEquals(1, controller.getDrools().facts(CONTROLLER_NAME, ToscaPolicy.class).stream()
+                        .filter((anotherPolicy) -> anotherPolicy == policy).count());
+
+        assertEquals(1, controller.getDrools().facts(CONTROLLER_NAME, ControlLoopParams.class).stream()
+                        .filter((params) -> params.getToscaPolicy() == policy).count());
+        return policy;
+    }
+
+    /**
+     * Installs a policy from policy/models (examples) repo.
+     */
+    protected ToscaPolicy setupPolicyFromResource(String resourcePath, String policyName)
+                    throws CoderException, InterruptedException {
+        return setupPolicy(getPolicyFromResource(resourcePath, policyName));
+    }
+
+
+    /**
+     * Installs a given policy.
+     */
+    protected ToscaPolicy setupPolicyFromFile(String policyPath)
+                    throws IOException, CoderException, InterruptedException {
+        return setupPolicy(getPolicyFromFile(policyPath));
+    }
+
+    /**
+     * Deletes a policy.
+     */
+    protected void deletePolicy(ToscaPolicy policy) throws InterruptedException {
+        ControlLoopParams clParams = clParameters(policy);
+        assertNotNull(clParams);
+
+        final KieObjectExpectedCallback<?> policyTracker = new KieObjectDeletedExpectedCallback<>(policy);
+        final KieObjectExpectedCallback<?> clParamsTracker = new KieObjectDeletedExpectedCallback<>(clParams);
+
+        controller.getDrools().delete(CONTROLLER_NAME, policy);
+        assertTrue(policyTracker.isNotified());
+        assertTrue(clParamsTracker.isNotified());
+
+        assertEquals(0, controller.getDrools().facts(CONTROLLER_NAME, ToscaPolicy.class).stream()
+                        .filter((anotherPolicy) -> anotherPolicy == policy).count());
+
+        assertEquals(0, controller.getDrools().facts(CONTROLLER_NAME, ControlLoopParams.class).stream()
+                        .filter((params) -> params.getPolicyName() == policy.getName()).count());
+    }
+
+    /**
+     * Prepare a PDP-D to test the Use Cases.
+     */
+    protected static void preparePdpD() throws IOException {
+        KieUtils.installArtifact(Paths.get("src/main/resources/META-INF/kmodule.xml").toFile(),
+                        Paths.get("src/test/resources/frankfurt.pom").toFile(),
+                        "src/main/resources/org/onap/policy/controlloop/",
+                        Collections.singletonList(Paths.get("src/main/resources/frankfurt.drl").toFile()));
+
+        repo.setConfigurationDir("src/test/resources/config");
+        pdpD.configure(new Properties());
+
+        controller = pdpD.createPolicyController(CONTROLLER_NAME, repo.getControllerProperties(CONTROLLER_NAME));
+        pdpD.start();
+
+        setupDroolsLogging();
+    }
+
+    /**
+     * Stop PDP-D.
+     */
+    protected static void stopPdpD() {
+        PolicyControllerConstants.getFactory().shutdown(CONTROLLER_NAME);
+        pdpD.stop();
+    }
+
+    /**
+     * Stops the http clients.
+     */
+    protected static void stopHttpClients() {
+        HttpClientFactoryInstance.getClientFactory().destroy();
+    }
+
+    /**
+     * Stop Simulators.
+     */
+    protected static void stopSimulators() {
+        HttpServletServerFactoryInstance.getServerFactory().destroy();
+    }
+
+    /**
+     * Creates a Topic Sink Callback tracker.
+     */
+    protected <T> TopicCallback<T> createTopicSinkCallback(String topicName, Class<T> clazz) {
+        return new TopicCallback<>(TopicEndpointManager.getManager().getNoopTopicSink(topicName), clazz);
+    }
+
+    /**
+     * Creates a Topic Sink Callback tracker.
+     */
+    protected <T> TopicCallback<T> createTopicSinkCallbackPlain(String topicName, Class<T> clazz, Coder coder) {
+        return new TopicCallbackCoder<>(TopicEndpointManager.getManager().getNoopTopicSink(topicName), clazz, coder);
+    }
+
+    /**
+     * Creates a Topic Source Callback tracker.
+     */
+    protected <T> TopicCallback<T> createTopicSourceCallback(String topicName, Class<T> clazz) {
+        return new TopicCallback<>(TopicEndpointManager.getManager().getNoopTopicSource(topicName), clazz);
+    }
+
+    /**
+     * Injects a message on a Topic Source.
+     */
+    protected void injectOnTopic(String topicName, Path onsetPath) throws IOException {
+        TopicEndpointManager.getManager().getNoopTopicSource(topicName)
+                        .offer(new String(Files.readAllBytes(onsetPath)));
+    }
+
+    /**
+     * Injects a message on a Topic Source, with the given substitution..
+     */
+    protected void injectOnTopic(String topicName, Path path, String newText) throws IOException {
+        String text = IOUtils.toString(path.toUri(), StandardCharsets.UTF_8);
+        text = text.replace("${replaceMe}", newText);
+        TopicEndpointManager.getManager().getNoopTopicSource(topicName).offer(text);
+    }
+
+    /**
+     * Waits for LOCK acquisition and getting a Permit from PDP-X to proceed.
+     */
+    protected void waitForLockAndPermit(ToscaPolicy policy, TopicCallback<VirtualControlLoopNotification> policyClMgt) {
+        String policyName = policy.getIdentifier().getName();
+
+        // TODO register a topic listener instead of using await() ?
+
+        await().until(() -> !policyClMgt.getMessages().isEmpty());
+        VirtualControlLoopNotification notif = policyClMgt.getMessages().remove();
+        assertEquals(ControlLoopNotificationType.ACTIVE, notif.getNotification());
+        assertEquals(policyName + ".EVENT", notif.getPolicyName());
+
+        await().until(() -> !policyClMgt.getMessages().isEmpty());
+        notif = policyClMgt.getMessages().remove();
+        assertEquals(ControlLoopNotificationType.OPERATION, notif.getNotification());
+        assertEquals(policyName + ".EVENT.MANAGER.PROCESSING", notif.getPolicyName());
+        assertThat(notif.getMessage()).startsWith("Sending guard query");
+
+        await().until(() -> !policyClMgt.getMessages().isEmpty());
+        notif = policyClMgt.getMessages().remove();
+        assertEquals(ControlLoopNotificationType.OPERATION, notif.getNotification());
+        assertEquals(policyName + ".EVENT.MANAGER.PROCESSING", notif.getPolicyName());
+        assertThat(notif.getMessage()).startsWith("Guard result").endsWith("Permit");
+    }
+
+    /**
+     * Waits for a FINAL SUCCESS transaction notification.
+     */
+    protected void waitForFinalSuccess(ToscaPolicy policy, TopicCallback<VirtualControlLoopNotification> policyClMgt) {
+        await().until(() -> !policyClMgt.getMessages().isEmpty());
+        assertEquals(ControlLoopNotificationType.FINAL_SUCCESS, policyClMgt.getMessages().peek().getNotification());
+        assertEquals(policy.getIdentifier().getName() + ".EVENT.MANAGER.FINAL",
+                        policyClMgt.getMessages().remove().getPolicyName());
+    }
+
+    /**
+     * Logs Modifications to Working Memory.
+     */
+    static class RuleListenerLogger implements RuleRuntimeEventListener {
+        @Override
+        public void objectInserted(ObjectInsertedEvent event) {
+            String ruleName = (event.getRule() != null) ? event.getRule().getName() : "null";
+            logger.info("RULE {}: inserted {}", ruleName, event.getObject());
+        }
+
+        @Override
+        public void objectUpdated(ObjectUpdatedEvent event) {
+            String ruleName = (event.getRule() != null) ? event.getRule().getName() : "null";
+            logger.info("RULE {}: updated {}", ruleName, event.getObject());
+
+        }
+
+        @Override
+        public void objectDeleted(ObjectDeletedEvent event) {
+            String ruleName = (event.getRule() != null) ? event.getRule().getName() : "null";
+            logger.info("RULE {}: deleted {}", ruleName, event.getOldObject());
+        }
+    }
+
+    /**
+     * Logs Rule Matches.
+     */
+    static class AgendaListenerLogger extends DefaultAgendaEventListener {
+        @Override
+        public void matchCreated(MatchCreatedEvent event) {
+            logger.info("RULE {}: match created", event.getMatch().getRule().getName());
+        }
+
+        @Override
+        public void matchCancelled(MatchCancelledEvent event) {
+            logger.info("RULE {}: match cancelled", event.getMatch().getRule().getName());
+        }
+
+        @Override
+        public void beforeMatchFired(BeforeMatchFiredEvent event) {
+            logger.info("RULE {}: before match fired", event.getMatch().getRule().getName());
+        }
+
+        @Override
+        public void afterMatchFired(AfterMatchFiredEvent event) {
+            logger.info("RULE {}: after match fired", event.getMatch().getRule().getName());
+        }
+    }
+
+    /**
+     * Base Class to track Working Memory updates for objects of type T.
+     */
+    abstract class KieObjectExpectedCallback<T> extends DefaultRuleRuntimeEventListener {
+        protected T subject;
+
+        protected CountDownLatch countDownLatch = new CountDownLatch(1);
+
+        public KieObjectExpectedCallback(T affected) {
+            subject = affected;
+            register();
+        }
+
+        public boolean isNotified() throws InterruptedException {
+            return countDownLatch.await(9L, TimeUnit.SECONDS);
+        }
+
+        protected void callbacked() {
+            unregister();
+            countDownLatch.countDown();
+        }
+
+        public KieObjectExpectedCallback<T> register() {
+            controller.getDrools().getContainer().getPolicySession(CONTROLLER_NAME).getKieSession()
+                            .addEventListener(this);
+            return this;
+        }
+
+        public KieObjectExpectedCallback<T> unregister() {
+            controller.getDrools().getContainer().getPolicySession(CONTROLLER_NAME).getKieSession()
+                            .removeEventListener(this);
+            return this;
+        }
+    }
+
+    /**
+     * Tracks inserts in Working Memory for an object of type T.
+     */
+    class KieObjectInsertedExpectedCallback<T> extends KieObjectExpectedCallback<T> {
+        public KieObjectInsertedExpectedCallback(T affected) {
+            super(affected);
+        }
+
+        @Override
+        public void objectInserted(ObjectInsertedEvent event) {
+            if (subject == event.getObject()) {
+                callbacked();
+            }
+        }
+    }
+
+    /**
+     * Tracks deletes in Working Memory of an object of type T.
+     */
+    class KieObjectDeletedExpectedCallback<T> extends KieObjectExpectedCallback<T> {
+        public KieObjectDeletedExpectedCallback(T affected) {
+            super(affected);
+        }
+
+        @Override
+        public void objectDeleted(ObjectDeletedEvent event) {
+            if (subject == event.getOldObject()) {
+                callbacked();
+            }
+        }
+    }
+
+    /**
+     * Tracks inserts in Working Memory for any object of class T.
+     */
+    class KieClassInsertedExpectedCallback<T> extends KieObjectInsertedExpectedCallback<T> {
+
+        public KieClassInsertedExpectedCallback(T affected) {
+            super(affected);
+        }
+
+        public void objectInserted(ObjectInsertedEvent event) {
+            if (subject == event.getObject().getClass()) {
+                callbacked();
+            }
+        }
+    }
+
+    /**
+     * Tracks callbacks from topics.
+     */
+    class TopicCallback<T> implements TopicListener {
+        protected final Topic topic;
+        protected final Class<T> expectedClass;
+
+        @Getter
+        protected Queue<T> messages = new LinkedList<>();
+
+        public TopicCallback(Topic topic, Class<T> expectedClass) {
+            this.topic = topic;
+            this.expectedClass = expectedClass;
+            this.topic.register(this);
+        }
+
+        public TopicCallback<T> register() {
+            this.topic.register(this);
+            return this;
+        }
+
+        public TopicCallback<T> unregister() {
+            this.topic.unregister(this);
+            return this;
+        }
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public void onTopicEvent(CommInfrastructure comm, String topic, String event) {
+            try {
+                messages.add((T) EventProtocolCoderConstants.getManager().decode(controller.getDrools().getGroupId(),
+                                controller.getDrools().getArtifactId(), topic, event));
+            } catch (Exception e) {
+                logger.warn("invalid mapping in topic {} for event {}", topic, event, e);
+            }
+        }
+    }
+
+    class TopicCallbackCoder<T> extends TopicCallback<T> {
+        private final Coder coder;
+
+        public TopicCallbackCoder(Topic topic, Class<T> expectedClass, Coder coder) {
+            super(topic, expectedClass);
+            this.coder = coder;
+        }
+
+        @Override
+        public void onTopicEvent(CommInfrastructure comm, String topic, String event) {
+            try {
+                messages.add((T) coder.decode(event, expectedClass));
+            } catch (Exception e) {
+                logger.warn("invalid mapping in topic {} for event {}", topic, event, e);
+            }
+        }
+
+    }
+}
diff --git a/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/VlbTest.java b/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/VlbTest.java
new file mode 100644
index 0000000..4e5a6e3
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/src/test/java/org/onap/policy/controlloop/VlbTest.java
@@ -0,0 +1,152 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.nio.file.Paths;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.onap.policy.common.utils.coder.CoderException;
+import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
+
+/**
+ * VLB Use Case Tests.
+ */
+public class VlbTest extends FrankfurtBase {
+
+    /**
+     * VLB Tosca Policy File.
+     */
+    private static final String TOSCA_LEGACY_POLICY_VLB = "src/test/resources/vlb/tosca-vlb.json";
+    private static final String TOSCA_COMPLIANT_POLICY_VLB = "src/test/resources/vlb/tosca-compliant-vlb.json";
+
+    /*
+     * VLB Use case Messages.
+     */
+    private static final String ONSET = "src/test/resources/vlb/vlb.onset.json";
+
+    /*
+     * Topic trackers used by the VLB use case.
+     */
+    private TopicCallback<VirtualControlLoopNotification> policyClMgt;
+
+    /*
+     * VLB Tosca Policy.
+     */
+    private ToscaPolicy policy;
+
+    /**
+     * Prepare PDP-D Framework for testing.
+     */
+    @BeforeClass
+    public static void prepareResouces() throws InterruptedException, IOException {
+        initConfigDir();
+        setupLogging();
+        preparePdpD();
+        setUpHttpClients();
+        setupSimulators();
+    }
+
+    /**
+     * Take down the resources used by the test framework.
+     */
+    @AfterClass
+    public static void takeDownResources() {
+        stopPdpD();
+        stopSimulators();
+    }
+
+    /**
+     * Observe Topics.
+     */
+    @Before
+    public void topicsRegistration() {
+        policyClMgt = createTopicSinkCallback(POLICY_CL_MGT_TOPIC, VirtualControlLoopNotification.class);
+    }
+
+    /**
+     * Unregister Topic Callbacks and uninstall the policy.
+     */
+    @After
+    public void topicsUnregistration() throws InterruptedException {
+        if (policyClMgt != null) {
+            policyClMgt.unregister();
+        }
+
+        // uninstall the policy
+        assertEquals(2, controller.getDrools().factCount(CONTROLLER_NAME));
+        if (policy != null) {
+            deletePolicy(policy);
+        }
+        assertEquals(0, controller.getDrools().factCount(CONTROLLER_NAME));
+    }
+
+    /**
+     * Sunny Day with Legacy Tosca Policy.
+     */
+    @Test
+    public void sunnyDayLegacy() throws InterruptedException, CoderException, IOException {
+        assertEquals(0, controller.getDrools().factCount(CONTROLLER_NAME));
+        policy = setupPolicyFromFile(TOSCA_LEGACY_POLICY_VLB);
+        assertEquals(2, controller.getDrools().factCount(CONTROLLER_NAME));
+
+        sunnyDay();
+    }
+
+    /**
+     * Sunny Day with Tosca Compliant Policy.
+     */
+    @Test
+    public void sunnyDayCompliant() throws InterruptedException, CoderException, IOException {
+        assertEquals(0, controller.getDrools().factCount(CONTROLLER_NAME));
+        policy = setupPolicyFromFile(TOSCA_COMPLIANT_POLICY_VLB);
+        assertEquals(2, controller.getDrools().factCount(CONTROLLER_NAME));
+
+        sunnyDay();
+    }
+
+    /**
+     * Sunny day scenario for the VCPE use case.
+     */
+    private void sunnyDay() throws IOException {
+
+        /* Inject an ONSET event over the DCAE topic */
+        injectOnTopic(DCAE_TOPIC, Paths.get(ONSET));
+
+        /* Wait to acquire a LOCK and a PDP-X PERMIT */
+        waitForLockAndPermit(policy, policyClMgt);
+
+        /* Ensure that the VLB SO Operation was successfully completed */
+
+        await().until(() -> !policyClMgt.getMessages().isEmpty());
+        assertEquals(ControlLoopNotificationType.OPERATION_SUCCESS,
+                        policyClMgt.getMessages().remove().getNotification());
+
+        /* --- VLB Transaction Completed --- */
+        waitForFinalSuccess(policy, policyClMgt);
+    }
+}
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/config/event-manager.properties b/controlloop/common/controller-frankfurt/src/test/resources/config/event-manager.properties
new file mode 100644
index 0000000..7416d04
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/src/test/resources/config/event-manager.properties
@@ -0,0 +1,70 @@
+#
+# ============LICENSE_START======================================================
+# ONAP
+# ===============================================================================
+# Copyright (C) 2020 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========================================================
+#
+
+# DB parameters
+operation.history.url=jdbc:h2:mem:Frankfurt
+operation.history.userName=sa
+operation.history.password=
+
+# Actor parameters
+#
+# Note: every operation must have at least one entry, otherwise it will not be
+# configured and started.  Thus some of them have a "placeholder" property.
+#
+
+actor.service.GUARD.disabled=true
+actor.service.GUARD.clientName=GUARD
+actor.service.GUARD.operations.Decision.path=decision
+
+actor.service.AAI.clientName=AAI
+actor.service.AAI.operations.CustomQuery.path=aai/v16/query
+actor.service.AAI.operations.Tenant.path=aai/v16/search/nodes-query
+
+actor.service.APPC.sinkTopic=APPC-LCM-READ
+actor.service.APPC.sourceTopic=APPC-LCM-WRITE
+actor.service.APPC.operations.ConfigModify.placeholder=
+actor.service.APPC.operations.Migrate.placeholder=
+actor.service.APPC.operations.Restart.placeholder=
+actor.service.APPC.operations.Rebuild.placeholder=
+
+# legacy APPC - must specify sink and source for each operation
+actor.service.APPC.operations.ModifyConfig.sinkTopic=APPC-CL
+actor.service.APPC.operations.ModifyConfig.sourceTopic=APPC-CL
+
+actor.service.CDS.clientName=CDS
+actor.service.CDS.grpcHost=localhost
+actor.service.CDS.grpcPort=7878
+actor.service.CDS.grpcUsername=grpc-username
+actor.service.CDS.grpcPassword=grpc-password
+actor.service.CDS.grpcTimeout=10
+actor.service.CDS.operations.xxx.yyy=
+
+actor.service.SDNC.clientName=SDNC
+actor.service.SDNC.operations.xxx.yyy=
+
+actor.service.SO.clientName=SO
+actor.service.SO.pathGet=orchestrationRequests/v5/
+actor.service.SO.maxGets=20
+actor.service.SO.waitSecGet=20
+actor.service.SO.operations.VF\ Module\ Create.path=serviceInstantiation/v7/serviceInstances
+actor.service.SO.operations.VF\ Module\ Delete.path=serviceInstances/v7
+
+actor.service.VFC.clientName=VFC
+actor.service.VFC.operations.xxx.yyy=
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/config/frankfurt-controller.properties b/controlloop/common/controller-frankfurt/src/test/resources/config/frankfurt-controller.properties
new file mode 100644
index 0000000..694f66e
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/src/test/resources/config/frankfurt-controller.properties
@@ -0,0 +1,65 @@
+#
+# ============LICENSE_START=======================================================
+# ONAP
+# ================================================================================
+# Copyright (C) 2020 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=========================================================
+#
+
+controller.name=frankfurt
+
+rules.groupId=org.onap.policy.controlloop
+rules.artifactId=frankfurt
+rules.version=1.0.0
+
+noop.source.topics=DCAE_TOPIC,APPC-CL,APPC-LCM-WRITE,SDNR-CL-RSP,POLICY-CL-MGT,APPC-LCM-READ,SDNR-CL,DCAE_CL_RSP
+
+noop.source.topics.DCAE_TOPIC.events=\
+    org.onap.policy.controlloop.CanonicalOnset,org.onap.policy.controlloop.CanonicalAbated
+noop.source.topics.DCAE_TOPIC.events.org.onap.policy.controlloop.CanonicalOnset.filter=\
+    [?($.closedLoopEventStatus == 'ONSET')]
+noop.source.topics.DCAE_TOPIC.events.org.onap.policy.controlloop.CanonicalAbated.filter=\
+    [?($.closedLoopEventStatus == 'ABATED')]
+noop.source.topics.DCAE_TOPIC.events.custom.gson=org.onap.policy.controlloop.util.Serialization,gson
+
+noop.source.topics.APPC-CL.events=org.onap.policy.appc.Response,org.onap.policy.appc.Request
+noop.source.topics.APPC-CL.events.org.onap.policy.appc.Response.filter=[?($.CommonHeader && $.Status)]
+noop.source.topics.APPC-CL.events.org.onap.policy.appc.Request.filter=[?($.CommonHeader && $.Action)]
+noop.source.topics.APPC-CL.events.custom.gson=org.onap.policy.appc.util.Serialization,gsonPretty
+
+noop.source.topics.APPC-LCM-WRITE.events=org.onap.policy.appclcm.AppcLcmDmaapWrapper
+noop.source.topics.APPC-LCM-WRITE.events.org.onap.policy.appclcm.AppcLcmDmaapWrapper.filter=[?($.type == 'response')]
+noop.source.topics.APPC-LCM-WRITE.events.custom.gson=org.onap.policy.appclcm.util.Serialization,gson
+
+noop.source.topics.SDNR-CL-RSP.events=org.onap.policy.sdnr.PciResponseWrapper
+noop.source.topics.SDNR-CL-RSP.events.org.onap.policy.sdnr.PciResponseWrapper.filter=[?($.type == 'response')]
+noop.source.topics.SDNR-CL-RSP.events.custom.gson=org.onap.policy.sdnr.util.Serialization,gson
+
+noop.source.topics.POLICY-CL-MGT.events=org.onap.policy.controlloop.VirtualControlLoopNotification
+noop.source.topics.POLICY-CL-MGT.events.custom.gson=org.onap.policy.controlloop.util.Serialization,gsonPretty
+
+noop.source.topics.APPC-LCM-READ.events=org.onap.policy.appclcm.AppcLcmDmaapWrapper
+noop.source.topics.APPC-LCM-READ.events.custom.gson=org.onap.policy.appclcm.util.Serialization,gson
+
+noop.source.topics.SDNR-CL.events=org.onap.policy.sdnr.PciRequestWrapper
+noop.source.topics.SDNR-CL.events.custom.gson=org.onap.policy.sdnr.util.Serialization,gson
+
+noop.source.topics.DCAE_CL_RSP.events=org.onap.policy.controlloop.ControlLoopResponse
+noop.source.topics.DCAE_CL_RSP.events.custom.gson=org.onap.policy.controlloop.util.Serialization,gsonPretty
+
+noop.sink.topics=APPC-CL,APPC-LCM-READ,POLICY-CL-MGT,SDNR-CL,DCAE_CL_RSP
+
+noop.sink.topics.POLICY-CL-MGT.events=org.onap.policy.controlloop.VirtualControlLoopNotification
+noop.sink.topics.POLICY-CL-MGT.events.custom.gson=org.onap.policy.controlloop.util.Serialization,gsonPretty
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/config/frankfurt-http-client.properties b/controlloop/common/controller-frankfurt/src/test/resources/config/frankfurt-http-client.properties
new file mode 100644
index 0000000..bb3a1a0
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/src/test/resources/config/frankfurt-http-client.properties
@@ -0,0 +1,52 @@
+#
+# ============LICENSE_START=======================================================
+# ONAP
+# ================================================================================
+# Copyright (C) 2020 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=========================================================
+#
+
+http.client.services=GUARD,AAI,SDNC,SO,VFC
+
+http.client.services.GUARD.managed=true
+http.client.services.GUARD.host=localhost
+http.client.services.GUARD.port=6669
+http.client.services.GUARD.username=pdpx
+http.client.services.GUARD.password=pdpx
+http.client.services.GUARD.contextUriPath=policy/pdpx/v1
+
+http.client.services.AAI.managed=true
+http.client.services.AAI.host=localhost
+http.client.services.AAI.port=6666
+http.client.services.AAI.contextUriPath=
+
+http.client.services.SDNC.managed=true
+http.client.services.SDNC.host=localhost
+http.client.services.SDNC.port=6665
+http.client.services.SDNC.username=sdnc
+http.client.services.SDNC.password=sdnc
+http.client.services.SDNC.contextUriPath=
+
+http.client.services.SO.managed=true
+http.client.services.SO.host=localhost
+http.client.services.SO.port=6667
+http.client.services.SO.contextUriPath=
+
+http.client.services.VFC.managed=true
+http.client.services.VFC.host=localhost
+http.client.services.VFC.port=6668
+http.client.services.VFC.username=VFC
+http.client.services.VFC.password=VFC
+http.client.services.VFC.contextUriPath=api/nslcm/v1
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/frankfurt.pom b/controlloop/common/controller-frankfurt/src/test/resources/frankfurt.pom
new file mode 100644
index 0000000..707e59c
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/src/test/resources/frankfurt.pom
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ============LICENSE_START=======================================================
+  ONAP
+  ================================================================================
+  Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+  ================================================================================
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  ============LICENSE_END=========================================================
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>org.onap.policy.controlloop</groupId>
+    <artifactId>frankfurt</artifactId>
+    <version>1.0.0</version>
+</project>
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vcpe/tosca-compliant-vcpe.json b/controlloop/common/controller-frankfurt/src/test/resources/vcpe/tosca-compliant-vcpe.json
new file mode 100644
index 0000000..b876446
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/src/test/resources/vcpe/tosca-compliant-vcpe.json
@@ -0,0 +1,37 @@
+{
+    "type": "onap.policies.controlloop.operational.common.Drools",
+    "type_version": "1.0.0",
+    "version": "1.0.0",
+    "name": "operational.restart",
+    "metadata": {
+        "policy-id": "operational.restart"
+    },
+    "properties": {
+        "id": "ControlLoop-vCPE-48f0c2c3-a172-4192-9ae3-052274181b6e",
+        "timeout": 3600,
+        "abatement": false,
+        "trigger": "unique-policy-id-1-restart",
+        "operations": [
+            {
+                "id": "unique-policy-id-1-restart",
+                "description": "Restart the VM",
+                "operation": {
+                    "actor": "APPC",
+                    "operation": "Restart",
+                    "target": {
+                        "targetType": "VM"
+                    }
+                },
+                "timeout": 1200,
+                "retries": 3,
+                "success": "final_success",
+                "failure": "final_failure",
+                "failure_timeout": "final_failure_timeout",
+                "failure_retries": "final_failure_retries",
+                "failure_exception": "final_failure_exception",
+                "failure_guard": "final_failure_guard"
+            }
+        ],
+        "controllerName": "frankfurt"
+    }
+}
\ No newline at end of file
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vcpe/tosca-legacy-vcpe.json b/controlloop/common/controller-frankfurt/src/test/resources/vcpe/tosca-legacy-vcpe.json
new file mode 100644
index 0000000..f42c07d
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/src/test/resources/vcpe/tosca-legacy-vcpe.json
@@ -0,0 +1,9 @@
+{
+  "type": "onap.policies.controlloop.Operational",
+  "type_version": "1.0.0",
+  "properties": {
+    "content": "controlLoop%3A%0A%20%20version%3A%202.0.0%0A%20%20controlLoopName%3A%20ControlLoop-vCPE-48f0c2c3-a172-4192-9ae3-052274181b6e%0A%20%20trigger_policy%3A%20unique-policy-id-1-restart%0A%20%20timeout%3A%203600%0A%20%20abatement%3A%20false%0A%20%0Apolicies%3A%0A%20%20-%20id%3A%20unique-policy-id-1-restart%0A%20%20%20%20name%3A%20Restart%20the%20VM%0A%20%20%20%20description%3A%0A%20%20%20%20actor%3A%20APPC%0A%20%20%20%20recipe%3A%20Restart%0A%20%20%20%20target%3A%0A%20%20%20%20%20%20type%3A%20VM%0A%20%20%20%20retry%3A%203%0A%20%20%20%20timeout%3A%201200%0A%20%20%20%20success%3A%20final_success%0A%20%20%20%20failure%3A%20final_failure%0A%20%20%20%20failure_timeout%3A%20final_failure_timeout%0A%20%20%20%20failure_retries%3A%20final_failure_retries%0A%20%20%20%20failure_exception%3A%20final_failure_exception%0A%20%20%20%20failure_guard%3A%20final_failure_guard"
+  },
+  "name": "vcpe",
+  "version": "1.0.0"
+}
\ No newline at end of file
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.appc.success.json b/controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.appc.success.json
new file mode 100644
index 0000000..3310217
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.appc.success.json
@@ -0,0 +1,22 @@
+{
+  "body": {
+    "output": {
+      "common-header": {
+        "timestamp": "2017-08-25T21:06:23.037Z",
+        "api-ver": "5.00",
+        "originator-id": "664be3d2-6c12-4f4b-a3e7-c349acced200",
+        "request-id": "664be3d2-6c12-4f4b-a3e7-c349acced200",
+        "sub-request-id": "${replaceMe}",
+        "flags": {}
+      },
+      "status": {
+        "code": 400,
+        "message": "Restart Successful"
+      }
+    }
+  },
+  "version": "2.0",
+  "rpc-name": "restart",
+  "correlation-id": "664be3d2-6c12-4f4b-a3e7-c349acced200-1",
+  "type": "response"
+}
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.onset.1.json b/controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.onset.1.json
new file mode 100644
index 0000000..d08ee47
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.onset.1.json
@@ -0,0 +1,16 @@
+{
+  "closedLoopControlName": "ControlLoop-vCPE-48f0c2c3-a172-4192-9ae3-052274181b6e",
+  "closedLoopAlarmStart": 1463679805324,
+  "closedLoopEventClient": "DCAE_INSTANCE_ID.dcae-tca",
+  "closedLoopEventStatus": "ONSET",
+  "requestID": "664be3d2-6c12-4f4b-a3e7-c349acced200",
+  "target_type": "VNF",
+  "target": "generic-vnf.vnf-id",
+  "AAI": {
+    "vserver.is-closed-loop-disabled": "false",
+    "vserver.prov-status": "ACTIVE",
+    "generic-vnf.vnf-id": "vCPE_Infrastructure_vGMUX_demo_app"
+  },
+  "from": "DCAE",
+  "version": "1.0.2"
+}
\ No newline at end of file
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.onset.2.json b/controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.onset.2.json
new file mode 100644
index 0000000..b8c7651
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.onset.2.json
@@ -0,0 +1,16 @@
+{
+  "closedLoopControlName": "ControlLoop-vCPE-48f0c2c3-a172-4192-9ae3-052274181b6e",
+  "closedLoopAlarmStart": 1463679805324,
+  "closedLoopEventClient": "DCAE_INSTANCE_ID.dcae-tca",
+  "closedLoopEventStatus": "ONSET",
+  "requestID": "8cf3cd05-1218-4224-931b-601494ffe55b",
+  "target_type": "VNF",
+  "target": "generic-vnf.vnf-id",
+  "AAI": {
+    "vserver.is-closed-loop-disabled": "false",
+    "vserver.prov-status": "ACTIVE",
+    "generic-vnf.vnf-id": "vCPE_Infrastructure_vGMUX_demo_app"
+  },
+  "from": "DCAE",
+  "version": "1.0.2"
+}
\ No newline at end of file
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.onset.3.json b/controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.onset.3.json
new file mode 100644
index 0000000..40f29b3
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/src/test/resources/vcpe/vcpe.onset.3.json
@@ -0,0 +1,17 @@
+{
+  "closedLoopControlName": "ControlLoop-vCPE-48f0c2c3-a172-4192-9ae3-052274181b6e",
+  "closedLoopAlarmStart": 1570722876324905,
+  "closedLoopAlarmEnd": 1570722876324999,
+  "closedLoopEventClient": "DCAE_INSTANCE_ID.dcae-tca",
+  "closedLoopEventStatus": "ONSET",
+  "requestID": "8cf3cd05-1218-4224-931b-601494ffe55b",
+  "target_type": "VNF",
+  "target": "generic-vnf.vnf-id",
+  "AAI": {
+    "vserver.is-closed-loop-disabled": "false",
+    "vserver.prov-status": "ACTIVE",
+    "generic-vnf.vnf-id": "vCPE_Infrastructure_vGMUX_demo_app"
+  },
+  "from": "DCAE",
+  "version": "1.0.2"
+}
\ No newline at end of file
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vfw/tosca-compliant-vfw.json b/controlloop/common/controller-frankfurt/src/test/resources/vfw/tosca-compliant-vfw.json
new file mode 100644
index 0000000..47cb09d
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/src/test/resources/vfw/tosca-compliant-vfw.json
@@ -0,0 +1,40 @@
+{
+    "type": "onap.policies.controlloop.operational.common.Drools",
+    "type_version": "1.0.0",
+    "name": "operational.modifyconfig",
+    "version": "1.0.0",
+    "metadata": {
+        "policy-id": "operational.modifyconfig"
+    },
+    "properties": {
+        "id": "ControlLoop-vFirewall-d0a1dfc6-94f5-4fd4-a5b5-4630b438850a",
+        "timeout": 60,
+        "abatement": false,
+        "trigger": "unique-policy-id-1-modifyConfig",
+        "operations": [
+            {
+                "id": "unique-policy-id-1-modifyConfig",
+                "description": "Modify the packet generator",
+                "operation": {
+                    "actor": "APPC",
+                    "operation": "ModifyConfig",
+                    "target": {
+                        "targetType": "VNF",
+                        "entityIds": {
+                            "resourceID": "bbb3cefd-01c8-413c-9bdd-2b92f9ca3d38"
+                        }
+                    }
+                },
+                "timeout": 300,
+                "retries": 0,
+                "success": "final_success",
+                "failure": "final_failure",
+                "failure_timeout": "final_failure_timeout",
+                "failure_retries": "final_failure_retries",
+                "failure_exception": "final_failure_exception",
+                "failure_guard": "final_failure_guard"
+            }
+        ],
+        "controllerName": "frankfurt"
+    }
+}
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vfw/tosca-vfw.json b/controlloop/common/controller-frankfurt/src/test/resources/vfw/tosca-vfw.json
new file mode 100644
index 0000000..5d1e352
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/src/test/resources/vfw/tosca-vfw.json
@@ -0,0 +1,9 @@
+{
+  "type": "onap.policies.controlloop.Operational",
+  "type_version": "1.0.0",
+  "properties": {
+    "content": "controlLoop%3A%0A%20%20version%3A%202.0.0%0A%20%20controlLoopName%3A%20ControlLoop-vFirewall-d0a1dfc6-94f5-4fd4-a5b5-4630b438850a%0A%20%20services%3A%0A%20%20%20%20-%20serviceInvariantUUID%3A%20f6937c86-584c-47ae-ad29-8d41d6f0cc7c%0A%20%20%20%20%20%20serviceUUID%3A%207be584e2-0bb2-4126-adaf-ced2c77ca0b3%0A%20%20%20%20%20%20serviceName%3A%20Service_Ete_Name7ba1fbde-6187-464a-a62d-d9dd25bdf4e8%0A%20%20trigger_policy%3A%20unique-policy-id-1-modifyConfig%0A%20%20timeout%3A%2060%0A%20%20abatement%3A%20false%0A%20%0Apolicies%3A%0A%20%20-%20id%3A%20unique-policy-id-1-modifyConfig%0A%20%20%20%20name%3A%20modify%20packet%20gen%20config%0A%20%20%20%20description%3A%0A%20%20%20%20actor%3A%20APPC%0A%20%20%20%20recipe%3A%20ModifyConfig%0A%20%20%20%20target%3A%0A%20%20%20%20%20%20resourceID%3A%20bbb3cefd-01c8-413c-9bdd-2b92f9ca3d38%0A%20%20%20%20%20%20type%3A%20VNF%0A%20%20%20%20retry%3A%200%0A%20%20%20%20timeout%3A%2030%0A%20%20%20%20success%3A%20final_success%0A%20%20%20%20failure%3A%20final_failure%0A%20%20%20%20failure_timeout%3A%20final_failure_timeout%0A%20%20%20%20failure_retries%3A%20final_failure_retries%0A%20%20%20%20failure_exception%3A%20final_failure_exception%0A%20%20%20%20failure_guard%3A%20final_failure_guard"
+  },
+  "name": "vfw",
+  "version": "1.0.0"
+}
\ No newline at end of file
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vfw/vfw.appc.success.json b/controlloop/common/controller-frankfurt/src/test/resources/vfw/vfw.appc.success.json
new file mode 100644
index 0000000..d7e6ec3
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/src/test/resources/vfw/vfw.appc.success.json
@@ -0,0 +1,17 @@
+{
+  "CommonHeader": {
+    "TimeStamp": 1506051879001,
+    "APIver": "1.01",
+    "RequestID": "c7c6a4aa-bb61-4a15-b831-ba1472dd4a65",
+    "SubRequestID": "${replaceMe}",
+    "RequestTrack": [],
+    "Flags": []
+  },
+  "Status": {
+    "Code": 400,
+    "Value": "SUCCESS"
+  },
+  "Payload": {
+    "generic-vnf.vnf-id": "jimmy-test-vnf2"
+  }
+}
\ No newline at end of file
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vfw/vfw.onset.json b/controlloop/common/controller-frankfurt/src/test/resources/vfw/vfw.onset.json
new file mode 100644
index 0000000..7782867
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/src/test/resources/vfw/vfw.onset.json
@@ -0,0 +1,17 @@
+{
+  "closedLoopControlName": "ControlLoop-vFirewall-d0a1dfc6-94f5-4fd4-a5b5-4630b438850a",
+  "closedLoopAlarmStart": 1463679805324,
+  "closedLoopEventClient": "microservice.stringmatcher",
+  "closedLoopEventStatus": "ONSET",
+  "requestID": "c7c6a4aa-bb61-4a15-b831-ba1472dd4a65",
+  "target_type": "VNF",
+  "target": "generic-vnf.vnf-name",
+  "AAI": {
+    "vserver.is-closed-loop-disabled": "false",
+    "vserver.prov-status": "ACTIVE",
+    "generic-vnf.vnf-name": "fw0002vm002fw002",
+    "vserver.vserver-name": "OzVServer"
+  },
+  "from": "DCAE",
+  "version": "1.0.2"
+}
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vlb/tosca-compliant-vlb.json b/controlloop/common/controller-frankfurt/src/test/resources/vlb/tosca-compliant-vlb.json
new file mode 100644
index 0000000..aeb22bb
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/src/test/resources/vlb/tosca-compliant-vlb.json
@@ -0,0 +1,48 @@
+{
+    "type": "onap.policies.controlloop.operational.common.Drools",
+    "type_version": "1.0.0",
+    "name": "operational.scaleout",
+    "version": "1.0.0",
+    "metadata": {
+        "policy-id": "operational.scaleout"
+    },
+    "properties": {
+        "id": "ControlLoop-vDNS-6f37f56d-a87d-4b85-b6a9-cc953cf779b3",
+        "timeout": 1200,
+        "abatement": false,
+        "trigger": "unique-policy-id-1-scale-up",
+        "operations": [
+            {
+                "id": "unique-policy-id-1-scale-up",
+                "description": "Create a new VF Module",
+                "operation": {
+                    "actor": "SO",
+                    "operation": "VF Module Create",
+                    "target": {
+                        "targetType": "VFMODULE",
+                        "entityIds": {
+                            "modelInvariantId": "e6130d03-56f1-4b0a-9a1d-e1b2ebc30e0e",
+                            "modelVersionId": "94b18b1d-cc91-4f43-911a-e6348665f292",
+                            "modelName": "VfwclVfwsnkBbefb8ce2bde..base_vfw..module-0",
+                            "modelVersion": 1,
+                            "modelCustomizationId": "47958575-138f-452a-8c8d-d89b595f8164"
+                        }
+                    },
+                    "payload": {
+                        "requestParameters": "{\"usePreload\":true,\"userParams\":[]}",
+                        "configurationParameters": "[{\"ip-addr\":\"$.vf-module-topology.vf-module-parameters.param[9]\",\"oam-ip-addr\":\"$.vf-module-topology.vf-module-parameters.param[16]\",\"enabled\":\"$.vf-module-topology.vf-module-parameters.param[23]\"}]"
+                    }
+                },
+                "timeout": 1200,
+                "retries": 0,
+                "success": "final_success",
+                "failure": "final_failure",
+                "failure_timeout": "final_failure_timeout",
+                "failure_retries": "final_failure_retries",
+                "failure_exception": "final_failure_exception",
+                "failure_guard": "final_failure_guard"
+            }
+        ],
+        "controllerName": "frankfurt"
+    }
+}
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vlb/tosca-vlb.json b/controlloop/common/controller-frankfurt/src/test/resources/vlb/tosca-vlb.json
new file mode 100644
index 0000000..5147d99
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/src/test/resources/vlb/tosca-vlb.json
@@ -0,0 +1,9 @@
+{
+  "type": "onap.policies.controlloop.Operational",
+  "type_version": "1.0.0",
+  "properties": {
+    "content": "controlLoop%3A%0A%20%20version%3A%202.0.0%0A%20%20controlLoopName%3A%20ControlLoop-vDNS-6f37f56d-a87d-4b85-b6a9-cc953cf779b3%0A%20%20services%3A%0A%20%20%20%20-%20serviceName%3A%20d4738992-6497-4dca-9db9%0A%20%20%20%20%20%20serviceInvariantUUID%3A%20dc112d6e-7e73-4777-9c6f-1a7fb5fd1b6f%0A%20%20%20%20%20%20serviceUUID%3A%202eea06c6-e1d3-4c3a-b9c4-478c506eeedf%0A%20%20trigger_policy%3A%20unique-policy-id-1-scale-up%0A%20%20timeout%3A%2060%0A%0Apolicies%3A%0A%20%20-%20id%3A%20unique-policy-id-1-scale-up%0A%20%20%20%20name%3A%20Create%20a%20new%20VF%20Module%0A%20%20%20%20description%3A%0A%20%20%20%20actor%3A%20SO%0A%20%20%20%20recipe%3A%20VF%20Module%20Create%0A%20%20%20%20target%3A%0A%20%20%20%20%20%20type%3A%20VFMODULE%0A%20%20%20%20%20%20modelInvariantId%3A%20e6130d03-56f1-4b0a-9a1d-e1b2ebc30e0e%0A%20%20%20%20%20%20modelVersionId%3A%2094b18b1d-cc91-4f43-911a-e6348665f292%0A%20%20%20%20%20%20modelName%3A%20VfwclVfwsnkBbefb8ce2bde..base_vfw..module-0%0A%20%20%20%20%20%20modelVersion%3A%201%0A%20%20%20%20%20%20modelCustomizationId%3A%2047958575-138f-452a-8c8d-d89b595f8164%0A%20%20%20%20payload%3A%0A%20%20%20%20%20%20requestParameters%3A%20%27%7B%22usePreload%22%3Atrue%2C%22userParams%22%3A%5B%5D%7D%27%0A%20%20%20%20%20%20configurationParameters%3A%20%27%5B%7B%22ip-addr%22%3A%22%24.vf-module-topology.vf-module-parameters.param%5B9%5D%22%2C%22oam-ip-addr%22%3A%22%24.vf-module-topology.vf-module-parameters.param%5B16%5D%22%2C%22enabled%22%3A%22%24.vf-module-topology.vf-module-parameters.param%5B23%5D%22%7D%5D%27%0A%20%20%20%20retry%3A%200%0A%20%20%20%20timeout%3A%2030%0A%20%20%20%20success%3A%20final_success%0A%20%20%20%20failure%3A%20final_failure%0A%20%20%20%20failure_timeout%3A%20final_failure_timeout%0A%20%20%20%20failure_retries%3A%20final_failure_retries%0A%20%20%20%20failure_exception%3A%20final_failure_exception%0A%20%20%20%20failure_guard%3A%20final_failure_guard%0A"
+  },
+  "name": "vlb",
+  "version": "1.0.0"
+}
\ No newline at end of file
diff --git a/controlloop/common/controller-frankfurt/src/test/resources/vlb/vlb.onset.json b/controlloop/common/controller-frankfurt/src/test/resources/vlb/vlb.onset.json
new file mode 100644
index 0000000..3360c0a
--- /dev/null
+++ b/controlloop/common/controller-frankfurt/src/test/resources/vlb/vlb.onset.json
@@ -0,0 +1,16 @@
+{
+  "closedLoopControlName": "ControlLoop-vDNS-6f37f56d-a87d-4b85-b6a9-cc953cf779b3",
+  "closedLoopAlarmStart": 1484677482204798,
+  "closedLoopEventClient": "DCAE_INSTANCE_ID.dcae-tca",
+  "closedLoopEventStatus": "ONSET",
+  "requestID": "e4f95e0c-a013-4530-8e59-c5c5f9e539b6",
+  "target_type": "VNF",
+  "target": "vserver.vserver-name",
+  "AAI": {
+    "vserver.is-closed-loop-disabled": "false",
+    "vserver.prov-status": "ACTIVE",
+    "vserver.vserver-name": "OzVServer"
+  },
+  "from": "DCAE",
+  "version": "1.0.2"
+}
\ No newline at end of file
diff --git a/controlloop/common/controller-usecases/pom.xml b/controlloop/common/controller-usecases/pom.xml
index 795b96f..fb29a81 100644
--- a/controlloop/common/controller-usecases/pom.xml
+++ b/controlloop/common/controller-usecases/pom.xml
@@ -36,11 +36,6 @@
     <name>${project.artifactId}</name>
     <description>Usecases Experimental Controller</description>
 
-    <properties>
-        <maven.compiler.source>1.8</maven.compiler.source>
-        <maven.compiler.target>1.8</maven.compiler.target>
-    </properties>
-
     <build>
         <plugins>
             <plugin>
diff --git a/controlloop/common/eventmanager/pom.xml b/controlloop/common/eventmanager/pom.xml
index 3cd069f..3c74515 100644
--- a/controlloop/common/eventmanager/pom.xml
+++ b/controlloop/common/eventmanager/pom.xml
@@ -69,6 +69,12 @@
         </dependency>
         <dependency>
             <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId>
+            <artifactId>actor.guard</artifactId>
+            <version>${policy.models.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId>
             <artifactId>actor.so</artifactId>
             <version>${policy.models.version}</version>
             <scope>provided</scope>
diff --git a/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/eventmanager/ControlLoopEventManager2.java b/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/eventmanager/ControlLoopEventManager2.java
new file mode 100644
index 0000000..dc2b513
--- /dev/null
+++ b/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/eventmanager/ControlLoopEventManager2.java
@@ -0,0 +1,614 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2017-2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.eventmanager;
+
+import static org.onap.policy.controlloop.ControlLoopTargetType.PNF;
+import static org.onap.policy.controlloop.ControlLoopTargetType.VM;
+import static org.onap.policy.controlloop.ControlLoopTargetType.VNF;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.commons.lang3.StringUtils;
+import org.drools.core.WorkingMemory;
+import org.kie.api.runtime.rule.FactHandle;
+import org.onap.policy.controlloop.ControlLoopEventStatus;
+import org.onap.policy.controlloop.ControlLoopException;
+import org.onap.policy.controlloop.ControlLoopNotificationType;
+import org.onap.policy.controlloop.ControlLoopOperation;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+import org.onap.policy.controlloop.VirtualControlLoopNotification;
+import org.onap.policy.controlloop.actorserviceprovider.ActorService;
+import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome;
+import org.onap.policy.controlloop.actorserviceprovider.controlloop.ControlLoopEventContext;
+import org.onap.policy.controlloop.drl.legacy.ControlLoopParams;
+import org.onap.policy.controlloop.ophistory.OperationHistoryDataManager;
+import org.onap.policy.controlloop.policy.FinalResult;
+import org.onap.policy.controlloop.policy.Policy;
+import org.onap.policy.controlloop.processor.ControlLoopProcessor;
+import org.onap.policy.drools.core.lock.LockCallback;
+import org.onap.policy.drools.system.PolicyEngineConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manager for a single control loop event. Once this has been created, the event can be
+ * retracted from working memory. Once this has been created, {@link #start()} should be
+ * invoked, and then {@link #nextStep()} should be invoked continually until
+ * {@link #isActive()} returns {@code false}, indicating that all steps have completed.
+ */
+@ToString(onlyExplicitlyIncluded = true)
+public class ControlLoopEventManager2 implements ManagerContext, Serializable {
+    private static final Logger logger = LoggerFactory.getLogger(ControlLoopEventManager2.class);
+    private static final long serialVersionUID = -1216568161322872641L;
+
+    private static final String EVENT_MANAGER_SERVICE_CONFIG = "config/event-manager.properties";
+    public static final String PROV_STATUS_ACTIVE = "ACTIVE";
+    private static final String VM_NAME = "VM_NAME";
+    private static final String VNF_NAME = "VNF_NAME";
+    public static final String GENERIC_VNF_VNF_ID = "generic-vnf.vnf-id";
+    public static final String GENERIC_VNF_VNF_NAME = "generic-vnf.vnf-name";
+    public static final String VSERVER_VSERVER_NAME = "vserver.vserver-name";
+    public static final String GENERIC_VNF_IS_CLOSED_LOOP_DISABLED = "generic-vnf.is-closed-loop-disabled";
+    public static final String VSERVER_IS_CLOSED_LOOP_DISABLED = "vserver.is-closed-loop-disabled";
+    public static final String PNF_IS_IN_MAINT = "pnf.in-maint";
+    public static final String GENERIC_VNF_PROV_STATUS = "generic-vnf.prov-status";
+    public static final String VSERVER_PROV_STATUS = "vserver.prov-status";
+    public static final String PNF_ID = "pnf.pnf-id";
+    public static final String PNF_NAME = "pnf.pnf-name";
+
+    private static final Set<String> VALID_TARGETS = Stream
+                    .of(VM_NAME, VNF_NAME, VSERVER_VSERVER_NAME, GENERIC_VNF_VNF_ID, GENERIC_VNF_VNF_NAME, PNF_NAME)
+                    .map(String::toLowerCase).collect(Collectors.toSet());
+
+    private static final Set<String> TRUE_VALUES = Set.of("true", "t", "yes", "y");
+
+    public enum NewEventStatus {
+        FIRST_ONSET, SUBSEQUENT_ONSET, FIRST_ABATEMENT, SUBSEQUENT_ABATEMENT, SYNTAX_ERROR
+    }
+
+    // TODO limit the number of policies that may be executed for a single event?
+
+    /**
+     * {@code True} if this object was created by this JVM instance, {@code false}
+     * otherwise. This will be {@code false} if this object is reconstituted from a
+     * persistent store or by transfer from another server.
+     */
+    private transient boolean createdByThisJvmInstance;
+
+    @Getter
+    @ToString.Include
+    public final String closedLoopControlName;
+    @Getter
+    @ToString.Include
+    private final UUID requestId;
+    private final ControlLoopEventContext context;
+    @ToString.Include
+    private int numOnsets = 1;
+    @ToString.Include
+    private int numAbatements = 0;
+    private VirtualControlLoopEvent abatement = null;
+
+    /**
+     * Time, in milliseconds, when the control loop will time out.
+     */
+    @Getter
+    private final long endTimeMs;
+
+    // fields extracted from the ControlLoopParams
+    @Getter
+    private final String policyName;
+    private final String policyScope;
+    private final String policyVersion;
+
+    private final LinkedList<ControlLoopOperation> controlLoopHistory = new LinkedList<>();
+
+    /**
+     * Maps a target entity to its lock.
+     */
+    private final transient Map<String, LockData> target2lock = new HashMap<>();
+
+    private final ControlLoopProcessor processor;
+    private final AtomicReference<ControlLoopOperationManager2> currentOperation = new AtomicReference<>();
+
+    private FinalResult finalResult = null;
+
+    @Getter
+    private VirtualControlLoopNotification notification;
+
+    @Getter
+    private boolean updated = false;
+
+    private final transient WorkingMemory workMem;
+    private transient FactHandle factHandle;
+
+
+    /**
+     * Constructs the object.
+     *
+     * @param params control loop parameters
+     * @param event event to be managed by this object
+     * @param workMem working memory to update if this changes
+     * @throws ControlLoopException if the event is invalid or if a YAML processor cannot
+     *         be created
+     */
+    public ControlLoopEventManager2(ControlLoopParams params, VirtualControlLoopEvent event, WorkingMemory workMem)
+                    throws ControlLoopException {
+
+        checkEventSyntax(event);
+
+        if (isClosedLoopDisabled(event)) {
+            throw new IllegalStateException("is-closed-loop-disabled is set to true on VServer or VNF");
+        }
+
+        if (isProvStatusInactive(event)) {
+            throw new IllegalStateException("prov-status is not ACTIVE on VServer or VNF");
+        }
+
+        this.createdByThisJvmInstance = true;
+        this.closedLoopControlName = params.getClosedLoopControlName();
+        this.requestId = event.getRequestId();
+        this.context = new ControlLoopEventContext(event);
+        this.policyName = params.getPolicyName();
+        this.policyScope = params.getPolicyScope();
+        this.policyVersion = params.getPolicyVersion();
+        this.processor = new ControlLoopProcessor(params.getToscaPolicy());
+        this.workMem = workMem;
+        this.endTimeMs = System.currentTimeMillis() + detmControlLoopTimeoutMs();
+    }
+
+    /**
+     * Starts the manager.
+     *
+     * @throws ControlLoopException if the processor cannot get a policy
+     */
+    public void start() throws ControlLoopException {
+        if (!isActive()) {
+            throw new IllegalStateException("manager is no longer active");
+        }
+
+        if ((factHandle = workMem.getFactHandle(this)) == null) {
+            throw new IllegalStateException("manager is not in working memory");
+        }
+
+        if (currentOperation.get() != null) {
+            throw new IllegalStateException("manager already started");
+        }
+
+        startOperation();
+    }
+
+    /**
+     * Starts an operation for the current processor policy.
+     *
+     * @throws ControlLoopException if the processor cannot get a policy
+     */
+    private synchronized void startOperation() throws ControlLoopException {
+
+        if ((finalResult = processor.checkIsCurrentPolicyFinal()) == null) {
+            // not final - start the next operation
+            currentOperation.set(makeOperationManager(context, processor.getCurrentPolicy()));
+            currentOperation.get().start(endTimeMs - System.currentTimeMillis());
+            return;
+        }
+
+        logger.info("final={} oper state={} for {}", finalResult, currentOperation.get().getState(), requestId);
+
+        notification = makeNotification();
+        notification.setHistory(controlLoopHistory);
+
+        switch (finalResult) {
+            case FINAL_FAILURE_EXCEPTION:
+                notification.setNotification(ControlLoopNotificationType.FINAL_FAILURE);
+                notification.setMessage("Exception in processing closed loop");
+                break;
+            case FINAL_SUCCESS:
+                notification.setNotification(ControlLoopNotificationType.FINAL_SUCCESS);
+                break;
+            case FINAL_OPENLOOP:
+                notification.setNotification(ControlLoopNotificationType.FINAL_OPENLOOP);
+                break;
+            case FINAL_FAILURE:
+            default:
+                notification.setNotification(ControlLoopNotificationType.FINAL_FAILURE);
+                break;
+        }
+    }
+
+    /**
+     * Starts the next step, whatever that may be.
+     */
+    public void nextStep() {
+        if (!isActive()) {
+            return;
+        }
+
+        updated = false;
+
+        try {
+            if (!currentOperation.get().nextStep()) {
+                // current operation is done - try the next
+                controlLoopHistory.addAll(currentOperation.get().getHistory());
+                processor.nextPolicyForResult(currentOperation.get().getOperationResult());
+                startOperation();
+            }
+
+        } catch (ControlLoopException | RuntimeException e) {
+            // processor problem - this is fatal
+            logger.warn("{}: cannot start next step for {}", closedLoopControlName, requestId, e);
+            notification = makeNotification();
+            notification.setNotification(ControlLoopNotificationType.FINAL_FAILURE);
+            notification.setMessage("Policy processing aborted due to policy error");
+            notification.setHistory(controlLoopHistory);
+            finalResult = FinalResult.FINAL_FAILURE_EXCEPTION;
+        }
+    }
+
+    /**
+     * Determines if the manager is still active.
+     *
+     * @return {@code true} if the manager is still active, {@code false} otherwise
+     */
+    public boolean isActive() {
+        return (createdByThisJvmInstance && finalResult == null);
+    }
+
+    /**
+     * Updates working memory if this changes.
+     *
+     * @param operation operation manager that was updated
+     */
+    @Override
+    public synchronized void updated(ControlLoopOperationManager2 operation) {
+        if (!isActive() || operation != currentOperation.get()) {
+            // no longer working on the given operation
+            return;
+        }
+
+        notification = makeNotification();
+
+        VirtualControlLoopEvent event = context.getEvent();
+
+        notification.setHistory(operation.getHistory());
+
+        switch (operation.getState()) {
+            case LOCK_DENIED:
+                notification.setNotification(ControlLoopNotificationType.REJECTED);
+                notification.setMessage("The target " + event.getAai().get(event.getTarget()) + " is already locked");
+                break;
+            case LOCK_LOST:
+                notification.setNotification(ControlLoopNotificationType.OPERATION_FAILURE);
+                notification.setMessage("The target " + event.getAai().get(event.getTarget()) + " is no longer locked");
+                break;
+            case GUARD_STARTED:
+                notification.setNotification(ControlLoopNotificationType.OPERATION);
+                notification.setMessage(
+                                "Sending guard query for " + operation.getActor() + " " + operation.getOperation());
+                break;
+            case GUARD_PERMITTED:
+                notification.setNotification(ControlLoopNotificationType.OPERATION);
+                notification.setMessage("Guard result for " + operation.getActor() + " " + operation.getOperation()
+                                + " is Permit");
+                break;
+            case GUARD_DENIED:
+                notification.setNotification(ControlLoopNotificationType.OPERATION);
+                notification.setMessage("Guard result for " + operation.getActor() + " " + operation.getOperation()
+                                + " is Deny");
+                break;
+            case OPERATION_SUCCESS:
+                notification.setNotification(ControlLoopNotificationType.OPERATION_SUCCESS);
+                break;
+
+            case CONTROL_LOOP_TIMEOUT:
+                logger.warn("{}: control loop timed out for {}", closedLoopControlName, requestId);
+                controlLoopHistory.addAll(currentOperation.get().getHistory());
+                notification.setNotification(ControlLoopNotificationType.FINAL_FAILURE);
+                notification.setMessage("Control Loop timed out");
+                notification.setHistory(controlLoopHistory);
+                finalResult = FinalResult.FINAL_FAILURE;
+                break;
+
+            case OPERATION_FAILURE:
+            default:
+                notification.setNotification(ControlLoopNotificationType.OPERATION_FAILURE);
+                break;
+        }
+
+        updated = true;
+        workMem.update(factHandle, this);
+    }
+
+    /**
+     * Cancels the current operation and frees all locks.
+     */
+    public void destroy() {
+        ControlLoopOperationManager2 oper = currentOperation.get();
+        if (oper != null) {
+            oper.cancel();
+        }
+
+        getBlockingExecutor().execute(this::freeAllLocks);
+    }
+
+    /**
+     * Frees all locks.
+     */
+    private void freeAllLocks() {
+        target2lock.values().forEach(LockData::free);
+    }
+
+    /**
+     * Makes a notification message for the current operation.
+     *
+     * @return a new notification
+     */
+    public VirtualControlLoopNotification makeNotification() {
+        VirtualControlLoopNotification notif = new VirtualControlLoopNotification();
+        notif.setNotification(ControlLoopNotificationType.OPERATION);
+        notif.setFrom("policy");
+        notif.setPolicyScope(policyScope);
+        notif.setPolicyVersion(policyVersion);
+
+        if (finalResult == null) {
+            ControlLoopOperationManager2 oper = currentOperation.get();
+            if (oper != null) {
+                notif.setMessage(oper.getOperationMessage());
+                notif.setHistory(oper.getHistory());
+            }
+        }
+
+        return notif;
+    }
+
+    /**
+     * An event onset/abatement.
+     *
+     * @param event the event
+     * @return the status
+     */
+    public NewEventStatus onNewEvent(VirtualControlLoopEvent event) {
+        try {
+            checkEventSyntax(event);
+
+            if (event.getClosedLoopEventStatus() == ControlLoopEventStatus.ONSET) {
+                if (event.equals(context.getEvent())) {
+                    return NewEventStatus.FIRST_ONSET;
+                }
+
+                numOnsets++;
+                return NewEventStatus.SUBSEQUENT_ONSET;
+
+            } else {
+                if (abatement == null) {
+                    abatement = event;
+                    numAbatements++;
+                    return NewEventStatus.FIRST_ABATEMENT;
+                } else {
+                    numAbatements++;
+                    return NewEventStatus.SUBSEQUENT_ABATEMENT;
+                }
+            }
+        } catch (ControlLoopException e) {
+            logger.error("{}: onNewEvent threw an exception", this, e);
+            return NewEventStatus.SYNTAX_ERROR;
+        }
+    }
+
+    /**
+     * Determines the overall control loop timeout.
+     *
+     * @return the policy timeout, in milliseconds, if specified, a default timeout
+     *         otherwise
+     */
+    private long detmControlLoopTimeoutMs() {
+        // validation checks preclude null or 0 timeout values in the policy
+        Integer timeout = processor.getControlLoop().getTimeout();
+        return TimeUnit.MILLISECONDS.convert(timeout, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Check an event syntax.
+     *
+     * @param event the event syntax
+     * @throws ControlLoopException if an error occurs
+     */
+    public void checkEventSyntax(VirtualControlLoopEvent event) throws ControlLoopException {
+        validateStatus(event);
+        if (StringUtils.isBlank(event.getClosedLoopControlName())) {
+            throw new ControlLoopException("No control loop name");
+        }
+        if (event.getRequestId() == null) {
+            throw new ControlLoopException("No request ID");
+        }
+        if (event.getClosedLoopEventStatus() == ControlLoopEventStatus.ABATED) {
+            return;
+        }
+        if (StringUtils.isBlank(event.getTarget())) {
+            throw new ControlLoopException("No target field");
+        } else if (!VALID_TARGETS.contains(event.getTarget().toLowerCase())) {
+            throw new ControlLoopException("target field invalid");
+        }
+        validateAaiData(event);
+    }
+
+    private void validateStatus(VirtualControlLoopEvent event) throws ControlLoopException {
+        if (event.getClosedLoopEventStatus() != ControlLoopEventStatus.ONSET
+                        && event.getClosedLoopEventStatus() != ControlLoopEventStatus.ABATED) {
+            throw new ControlLoopException("Invalid value in closedLoopEventStatus");
+        }
+    }
+
+    private void validateAaiData(VirtualControlLoopEvent event) throws ControlLoopException {
+        Map<String, String> eventAai = event.getAai();
+        if (eventAai == null) {
+            throw new ControlLoopException("AAI is null");
+        }
+        if (event.getTargetType() == null) {
+            throw new ControlLoopException("The Target type is null");
+        }
+        switch (event.getTargetType()) {
+            case VM:
+            case VNF:
+                validateAaiVmVnfData(eventAai);
+                return;
+            case PNF:
+                validateAaiPnfData(eventAai);
+                return;
+            default:
+                throw new ControlLoopException("The target type is not supported");
+        }
+    }
+
+    private void validateAaiVmVnfData(Map<String, String> eventAai) throws ControlLoopException {
+        if (eventAai.get(GENERIC_VNF_VNF_ID) == null && eventAai.get(VSERVER_VSERVER_NAME) == null
+                        && eventAai.get(GENERIC_VNF_VNF_NAME) == null) {
+            throw new ControlLoopException(
+                            "generic-vnf.vnf-id or generic-vnf.vnf-name or vserver.vserver-name information missing");
+        }
+    }
+
+    private void validateAaiPnfData(Map<String, String> eventAai) throws ControlLoopException {
+        if (eventAai.get(PNF_NAME) == null) {
+            throw new ControlLoopException("AAI PNF object key pnf-name is missing");
+        }
+    }
+
+    /**
+     * Is closed loop disabled for an event.
+     *
+     * @param event the event
+     * @return <code>true</code> if the control loop is disabled, <code>false</code>
+     *         otherwise
+     */
+    public static boolean isClosedLoopDisabled(VirtualControlLoopEvent event) {
+        Map<String, String> aai = event.getAai();
+        return (isAaiTrue(aai.get(VSERVER_IS_CLOSED_LOOP_DISABLED))
+                        || isAaiTrue(aai.get(GENERIC_VNF_IS_CLOSED_LOOP_DISABLED))
+                        || isAaiTrue(aai.get(PNF_IS_IN_MAINT)));
+    }
+
+    /**
+     * Does provisioning status, for an event, have a value other than ACTIVE.
+     *
+     * @param event the event
+     * @return {@code true} if the provisioning status is neither ACTIVE nor {@code null},
+     *         {@code false} otherwise
+     */
+    protected static boolean isProvStatusInactive(VirtualControlLoopEvent event) {
+        Map<String, String> aai = event.getAai();
+        return !(PROV_STATUS_ACTIVE.equals(aai.getOrDefault(VSERVER_PROV_STATUS, PROV_STATUS_ACTIVE))
+                        && PROV_STATUS_ACTIVE.equals(aai.getOrDefault(GENERIC_VNF_PROV_STATUS, PROV_STATUS_ACTIVE)));
+    }
+
+    /**
+     * Determines the boolean value represented by the given AAI field value.
+     *
+     * @param aaiValue value to be examined
+     * @return the boolean value represented by the field value, or {@code false} if the
+     *         value is {@code null}
+     */
+    protected static boolean isAaiTrue(String aaiValue) {
+        return (aaiValue != null && TRUE_VALUES.contains(aaiValue.toLowerCase()));
+    }
+
+    /**
+     * Requests a lock. This requests the lock for the time that remains before the
+     * timeout expires. This avoids having to extend the lock.
+     *
+     * @param targetEntity entity to be locked
+     * @param lockUnavailableCallback function to be invoked if the lock is
+     *        unavailable/lost
+     * @return a future that can be used to await the lock
+     */
+    @Override
+    public synchronized CompletableFuture<OperationOutcome> requestLock(String targetEntity,
+                    Consumer<OperationOutcome> lockUnavailableCallback) {
+
+        long remainingMs = endTimeMs - System.currentTimeMillis();
+        int remainingSec = 15 + Math.max(0, (int) TimeUnit.SECONDS.convert(remainingMs, TimeUnit.MILLISECONDS));
+
+        LockData data = target2lock.computeIfAbsent(targetEntity, key -> {
+            LockData data2 = new LockData(key, requestId);
+            makeLock(targetEntity, requestId.toString(), remainingSec, data2);
+            return data2;
+        });
+
+        data.addUnavailableCallback(lockUnavailableCallback);
+
+        return data.getFuture();
+    }
+
+    /**
+     * Initializes various components, on demand.
+     */
+    private static class LazyInitData {
+        private static final OperationHistoryDataManager DATA_MANAGER;
+        private static final ActorService ACTOR_SERVICE;
+
+        static {
+            EventManagerServices services = new EventManagerServices(EVENT_MANAGER_SERVICE_CONFIG);
+            ACTOR_SERVICE = services.getActorService();
+            DATA_MANAGER = services.getDataManager();
+        }
+    }
+
+    // the following methods may be overridden by junit tests
+
+    protected ControlLoopOperationManager2 makeOperationManager(ControlLoopEventContext ctx, Policy policy) {
+        return new ControlLoopOperationManager2(this, ctx, policy, getExecutor());
+    }
+
+    protected Executor getExecutor() {
+        return ForkJoinPool.commonPool();
+    }
+
+    protected ExecutorService getBlockingExecutor() {
+        return PolicyEngineConstants.getManager().getExecutorService();
+    }
+
+    protected void makeLock(String targetEntity, String requestId, int holdSec, LockCallback callback) {
+        PolicyEngineConstants.getManager().createLock(targetEntity, requestId, holdSec, callback, false);
+    }
+
+    @Override
+    public ActorService getActorService() {
+        return LazyInitData.ACTOR_SERVICE;
+    }
+
+    @Override
+    public OperationHistoryDataManager getDataManager() {
+        return LazyInitData.DATA_MANAGER;
+    }
+}
diff --git a/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/eventmanager/ControlLoopOperationManager2.java b/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/eventmanager/ControlLoopOperationManager2.java
new file mode 100644
index 0000000..6bdaa15
--- /dev/null
+++ b/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/eventmanager/ControlLoopOperationManager2.java
@@ -0,0 +1,684 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2017-2020 AT&T Intellectual Property. All rights reserved.
+ * Modifications Copyright (C) 2019 Huawei Technologies Co., Ltd. All rights reserved.
+ * Modifications Copyright (C) 2019 Tech Mahindra
+ * Modifications Copyright (C) 2019 Bell Canada.
+ * ================================================================================
+ * 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.controlloop.eventmanager;
+
+import java.io.Serializable;
+import java.time.Instant;
+import java.util.Deque;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.ToString;
+import org.onap.policy.aai.AaiConstants;
+import org.onap.policy.aai.AaiCqResponse;
+import org.onap.policy.controlloop.ControlLoopOperation;
+import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome;
+import org.onap.policy.controlloop.actorserviceprovider.controlloop.ControlLoopEventContext;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams;
+import org.onap.policy.controlloop.actorserviceprovider.pipeline.PipelineUtil;
+import org.onap.policy.controlloop.policy.Policy;
+import org.onap.policy.controlloop.policy.PolicyResult;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manages a single Operation for a single event. Once this has been created,
+ * {@link #start()} should be invoked, and then {@link #nextStep()} should be invoked
+ * continually until it returns {@code false}, indicating that all steps have completed.
+ */
+@ToString(onlyExplicitlyIncluded = true)
+public class ControlLoopOperationManager2 implements Serializable {
+    private static final long serialVersionUID = -3773199283624595410L;
+    private static final Logger logger = LoggerFactory.getLogger(ControlLoopOperationManager2.class);
+    private static final String CL_TIMEOUT_ACTOR = "-CL-TIMEOUT-";
+    public static final String LOCK_ACTOR = "LOCK";
+    public static final String LOCK_OPERATION = "Lock";
+    private static final String GUARD_ACTOR = "GUARD";
+    public static final String VSERVER_VSERVER_NAME = "vserver.vserver-name";
+    public static final String GENERIC_VNF_VNF_NAME = "generic-vnf.vnf-name";
+    public static final String GENERIC_VNF_VNF_ID = "generic-vnf.vnf-id";
+    public static final String PNF_NAME = "pnf.pnf-name";
+
+    // @formatter:off
+    public enum State {
+        ACTIVE,
+        LOCK_DENIED,
+        LOCK_LOST,
+        GUARD_STARTED,
+        GUARD_PERMITTED,
+        GUARD_DENIED,
+        OPERATION_SUCCESS,
+        OPERATION_FAILURE,
+        CONTROL_LOOP_TIMEOUT
+    }
+    // @formatter:on
+
+    private final transient ManagerContext operContext;
+    private final transient ControlLoopEventContext eventContext;
+    private final Policy policy;
+
+    @Getter
+    @ToString.Include
+    private State state = State.ACTIVE;
+
+    @ToString.Include
+    private final String requestId;
+
+    @ToString.Include
+    private final String policyId;
+
+    /**
+     * Bumped each time the "complete" callback is invoked by the Actor, provided it's for
+     * this operation.
+     */
+    @ToString.Include
+    private int attempts = 0;
+
+    private final Deque<Operation> operationHistory = new ConcurrentLinkedDeque<>();
+
+    /**
+     * Queue of outcomes yet to be processed. Outcomes are added to this each time the
+     * "start" or "complete" callback is invoked.
+     */
+    @Getter(AccessLevel.PROTECTED)
+    private final transient Deque<OperationOutcome> outcomes = new ConcurrentLinkedDeque<>();
+
+    /**
+     * Used to cancel the running operation.
+     */
+    @Getter(AccessLevel.PROTECTED)
+    private transient CompletableFuture<OperationOutcome> future = null;
+
+    /**
+     * Target entity. Determined after the lock is granted, though it may require the
+     * custom query to be performed first.
+     */
+    @Getter
+    private String targetEntity;
+
+    @Getter(AccessLevel.PROTECTED)
+    private final transient ControlLoopOperationParams params;
+    private final transient PipelineUtil taskUtil;
+
+    /**
+     * Time when the lock was first requested.
+     */
+    private transient AtomicReference<Instant> lockStart = new AtomicReference<>();
+
+    // values extracted from the policy
+    @Getter
+    private final String actor;
+    @Getter
+    private final String operation;
+
+
+    /**
+     * Construct an instance.
+     *
+     * @param operContext this operation's context
+     * @param context event context
+     * @param policy operation's policy
+     * @param executor executor for the Operation
+     */
+    public ControlLoopOperationManager2(ManagerContext operContext, ControlLoopEventContext context, Policy policy,
+                    Executor executor) {
+
+        this.operContext = operContext;
+        this.eventContext = context;
+        this.policy = policy;
+        this.requestId = context.getEvent().getRequestId().toString();
+        this.policyId = "" + policy.getId();
+        this.actor = policy.getActor();
+        this.operation = policy.getRecipe();
+
+        // @formatter:off
+        params = ControlLoopOperationParams.builder()
+                        .actorService(operContext.getActorService())
+                        .actor(actor)
+                        .operation(operation)
+                        .context(context)
+                        .executor(executor)
+                        .target(policy.getTarget())
+                        .startCallback(this::onStart)
+                        .completeCallback(this::onComplete)
+                        .build();
+        // @formatter:on
+
+        taskUtil = new PipelineUtil(params);
+    }
+
+    //
+    // Internal class used for tracking
+    //
+    @Getter
+    @ToString
+    private class Operation implements Serializable {
+        private static final long serialVersionUID = 1L;
+
+        private int attempt;
+        private PolicyResult policyResult;
+        private ControlLoopOperation clOperation;
+
+        /**
+         * Constructs the object.
+         *
+         * @param outcome outcome of the operation
+         */
+        public Operation(OperationOutcome outcome) {
+            attempt = ControlLoopOperationManager2.this.attempts;
+            policyResult = outcome.getResult();
+            clOperation = outcome.toControlLoopOperation();
+        }
+    }
+
+    /**
+     * Start the operation, first acquiring any locks that are needed. This should not
+     * throw any exceptions, but will, instead, invoke the callbacks with exceptions.
+     *
+     * @param remainingMs time remaining, in milliseconds, for the control loop
+     */
+    @SuppressWarnings("unchecked")
+    public synchronized void start(long remainingMs) {
+        // this is synchronized while we update "future"
+
+        try {
+            // provide a default, in case something fails before requestLock() is called
+            lockStart.set(Instant.now());
+
+            // @formatter:off
+            future = taskUtil.sequence(
+                this::detmTarget,
+                this::requestLock,
+                this::startOperation);
+            // @formatter:on
+
+            // handle any exceptions that may be thrown, set timeout, and handle timeout
+
+            // @formatter:off
+            future.exceptionally(this::handleException)
+                    .orTimeout(remainingMs, TimeUnit.MILLISECONDS)
+                    .exceptionally(this::handleTimeout);
+            // @formatter:on
+
+        } catch (RuntimeException e) {
+            handleException(e);
+        }
+    }
+
+    /**
+     * Start the operation, after the lock has been acquired.
+     *
+     * @return
+     */
+    private CompletableFuture<OperationOutcome> startOperation() {
+        // @formatter:off
+        ControlLoopOperationParams params2 = params.toBuilder()
+                    .payload(new LinkedHashMap<>())
+                    .retry(policy.getRetry())
+                    .timeoutSec(policy.getTimeout())
+                    .targetEntity(targetEntity)
+                    .build();
+        // @formatter:on
+
+        if (policy.getPayload() != null) {
+            params2.getPayload().putAll(policy.getPayload());
+        }
+
+        return params2.start();
+    }
+
+    /**
+     * Handles exceptions that may be generated.
+     *
+     * @param thrown exception that was generated
+     * @return {@code null}
+     */
+    private OperationOutcome handleException(Throwable thrown) {
+        if (thrown instanceof CancellationException || thrown.getCause() instanceof CancellationException) {
+            return null;
+        }
+
+        logger.warn("{}.{}: exception starting operation for {}", actor, operation, requestId, thrown);
+        OperationOutcome outcome = taskUtil.setOutcome(params.makeOutcome(), thrown);
+        outcome.setStart(lockStart.get());
+        outcome.setEnd(Instant.now());
+        outcome.setFinalOutcome(true);
+        onComplete(outcome);
+
+        // this outcome is not used so just return "null"
+        return null;
+    }
+
+    /**
+     * Handles control loop timeout exception.
+     *
+     * @param thrown exception that was generated
+     * @return {@code null}
+     */
+    private OperationOutcome handleTimeout(Throwable thrown) {
+        logger.warn("{}.{}: control loop timeout for {}", actor, operation, requestId, thrown);
+
+        OperationOutcome outcome = taskUtil.setOutcome(params.makeOutcome(), thrown);
+        outcome.setActor(CL_TIMEOUT_ACTOR);
+        outcome.setOperation(null);
+        outcome.setStart(lockStart.get());
+        outcome.setEnd(Instant.now());
+        outcome.setFinalOutcome(true);
+        onComplete(outcome);
+
+        // cancel the operation, if it's still running
+        future.cancel(false);
+
+        // this outcome is not used so just return "null"
+        return null;
+    }
+
+    /**
+     * Cancels the operation.
+     */
+    public void cancel() {
+        synchronized (this) {
+            if (future == null) {
+                return;
+            }
+        }
+
+        future.cancel(false);
+    }
+
+    /**
+     * Requests a lock on the {@link #targetEntity}.
+     *
+     * @return a future to await the lock
+     */
+    private CompletableFuture<OperationOutcome> requestLock() {
+        /*
+         * Failures are handled via the callback, and successes are discarded by
+         * sequence(), without passing them to onComplete().
+         *
+         * Return a COPY of the future so that if we try to cancel it, we'll only cancel
+         * the copy, not the original. This is done by tacking thenApply() onto the end.
+         */
+        lockStart.set(Instant.now());
+        return operContext.requestLock(targetEntity, this::lockUnavailable).thenApply(outcome -> outcome);
+    }
+
+    /**
+     * Indicates that the lock on the target entity is unavailable.
+     *
+     * @param outcome lock outcome
+     */
+    private void lockUnavailable(OperationOutcome outcome) {
+
+        // Note: NEVER invoke onStart() for locks; only invoke onComplete()
+        onComplete(outcome);
+
+        /*
+         * Now that we've added the lock outcome to the queue, ensure the future is
+         * canceled, which may, itself, generate an operation outcome.
+         */
+        cancel();
+    }
+
+    /**
+     * Handles responses provided via the "start" callback. Note: this is never be invoked
+     * for locks; only {@link #onComplete(OperationOutcome)} is invoked for locks.
+     *
+     * @param outcome outcome provided to the callback
+     */
+    private void onStart(OperationOutcome outcome) {
+        if (GUARD_ACTOR.equals(outcome.getActor())) {
+            addOutcome(outcome);
+        }
+    }
+
+    /**
+     * Handles responses provided via the "complete" callback. Note: this is never invoked
+     * for "successful" locks.
+     *
+     * @param outcome outcome provided to the callback
+     */
+    private void onComplete(OperationOutcome outcome) {
+
+        switch (outcome.getActor()) {
+            case LOCK_ACTOR:
+            case GUARD_ACTOR:
+            case CL_TIMEOUT_ACTOR:
+                addOutcome(outcome);
+                break;
+
+            default:
+                if (outcome.isFor(actor, operation)) {
+                    addOutcome(outcome);
+                }
+                break;
+        }
+    }
+
+    /**
+     * Adds an outcome to {@link #outcomes}.
+     *
+     * @param outcome outcome to be added
+     */
+    private synchronized void addOutcome(OperationOutcome outcome) {
+        /*
+         * This is synchronized to prevent nextStep() from invoking processOutcome() at
+         * the same time.
+         */
+
+        logger.debug("added outcome={} for {}", outcome, requestId);
+        outcomes.add(outcome);
+
+        if (outcomes.peekFirst() == outcomes.peekLast()) {
+            // this is the first outcome in the queue - process it
+            processOutcome();
+        }
+    }
+
+    /**
+     * Looks for the next step in the queue.
+     *
+     * @return {@code true} if more responses are expected, {@code false} otherwise
+     */
+    public synchronized boolean nextStep() {
+        switch (state) {
+            case LOCK_DENIED:
+            case LOCK_LOST:
+            case GUARD_DENIED:
+            case CONTROL_LOOP_TIMEOUT:
+                return false;
+            default:
+                break;
+        }
+
+        OperationOutcome outcome = outcomes.peek();
+        if (outcome == null) {
+            // empty queue
+            return true;
+        }
+
+        if (outcome.isFinalOutcome() && outcome.isFor(actor, operation)) {
+            return false;
+        }
+
+        // first item has been processed, remove it
+        outcomes.remove();
+        if (!outcomes.isEmpty()) {
+            // have a new "first" item - process it
+            processOutcome();
+        }
+
+        return true;
+    }
+
+    /**
+     * Processes the first item in {@link #outcomes}. Sets the state, increments
+     * {@link #attempts}, if appropriate, and stores the operation history in the DB.
+     */
+    private synchronized void processOutcome() {
+        OperationOutcome outcome = outcomes.peek();
+        logger.debug("process outcome={} for {}", outcome, requestId);
+
+        switch (outcome.getActor()) {
+
+            case CL_TIMEOUT_ACTOR:
+                state = State.CONTROL_LOOP_TIMEOUT;
+                break;
+
+            case LOCK_ACTOR:
+                // lock is no longer available
+                if (state == State.ACTIVE) {
+                    state = State.LOCK_DENIED;
+                    storeFailureInDataBase(outcome, PolicyResult.FAILURE_GUARD, "Operation denied by Lock");
+                } else {
+                    state = State.LOCK_LOST;
+                    storeFailureInDataBase(outcome, PolicyResult.FAILURE, "Operation aborted by Lock");
+                }
+                break;
+
+            case GUARD_ACTOR:
+                if (outcome.getEnd() == null) {
+                    state = State.GUARD_STARTED;
+                } else if (outcome.getResult() == PolicyResult.SUCCESS) {
+                    state = State.GUARD_PERMITTED;
+                } else {
+                    state = State.GUARD_DENIED;
+                    storeFailureInDataBase(outcome, PolicyResult.FAILURE_GUARD, "Operation denied by Guard");
+                }
+                break;
+
+            default:
+                // operation completed
+                ++attempts;
+                state = (outcome.getResult() == PolicyResult.SUCCESS ? State.OPERATION_SUCCESS
+                                : State.OPERATION_FAILURE);
+                operationHistory.add(new Operation(outcome));
+                storeOperationInDataBase();
+                break;
+        }
+
+        // indicate that this has changed
+        operContext.updated(this);
+    }
+
+    /**
+     * Get the operation, as a message.
+     *
+     * @return the operation, as a message
+     */
+    public String getOperationMessage() {
+        Operation last = operationHistory.peekLast();
+        return (last == null ? null : last.getClOperation().toMessage());
+    }
+
+    /**
+     * Gets the operation result.
+     *
+     * @return the operation result
+     */
+    public PolicyResult getOperationResult() {
+        Operation last = operationHistory.peekLast();
+        return (last == null ? PolicyResult.FAILURE_EXCEPTION : last.getPolicyResult());
+    }
+
+    /**
+     * Get the latest operation history.
+     *
+     * @return the latest operation history
+     */
+    public String getOperationHistory() {
+        Operation last = operationHistory.peekLast();
+        return (last == null ? null : last.clOperation.toHistory());
+    }
+
+    /**
+     * Get the history.
+     *
+     * @return the list of control loop operations
+     */
+    public List<ControlLoopOperation> getHistory() {
+        return operationHistory.stream().map(Operation::getClOperation).map(ControlLoopOperation::new)
+                        .collect(Collectors.toList());
+    }
+
+    /**
+     * Stores a failure in the DB.
+     *
+     * @param outcome operation outcome
+     * @param result result to put into the DB
+     * @param message message to put into the DB
+     */
+    private void storeFailureInDataBase(OperationOutcome outcome, PolicyResult result, String message) {
+        outcome.setActor(actor);
+        outcome.setOperation(operation);
+        outcome.setMessage(message);
+        outcome.setResult(result);
+
+        operationHistory.add(new Operation(outcome));
+        storeOperationInDataBase();
+    }
+
+    /**
+     * Stores the latest operation in the DB.
+     */
+    private void storeOperationInDataBase() {
+        operContext.getDataManager().store(requestId, eventContext.getEvent(),
+                        operationHistory.peekLast().getClOperation());
+    }
+
+    /**
+     * Determines the target entity.
+     *
+     * @return a future to determine the target entity, or {@code null} if the entity has
+     *         already been determined
+     */
+    protected CompletableFuture<OperationOutcome> detmTarget() {
+        if (policy.getTarget() == null) {
+            throw new IllegalArgumentException("The target is null");
+        }
+
+        if (policy.getTarget().getType() == null) {
+            throw new IllegalArgumentException("The target type is null");
+        }
+
+        switch (policy.getTarget().getType()) {
+            case PNF:
+                return detmPnfTarget();
+            case VM:
+            case VNF:
+            case VFMODULE:
+                return detmVfModuleTarget();
+            default:
+                throw new IllegalArgumentException("The target type is not supported");
+        }
+    }
+
+    /**
+     * Determines the PNF target entity.
+     *
+     * @return a future to determine the target entity, or {@code null} if the entity has
+     *         already been determined
+     */
+    private CompletableFuture<OperationOutcome> detmPnfTarget() {
+        if (!PNF_NAME.equalsIgnoreCase(eventContext.getEvent().getTarget())) {
+            throw new IllegalArgumentException("Target does not match target type");
+        }
+
+        targetEntity = eventContext.getEnrichment().get(PNF_NAME);
+        if (targetEntity == null) {
+            throw new IllegalArgumentException("AAI section is missing " + PNF_NAME);
+        }
+
+        return null;
+    }
+
+    /**
+     * Determines the VF Module target entity.
+     *
+     * @return a future to determine the target entity, or {@code null} if the entity has
+     *         already been determined
+     */
+    private CompletableFuture<OperationOutcome> detmVfModuleTarget() {
+        String targetFieldName = eventContext.getEvent().getTarget();
+        if (targetFieldName == null) {
+            throw new IllegalArgumentException("Target is null");
+        }
+
+        switch (targetFieldName.toLowerCase()) {
+            case VSERVER_VSERVER_NAME:
+                targetEntity = eventContext.getEnrichment().get(VSERVER_VSERVER_NAME);
+                break;
+            case GENERIC_VNF_VNF_ID:
+                targetEntity = eventContext.getEnrichment().get(GENERIC_VNF_VNF_ID);
+                break;
+            case GENERIC_VNF_VNF_NAME:
+                return detmVnfName();
+            default:
+                throw new IllegalArgumentException("Target does not match target type");
+        }
+
+        if (targetEntity == null) {
+            throw new IllegalArgumentException("Enrichment data is missing " + targetFieldName);
+        }
+
+        return null;
+    }
+
+    /**
+     * Determines the VNF Name target entity.
+     *
+     * @return a future to determine the target entity, or {@code null} if the entity has
+     *         already been determined
+     */
+    @SuppressWarnings("unchecked")
+    private CompletableFuture<OperationOutcome> detmVnfName() {
+        // if the onset is enriched with the vnf-id, we don't need an A&AI response
+        targetEntity = eventContext.getEnrichment().get(GENERIC_VNF_VNF_ID);
+        if (targetEntity != null) {
+            return null;
+        }
+
+        // vnf-id was not in the onset - obtain it via the custom query
+
+        // @formatter:off
+        ControlLoopOperationParams cqparams = params.toBuilder()
+                        .actor(AaiConstants.ACTOR_NAME)
+                        .operation(AaiCqResponse.OPERATION)
+                        .targetEntity("")
+                        .build();
+        // @formatter:on
+
+        // perform custom query and then extract the VNF ID from it
+        return taskUtil.sequence(() -> eventContext.obtain(AaiCqResponse.CONTEXT_KEY, cqparams),
+                        this::extractVnfFromCq);
+    }
+
+    /**
+     * Extracts the VNF Name target entity from the custom query data.
+     *
+     * @return {@code null}
+     */
+    private CompletableFuture<OperationOutcome> extractVnfFromCq() {
+        // already have the CQ data
+        AaiCqResponse cq = eventContext.getProperty(AaiCqResponse.CONTEXT_KEY);
+        if (cq.getDefaultGenericVnf() == null) {
+            throw new IllegalArgumentException("No vnf-id found");
+        }
+
+        targetEntity = cq.getDefaultGenericVnf().getVnfId();
+        if (targetEntity == null) {
+            throw new IllegalArgumentException("No vnf-id found");
+        }
+
+        return null;
+    }
+}
diff --git a/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/eventmanager/EventManagerServices.java b/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/eventmanager/EventManagerServices.java
new file mode 100644
index 0000000..d8668e4
--- /dev/null
+++ b/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/eventmanager/EventManagerServices.java
@@ -0,0 +1,179 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.eventmanager;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.Properties;
+import lombok.Getter;
+import org.onap.policy.common.parameters.ValidationResult;
+import org.onap.policy.common.utils.resources.ResourceUtils;
+import org.onap.policy.controlloop.actor.guard.GuardActorServiceProvider;
+import org.onap.policy.controlloop.actor.guard.GuardConfig;
+import org.onap.policy.controlloop.actor.guard.GuardOperation;
+import org.onap.policy.controlloop.actor.guard.GuardOperator;
+import org.onap.policy.controlloop.actorserviceprovider.ActorService;
+import org.onap.policy.controlloop.actorserviceprovider.Util;
+import org.onap.policy.controlloop.ophistory.OperationHistoryDataManager;
+import org.onap.policy.controlloop.ophistory.OperationHistoryDataManagerImpl;
+import org.onap.policy.controlloop.ophistory.OperationHistoryDataManagerParams;
+import org.onap.policy.controlloop.ophistory.OperationHistoryDataManagerStub;
+import org.onap.policy.controlloop.utils.ControlLoopUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Services used by the ControlLoopEventManager.
+ */
+@Getter
+public class EventManagerServices {
+    public static final Logger logger = LoggerFactory.getLogger(EventManagerServices.class);
+    public static final String ACTOR_SERVICE_PROPERTIES = "actor.service";
+    public static final String DATA_MANAGER_PROPERTIES = "operation.history";
+
+    public final ActorService actorService = new ActorService();
+
+    // assume we're using a stub until proven otherwise
+    public final OperationHistoryDataManager dataManager;
+
+    /**
+     * Constructs the object. Configures and starts the actor service. Initializes
+     * {@link #dataManager}, to a "real" data manager, if guards are enabled.
+     *
+     * @param configFileName configuration file name
+     */
+    public EventManagerServices(String configFileName) {
+        // configure and start actor services
+        Properties props = startActorService(configFileName);
+
+        if (isGuardEnabled()) {
+            // guards are enabled - use a real data manager
+            dataManager = makeDataManager(props);
+        } else {
+            // guards are disabled - use a stub data manager
+            dataManager = new OperationHistoryDataManagerStub();
+        }
+    }
+
+    /**
+     * Configures and starts the actor service.
+     *
+     * @param configFileName configuration file name
+     * @return the properties that were loaded from the configuration file
+     */
+    public Properties startActorService(String configFileName) {
+        try (InputStream inpstr = openConfigFile(configFileName)) {
+            Properties props = new Properties();
+            props.load(inpstr);
+
+            Map<String, Object> parameters = ControlLoopUtils.toObject(props, ACTOR_SERVICE_PROPERTIES);
+            ControlLoopUtils.compressLists(parameters);
+
+            actorService.configure(parameters);
+            actorService.start();
+
+            return props;
+
+        } catch (RuntimeException | IOException e) {
+            logger.error("cannot configure/start actor service");
+            throw new IllegalStateException(e);
+        }
+    }
+
+    /**
+     * Opens the config file.
+     *
+     * @param configFileName configuration file name
+     * @return the file's input stream
+     * @throws FileNotFoundException if the file cannot be found
+     */
+    private InputStream openConfigFile(String configFileName) throws FileNotFoundException {
+        InputStream inpstr = ResourceUtils.getResourceAsStream(configFileName);
+        if (inpstr == null) {
+            throw new FileNotFoundException(configFileName);
+        }
+
+        return inpstr;
+    }
+
+    /**
+     * Determines if guards are enabled.
+     *
+     * @return {@code true} if guards are enabled, {@code false} otherwise
+     */
+    public boolean isGuardEnabled() {
+        try {
+            GuardOperator guard = (GuardOperator) getActorService().getActor(GuardActorServiceProvider.NAME)
+                            .getOperator(GuardOperation.NAME);
+            if (!guard.isConfigured()) {
+                logger.warn("cannot check 'disabled' property in GUARD actor - assuming disabled");
+                return false;
+            }
+
+            GuardConfig config = (GuardConfig) guard.getCurrentConfig();
+            if (config.isDisabled()) {
+                logger.warn("guard disabled");
+                return false;
+            }
+
+            if (!guard.isAlive()) {
+                logger.warn("guard actor is not running");
+                return false;
+            }
+
+            return true;
+
+        } catch (RuntimeException e) {
+            logger.warn("cannot check 'disabled' property in GUARD actor - assuming disabled", e);
+            return false;
+        }
+    }
+
+    /**
+     * Makes and starts the data manager.
+     *
+     * @param props properties with which to configure the data manager
+     * @return a new data manager
+     */
+    public OperationHistoryDataManagerImpl makeDataManager(Properties props) {
+        try {
+            Map<String, Object> parameters = ControlLoopUtils.toObject(props, DATA_MANAGER_PROPERTIES);
+            OperationHistoryDataManagerParams params = Util.translate(DATA_MANAGER_PROPERTIES, parameters,
+                            OperationHistoryDataManagerParams.class);
+            ValidationResult result = params.validate(DATA_MANAGER_PROPERTIES);
+            if (!result.isValid()) {
+                throw new IllegalArgumentException("invalid data manager properties:\n" + result.getResult());
+            }
+
+            OperationHistoryDataManagerImpl mgr = new OperationHistoryDataManagerImpl(params);
+            mgr.start();
+
+            return mgr;
+
+        } catch (RuntimeException e) {
+            logger.error("cannot start operation history data manager");
+            actorService.stop();
+            throw e;
+        }
+    }
+}
diff --git a/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/eventmanager/LockData.java b/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/eventmanager/LockData.java
new file mode 100644
index 0000000..8356000
--- /dev/null
+++ b/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/eventmanager/LockData.java
@@ -0,0 +1,181 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2017-2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.eventmanager;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import org.onap.policy.controlloop.ControlLoopOperation;
+import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome;
+import org.onap.policy.controlloop.policy.PolicyResult;
+import org.onap.policy.drools.core.lock.Lock;
+import org.onap.policy.drools.core.lock.LockCallback;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * Data for an individual lock.
+ */
+public class LockData implements LockCallback {
+    private static final Logger logger = LoggerFactory.getLogger(LockData.class);
+
+    private final String targetEntity;
+    private final UUID requestId;
+
+    /**
+     * Time when this was created.
+     */
+    private final Instant createTime = Instant.now();
+
+    /**
+     * Future for obtaining the lock. Initially incomplete.
+     */
+    private final AtomicReference<CompletableFuture<OperationOutcome>> future =
+                    new AtomicReference<>(new CompletableFuture<>());
+
+    /**
+     * The lock.
+     */
+    private Lock theLock = null;
+
+    /**
+     * Listeners to invoke if the lock is unavailable/lost.
+     */
+    private final List<Consumer<OperationOutcome>> unavailableCallbacks = new ArrayList<>();
+
+    /**
+     * Set to a failed outcome, if the lock becomes unavailable.
+     */
+    private OperationOutcome failedOutcome = null;
+
+
+    /**
+     * Constructs the object.
+     *
+     * @param targetEntity target entity
+     */
+    public LockData(String targetEntity, UUID requestId) {
+        this.targetEntity = targetEntity;
+        this.requestId = requestId;
+    }
+
+    /**
+     * Gets the future to be completed when the lock operation completes.
+     *
+     * @return the lock operation future
+     */
+    public CompletableFuture<OperationOutcome> getFuture() {
+        return future.get();
+    }
+
+    /**
+     * Adds a callback to be invoked if the lock becomes unavailable.
+     *
+     * @param callback callback to be added
+     */
+    public void addUnavailableCallback(Consumer<OperationOutcome> callback) {
+        synchronized (this) {
+            if (failedOutcome == null) {
+                // hasn't failed yet - add it to the list
+                unavailableCallbacks.add(callback);
+                return;
+            }
+        }
+
+        // already failed - invoke the callback immediately
+        callback.accept(failedOutcome);
+    }
+
+    /**
+     * Frees the lock.
+     */
+    public void free() {
+        Lock lock;
+
+        synchronized (this) {
+            if ((lock = theLock) == null) {
+                return;
+            }
+        }
+
+        lock.free();
+    }
+
+    @Override
+    public synchronized void lockAvailable(Lock lock) {
+        logger.warn("lock granted on {} for {}", targetEntity, requestId);
+        theLock = lock;
+
+        OperationOutcome outcome = makeOutcome();
+        outcome.setResult(PolicyResult.SUCCESS);
+        outcome.setMessage(ControlLoopOperation.SUCCESS_MSG);
+
+        future.get().complete(outcome);
+    }
+
+    @Override
+    public void lockUnavailable(Lock unused) {
+        synchronized (this) {
+            logger.warn("lock unavailable on {} for {}", targetEntity, requestId);
+            failedOutcome = makeOutcome();
+            failedOutcome.setResult(PolicyResult.FAILURE);
+            failedOutcome.setMessage(ControlLoopOperation.FAILED_MSG);
+        }
+
+        /*
+         * In case the future was already completed successfully, replace it with a failed
+         * future, but complete the old one, too, in case it wasn't completed yet.
+         */
+        future.getAndSet(CompletableFuture.completedFuture(failedOutcome)).complete(failedOutcome);
+
+        for (Consumer<OperationOutcome> callback : unavailableCallbacks) {
+            try {
+                callback.accept(new OperationOutcome(failedOutcome));
+            } catch (RuntimeException e) {
+                logger.warn("lock callback threw an exception for {}", requestId, e);
+            }
+        }
+
+        unavailableCallbacks.clear();
+    }
+
+    /**
+     * Makes a lock operation outcome.
+     *
+     * @return a new lock operation outcome
+     */
+    private OperationOutcome makeOutcome() {
+        OperationOutcome outcome = new OperationOutcome();
+        outcome.setActor(ControlLoopOperationManager2.LOCK_ACTOR);
+        outcome.setOperation(ControlLoopOperationManager2.LOCK_OPERATION);
+        outcome.setTarget(targetEntity);
+        outcome.setFinalOutcome(true);
+        outcome.setStart(createTime);
+        outcome.setEnd(Instant.now());
+
+        return outcome;
+    }
+}
diff --git a/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/eventmanager/ManagerContext.java b/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/eventmanager/ManagerContext.java
new file mode 100644
index 0000000..0dcd302
--- /dev/null
+++ b/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/eventmanager/ManagerContext.java
@@ -0,0 +1,64 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.eventmanager;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+import org.onap.policy.controlloop.actorserviceprovider.ActorService;
+import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome;
+import org.onap.policy.controlloop.ophistory.OperationHistoryDataManager;
+
+/**
+ * Context for the Operation Manager.
+ */
+public interface ManagerContext {
+
+    /**
+     * Gets the actor service.
+     *
+     * @return the actor service
+     */
+    ActorService getActorService();
+
+    /**
+     * Gets the operation history data manager.
+     *
+     * @return the operation history data manager
+     */
+    OperationHistoryDataManager getDataManager();
+
+    /**
+     * Requests a lock on the specified target.
+     *
+     * @param target target to be locked
+     * @param lockUnavailableCallback callback to be invoked if the lock is
+     *        unavailable/lost
+     * @return a future to await the lock
+     */
+    CompletableFuture<OperationOutcome> requestLock(String target, Consumer<OperationOutcome> lockUnavailableCallback);
+
+    /**
+     * Indicates that the given operation manager has been updated.
+     *
+     * @param operationMgr operation manager that has been updated
+     */
+    void updated(ControlLoopOperationManager2 operationMgr);
+}
diff --git a/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/ophistory/OperationHistoryDataManager.java b/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/ophistory/OperationHistoryDataManager.java
new file mode 100644
index 0000000..a1774ea
--- /dev/null
+++ b/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/ophistory/OperationHistoryDataManager.java
@@ -0,0 +1,50 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.ophistory;
+
+import org.onap.policy.controlloop.ControlLoopOperation;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+
+/**
+ * Data manager for the Operation History table.
+ */
+public interface OperationHistoryDataManager {
+
+    /**
+     * Stores an operation in the DB. If the queue is full, then the oldest records is
+     * discarded.
+     *
+     * @param requestId request ID
+     * @param event event with which the operation is associated
+     * @param operation operation to be stored
+     */
+    void store(String requestId, VirtualControlLoopEvent event, ControlLoopOperation operation);
+
+    /**
+     * Starts the background thread.
+     */
+    public void start();
+
+    /**
+     * Stops the background thread and places an "end" item into {@link #operations}.
+     */
+    public void stop();
+}
diff --git a/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/ophistory/OperationHistoryDataManagerImpl.java b/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/ophistory/OperationHistoryDataManagerImpl.java
new file mode 100644
index 0000000..f7576f1
--- /dev/null
+++ b/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/ophistory/OperationHistoryDataManagerImpl.java
@@ -0,0 +1,295 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.ophistory;
+
+import java.util.Date;
+import java.util.Properties;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.function.Consumer;
+import javax.persistence.EntityManager;
+import javax.persistence.EntityManagerFactory;
+import javax.persistence.Persistence;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+import org.eclipse.persistence.config.PersistenceUnitProperties;
+import org.onap.policy.common.parameters.ValidationResult;
+import org.onap.policy.common.utils.jpa.EntityMgrCloser;
+import org.onap.policy.common.utils.jpa.EntityTransCloser;
+import org.onap.policy.controlloop.ControlLoopOperation;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+import org.onap.policy.database.operationshistory.Dbao;
+import org.onap.policy.guard.Util;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Data manager that stores records in the DB, asynchronously, using a background thread.
+ */
+public class OperationHistoryDataManagerImpl implements OperationHistoryDataManager {
+    private static final Logger logger = LoggerFactory.getLogger(OperationHistoryDataManagerImpl.class);
+
+    /**
+     * Added to the end of {@link #operations} when {@link #stop()} is called. This is
+     * used to get the background thread out of a blocking wait for the next record.
+     */
+    private static final Record END_MARKER = new Record();
+
+    // copied from the parameters
+    private final int maxQueueLength;
+    private final int batchSize;
+
+    private final EntityManagerFactory emFactory;
+
+    /**
+     * Thread that takes records from {@link #operations} and stores them in the DB.
+     */
+    private Thread thread;
+
+    /**
+     * Set to {@code true} to stop the background thread.
+     */
+    private boolean stopped = false;
+
+    /**
+     * Queue of operations waiting to be stored in the DB. When {@link #stop()} is called,
+     * an {@link #END_MARKER} is added to the end of the queue.
+     */
+    private final BlockingQueue<Record> operations = new LinkedBlockingQueue<>();
+
+    /**
+     * Number of records that have been added to the DB by this data manager instance.
+     */
+    @Getter
+    private long recordsAdded = 0;
+
+
+    /**
+     * Constructs the object.
+     *
+     * @param params data manager parameters
+     */
+    public OperationHistoryDataManagerImpl(OperationHistoryDataManagerParams params) {
+        ValidationResult result = params.validate("data-manager-properties");
+        if (!result.isValid()) {
+            throw new IllegalArgumentException(result.getResult());
+        }
+
+        this.maxQueueLength = params.getMaxQueueLength();
+        this.batchSize = params.getBatchSize();
+
+        // create the factory using the properties
+        Properties props = toProperties(params);
+        this.emFactory = makeEntityManagerFactory(params.getPersistenceUnit(), props);
+    }
+
+    @Override
+    public synchronized void start() {
+        if (stopped || thread != null) {
+            // already started
+            return;
+        }
+
+        thread = makeThread(emFactory, this::run);
+        thread.setDaemon(true);
+        thread.start();
+    }
+
+    @Override
+    public synchronized void stop() {
+        stopped = true;
+
+        if (thread == null) {
+            // no thread to close the factory - do it here
+            emFactory.close();
+
+        } else {
+            // the thread will close the factory when it sees the end marker
+            operations.add(END_MARKER);
+        }
+    }
+
+    @Override
+    public synchronized void store(String requestId, VirtualControlLoopEvent event, ControlLoopOperation operation) {
+
+        if (stopped) {
+            logger.warn("operation history thread is stopped, discarding requestId={} event={} operation={}", requestId,
+                            event, operation);
+            return;
+        }
+
+        operations.add(new Record(requestId, event, operation));
+
+        if (operations.size() > maxQueueLength) {
+            Record discarded = operations.remove();
+            logger.warn("too many items to store in the operation history table, discarding {}", discarded);
+        }
+    }
+
+    /**
+     * Takes records from {@link #operations} and stores them in the queue. Continues to
+     * run until {@link #stop()} is invoked, or the thread is interrupted.
+     *
+     * @param emfactory entity manager factory
+     */
+    private void run(EntityManagerFactory emfactory) {
+        try {
+            // store records until stopped, continuing if an exception occurs
+            while (!stopped) {
+                try {
+                    Record triple = operations.take();
+                    storeBatch(emfactory.createEntityManager(), triple);
+
+                } catch (RuntimeException e) {
+                    logger.error("failed to save data to operation history table", e);
+
+                } catch (InterruptedException e) {
+                    logger.error("interrupted, discarding remaining operation history data", e);
+                    Thread.currentThread().interrupt();
+                    return;
+                }
+            }
+
+            storeRemainingRecords(emfactory);
+
+        } finally {
+            synchronized (this) {
+                stopped = true;
+            }
+
+            emfactory.close();
+        }
+    }
+
+    /**
+     * Store any remaining records, but stop at the first exception.
+     *
+     * @param emfactory entity manager factory
+     */
+    private void storeRemainingRecords(EntityManagerFactory emfactory) {
+        try {
+            while (!operations.isEmpty()) {
+                storeBatch(emfactory.createEntityManager(), operations.poll());
+            }
+
+        } catch (RuntimeException e) {
+            logger.error("failed to save remaining data to operation history table", e);
+        }
+    }
+
+    /**
+     * Stores a batch of records.
+     *
+     * @param entityManager entity manager
+     * @param firstRecord first record to be stored
+     */
+    private void storeBatch(EntityManager entityManager, Record firstRecord) {
+
+        try (EntityMgrCloser emc = new EntityMgrCloser(entityManager);
+                        EntityTransCloser trans = new EntityTransCloser(entityManager.getTransaction())) {
+
+            int nrecords = 0;
+            Record record = firstRecord;
+
+            while (record != null && record != END_MARKER) {
+                storeRecord(entityManager, record);
+
+                if (++nrecords >= batchSize) {
+                    break;
+                }
+
+                record = operations.poll();
+            }
+
+            trans.commit();
+            recordsAdded += nrecords;
+        }
+    }
+
+    /**
+     * Stores a record.
+     *
+     * @param entityManager entity manager
+     * @param record record to be stored
+     */
+    private void storeRecord(EntityManager entityMgr, Record record) {
+
+        Dbao newEntry = new Dbao();
+
+        final VirtualControlLoopEvent event = record.getEvent();
+        final ControlLoopOperation operation = record.getOperation();
+
+        newEntry.setClosedLoopName(event.getClosedLoopControlName());
+        newEntry.setRequestId(record.getRequestId());
+        newEntry.setActor(operation.getActor());
+        newEntry.setOperation(operation.getOperation());
+        newEntry.setTarget(operation.getTarget());
+        newEntry.setSubrequestId(operation.getSubRequestId());
+        newEntry.setMessage(operation.getMessage());
+        newEntry.setOutcome(operation.getOutcome());
+        if (operation.getStart() != null) {
+            newEntry.setStarttime(new Date(operation.getStart().toEpochMilli()));
+        }
+        if (operation.getEnd() != null) {
+            newEntry.setEndtime(new Date(operation.getEnd().toEpochMilli()));
+        }
+
+        entityMgr.persist(newEntry);
+    }
+
+    /**
+     * Converts the parameters to Properties.
+     *
+     * @param params parameters to be converted
+     * @return a new property set
+     */
+    private Properties toProperties(OperationHistoryDataManagerParams params) {
+        Properties props = new Properties();
+        props.put(Util.ECLIPSE_LINK_KEY_URL, params.getUrl());
+        props.put(Util.ECLIPSE_LINK_KEY_USER, params.getUserName());
+        props.put(Util.ECLIPSE_LINK_KEY_PASS, params.getPassword());
+        props.put(PersistenceUnitProperties.CLASSLOADER, getClass().getClassLoader());
+
+        return props;
+    }
+
+    @Getter
+    @NoArgsConstructor
+    @AllArgsConstructor
+    @ToString
+    private static class Record {
+        private String requestId;
+        private VirtualControlLoopEvent event;
+        private ControlLoopOperation operation;
+    }
+
+    // the following may be overridden by junit tests
+
+    protected EntityManagerFactory makeEntityManagerFactory(String opsHistPu, Properties props) {
+        return Persistence.createEntityManagerFactory(opsHistPu, props);
+    }
+
+    protected Thread makeThread(EntityManagerFactory emfactory, Consumer<EntityManagerFactory> command) {
+        return new Thread(() -> command.accept(emfactory));
+    }
+}
diff --git a/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/ophistory/OperationHistoryDataManagerParams.java b/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/ophistory/OperationHistoryDataManagerParams.java
new file mode 100644
index 0000000..fc919d8
--- /dev/null
+++ b/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/ophistory/OperationHistoryDataManagerParams.java
@@ -0,0 +1,80 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.ophistory;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.onap.policy.common.parameters.BeanValidator;
+import org.onap.policy.common.parameters.ValidationResult;
+import org.onap.policy.common.parameters.annotations.Min;
+import org.onap.policy.common.parameters.annotations.NotBlank;
+import org.onap.policy.common.parameters.annotations.NotNull;
+
+/**
+ * Parameters for a Data Manager.
+ */
+@NotNull
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class OperationHistoryDataManagerParams {
+    public static final String DEFAULT_PU = "OperationsHistoryPU";
+
+    @NotBlank
+    private String url;
+    @NotBlank
+    private String userName;
+
+    // may be blank
+    private String password;
+
+    @Builder.Default
+    private String persistenceUnit = DEFAULT_PU;
+
+    /**
+     * Maximum number of records that can be waiting to be inserted into the DB. When the
+     * limit is reached, the oldest records are discarded.
+     */
+    @Min(1)
+    @Builder.Default
+    private int maxQueueLength = 10000;
+
+    /**
+     * Number of records to add the DB in one transaction.
+     */
+    @Min(1)
+    @Builder.Default
+    private int batchSize = 100;
+
+    /**
+     * Validates the parameters.
+     *
+     * @param resultName name of the result
+     *
+     * @return the validation result
+     */
+    public ValidationResult validate(String resultName) {
+        return new BeanValidator().validateTop(resultName, this);
+    }
+}
diff --git a/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/ophistory/OperationHistoryDataManagerStub.java b/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/ophistory/OperationHistoryDataManagerStub.java
new file mode 100644
index 0000000..e1e0cbe
--- /dev/null
+++ b/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/ophistory/OperationHistoryDataManagerStub.java
@@ -0,0 +1,45 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.ophistory;
+
+import org.onap.policy.controlloop.ControlLoopOperation;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+
+/**
+ * Stub implementation of a data manager; all methods return without doing anything.
+ */
+public class OperationHistoryDataManagerStub implements OperationHistoryDataManager {
+
+    @Override
+    public void store(String requestId, VirtualControlLoopEvent event, ControlLoopOperation operation) {
+        // do nothing
+    }
+
+    @Override
+    public void start() {
+        // do nothing
+    }
+
+    @Override
+    public void stop() {
+        // do nothing
+    }
+}
diff --git a/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/processor/ControlLoopProcessor.java b/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/processor/ControlLoopProcessor.java
index 4ef1f75..1544622 100644
--- a/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/processor/ControlLoopProcessor.java
+++ b/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/processor/ControlLoopProcessor.java
@@ -57,15 +57,16 @@
     private final ControlLoopPolicy policy;
     private String currentNestedPolicyId = null;
 
+    // not serializable, thus must be transient
     @Getter
-    private ToscaPolicy toscaOpPolicy;
+    private transient ToscaPolicy toscaOpPolicy;
 
     @Getter
     private DroolsPolicy domainOpPolicy;
 
     /**
      * Construct an instance from yaml.
-     * 
+     *
      * @param yaml the yaml
      * @throws ControlLoopException if an error occurs
      */
@@ -198,7 +199,7 @@
 
     /**
      * Get the current policy.
-     * 
+     *
      * @return the current policy
      * @throws ControlLoopException if an error occurs
      */
@@ -217,7 +218,7 @@
 
     /**
      * Get the next policy given a result of the current policy.
-     * 
+     *
      * @param result the result of the current policy
      * @throws ControlLoopException if an error occurs
      */
diff --git a/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/utils/ControlLoopUtils.java b/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/utils/ControlLoopUtils.java
index b5d95fe..d311b07 100644
--- a/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/utils/ControlLoopUtils.java
+++ b/controlloop/common/eventmanager/src/main/java/org/onap/policy/controlloop/utils/ControlLoopUtils.java
@@ -18,6 +18,14 @@
 
 package org.onap.policy.controlloop.utils;
 
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import org.onap.policy.controlloop.ControlLoopException;
 import org.onap.policy.controlloop.drl.legacy.ControlLoopParams;
 import org.onap.policy.controlloop.processor.ControlLoopProcessor;
@@ -31,6 +39,7 @@
 public class ControlLoopUtils {
 
     public static final Logger logger = LoggerFactory.getLogger(ControlLoopUtils.class);
+    private static final Pattern NAME_PAT = Pattern.compile("(.*)\\[(\\d{1,3})\\]");
 
     private ControlLoopUtils() {
         super();
@@ -51,4 +60,205 @@
         }
     }
 
+    // TODO move the following to policy-common/utils
+
+    /**
+     * Converts a set of properties to a Map. Supports json-path style property names with
+     * "." separating components, where components may have an optional subscript.
+     *
+     * @param properties properties to be converted
+     * @param prefix properties whose names begin with this prefix are included. The
+     *        prefix is stripped from the name before adding the value to the map
+     * @return a hierarchical map representing the properties
+     */
+    public static Map<String, Object> toObject(Properties properties, String prefix) {
+        String dottedPrefix = prefix + (prefix.isEmpty() || prefix.endsWith(".") ? "" : ".");
+        int pfxlen = dottedPrefix.length();
+
+        Map<String, Object> map = new LinkedHashMap<>();
+
+        for (String name : properties.stringPropertyNames()) {
+            if (name.startsWith(dottedPrefix)) {
+                String[] components = name.substring(pfxlen).split("[.]");
+                setProperty(map, components, properties.getProperty(name));
+            }
+        }
+
+        return map;
+    }
+
+    /**
+     * Sets a property within a hierarchical map.
+     *
+     * @param map map into which the value should be placed
+     * @param names property name components
+     * @param value value to be placed into the map
+     */
+    private static void setProperty(Map<String, Object> map, String[] names, String value) {
+        Map<String, Object> node = map;
+
+        final int lastComp = names.length - 1;
+
+        // process all but the final component
+        for (int comp = 0; comp < lastComp; ++comp) {
+            node = getNode(node, names[comp]);
+        }
+
+        // process the final component
+        String name = names[lastComp];
+        Matcher matcher = NAME_PAT.matcher(name);
+
+        if (!matcher.matches()) {
+            // no subscript
+            node.put(name, value);
+            return;
+        }
+
+        // subscripted
+        List<Object> array = getArray(node, matcher.group(1));
+        int index = Integer.parseInt(matcher.group(2));
+        expand(array, index);
+        array.set(index, value);
+    }
+
+    /**
+     * Gets a node.
+     *
+     * @param map map from which to get the object
+     * @param name name of the element to get from the map, with an optional subscript
+     * @return a Map
+     */
+    @SuppressWarnings("unchecked")
+    private static Map<String, Object> getNode(Map<String, Object> map, String name) {
+        Matcher matcher = NAME_PAT.matcher(name);
+
+        if (!matcher.matches()) {
+            // no subscript
+            return getObject(map, name);
+        }
+
+        // subscripted
+        List<Object> array = getArray(map, matcher.group(1));
+        int index = Integer.parseInt(matcher.group(2));
+        expand(array, index);
+
+        Object item = array.get(index);
+        if (item instanceof Map) {
+            return (Map<String, Object>) item;
+
+        } else {
+            LinkedHashMap<String, Object> result = new LinkedHashMap<>();
+            array.set(index, result);
+            return result;
+        }
+    }
+
+    /**
+     * Ensures that an array's size is large enough to hold the specified element.
+     *
+     * @param array array to be expanded
+     * @param index index of the desired element
+     */
+    private static void expand(List<Object> array, int index) {
+        while (array.size() <= index) {
+            array.add(null);
+        }
+    }
+
+    /**
+     * Gets an object (i.e., Map) from a map. If the particular element is not a Map, then
+     * it is replaced with an empty Map.
+     *
+     * @param map map from which to get the object
+     * @param name name of the element to get from the map, without any subscript
+     * @return a Map
+     */
+    private static Map<String, Object> getObject(Map<String, Object> map, String name) {
+        @SuppressWarnings("unchecked")
+        Map<String, Object> result = (Map<String, Object>) map.compute(name, (key, value) -> {
+            if (value instanceof Map) {
+                return value;
+            } else {
+                return new LinkedHashMap<>();
+            }
+        });
+
+        return result;
+    }
+
+    /**
+     * Gets an array from a map. If the particular element is not an array, then it is
+     * replaced with an empty array.
+     *
+     * @param map map from which to get the array
+     * @param name name of the element to get from the map, without any subscript
+     * @return an array
+     */
+    private static List<Object> getArray(Map<String, Object> map, String name) {
+        @SuppressWarnings("unchecked")
+        List<Object> result = (List<Object>) map.compute(name, (key, value) -> {
+            if (value instanceof List) {
+                return value;
+            } else {
+                return new ArrayList<>();
+            }
+        });
+
+        return result;
+    }
+
+    /**
+     * Compresses lists contained within a generic object, removing all {@code null}
+     * items.
+     *
+     * @param object object to be compressed
+     * @return the original object, modified in place
+     */
+    public static Object compressLists(Object object) {
+        if (object instanceof Map) {
+            @SuppressWarnings("unchecked")
+            Map<String, Object> asMap = (Map<String, Object>) object;
+            compressMapValues(asMap);
+
+        } else if (object instanceof List) {
+            @SuppressWarnings("unchecked")
+            List<Object> asList = (List<Object>) object;
+            compressListItems(asList);
+        }
+
+        // else: ignore anything else
+
+        return object;
+    }
+
+    /**
+     * Walks a hierarchical map and removes {@code null} items found in any Lists.
+     *
+     * @param map map whose lists are to be compressed
+     */
+    private static void compressMapValues(Map<String, Object> map) {
+        for (Object value : map.values()) {
+            compressLists(value);
+        }
+    }
+
+    /**
+     * Removes {@code null} items from the list. In addition, it walks the items within
+     * the list, compressing them, as well.
+     *
+     * @param list the list to be compressed
+     */
+    private static void compressListItems(List<Object> list) {
+        Iterator<Object> iter = list.iterator();
+        while (iter.hasNext()) {
+            Object item = iter.next();
+            if (item == null) {
+                // null item - remove it
+                iter.remove();
+
+            } else {
+                compressLists(item);
+            }
+        }
+    }
 }
diff --git a/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/eventmanager/ControlLoopEventManager2Test.java b/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/eventmanager/ControlLoopEventManager2Test.java
new file mode 100644
index 0000000..522d9f5
--- /dev/null
+++ b/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/eventmanager/ControlLoopEventManager2Test.java
@@ -0,0 +1,812 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.eventmanager;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+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.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
+import org.drools.core.WorkingMemory;
+import org.junit.Before;
+import org.junit.Test;
+import org.kie.api.runtime.rule.FactHandle;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.onap.policy.common.utils.coder.Coder;
+import org.onap.policy.common.utils.coder.CoderException;
+import org.onap.policy.common.utils.coder.StandardYamlCoder;
+import org.onap.policy.common.utils.io.Serializer;
+import org.onap.policy.common.utils.resources.ResourceUtils;
+import org.onap.policy.controlloop.ControlLoopEventStatus;
+import org.onap.policy.controlloop.ControlLoopException;
+import org.onap.policy.controlloop.ControlLoopNotificationType;
+import org.onap.policy.controlloop.ControlLoopOperation;
+import org.onap.policy.controlloop.ControlLoopTargetType;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+import org.onap.policy.controlloop.VirtualControlLoopNotification;
+import org.onap.policy.controlloop.actorserviceprovider.ActorService;
+import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome;
+import org.onap.policy.controlloop.actorserviceprovider.controlloop.ControlLoopEventContext;
+import org.onap.policy.controlloop.drl.legacy.ControlLoopParams;
+import org.onap.policy.controlloop.eventmanager.ControlLoopEventManager2.NewEventStatus;
+import org.onap.policy.controlloop.eventmanager.ControlLoopOperationManager2.State;
+import org.onap.policy.controlloop.ophistory.OperationHistoryDataManager;
+import org.onap.policy.controlloop.policy.Policy;
+import org.onap.policy.controlloop.policy.PolicyResult;
+import org.onap.policy.controlloop.policy.Target;
+import org.onap.policy.controlloop.policy.TargetType;
+import org.onap.policy.drools.core.lock.LockCallback;
+import org.onap.policy.drools.core.lock.LockImpl;
+import org.onap.policy.drools.core.lock.LockState;
+import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
+import org.onap.policy.models.tosca.authorative.concepts.ToscaServiceTemplate;
+
+public class ControlLoopEventManager2Test {
+    private static final UUID REQ_ID = UUID.randomUUID();
+    private static final String CL_NAME = "my-closed-loop-name";
+    private static final String POLICY_NAME = "my-policy-name";
+    private static final String POLICY_SCOPE = "my-scope";
+    private static final String POLICY_VERSION = "1.2.3";
+    private static final String MY_TARGET = "my-target";
+    private static final String LOCK1 = "my-lock-A";
+    private static final String LOCK2 = "my-lock-B";
+    private static final Coder yamlCoder = new StandardYamlCoder();
+
+    @Mock
+    private WorkingMemory workMem;
+    @Mock
+    private Consumer<OperationOutcome> callback1;
+    @Mock
+    private Consumer<OperationOutcome> callback2;
+    @Mock
+    private Consumer<OperationOutcome> callback3;
+    @Mock
+    private FactHandle factHandle;
+    @Mock
+    private ActorService actors;
+    @Mock
+    private OperationHistoryDataManager dataMgr;
+    @Mock
+    private ControlLoopOperationManager2 oper1;
+    @Mock
+    private ControlLoopOperationManager2 oper2;
+    @Mock
+    private ControlLoopOperationManager2 oper3;
+    @Mock
+    private ExecutorService executor;
+
+    private long preCreateTimeMs;
+    private List<LockImpl> locks;
+    private Target target;
+    private ToscaPolicy tosca;
+    private ControlLoopParams params;
+    private VirtualControlLoopEvent event;
+    private int updateCount;
+    private ControlLoopEventManager2 mgr;
+
+    /**
+     * Sets up.
+     */
+    @Before
+    public void setUp() throws ControlLoopException, CoderException {
+        MockitoAnnotations.initMocks(this);
+
+        when(oper1.getHistory()).thenReturn(makeHistory("A"));
+        when(oper2.getHistory()).thenReturn(makeHistory("B"));
+        when(oper3.getHistory()).thenReturn(makeHistory("C"));
+
+        when(oper1.getActor()).thenReturn("First");
+        when(oper1.getOperation()).thenReturn("OperationA");
+        when(oper1.getOperationMessage()).thenReturn("message-A");
+
+        when(oper2.getActor()).thenReturn("Second");
+        when(oper2.getOperation()).thenReturn("OperationB");
+        when(oper2.getOperationMessage()).thenReturn("message-B");
+
+        when(oper3.getActor()).thenReturn("Third");
+        when(oper3.getOperation()).thenReturn("OperationC");
+        when(oper3.getOperationMessage()).thenReturn("message-C");
+
+        when(workMem.getFactHandle(any())).thenReturn(factHandle);
+
+        event = new VirtualControlLoopEvent();
+        event.setRequestId(REQ_ID);
+        event.setTarget(ControlLoopOperationManager2.VSERVER_VSERVER_NAME);
+        event.setAai(new TreeMap<>(Map.of(ControlLoopOperationManager2.VSERVER_VSERVER_NAME, MY_TARGET)));
+        event.setClosedLoopEventStatus(ControlLoopEventStatus.ONSET);
+        event.setClosedLoopControlName(CL_NAME);
+        event.setTargetType(TargetType.VNF.toString());
+
+        target = new Target();
+        target.setType(TargetType.VNF);
+
+        params = new ControlLoopParams();
+        params.setClosedLoopControlName(CL_NAME);
+        params.setPolicyName(POLICY_NAME);
+        params.setPolicyScope(POLICY_SCOPE);
+        params.setPolicyVersion(POLICY_VERSION);
+
+        loadPolicy("eventManager/event-mgr-simple.yaml");
+
+        locks = new ArrayList<>();
+
+        updateCount = 0;
+
+        preCreateTimeMs = System.currentTimeMillis();
+
+        mgr = new MyManagerWithOper(params, event, workMem);
+    }
+
+    @Test
+    public void testConstructor() {
+        assertEquals(POLICY_NAME, mgr.getPolicyName());
+
+        Map<String, String> orig = event.getAai();
+
+        event.setAai(addAai(orig, ControlLoopEventManager2.VSERVER_IS_CLOSED_LOOP_DISABLED, "true"));
+        assertThatThrownBy(() -> new ControlLoopEventManager2(params, event, workMem))
+                        .hasMessage("is-closed-loop-disabled is set to true on VServer or VNF");
+
+        event.setAai(addAai(orig, ControlLoopEventManager2.VSERVER_PROV_STATUS, "inactive"));
+        assertThatThrownBy(() -> new ControlLoopEventManager2(params, event, workMem))
+                        .hasMessage("prov-status is not ACTIVE on VServer or VNF");
+
+        // valid
+        event.setAai(orig);
+        assertThatCode(() -> mgr.checkEventSyntax(event)).doesNotThrowAnyException();
+
+        // invalid
+        event.setTarget("unknown-target");
+        assertThatThrownBy(() -> new ControlLoopEventManager2(params, event, workMem));
+    }
+
+    /**
+     * Runs through a policy that has several operations.
+     */
+    @Test
+    public void testMultiOperation() throws Exception {
+
+        loadPolicy("eventManager/event-mgr-multi.yaml");
+
+        mgr = new MyManagerWithOper(params, event, workMem);
+        mgr.start();
+
+        for (ControlLoopOperationManager2 oper : Arrays.asList(oper1, oper2, oper3)) {
+            assertTrue(mgr.isActive());
+            nextStep(oper, true, PolicyResult.SUCCESS);
+            runRule();
+
+            assertTrue(mgr.isActive());
+            nextStep(oper, false, PolicyResult.SUCCESS);
+            runRule();
+        }
+
+        assertFalse(mgr.isActive());
+    }
+
+    @Test
+    public void testStart() throws Exception {
+        // start it
+        mgr.start();
+
+        // cannot re-start
+        assertThatCode(() -> mgr.start()).isInstanceOf(IllegalStateException.class)
+                        .hasMessage("manager already started");
+    }
+
+    /**
+     * Tests start() error cases.
+     */
+    @Test
+    public void testStartErrors() throws Exception {
+        // wrong jvm
+        ControlLoopEventManager2 mgr2 = new ControlLoopEventManager2(params, event, workMem);
+        ControlLoopEventManager2 mgr3 = Serializer.roundTrip(mgr2);
+        assertThatCode(() -> mgr3.start()).isInstanceOf(IllegalStateException.class)
+                        .hasMessage("manager is no longer active");
+
+        // no fact handle
+        when(workMem.getFactHandle(any())).thenReturn(null);
+        assertThatCode(() -> mgr.start()).isInstanceOf(IllegalStateException.class)
+                        .hasMessage("manager is not in working memory");
+    }
+
+    @Test
+    public void testNextStep_testStartOperationSuccess() throws ControlLoopException {
+        runOperation(PolicyResult.SUCCESS);
+
+        VirtualControlLoopNotification notif = mgr.getNotification();
+        assertEquals(ControlLoopNotificationType.FINAL_SUCCESS, notif.getNotification());
+        assertNull(notif.getMessage());
+
+        assertThatCode(() -> mgr.nextStep()).doesNotThrowAnyException();
+    }
+
+    /**
+     * Tests nextStep() when the next step is invalid, which should cause an exception to
+     * be thrown by the processor.
+     */
+    @Test
+    public void testNextStepMissing() throws Exception {
+        mgr.start();
+
+        when(oper1.nextStep()).thenThrow(new IllegalArgumentException("expected exception"));
+
+        mgr.nextStep();
+
+        assertFalse(mgr.isActive());
+
+        VirtualControlLoopNotification notif = mgr.getNotification();
+        assertEquals(ControlLoopNotificationType.FINAL_FAILURE, notif.getNotification());
+        assertEquals("Policy processing aborted due to policy error", notif.getMessage());
+        assertTrue(notif.getHistory().isEmpty());
+    }
+
+    /**
+     * Tests startOperation() with FINAL_FAILURE_EXCEPTION.
+     */
+    @Test
+    public void testStartOperationException() throws ControlLoopException {
+        runOperation(PolicyResult.FAILURE_EXCEPTION);
+
+        VirtualControlLoopNotification notif = mgr.getNotification();
+        assertEquals(ControlLoopNotificationType.FINAL_FAILURE, notif.getNotification());
+        assertEquals("Exception in processing closed loop", notif.getMessage());
+    }
+
+    /**
+     * Tests startOperation() with FINAL_FAILURE.
+     */
+    @Test
+    public void testStartOperationFailure() throws ControlLoopException {
+        runOperation(PolicyResult.FAILURE);
+
+        VirtualControlLoopNotification notif = mgr.getNotification();
+        assertEquals(ControlLoopNotificationType.FINAL_FAILURE, notif.getNotification());
+        assertNull(notif.getMessage());
+    }
+
+    /**
+     * Tests startOperation() with FINAL_OPENLOOP.
+     */
+    @Test
+    public void testStartOperationOpenLoop() throws ControlLoopException {
+        runOperation(PolicyResult.FAILURE_GUARD);
+
+        VirtualControlLoopNotification notif = mgr.getNotification();
+        assertEquals(ControlLoopNotificationType.FINAL_OPENLOOP, notif.getNotification());
+        assertNull(notif.getMessage());
+    }
+
+    @Test
+    public void testIsActive() throws Exception {
+        mgr = new ControlLoopEventManager2(params, event, workMem);
+        assertTrue(mgr.isActive());
+
+        ControlLoopEventManager2 mgr2 = Serializer.roundTrip(mgr);
+        assertFalse(mgr2.isActive());
+    }
+
+    @Test
+    public void testUpdated() throws ControlLoopException {
+        mgr.start();
+
+        // not the active operation - should be ignored
+        mgr.updated(oper3);
+        verify(workMem, never()).update(any(), any());
+
+        VirtualControlLoopNotification notif;
+
+        // check notification data
+        when(oper1.getState()).thenReturn(State.LOCK_DENIED);
+        mgr.updated(oper1);
+        notif = mgr.getNotification();
+        assertNotNull(notif.getHistory());
+
+        /*
+         * try the various cases
+         */
+        when(oper1.getState()).thenReturn(State.LOCK_DENIED);
+        mgr.updated(oper1);
+        verifyNotification(ControlLoopNotificationType.REJECTED, "The target my-target is already locked");
+
+        when(oper1.getState()).thenReturn(State.LOCK_LOST);
+        mgr.updated(oper1);
+        verifyNotification(ControlLoopNotificationType.OPERATION_FAILURE, "The target my-target is no longer locked");
+
+        when(oper1.getState()).thenReturn(State.GUARD_STARTED);
+        mgr.updated(oper1);
+        verifyNotification(ControlLoopNotificationType.OPERATION, "Sending guard query for First OperationA");
+
+        when(oper1.getState()).thenReturn(State.GUARD_PERMITTED);
+        mgr.updated(oper1);
+        verifyNotification(ControlLoopNotificationType.OPERATION, "Guard result for First OperationA is Permit");
+
+        when(oper1.getState()).thenReturn(State.GUARD_DENIED);
+        mgr.updated(oper1);
+        verifyNotification(ControlLoopNotificationType.OPERATION, "Guard result for First OperationA is Deny");
+
+        when(oper1.getState()).thenReturn(State.OPERATION_SUCCESS);
+        mgr.updated(oper1);
+        verifyNotification(ControlLoopNotificationType.OPERATION_SUCCESS, "message-A");
+
+        when(oper1.getState()).thenReturn(State.OPERATION_FAILURE);
+        mgr.updated(oper1);
+        verifyNotification(ControlLoopNotificationType.OPERATION_FAILURE, "message-A");
+
+        // should still be active
+        assertTrue(mgr.isActive());
+
+        /*
+         * control loop time
+         */
+        when(oper1.getState()).thenReturn(State.CONTROL_LOOP_TIMEOUT);
+        mgr.updated(oper1);
+        verifyNotification(ControlLoopNotificationType.FINAL_FAILURE, "Control Loop timed out");
+
+        // should now be done
+        assertFalse(mgr.isActive());
+    }
+
+    @Test
+    public void testDestroy() {
+        mgr.requestLock(LOCK1, callback1);
+        mgr.requestLock(LOCK2, callback2);
+        mgr.requestLock(LOCK1, callback3);
+
+        mgr.destroy();
+
+        freeLocks();
+
+        for (LockImpl lock : locks) {
+            assertTrue(lock.isUnavailable());
+        }
+    }
+
+    /**
+     * Tests destroy() once it has been started.
+     */
+    @Test
+    public void testDestroyStarted() throws ControlLoopException {
+        mgr.start();
+
+        mgr.requestLock(LOCK1, callback1);
+        mgr.requestLock(LOCK2, callback2);
+        mgr.requestLock(LOCK1, callback3);
+
+        mgr.destroy();
+
+        freeLocks();
+
+        // should have canceled the operation
+        verify(oper1).cancel();
+
+        for (LockImpl lock : locks) {
+            assertTrue(lock.isUnavailable());
+        }
+    }
+
+    @Test
+    public void testMakeNotification() throws ControlLoopException {
+        mgr.start();
+
+        nextStep(oper1, true, PolicyResult.SUCCESS);
+        runRule();
+
+        // check notification while running
+        VirtualControlLoopNotification notif = mgr.getNotification();
+        assertEquals("message-A", notif.getMessage());
+
+        List<ControlLoopOperation> history = notif.getHistory();
+        assertNotNull(history);
+
+        nextStep(oper1, false, PolicyResult.SUCCESS);
+        runRule();
+
+        assertFalse(mgr.isActive());
+
+        // check notification when complete
+        notif = mgr.getNotification();
+        assertNull(notif.getMessage());
+        assertEquals(history, notif.getHistory());
+    }
+
+    @Test
+    public void testOnNewEvent() {
+        VirtualControlLoopEvent event2 = new VirtualControlLoopEvent(event);
+        assertEquals(NewEventStatus.FIRST_ONSET, mgr.onNewEvent(event2));
+
+        event2.setPayload("other payload");
+        assertEquals(NewEventStatus.SUBSEQUENT_ONSET, mgr.onNewEvent(event2));
+        assertEquals(NewEventStatus.SUBSEQUENT_ONSET, mgr.onNewEvent(event2));
+        assertEquals(NewEventStatus.FIRST_ONSET, mgr.onNewEvent(event));
+
+        event2.setClosedLoopEventStatus(ControlLoopEventStatus.ABATED);
+        assertEquals(NewEventStatus.FIRST_ABATEMENT, mgr.onNewEvent(event2));
+
+        assertEquals(NewEventStatus.SUBSEQUENT_ABATEMENT, mgr.onNewEvent(event2));
+        assertEquals(NewEventStatus.SUBSEQUENT_ABATEMENT, mgr.onNewEvent(event2));
+
+        event2.setClosedLoopEventStatus(null);
+        assertEquals(NewEventStatus.SYNTAX_ERROR, mgr.onNewEvent(event2));
+    }
+
+    @Test
+    public void testDetmControlLoopTimeoutMs() throws Exception {
+        verifyTimeout(1200 * 1000L);
+    }
+
+    private void verifyTimeout(long timeMs) {
+        long end = mgr.getEndTimeMs();
+        assertTrue(end >= preCreateTimeMs + timeMs);
+        assertTrue(end < preCreateTimeMs + timeMs + 5000);
+    }
+
+    @Test
+    public void testCheckEventSyntax() {
+        // initially, it's valid
+        assertThatCode(() -> mgr.checkEventSyntax(event)).doesNotThrowAnyException();
+
+        event.setTarget("unknown-target");
+        assertThatCode(() -> mgr.checkEventSyntax(event)).isInstanceOf(ControlLoopException.class)
+                        .hasMessage("target field invalid");
+
+        event.setTarget(null);
+        assertThatCode(() -> mgr.checkEventSyntax(event)).isInstanceOf(ControlLoopException.class)
+                        .hasMessage("No target field");
+
+        // abated supersedes previous errors - so it shouldn't throw an exception
+        event.setClosedLoopEventStatus(ControlLoopEventStatus.ABATED);
+        assertThatCode(() -> mgr.checkEventSyntax(event)).doesNotThrowAnyException();
+
+        event.setRequestId(null);
+        assertThatCode(() -> mgr.checkEventSyntax(event)).isInstanceOf(ControlLoopException.class)
+                        .hasMessage("No request ID");
+
+        event.setClosedLoopControlName(null);
+        assertThatCode(() -> mgr.checkEventSyntax(event)).isInstanceOf(ControlLoopException.class)
+                        .hasMessage("No control loop name");
+    }
+
+    @Test
+    public void testValidateStatus() {
+        event.setClosedLoopEventStatus(ControlLoopEventStatus.ONSET);
+        assertThatCode(() -> mgr.checkEventSyntax(event)).doesNotThrowAnyException();
+
+        event.setClosedLoopEventStatus(ControlLoopEventStatus.ABATED);
+        assertThatCode(() -> mgr.checkEventSyntax(event)).doesNotThrowAnyException();
+
+        event.setClosedLoopEventStatus(null);
+        assertThatCode(() -> mgr.checkEventSyntax(event)).isInstanceOf(ControlLoopException.class)
+                        .hasMessage("Invalid value in closedLoopEventStatus");
+    }
+
+    @Test
+    public void testValidateAaiData() {
+        event.setTargetType("unknown-target-type");
+        assertThatCode(() -> mgr.checkEventSyntax(event)).isInstanceOf(ControlLoopException.class)
+                        .hasMessage("The target type is not supported");
+
+        event.setTargetType(null);
+        assertThatCode(() -> mgr.checkEventSyntax(event)).isInstanceOf(ControlLoopException.class)
+                        .hasMessage("The Target type is null");
+
+        event.setAai(null);
+        assertThatCode(() -> mgr.checkEventSyntax(event)).isInstanceOf(ControlLoopException.class)
+                        .hasMessage("AAI is null");
+
+        // VM case
+        event.setTargetType(ControlLoopTargetType.VM);
+        event.setAai(Map.of(ControlLoopEventManager2.GENERIC_VNF_VNF_ID, MY_TARGET));
+        assertThatCode(() -> mgr.checkEventSyntax(event)).doesNotThrowAnyException();
+
+        event.setAai(Map.of());
+        assertThatCode(() -> mgr.checkEventSyntax(event)).isInstanceOf(ControlLoopException.class);
+
+        // VNF case
+        event.setTargetType(ControlLoopTargetType.VNF);
+        event.setAai(Map.of(ControlLoopEventManager2.GENERIC_VNF_VNF_ID, MY_TARGET));
+        assertThatCode(() -> mgr.checkEventSyntax(event)).doesNotThrowAnyException();
+
+        event.setAai(Map.of());
+        assertThatCode(() -> mgr.checkEventSyntax(event)).isInstanceOf(ControlLoopException.class);
+
+        // PNF case
+        event.setTargetType(ControlLoopTargetType.PNF);
+        event.setAai(Map.of(ControlLoopEventManager2.PNF_NAME, MY_TARGET));
+        assertThatCode(() -> mgr.checkEventSyntax(event)).doesNotThrowAnyException();
+
+        event.setAai(Map.of());
+        assertThatCode(() -> mgr.checkEventSyntax(event)).isInstanceOf(ControlLoopException.class);
+    }
+
+    @Test
+    public void testValidateAaiVmVnfData() {
+        event.setTargetType(ControlLoopTargetType.VM);
+        event.setAai(Map.of(ControlLoopEventManager2.GENERIC_VNF_VNF_ID, MY_TARGET));
+        assertThatCode(() -> mgr.checkEventSyntax(event)).doesNotThrowAnyException();
+
+        event.setAai(Map.of(ControlLoopEventManager2.VSERVER_VSERVER_NAME, MY_TARGET));
+        assertThatCode(() -> mgr.checkEventSyntax(event)).doesNotThrowAnyException();
+
+        event.setAai(Map.of(ControlLoopEventManager2.GENERIC_VNF_VNF_NAME, MY_TARGET));
+        assertThatCode(() -> mgr.checkEventSyntax(event)).doesNotThrowAnyException();
+
+        event.setAai(Map.of());
+        assertThatCode(() -> mgr.checkEventSyntax(event)).isInstanceOf(ControlLoopException.class).hasMessage(
+                        "generic-vnf.vnf-id or generic-vnf.vnf-name or vserver.vserver-name information missing");
+    }
+
+    @Test
+    public void testValidateAaiPnfData() {
+        event.setTargetType(ControlLoopTargetType.PNF);
+        event.setAai(Map.of(ControlLoopEventManager2.PNF_NAME, MY_TARGET));
+        assertThatCode(() -> mgr.checkEventSyntax(event)).doesNotThrowAnyException();
+
+        event.setAai(Map.of());
+        assertThatCode(() -> mgr.checkEventSyntax(event)).isInstanceOf(ControlLoopException.class)
+                        .hasMessage("AAI PNF object key pnf-name is missing");
+    }
+
+    @Test
+    public void testIsClosedLoopDisabled() {
+        Map<String, String> orig = event.getAai();
+
+        event.setAai(addAai(orig, ControlLoopEventManager2.VSERVER_IS_CLOSED_LOOP_DISABLED, "true"));
+        assertThatThrownBy(() -> new ControlLoopEventManager2(params, event, workMem));
+
+        event.setAai(addAai(orig, ControlLoopEventManager2.GENERIC_VNF_IS_CLOSED_LOOP_DISABLED, "true"));
+        assertThatThrownBy(() -> new ControlLoopEventManager2(params, event, workMem));
+
+        event.setAai(addAai(orig, ControlLoopEventManager2.PNF_IS_IN_MAINT, "true"));
+        assertThatThrownBy(() -> new ControlLoopEventManager2(params, event, workMem));
+    }
+
+    private Map<String, String> addAai(Map<String, String> original, String key, String value) {
+        Map<String, String> map = new TreeMap<>(original);
+        map.put(key, value);
+        return map;
+    }
+
+    @Test
+    public void testIsProvStatusInactive() {
+        Map<String, String> orig = event.getAai();
+
+        event.setAai(addAai(orig, ControlLoopEventManager2.VSERVER_PROV_STATUS, "ACTIVE"));
+        assertThatCode(() -> new ControlLoopEventManager2(params, event, workMem)).doesNotThrowAnyException();
+
+        event.setAai(addAai(orig, ControlLoopEventManager2.VSERVER_PROV_STATUS, "inactive"));
+        assertThatThrownBy(() -> new ControlLoopEventManager2(params, event, workMem));
+
+        event.setAai(addAai(orig, ControlLoopEventManager2.GENERIC_VNF_PROV_STATUS, "ACTIVE"));
+        assertThatCode(() -> new ControlLoopEventManager2(params, event, workMem)).doesNotThrowAnyException();
+
+        event.setAai(addAai(orig, ControlLoopEventManager2.GENERIC_VNF_PROV_STATUS, "inactive"));
+        assertThatThrownBy(() -> new ControlLoopEventManager2(params, event, workMem));
+    }
+
+    @Test
+    public void testIsAaiTrue() {
+        Map<String, String> orig = event.getAai();
+
+        for (String value : Arrays.asList("yes", "y", "true", "t", "yEs", "trUe")) {
+            event.setAai(addAai(orig, ControlLoopEventManager2.VSERVER_IS_CLOSED_LOOP_DISABLED, value));
+            assertThatThrownBy(() -> new ControlLoopEventManager2(params, event, workMem));
+        }
+
+        event.setAai(addAai(orig, ControlLoopEventManager2.VSERVER_IS_CLOSED_LOOP_DISABLED, "false"));
+        assertThatCode(() -> new ControlLoopEventManager2(params, event, workMem)).doesNotThrowAnyException();
+
+        event.setAai(addAai(orig, ControlLoopEventManager2.VSERVER_IS_CLOSED_LOOP_DISABLED, "no"));
+        assertThatCode(() -> new ControlLoopEventManager2(params, event, workMem)).doesNotThrowAnyException();
+    }
+
+    @Test
+    public void testRequestLock() {
+        final CompletableFuture<OperationOutcome> future1 = mgr.requestLock(LOCK1, callback1);
+        final CompletableFuture<OperationOutcome> future2 = mgr.requestLock(LOCK2, callback2);
+        assertSame(future1, mgr.requestLock(LOCK1, callback3));
+
+        assertEquals(2, locks.size());
+
+        assertTrue(future1.isDone());
+        assertTrue(future2.isDone());
+
+        verify(callback1, never()).accept(any());
+        verify(callback2, never()).accept(any());
+        verify(callback3, never()).accept(any());
+
+        // indicate that the first lock failed
+        locks.get(0).notifyUnavailable();
+
+        verify(callback1).accept(any());
+        verify(callback2, never()).accept(any());
+        verify(callback3).accept(any());
+    }
+
+    @Test
+    public void testMakeOperationManager() throws ControlLoopException {
+        // use a manager that creates real operation managers
+        mgr = new MyManager(params, event, workMem);
+
+        assertThatCode(() -> mgr.start()).doesNotThrowAnyException();
+    }
+
+    @Test
+    public void testGetBlockingExecutor() throws Exception {
+        mgr = new ControlLoopEventManager2(params, event, workMem);
+        assertThatCode(() -> mgr.getBlockingExecutor()).doesNotThrowAnyException();
+    }
+
+    @Test
+    public void testToString() {
+        assertNotNull(mgr.toString());
+    }
+
+
+    private void nextStep(ControlLoopOperationManager2 oper, boolean moreSteps, PolicyResult result) {
+        when(oper.nextStep()).thenReturn(moreSteps);
+        when(oper.getOperationResult()).thenReturn(result);
+
+        if (result == PolicyResult.SUCCESS) {
+            when(oper.getState()).thenReturn(State.OPERATION_SUCCESS);
+        } else {
+            when(oper.getState()).thenReturn(State.OPERATION_FAILURE);
+        }
+
+        mgr.updated(oper);
+
+        updateCount++;
+
+        verify(workMem, times(updateCount)).update(factHandle, mgr);
+    }
+
+    private void runRule() {
+        assertTrue(mgr.isActive());
+        mgr.nextStep();
+    }
+
+    private void runOperation(PolicyResult finalResult) throws ControlLoopException {
+        mgr.start();
+        verify(oper1).start(anyLong());
+
+        assertTrue(mgr.isActive());
+
+        nextStep(oper1, true, PolicyResult.SUCCESS);
+        runRule();
+
+        nextStep(oper1, false, finalResult);
+        runRule();
+
+        assertFalse(mgr.isActive());
+
+        // should have no effect, because it's done
+        mgr.updated(oper1);
+        verify(workMem, times(updateCount)).update(any(), any());
+    }
+
+    private void verifyNotification(ControlLoopNotificationType expectedType, String expectedMsg) {
+        VirtualControlLoopNotification notif = mgr.getNotification();
+        assertEquals(expectedType, notif.getNotification());
+        assertEquals(expectedMsg, notif.getMessage());
+    }
+
+    private List<ControlLoopOperation> makeHistory(String message) {
+        ControlLoopOperation clo = new ControlLoopOperation();
+        clo.setMessage("history-" + message);
+
+        return List.of(clo);
+    }
+
+    private void loadPolicy(String fileName) throws CoderException {
+        ToscaServiceTemplate template =
+                        yamlCoder.decode(ResourceUtils.getResourceAsString(fileName), ToscaServiceTemplate.class);
+        tosca = template.getToscaTopologyTemplate().getPolicies().get(0).values().iterator().next();
+
+        params.setToscaPolicy(tosca);
+    }
+
+    private void freeLocks() {
+        ArgumentCaptor<Runnable> runCaptor = ArgumentCaptor.forClass(Runnable.class);
+        verify(executor).execute(runCaptor.capture());
+
+        runCaptor.getValue().run();
+    }
+
+
+    private class MyManager extends ControlLoopEventManager2 {
+        private static final long serialVersionUID = 1L;
+
+        public MyManager(ControlLoopParams params, VirtualControlLoopEvent event, WorkingMemory workMem)
+                        throws ControlLoopException {
+
+            super(params, event, workMem);
+        }
+
+        @Override
+        protected ExecutorService getBlockingExecutor() {
+            return executor;
+        }
+
+        @Override
+        protected void makeLock(String targetEntity, String requestId, int holdSec, LockCallback callback) {
+            LockImpl lock = new LockImpl(LockState.ACTIVE, targetEntity, requestId, holdSec, callback);
+            locks.add(lock);
+            callback.lockAvailable(lock);
+        }
+
+        @Override
+        public ActorService getActorService() {
+            return actors;
+        }
+
+        @Override
+        public OperationHistoryDataManager getDataManager() {
+            return dataMgr;
+        }
+    }
+
+
+    private class MyManagerWithOper extends MyManager {
+        private static final long serialVersionUID = 1L;
+
+        public MyManagerWithOper(ControlLoopParams params, VirtualControlLoopEvent event, WorkingMemory workMem)
+                        throws ControlLoopException {
+
+            super(params, event, workMem);
+        }
+
+        @Override
+        protected ControlLoopOperationManager2 makeOperationManager(ControlLoopEventContext ctx, Policy policy) {
+            switch (policy.getActor()) {
+                case "First":
+                    return oper1;
+                case "Second":
+                    return oper2;
+                case "Third":
+                    return oper3;
+                default:
+                    throw new IllegalArgumentException("unknown policy actor " + policy.getActor());
+            }
+        }
+    }
+}
diff --git a/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/eventmanager/ControlLoopOperationManager2Test.java b/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/eventmanager/ControlLoopOperationManager2Test.java
new file mode 100644
index 0000000..a14cc17
--- /dev/null
+++ b/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/eventmanager/ControlLoopOperationManager2Test.java
@@ -0,0 +1,936 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.eventmanager;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+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.time.Instant;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.onap.aai.domain.yang.GenericVnf;
+import org.onap.policy.aai.AaiCqResponse;
+import org.onap.policy.common.utils.time.PseudoExecutor;
+import org.onap.policy.controlloop.ControlLoopOperation;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+import org.onap.policy.controlloop.actor.guard.GuardActorServiceProvider;
+import org.onap.policy.controlloop.actor.guard.GuardOperation;
+import org.onap.policy.controlloop.actorserviceprovider.ActorService;
+import org.onap.policy.controlloop.actorserviceprovider.Operation;
+import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome;
+import org.onap.policy.controlloop.actorserviceprovider.Operator;
+import org.onap.policy.controlloop.actorserviceprovider.controlloop.ControlLoopEventContext;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams;
+import org.onap.policy.controlloop.actorserviceprovider.spi.Actor;
+import org.onap.policy.controlloop.ophistory.OperationHistoryDataManager;
+import org.onap.policy.controlloop.policy.Policy;
+import org.onap.policy.controlloop.policy.PolicyResult;
+import org.onap.policy.controlloop.policy.Target;
+import org.onap.policy.controlloop.policy.TargetType;
+
+public class ControlLoopOperationManager2Test {
+    private static final UUID REQ_ID = UUID.randomUUID();
+    private static final String MISMATCH = "mismatch";
+    private static final String POLICY_ID = "my-policy";
+    private static final String POLICY_ACTOR = "my-actor";
+    private static final String POLICY_OPERATION = "my-operation";
+    private static final String OTHER_ACTOR = "another-actor";
+    private static final String MY_TARGET = "my-target";
+    private static final String MY_VNF_ID = "my-vnf-id";
+    private static final String PAYLOAD_KEY = "payload-key";
+    private static final String PAYLOAD_VALUE = "payload-value";
+    private static final long REMAINING_MS = 5000;
+    private static final int MAX_RUN = 100;
+    private static final Integer POLICY_RETRY = 3;
+    private static final Integer POLICY_TIMEOUT = 20;
+    private static final IllegalArgumentException EXPECTED_EXCEPTION =
+                    new IllegalArgumentException("expected exception");
+
+    @Captor
+    private ArgumentCaptor<Consumer<OperationOutcome>> lockCallback;
+
+    @Mock
+    private OperationHistoryDataManager dataMgr;
+    @Mock
+    private ManagerContext mgrctx;
+    @Mock
+    private Operator policyOperator;
+    @Mock
+    private Operation policyOperation;
+    @Mock
+    private Actor policyActor;
+    @Mock
+    private ActorService actors;
+    @Mock
+    private AaiCqResponse cqdata;
+    @Mock
+    private GenericVnf vnf;
+
+    private CompletableFuture<OperationOutcome> lockFuture;
+    private CompletableFuture<OperationOutcome> policyFuture;
+    private Target target;
+    private Map<String, String> payload;
+    private Policy policy;
+    private VirtualControlLoopEvent event;
+    private ControlLoopEventContext context;
+    private PseudoExecutor executor;
+    private ControlLoopOperationManager2 mgr;
+
+    /**
+     * Sets up.
+     */
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        lockFuture = new CompletableFuture<>();
+        policyFuture = new CompletableFuture<>();
+
+        when(mgrctx.getActorService()).thenReturn(actors);
+        when(mgrctx.getDataManager()).thenReturn(dataMgr);
+        when(mgrctx.requestLock(any(), any())).thenReturn(lockFuture);
+
+        // configure policy operation
+        when(actors.getActor(POLICY_ACTOR)).thenReturn(policyActor);
+        when(policyActor.getOperator(POLICY_OPERATION)).thenReturn(policyOperator);
+        when(policyOperator.buildOperation(any())).thenReturn(policyOperation);
+        when(policyOperation.start()).thenReturn(policyFuture);
+
+        when(vnf.getVnfId()).thenReturn(MY_VNF_ID);
+        when(cqdata.getDefaultGenericVnf()).thenReturn(vnf);
+
+        target = new Target();
+        target.setType(TargetType.VM);
+
+        payload = Map.of(PAYLOAD_KEY, PAYLOAD_VALUE);
+
+        policy = new Policy();
+        policy.setId(POLICY_ID);
+        policy.setActor(POLICY_ACTOR);
+        policy.setRecipe(POLICY_OPERATION);
+        policy.setTarget(target);
+        policy.setPayload(payload);
+        policy.setRetry(POLICY_RETRY);
+        policy.setTimeout(POLICY_TIMEOUT);
+
+        event = new VirtualControlLoopEvent();
+        event.setRequestId(REQ_ID);
+        event.setTarget(ControlLoopOperationManager2.VSERVER_VSERVER_NAME);
+        event.setAai(new TreeMap<>(Map.of(ControlLoopOperationManager2.VSERVER_VSERVER_NAME, MY_TARGET)));
+
+        context = new ControlLoopEventContext(event);
+        context.setProperty(AaiCqResponse.CONTEXT_KEY, cqdata);
+
+        executor = new PseudoExecutor();
+
+        mgr = new ControlLoopOperationManager2(mgrctx, context, policy, executor);
+    }
+
+    @Test
+    public void testStart() {
+        mgr.start(REMAINING_MS);
+
+        // should have determined the target entity by now
+        assertEquals(MY_TARGET, mgr.getTargetEntity());
+
+        verify(mgrctx).requestLock(eq(MY_TARGET), any());
+
+        lockFuture.complete(new OperationOutcome());
+        genGuardOutcome();
+        policyFuture.complete(genOpOutcome());
+        runToCompletion();
+
+        assertEquals(ControlLoopOperationManager2.State.GUARD_STARTED, mgr.getState());
+
+        assertTrue(mgr.nextStep());
+        assertEquals(ControlLoopOperationManager2.State.GUARD_PERMITTED, mgr.getState());
+
+        assertTrue(mgr.nextStep());
+        assertEquals(ControlLoopOperationManager2.State.OPERATION_SUCCESS, mgr.getState());
+
+        assertFalse(mgr.nextStep());
+
+        OperationOutcome outcome = mgr.getOutcomes().peek();
+        assertEquals(PolicyResult.SUCCESS, outcome.getResult());
+        assertTrue(outcome.isFinalOutcome());
+
+        verify(mgrctx, times(3)).updated(mgr);
+    }
+
+    /**
+     * Tests start() when detmTarget() (i.e., the first task) throws an exception.
+     */
+    @Test
+    public void testStartDetmTargetException() {
+        policy.setTarget(null);
+        mgr.start(REMAINING_MS);
+
+        runToCompletion();
+
+        assertFalse(mgr.nextStep());
+        assertEquals(ControlLoopOperationManager2.State.OPERATION_FAILURE, mgr.getState());
+
+        // should have called update() for operation-start, but not for any nextStep()
+        verify(mgrctx).updated(mgr);
+    }
+
+    /**
+     * Tests start() when a subsequent task throws an exception.
+     */
+    @Test
+    public void testStartException() {
+        when(policyOperation.start()).thenThrow(EXPECTED_EXCEPTION);
+
+        mgr.start(REMAINING_MS);
+
+        lockFuture.complete(new OperationOutcome());
+        runToCompletion();
+
+        assertFalse(mgr.nextStep());
+        assertEquals(ControlLoopOperationManager2.State.OPERATION_FAILURE, mgr.getState());
+
+        // should have called update() for operation-start, but not for any nextStep()
+        verify(mgrctx).updated(mgr);
+    }
+
+    /**
+     * Tests start() when the control loop times out.
+     */
+    @Test
+    public void testStartClTimeout_testHandleTimeout() throws InterruptedException {
+        // catch the callback when it times out
+        CountDownLatch updatedLatch = new CountDownLatch(1);
+        doAnswer(args -> {
+            updatedLatch.countDown();
+            return null;
+        }).when(mgrctx).updated(any());
+
+        long tstart = System.currentTimeMillis();
+
+        // give it a short timeout
+        mgr.start(100);
+
+        assertTrue(updatedLatch.await(5, TimeUnit.SECONDS));
+        assertTrue(System.currentTimeMillis() - tstart >= 100);
+
+        // don't generate any responses
+        runToCompletion();
+
+        // wait for the future to be canceled, via a background thread
+        CountDownLatch futureLatch = new CountDownLatch(1);
+        mgr.getFuture().whenComplete((unused, thrown) -> futureLatch.countDown());
+        assertTrue(futureLatch.await(5, TimeUnit.SECONDS));
+
+        // lock should have been canceled
+        assertTrue(mgr.getFuture().isCancelled());
+
+        assertFalse(mgr.nextStep());
+        assertEquals(ControlLoopOperationManager2.State.CONTROL_LOOP_TIMEOUT, mgr.getState());
+
+        // should have called update() for operation-start, but not for any nextStep()
+        verify(mgrctx).updated(mgr);
+
+        // should not have tried to store anything in the DB
+        verify(dataMgr, never()).store(any(), any(), any());
+    }
+
+    @Test
+    public void testStartOperation() {
+        mgr.start(REMAINING_MS);
+
+        lockFuture.complete(new OperationOutcome());
+        genGuardOutcome();
+        runToCompletion();
+
+        verify(policyOperation).start();
+
+        ArgumentCaptor<ControlLoopOperationParams> captor = ArgumentCaptor.forClass(ControlLoopOperationParams.class);
+        verify(policyOperator).buildOperation(captor.capture());
+
+        ControlLoopOperationParams params = captor.getValue();
+
+        assertNotNull(params);
+        assertEquals(POLICY_ACTOR, params.getActor());
+        assertSame(actors, params.getActorService());
+        assertNotNull(params.getCompleteCallback());
+        assertSame(context, params.getContext());
+        assertSame(executor, params.getExecutor());
+        assertEquals(POLICY_OPERATION, params.getOperation());
+        assertEquals(payload, params.getPayload());
+        assertSame(REQ_ID, params.getRequestId());
+        assertSame(POLICY_RETRY, params.getRetry());
+        assertNotNull(params.getStartCallback());
+        assertSame(target, params.getTarget());
+        assertEquals(MY_TARGET, params.getTargetEntity());
+        assertSame(POLICY_TIMEOUT, params.getTimeoutSec());
+    }
+
+    @Test
+    public void testStartOperationNullPayload() {
+        policy.setPayload(null);
+        mgr.start(REMAINING_MS);
+
+        lockFuture.complete(new OperationOutcome());
+        genGuardOutcome();
+        runToCompletion();
+
+        verify(policyOperation).start();
+
+        ArgumentCaptor<ControlLoopOperationParams> captor = ArgumentCaptor.forClass(ControlLoopOperationParams.class);
+        verify(policyOperator).buildOperation(captor.capture());
+
+        ControlLoopOperationParams params = captor.getValue();
+
+        assertNotNull(params);
+        assertEquals(POLICY_ACTOR, params.getActor());
+        assertSame(actors, params.getActorService());
+        assertNotNull(params.getCompleteCallback());
+        assertSame(context, params.getContext());
+        assertSame(executor, params.getExecutor());
+        assertEquals(POLICY_OPERATION, params.getOperation());
+        assertTrue(params.getPayload().isEmpty());
+        assertSame(REQ_ID, params.getRequestId());
+        assertSame(POLICY_RETRY, params.getRetry());
+        assertNotNull(params.getStartCallback());
+        assertSame(target, params.getTarget());
+        assertEquals(MY_TARGET, params.getTargetEntity());
+        assertSame(POLICY_TIMEOUT, params.getTimeoutSec());
+    }
+
+    @Test
+    public void testGetOperationMessage() {
+        // no history yet
+        assertNull(mgr.getOperationMessage());
+
+        runCyle();
+        assertThat(mgr.getOperationMessage()).contains("actor=my-actor").contains("operation=my-operation");
+    }
+
+    @Test
+    public void testGetOperationResult() {
+        // no history yet
+        assertNotNull(mgr.getOperationResult());
+
+        runCyle();
+        assertEquals(PolicyResult.SUCCESS, mgr.getOperationResult());
+    }
+
+    /**
+     * Tests getOperationResult() when it ends in a failure.
+     */
+    @Test
+    public void testGetOperationResultFailure() {
+        mgr.start(REMAINING_MS);
+
+        genLockFailure();
+        runToCompletion();
+
+        assertEquals(PolicyResult.FAILURE_GUARD, mgr.getOperationResult());
+    }
+
+    /**
+     * Tests handleException() when the exception is a "cancel".
+     */
+    @Test
+    public void testHandleExceptionCanceled() {
+        lockFuture.cancel(false);
+
+        mgr.start(REMAINING_MS);
+
+        runToCompletion();
+
+        assertTrue(mgr.nextStep());
+        assertEquals(ControlLoopOperationManager2.State.ACTIVE, mgr.getState());
+    }
+
+    @Test
+    public void testCancel() {
+        mgr.start(REMAINING_MS);
+
+        mgr.cancel();
+        assertTrue(mgr.getFuture().isCancelled());
+    }
+
+    /**
+     * Tests cancel() when the operation hasn't been started.
+     */
+    @Test
+    public void testCancelNotStarted() {
+        assertNull(mgr.getFuture());
+
+        mgr.cancel();
+        assertNull(mgr.getFuture());
+    }
+
+    @Test
+    public void testLockUnavailable() {
+        mgr.start(REMAINING_MS);
+
+        runToCompletion();
+
+        // lock failure outcome
+        final OperationOutcome outcome = genLockFailure();
+
+        runToCompletion();
+
+        assertFalse(mgr.nextStep());
+        assertEquals(ControlLoopOperationManager2.State.LOCK_DENIED, mgr.getState());
+
+        assertEquals(outcome, mgr.getOutcomes().peek());
+
+        // should have called update() for operation-start, but not for any nextStep()
+        verify(mgrctx).updated(mgr);
+    }
+
+    /**
+     * Tests onStart() and onComplete() with other actors.
+     */
+    @Test
+    public void testOnStart_testOnComplete() {
+        mgr.start(REMAINING_MS);
+
+        lockFuture.complete(new OperationOutcome());
+        genGuardOutcome();
+
+        // generate failure outcome for ANOTHER actor - should be ignored
+        OperationOutcome outcome = mgr.getParams().makeOutcome();
+        outcome.setActor(OTHER_ACTOR);
+        outcome.setResult(PolicyResult.FAILURE);
+        outcome.setStart(Instant.now());
+        mgr.getParams().callbackStarted(new OperationOutcome(outcome));
+
+        outcome.setEnd(Instant.now());
+        mgr.getParams().callbackCompleted(outcome);
+
+        policyFuture.complete(genOpOutcome());
+        runToCompletion();
+
+        // should not include the other actor's outcome
+        assertEquals(ControlLoopOperationManager2.State.GUARD_STARTED, mgr.getState());
+
+        assertTrue(mgr.nextStep());
+        assertEquals(ControlLoopOperationManager2.State.GUARD_PERMITTED, mgr.getState());
+
+        assertTrue(mgr.nextStep());
+        assertEquals(ControlLoopOperationManager2.State.OPERATION_SUCCESS, mgr.getState());
+
+        assertFalse(mgr.nextStep());
+
+        assertEquals(PolicyResult.SUCCESS, mgr.getOutcomes().peek().getResult());
+
+        verify(mgrctx, times(3)).updated(mgr);
+    }
+
+    @Test
+    public void testNextStep() {
+        mgr.start(REMAINING_MS);
+
+        // only do the lock and the guard
+        lockFuture.complete(new OperationOutcome());
+        genGuardOutcome();
+        runToCompletion();
+
+        assertEquals(ControlLoopOperationManager2.State.GUARD_STARTED, mgr.getState());
+
+        assertTrue(mgr.nextStep());
+        assertEquals(ControlLoopOperationManager2.State.GUARD_PERMITTED, mgr.getState());
+
+        assertTrue(mgr.nextStep());
+        assertTrue(mgr.nextStep());
+
+        verify(mgrctx, times(2)).updated(mgr);
+    }
+
+    /**
+     * Tests processOutcome() when the lock is denied.
+     */
+    @Test
+    public void testProcessOutcomeLockDenied() {
+        mgr.start(REMAINING_MS);
+
+        // unavailable from the start => "denied"
+        genLockFailure();
+
+        runToCompletion();
+
+        assertEquals(ControlLoopOperationManager2.State.LOCK_DENIED, mgr.getState());
+
+        assertFalse(mgr.nextStep());
+        verify(mgrctx).updated(mgr);
+
+        verifyDb(1, PolicyResult.FAILURE_GUARD, "Operation denied by Lock");
+    }
+
+    /**
+     * Tests processOutcome() when the lock is lost.
+     */
+    @Test
+    public void testProcessOutcomeLockLost() {
+        mgr.start(REMAINING_MS);
+
+        // indicate lock success initially
+        lockFuture.complete(new OperationOutcome());
+
+        // do the guard
+        genGuardOutcome();
+
+        // now generate a lock failure => "lost"
+        genLockFailure();
+
+        runToCompletion();
+
+        assertEquals(ControlLoopOperationManager2.State.GUARD_STARTED, mgr.getState());
+
+        assertTrue(mgr.nextStep());
+        assertEquals(ControlLoopOperationManager2.State.GUARD_PERMITTED, mgr.getState());
+
+        assertTrue(mgr.nextStep());
+        assertEquals(ControlLoopOperationManager2.State.LOCK_LOST, mgr.getState());
+
+        assertFalse(mgr.nextStep());
+        verify(mgrctx, times(3)).updated(mgr);
+
+        verifyDb(1, PolicyResult.FAILURE, "Operation aborted by Lock");
+    }
+
+    /**
+     * Tests processOutcome() when the guard is permitted.
+     */
+    @Test
+    public void testProcessOutcomeGuardPermit() {
+        mgr.start(REMAINING_MS);
+
+        lockFuture.complete(new OperationOutcome());
+        genGuardOutcome();
+
+        runToCompletion();
+
+        assertEquals(ControlLoopOperationManager2.State.GUARD_STARTED, mgr.getState());
+
+        assertTrue(mgr.nextStep());
+        assertEquals(ControlLoopOperationManager2.State.GUARD_PERMITTED, mgr.getState());
+
+        assertTrue(mgr.nextStep());
+        verify(mgrctx, times(2)).updated(mgr);
+
+        verify(dataMgr, never()).store(any(), any(), any());
+    }
+
+    /**
+     * Tests processOutcome() when the guard is permitted.
+     */
+    @Test
+    public void testProcessOutcomeGuardDenied() {
+        mgr.start(REMAINING_MS);
+
+        lockFuture.complete(new OperationOutcome());
+        genGuardOutcome(false);
+
+        runToCompletion();
+
+        assertEquals(ControlLoopOperationManager2.State.GUARD_STARTED, mgr.getState());
+
+        assertTrue(mgr.nextStep());
+        assertEquals(ControlLoopOperationManager2.State.GUARD_DENIED, mgr.getState());
+
+        assertFalse(mgr.nextStep());
+        verify(mgrctx, times(2)).updated(mgr);
+
+        verifyDb(1, PolicyResult.FAILURE_GUARD, "Operation denied by Guard");
+    }
+
+    /**
+     * Tests processOutcome() when the operation is a success.
+     */
+    @Test
+    public void testProcessOutcomeOperSuccess() {
+        mgr.start(REMAINING_MS);
+
+        lockFuture.complete(new OperationOutcome());
+        genGuardOutcome();
+        genOpOutcome();
+
+        runToCompletion();
+
+        assertEquals(ControlLoopOperationManager2.State.GUARD_STARTED, mgr.getState());
+
+        assertTrue(mgr.nextStep());
+        assertEquals(ControlLoopOperationManager2.State.GUARD_PERMITTED, mgr.getState());
+
+        assertTrue(mgr.nextStep());
+        assertEquals(ControlLoopOperationManager2.State.OPERATION_SUCCESS, mgr.getState());
+
+        assertFalse(mgr.nextStep());
+        verify(mgrctx, times(3)).updated(mgr);
+
+        verifyDb(1, PolicyResult.SUCCESS, null);
+    }
+
+    /**
+     * Tests processOutcome() when the operation is a failure.
+     */
+    @Test
+    public void testProcessOutcomeOperFailure() {
+        mgr.start(REMAINING_MS);
+
+        lockFuture.complete(new OperationOutcome());
+        genGuardOutcome();
+        genOpOutcome(false);
+
+        runToCompletion();
+
+        assertEquals(ControlLoopOperationManager2.State.GUARD_STARTED, mgr.getState());
+
+        assertTrue(mgr.nextStep());
+        assertEquals(ControlLoopOperationManager2.State.GUARD_PERMITTED, mgr.getState());
+
+        assertTrue(mgr.nextStep());
+        assertEquals(ControlLoopOperationManager2.State.OPERATION_FAILURE, mgr.getState());
+        verifyDb(1, PolicyResult.FAILURE, null);
+
+        assertThat(mgr.toString()).contains("attempts=1");
+
+        // next failure
+        genOpOutcome(false);
+        runToCompletion();
+
+        assertTrue(mgr.nextStep());
+        assertEquals(ControlLoopOperationManager2.State.OPERATION_FAILURE, mgr.getState());
+        verifyDb(2, PolicyResult.FAILURE, null);
+
+        assertThat(mgr.toString()).contains("attempts=2");
+
+        // and finally a success
+        genOpOutcome();
+
+        assertTrue(mgr.nextStep());
+        assertEquals(ControlLoopOperationManager2.State.OPERATION_SUCCESS, mgr.getState());
+        verifyDb(3, PolicyResult.SUCCESS, null);
+
+        assertThat(mgr.toString()).contains("attempts=3");
+
+        assertFalse(mgr.nextStep());
+        verify(mgrctx, times(5)).updated(mgr);
+    }
+
+    @Test
+    public void testGetOperationHistory() {
+        // no history yet
+        assertNull(mgr.getOperationHistory());
+
+        runCyle();
+        assertThat(mgr.getOperationHistory()).contains("actor=my-actor").contains("operation=my-operation")
+                        .contains("outcome=Success");
+    }
+
+    @Test
+    public void testGetHistory() {
+        // no history yet
+        assertEquals(0, mgr.getHistory().size());
+
+        runCyle();
+        assertEquals(1, mgr.getHistory().size());
+    }
+
+    @Test
+    public void testDetmTargetVm() {
+        target.setType(TargetType.VM);
+        assertNull(mgr.detmTarget());
+        assertEquals(MY_TARGET, mgr.getTargetEntity());
+
+        target.setType(TargetType.VNF);
+        assertNull(mgr.detmTarget());
+        assertEquals(MY_TARGET, mgr.getTargetEntity());
+
+        target.setType(TargetType.VFMODULE);
+        assertNull(mgr.detmTarget());
+        assertEquals(MY_TARGET, mgr.getTargetEntity());
+
+        // unsupported type
+        target.setType(TargetType.VFC);
+        assertThatIllegalArgumentException().isThrownBy(() -> mgr.detmTarget())
+                        .withMessage("The target type is not supported");
+
+        // null type
+        target.setType(null);
+        assertThatIllegalArgumentException().isThrownBy(() -> mgr.detmTarget()).withMessage("The target type is null");
+
+        // null target
+        policy.setTarget(null);
+        assertThatIllegalArgumentException().isThrownBy(() -> mgr.detmTarget()).withMessage("The target is null");
+    }
+
+    @Test
+    public void testDetmPnfTarget() {
+        setTargetPnf();
+        assertNull(mgr.detmTarget());
+        assertEquals(MY_TARGET, mgr.getTargetEntity());
+
+        // missing enrichment data
+        event.getAai().clear();
+        assertThatIllegalArgumentException().isThrownBy(() -> mgr.detmTarget())
+                        .withMessage("AAI section is missing " + ControlLoopOperationManager2.PNF_NAME);
+
+        // wrong target
+        event.setTarget(MISMATCH);
+        assertThatIllegalArgumentException().isThrownBy(() -> mgr.detmTarget())
+                        .withMessage("Target does not match target type");
+    }
+
+    @Test
+    public void testDetmVfModuleTarget() {
+        // vserver
+        event.setTarget(ControlLoopOperationManager2.VSERVER_VSERVER_NAME);
+        event.getAai().clear();
+        event.getAai().putAll(Map.of(ControlLoopOperationManager2.VSERVER_VSERVER_NAME, MY_TARGET));
+        assertNull(mgr.detmTarget());
+        assertEquals(MY_TARGET, mgr.getTargetEntity());
+
+        // vnf-id
+        event.setTarget(ControlLoopOperationManager2.GENERIC_VNF_VNF_ID);
+        event.getAai().clear();
+        event.getAai().putAll(Map.of(ControlLoopOperationManager2.GENERIC_VNF_VNF_ID, MY_TARGET));
+        assertNull(mgr.detmTarget());
+        assertEquals(MY_TARGET, mgr.getTargetEntity());
+
+        // wrong type
+        event.setTarget(MISMATCH);
+        assertThatIllegalArgumentException().isThrownBy(() -> mgr.detmTarget())
+                        .withMessage("Target does not match target type");
+
+        // missing enrichment data
+        event.setTarget(ControlLoopOperationManager2.VSERVER_VSERVER_NAME);
+        event.getAai().clear();
+        assertThatIllegalArgumentException().isThrownBy(() -> mgr.detmTarget())
+                        .withMessage("Enrichment data is missing " + ControlLoopOperationManager2.VSERVER_VSERVER_NAME);
+
+        // null target
+        event.setTarget(null);
+        assertThatIllegalArgumentException().isThrownBy(() -> mgr.detmTarget()).withMessage("Target is null");
+    }
+
+    @Test
+    public void testDetmVnfName() {
+        setTargetVnfName();
+        assertNull(mgr.detmTarget());
+        assertEquals(MY_TARGET, mgr.getTargetEntity());
+
+        // force it to be gotten from the CQ data
+        event.getAai().clear();
+        assertNull(mgr.detmTarget());
+        assertEquals(MY_VNF_ID, mgr.getTargetEntity());
+    }
+
+    @Test
+    public void testExtractVnfFromCq() {
+        // force it to be gotten from the CQ data
+        setTargetVnfName();
+        event.getAai().clear();
+
+        // missing vnf id in CQ data
+        when(vnf.getVnfId()).thenReturn(null);
+        assertThatIllegalArgumentException().isThrownBy(() -> mgr.detmTarget()).withMessage("No vnf-id found");
+
+        // missing default vnf in CQ data
+        when(cqdata.getDefaultGenericVnf()).thenReturn(null);
+        assertThatIllegalArgumentException().isThrownBy(() -> mgr.detmTarget()).withMessage("No vnf-id found");
+    }
+
+    @Test
+    public void testGetState_testGetActor_testGetOperation() {
+        assertEquals(ControlLoopOperationManager2.State.ACTIVE, mgr.getState());
+        assertEquals(POLICY_ACTOR, mgr.getActor());
+        assertEquals(POLICY_OPERATION, mgr.getOperation());
+    }
+
+    @Test
+    public void testToString() {
+        assertThat(mgr.toString()).contains("state").contains("requestId").contains("policyId").contains("attempts");
+    }
+
+    /**
+     * Runs a cycle, from start to completion.
+     */
+    private void runCyle() {
+        mgr.start(REMAINING_MS);
+
+        lockFuture.complete(new OperationOutcome());
+        genGuardOutcome();
+        genOpOutcome();
+
+        runToCompletion();
+
+        assertTrue(mgr.nextStep());
+        assertTrue(mgr.nextStep());
+        assertFalse(mgr.nextStep());
+    }
+
+    /**
+     * Runs everything until the executor queue is empty.
+     */
+    private void runToCompletion() {
+        assertTrue(executor.runAll(MAX_RUN));
+    }
+
+    /**
+     * Generates a failure outcome for the lock, and invokes the callbacks.
+     *
+     * @return the generated outcome
+     */
+    private OperationOutcome genLockFailure() {
+        OperationOutcome outcome = new OperationOutcome();
+        outcome.setActor(ControlLoopOperationManager2.LOCK_ACTOR);
+        outcome.setOperation(ControlLoopOperationManager2.LOCK_OPERATION);
+        outcome.setResult(PolicyResult.FAILURE);
+        outcome.setStart(Instant.now());
+        outcome.setEnd(Instant.now());
+        outcome.setFinalOutcome(true);
+
+        verify(mgrctx).requestLock(eq(MY_TARGET), lockCallback.capture());
+        lockCallback.getValue().accept(outcome);
+
+        lockFuture.complete(outcome);
+
+        return outcome;
+    }
+
+    /**
+     * Generates an outcome for the guard, and invokes the callbacks.
+     *
+     * @return the generated outcome
+     */
+    private OperationOutcome genGuardOutcome() {
+        return genGuardOutcome(true);
+    }
+
+    /**
+     * Generates an outcome for the guard, and invokes the callbacks.
+     *
+     * @param permit {@code true} if the guard should be permitted, {@code false} if
+     *        denied
+     * @return the generated outcome
+     */
+    private OperationOutcome genGuardOutcome(boolean permit) {
+        OperationOutcome outcome = mgr.getParams().makeOutcome();
+        outcome.setActor(GuardActorServiceProvider.NAME);
+        outcome.setOperation(GuardOperation.NAME);
+        outcome.setStart(Instant.now());
+        mgr.getParams().callbackStarted(new OperationOutcome(outcome));
+
+        if (!permit) {
+            outcome.setResult(PolicyResult.FAILURE);
+        }
+
+        outcome.setEnd(Instant.now());
+        mgr.getParams().callbackCompleted(outcome);
+
+        return outcome;
+    }
+
+    /**
+     * Generates an outcome for the operation, itself, and invokes the callbacks.
+     *
+     * @return the generated outcome
+     */
+    private OperationOutcome genOpOutcome() {
+        return genOpOutcome(true);
+    }
+
+    /**
+     * Generates an outcome for the operation, itself, and invokes the callbacks.
+     *
+     * @param success {@code true} if the outcome should be a success, {@code false} if a
+     *        failure
+     * @return the generated outcome
+     */
+    private OperationOutcome genOpOutcome(boolean success) {
+        OperationOutcome outcome = mgr.getParams().makeOutcome();
+        outcome.setStart(Instant.now());
+        mgr.getParams().callbackStarted(new OperationOutcome(outcome));
+
+        if (success) {
+            outcome.setFinalOutcome(true);
+        } else {
+            outcome.setResult(PolicyResult.FAILURE);
+        }
+
+        outcome.setEnd(Instant.now());
+        mgr.getParams().callbackCompleted(outcome);
+
+        return outcome;
+    }
+
+    /**
+     * Configures the data for a PNF target.
+     */
+    private void setTargetPnf() {
+        event.setTarget(ControlLoopOperationManager2.PNF_NAME);
+        event.getAai().clear();
+        event.getAai().putAll(Map.of(ControlLoopOperationManager2.PNF_NAME, MY_TARGET));
+
+        target.setType(TargetType.PNF);
+    }
+
+    /**
+     * Configures the data for a VNF-NAME target.
+     */
+    private void setTargetVnfName() {
+        event.setTarget(ControlLoopOperationManager2.GENERIC_VNF_VNF_NAME);
+        event.getAai().clear();
+        event.getAai().putAll(Map.of(ControlLoopOperationManager2.GENERIC_VNF_VNF_ID, MY_TARGET));
+
+        target.setType(TargetType.VNF);
+    }
+
+    private void verifyDb(int nrecords, PolicyResult expectedResult, String expectedMsg) {
+        ArgumentCaptor<ControlLoopOperation> captor = ArgumentCaptor.forClass(ControlLoopOperation.class);
+        verify(dataMgr, times(nrecords)).store(any(), any(), captor.capture());
+
+        ControlLoopOperation oper = captor.getValue();
+
+        assertEquals(expectedResult.toString(), oper.getOutcome());
+        assertEquals(expectedMsg, oper.getMessage());
+    }
+}
diff --git a/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/eventmanager/EventManagerServicesTest.java b/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/eventmanager/EventManagerServicesTest.java
new file mode 100644
index 0000000..b32fb44
--- /dev/null
+++ b/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/eventmanager/EventManagerServicesTest.java
@@ -0,0 +1,120 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.eventmanager;
+
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Properties;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.onap.policy.common.endpoints.http.client.HttpClientFactoryInstance;
+import org.onap.policy.controlloop.actorserviceprovider.ActorService;
+import org.onap.policy.controlloop.ophistory.OperationHistoryDataManagerImpl;
+import org.onap.policy.controlloop.ophistory.OperationHistoryDataManagerStub;
+import org.onap.policy.drools.utils.PropertyUtil;
+
+public class EventManagerServicesTest {
+    private static final String FILEPFX = "eventService/";
+    private static final IllegalArgumentException EXPECTED_EXCEPTION =
+                    new IllegalArgumentException("expected exception");
+
+    private EventManagerServices services;
+
+    /**
+     * Configures HTTP clients.
+     */
+    @BeforeClass
+    public static void setUpBeforeClass() throws Exception {
+        // start with a clean slate
+        HttpClientFactoryInstance.getClientFactory().destroy();
+
+        Properties props =
+                        PropertyUtil.getProperties("src/test/resources/eventService/event-svc-http-client.properties");
+        HttpClientFactoryInstance.getClientFactory().build(props);
+    }
+
+    @AfterClass
+    public static void teatDownBeforeClass() {
+        HttpClientFactoryInstance.getClientFactory().destroy();
+    }
+
+    @After
+    public void tearDown() {
+        closeDb();
+    }
+
+    @Test
+    public void testEventManagerServices_testGetActorService() {
+        // try with guard disabled - should use DB stub
+        services = new EventManagerServices(FILEPFX + "event-svc-guard-disabled.properties");
+        assertTrue(services.getDataManager() instanceof OperationHistoryDataManagerStub);
+        assertNotNull(services.getActorService());
+
+        // try with guard enabled - should create a DB connection
+        services = new EventManagerServices(FILEPFX + "event-svc-with-db.properties");
+        assertTrue(services.getDataManager() instanceof OperationHistoryDataManagerImpl);
+        assertNotNull(services.getActorService());
+    }
+
+    @Test
+    public void testStartActorService() {
+        // config file not found
+        assertThatIllegalStateException().isThrownBy(() -> new EventManagerServices("missing-config-file"));
+    }
+
+    @Test
+    public void testIsGuardEnabled() {
+        // cannot check guard
+        services = new EventManagerServices(FILEPFX + "event-svc-no-guard-actor.properties");
+        assertTrue(services.getDataManager() instanceof OperationHistoryDataManagerStub);
+
+        // force exception when checking for guard operator
+        services = new EventManagerServices(FILEPFX + "event-svc-with-db.properties") {
+            @Override
+            public ActorService getActorService() {
+                ActorService svc = mock(ActorService.class);
+                when(svc.getActor(any())).thenThrow(EXPECTED_EXCEPTION);
+                return svc;
+            }
+        };
+        assertTrue(services.getDataManager() instanceof OperationHistoryDataManagerStub);
+    }
+
+    @Test
+    public void testMakeDataManager() {
+        assertThatThrownBy(() -> new EventManagerServices(FILEPFX + "event-svc-invalid-db.properties"));
+    }
+
+
+    private void closeDb() {
+        if (services != null) {
+            services.getDataManager().stop();
+        }
+    }
+}
diff --git a/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/eventmanager/LockDataTest.java b/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/eventmanager/LockDataTest.java
new file mode 100644
index 0000000..dc470e7
--- /dev/null
+++ b/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/eventmanager/LockDataTest.java
@@ -0,0 +1,193 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.eventmanager;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import java.time.Instant;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.onap.policy.controlloop.ControlLoopOperation;
+import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome;
+import org.onap.policy.controlloop.policy.PolicyResult;
+import org.onap.policy.drools.core.lock.Lock;
+
+public class LockDataTest {
+
+    private static final String ENTITY = "my-entity";
+    private static final UUID REQ_ID = UUID.randomUUID();
+
+    @Mock
+    private Lock lock;
+    @Mock
+    private Consumer<OperationOutcome> callback1;
+    @Mock
+    private Consumer<OperationOutcome> callback2;
+    @Mock
+    private Consumer<OperationOutcome> callback3;
+
+    private LockData data;
+
+    /**
+     * Sets up.
+     */
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        data = new LockData(ENTITY, REQ_ID);
+    }
+
+    @Test
+    public void testGetFuture() {
+        CompletableFuture<OperationOutcome> future = data.getFuture();
+        assertNotNull(future);
+        assertFalse(future.isDone());
+    }
+
+    @Test
+    public void testAddUnavailableCallback() {
+        data.addUnavailableCallback(callback1);
+        data.addUnavailableCallback(callback2);
+
+        data.lockAvailable(lock);
+        verify(callback1, never()).accept(any());
+        verify(callback2, never()).accept(any());
+
+        data.lockUnavailable(lock);
+        verify(callback1).accept(any());
+        verify(callback2).accept(any());
+    }
+
+    /**
+     * Tests addUnavailableCallback() when the lock never becomes available.
+     */
+    @Test
+    public void testAddUnavailableCallbackNeverAvailable() {
+        data.addUnavailableCallback(callback1);
+        data.addUnavailableCallback(callback2);
+
+        data.lockUnavailable(lock);
+        verify(callback1).accept(any());
+        verify(callback2).accept(any());
+
+        data.addUnavailableCallback(callback3);
+        verify(callback3).accept(any());
+    }
+
+    @Test
+    public void testFree() {
+        // no lock yet
+        assertThatCode(() -> data.free()).doesNotThrowAnyException();
+
+        // no with a lock
+        data.lockAvailable(lock);
+        data.free();
+        verify(lock).free();
+    }
+
+    @Test
+    public void testLockAvailable() throws Exception {
+        data.addUnavailableCallback(callback1);
+        data.addUnavailableCallback(callback2);
+
+        CompletableFuture<OperationOutcome> future = data.getFuture();
+        data.lockAvailable(lock);
+
+        assertSame(future, data.getFuture());
+
+        assertTrue(future.isDone());
+        OperationOutcome outcome = future.get();
+        assertEquals(ControlLoopOperationManager2.LOCK_ACTOR, outcome.getActor());
+        assertEquals(ControlLoopOperationManager2.LOCK_OPERATION, outcome.getOperation());
+        assertEquals(ENTITY, outcome.getTarget());
+        assertEquals(PolicyResult.SUCCESS, outcome.getResult());
+        assertEquals(ControlLoopOperation.SUCCESS_MSG, outcome.getMessage());
+
+        Instant start = outcome.getStart();
+        assertNotNull(start);
+
+        Instant end = outcome.getEnd();
+        assertNotNull(end);
+        assertTrue(start.compareTo(end) <= 0);
+
+        verify(callback1, never()).accept(any());
+        verify(callback2, never()).accept(any());
+    }
+
+    @Test
+    public void testLockUnavailable() throws Exception {
+        data.addUnavailableCallback(callback1);
+        data.addUnavailableCallback(callback2);
+        data.addUnavailableCallback(callback3);
+
+        // arrange for callback2 to throw an exception
+        doThrow(new IllegalStateException("expected exception")).when(callback2).accept(any());
+
+        CompletableFuture<OperationOutcome> future = data.getFuture();
+        assertNotNull(future);
+        data.lockUnavailable(lock);
+
+        CompletableFuture<OperationOutcome> future2 = data.getFuture();
+        assertNotNull(future2);
+
+        assertNotSame(future, future2);
+
+        assertTrue(future.isDone());
+        OperationOutcome outcome = future.get();
+
+        assertTrue(future2.isDone());
+        assertSame(outcome, future2.get());
+
+        assertEquals(ControlLoopOperationManager2.LOCK_ACTOR, outcome.getActor());
+        assertEquals(ControlLoopOperationManager2.LOCK_OPERATION, outcome.getOperation());
+        assertEquals(ENTITY, outcome.getTarget());
+        assertEquals(PolicyResult.FAILURE, outcome.getResult());
+        assertEquals(ControlLoopOperation.FAILED_MSG, outcome.getMessage());
+
+        Instant start = outcome.getStart();
+        assertNotNull(start);
+
+        Instant end = outcome.getEnd();
+        assertNotNull(end);
+        assertTrue(start.compareTo(end) <= 0);
+
+        verify(callback1).accept(eq(outcome));
+        verify(callback2).accept(eq(outcome));
+        verify(callback3).accept(eq(outcome));
+    }
+}
diff --git a/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/ophistory/OperationHistoryDataManagerImplTest.java b/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/ophistory/OperationHistoryDataManagerImplTest.java
new file mode 100644
index 0000000..8e3c1fa
--- /dev/null
+++ b/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/ophistory/OperationHistoryDataManagerImplTest.java
@@ -0,0 +1,379 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.ophistory;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.awaitility.Awaitility.await;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.time.Instant;
+import java.util.Properties;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import javax.persistence.EntityManagerFactory;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.onap.policy.controlloop.ControlLoopOperation;
+import org.onap.policy.controlloop.VirtualControlLoopEvent;
+import org.onap.policy.controlloop.ophistory.OperationHistoryDataManagerParams.OperationHistoryDataManagerParamsBuilder;
+
+public class OperationHistoryDataManagerImplTest {
+
+    private static final IllegalStateException EXPECTED_EXCEPTION = new IllegalStateException("expected exception");
+    private static final String MY_TARGET = "my-target";
+    private static final String REQ_ID = "my-request-id";
+    private static final int BATCH_SIZE = 5;
+    private static final int MAX_QUEUE_LENGTH = 23;
+
+    private static EntityManagerFactory emf;
+
+    @Mock
+    private Thread thread;
+
+    private OperationHistoryDataManagerParams params;
+    private Consumer<EntityManagerFactory> threadFunction;
+    private VirtualControlLoopEvent event;
+    private ControlLoopOperation operation;
+    private EntityManagerFactory emfSpy;
+
+    // decremented when the thread function completes
+    private CountDownLatch finished;
+
+    private OperationHistoryDataManagerImpl mgr;
+
+
+    /**
+     * Sets up for all tests.
+     */
+    @BeforeClass
+    public static void setUpBeforeClass() {
+        OperationHistoryDataManagerParams params = makeBuilder().build();
+
+        // capture the entity manager factory for re-use
+        new OperationHistoryDataManagerImpl(params) {
+            @Override
+            protected EntityManagerFactory makeEntityManagerFactory(String opsHistPu, Properties props) {
+                emf = super.makeEntityManagerFactory(opsHistPu, props);
+                return emf;
+            }
+        };
+    }
+
+    /**
+     * Restores the environment after all tests.
+     */
+    @AfterClass
+    public static void tearDownAfterClass() {
+        emf.close();
+    }
+
+    /**
+     * Sets up for an individual test.
+     */
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        event = new VirtualControlLoopEvent();
+        event.setRequestId(UUID.randomUUID());
+
+        operation = new ControlLoopOperation();
+        operation.setTarget(MY_TARGET);
+
+        threadFunction = null;
+        finished = new CountDownLatch(1);
+
+        // prevent the "real" emf from being closed
+        emfSpy = spy(emf);
+        doAnswer(ans -> null).when(emfSpy).close();
+
+        params = makeBuilder().build();
+
+        mgr = new PseudoThread();
+        mgr.start();
+    }
+
+    @After
+    public void tearDown() {
+        mgr.stop();
+    }
+
+    @Test
+    public void testConstructor() {
+        // use a thread and manager that haven't been started yet
+        thread = mock(Thread.class);
+        mgr = new PseudoThread();
+
+        // should not start the thread before start() is called
+        verify(thread, never()).start();
+
+        mgr.start();
+
+        // should have started the thread
+        verify(thread).start();
+
+        // invalid properties
+        params.setUrl(null);
+        assertThatCode(() -> new PseudoThread()).isInstanceOf(IllegalArgumentException.class)
+                        .hasMessageContaining("data-manager-properties");
+    }
+
+    @Test
+    public void testStart() {
+        // this should have no effect
+        mgr.start();
+
+        mgr.stop();
+
+        // this should also have no effect
+        assertThatCode(() -> mgr.start()).doesNotThrowAnyException();
+    }
+
+    @Test
+    public void testStore_testStop() throws InterruptedException {
+        // store
+        mgr.store(REQ_ID, event, operation);
+
+        runThread();
+
+        assertEquals(1, mgr.getRecordsAdded());
+    }
+
+    /**
+     * Tests stop() when the manager isn't running.
+     */
+    @Test
+    public void testStopNotRunning() {
+        // use a manager that hasn't been started yet
+        mgr = new PseudoThread();
+        mgr.stop();
+
+        verify(emfSpy).close();
+    }
+
+    /**
+     * Tests store() when it is already stopped.
+     */
+    @Test
+    public void testStoreAlreadyStopped() throws InterruptedException {
+        mgr.stop();
+
+        // store
+        mgr.store(REQ_ID, event, operation);
+
+        assertEquals(0, mgr.getRecordsAdded());
+    }
+
+    /**
+     * Tests store() when when the queue is full.
+     */
+    @Test
+    public void testStoreTooManyItems() throws InterruptedException {
+        final int nextra = 5;
+        for (int nitems = 0; nitems < MAX_QUEUE_LENGTH + nextra; ++nitems) {
+            mgr.store(REQ_ID, event, operation);
+        }
+
+        runThread();
+
+        assertEquals(MAX_QUEUE_LENGTH, mgr.getRecordsAdded());
+    }
+
+    @Test
+    public void testRun() throws InterruptedException {
+
+        // trigger thread shutdown when it completes this batch
+        when(emfSpy.createEntityManager()).thenAnswer(ans -> {
+            mgr.stop();
+            return emf.createEntityManager();
+        });
+
+
+        mgr = new RealThread();
+        mgr.start();
+
+        mgr.store(REQ_ID, event, operation);
+        mgr.store(REQ_ID, event, operation);
+        mgr.store(REQ_ID, event, operation);
+
+        waitForThread();
+
+        verify(emfSpy).close();
+
+        assertEquals(3, mgr.getRecordsAdded());
+    }
+
+    private void waitForThread() {
+        await().atMost(5, TimeUnit.SECONDS).until(() -> !thread.isAlive());
+    }
+
+    /**
+     * Tests run() when the entity manager throws an exception.
+     */
+    @Test
+    public void testRunException() throws InterruptedException {
+        AtomicInteger count = new AtomicInteger(0);
+
+        when(emfSpy.createEntityManager()).thenAnswer(ans -> {
+            if (count.incrementAndGet() == 2) {
+                // interrupt during one of the attempts
+                thread.interrupt();
+            }
+
+            // throw an exception for each record
+            throw EXPECTED_EXCEPTION;
+        });
+
+
+        mgr = new RealThread();
+        mgr.start();
+
+        mgr.store(REQ_ID, event, operation);
+        mgr.store(REQ_ID, event, operation);
+        mgr.store(REQ_ID, event, operation);
+
+        waitForThread();
+
+        verify(emfSpy).close();
+    }
+
+    /**
+     * Tests storeRemainingRecords() when the entity manager throws an exception.
+     */
+    @Test
+    public void testStoreRemainingRecordsException() throws InterruptedException {
+        // arrange to throw an exception
+        when(emfSpy.createEntityManager()).thenThrow(EXPECTED_EXCEPTION);
+
+        mgr.store(REQ_ID, event, operation);
+
+        runThread();
+    }
+
+    @Test
+    public void testStoreRecord() throws InterruptedException {
+        // no start time
+        mgr.store(REQ_ID, event, operation);
+
+        // no start time
+        operation = new ControlLoopOperation(operation);
+        operation.setStart(Instant.now());
+        mgr.store(REQ_ID, event, operation);
+
+        // both start and end times
+        operation = new ControlLoopOperation(operation);
+        operation.setEnd(Instant.now());
+        mgr.store(REQ_ID, event, operation);
+
+        // only end time
+        operation = new ControlLoopOperation(operation);
+        operation.setStart(null);
+        mgr.store(REQ_ID, event, operation);
+
+        runThread();
+
+        // all of them should have been stored
+        assertEquals(4, mgr.getRecordsAdded());
+    }
+
+    private void runThread() throws InterruptedException {
+        if (threadFunction == null) {
+            return;
+        }
+
+        Thread thread2 = new Thread(() -> {
+            threadFunction.accept(emfSpy);
+            finished.countDown();
+        });
+
+        thread2.setDaemon(true);
+        thread2.start();
+
+        mgr.stop();
+
+        assertTrue(finished.await(5, TimeUnit.SECONDS));
+    }
+
+    private static OperationHistoryDataManagerParamsBuilder makeBuilder() {
+        // @formatter:off
+        return OperationHistoryDataManagerParams.builder()
+                        .url("jdbc:h2:mem:" + OperationHistoryDataManagerImplTest.class.getSimpleName())
+                        .userName("sa")
+                        .password("")
+                        .batchSize(BATCH_SIZE)
+                        .maxQueueLength(MAX_QUEUE_LENGTH);
+        // @formatter:on
+    }
+
+    /**
+     * Manager that uses the shared DB.
+     */
+    private class SharedDb extends OperationHistoryDataManagerImpl {
+        public SharedDb() {
+            super(params);
+        }
+
+        @Override
+        protected EntityManagerFactory makeEntityManagerFactory(String opsHistPu, Properties props) {
+            // re-use the same factory to avoid re-creating the DB for each test
+            return emfSpy;
+        }
+    }
+
+    /**
+     * Manager that uses the shared DB and a pseudo thread.
+     */
+    private class PseudoThread extends SharedDb {
+
+        @Override
+        protected Thread makeThread(EntityManagerFactory emfactory, Consumer<EntityManagerFactory> command) {
+            threadFunction = command;
+            return thread;
+        }
+    }
+
+    /**
+     * Manager that uses the shared DB and catches the thread.
+     */
+    private class RealThread extends SharedDb {
+
+        @Override
+        protected Thread makeThread(EntityManagerFactory emfactory, Consumer<EntityManagerFactory> command) {
+            thread = super.makeThread(emfactory, command);
+            return thread;
+        }
+    }
+}
diff --git a/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/ophistory/OperationHistoryDataManagerParamsTest.java b/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/ophistory/OperationHistoryDataManagerParamsTest.java
new file mode 100644
index 0000000..aeeac47
--- /dev/null
+++ b/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/ophistory/OperationHistoryDataManagerParamsTest.java
@@ -0,0 +1,115 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.ophistory;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.function.Consumer;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.common.parameters.ValidationResult;
+import org.onap.policy.controlloop.ophistory.OperationHistoryDataManagerParams.OperationHistoryDataManagerParamsBuilder;
+
+public class OperationHistoryDataManagerParamsTest {
+    private static final String CONTAINER = "my-container";
+    private static final int BATCH_SIZE = 10;
+    private static final int MAX_QUEUE_LENGTH = 20;
+    private static final String MY_PASS = "my-pass";
+    private static final String MY_PU = "my-pu";
+    private static final String MY_URL = "my-url";
+    private static final String MY_USER = "my-user";
+
+    private OperationHistoryDataManagerParams params;
+
+    @Before
+    public void setUp() {
+        params = makeBuilder().build();
+    }
+
+    @Test
+    public void test() {
+        assertEquals(BATCH_SIZE, params.getBatchSize());
+        assertEquals(MAX_QUEUE_LENGTH, params.getMaxQueueLength());
+        assertEquals(MY_PASS, params.getPassword());
+        assertEquals(OperationHistoryDataManagerParams.DEFAULT_PU, params.getPersistenceUnit());
+        assertEquals(MY_URL, params.getUrl());
+        assertEquals(MY_USER, params.getUserName());
+
+        // use specified PU
+        assertEquals(MY_PU, makeBuilder().persistenceUnit(MY_PU).build().getPersistenceUnit());
+    }
+
+    @Test
+    public void testValidate() {
+        assertTrue(params.validate(CONTAINER).isValid());
+
+        testValidateField("url", "null", params2 -> params2.setUrl(null));
+        testValidateField("userName", "null", params2 -> params2.setUserName(null));
+        testValidateField("password", "null", params2 -> params2.setPassword(null));
+        testValidateField("persistenceUnit", "null", params2 -> params2.setPersistenceUnit(null));
+
+        // check edge cases
+        params.setBatchSize(0);
+        assertFalse(params.validate(CONTAINER).isValid());
+
+        params.setBatchSize(1);
+        assertTrue(params.validate(CONTAINER).isValid());
+
+        params.setMaxQueueLength(0);
+        assertFalse(params.validate(CONTAINER).isValid());
+
+        params.setMaxQueueLength(1);
+        assertTrue(params.validate(CONTAINER).isValid());
+
+        // blank password is ok
+        params.setPassword("");
+        assertTrue(params.validate(CONTAINER).isValid());
+    }
+
+    private void testValidateField(String fieldName, String expected,
+                    Consumer<OperationHistoryDataManagerParams> makeInvalid) {
+
+        // original params should be valid
+        ValidationResult result = params.validate(CONTAINER);
+        assertTrue(fieldName, result.isValid());
+
+        // make invalid params
+        OperationHistoryDataManagerParams params2 = makeBuilder().build();
+        makeInvalid.accept(params2);
+        result = params2.validate(CONTAINER);
+        assertFalse(fieldName, result.isValid());
+        assertThat(result.getResult()).contains(CONTAINER).contains(fieldName).contains(expected);
+    }
+
+    private OperationHistoryDataManagerParamsBuilder makeBuilder() {
+        // @formatter:off
+        return OperationHistoryDataManagerParams.builder()
+                        .batchSize(BATCH_SIZE)
+                        .maxQueueLength(MAX_QUEUE_LENGTH)
+                        .password(MY_PASS)
+                        .url(MY_URL)
+                        .userName(MY_USER);
+        // @formatter:on
+    }
+}
diff --git a/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/ophistory/OperationHistoryDataManagerStubTest.java b/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/ophistory/OperationHistoryDataManagerStubTest.java
new file mode 100644
index 0000000..f4a7ff8
--- /dev/null
+++ b/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/ophistory/OperationHistoryDataManagerStubTest.java
@@ -0,0 +1,36 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.ophistory;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+import org.junit.Test;
+
+public class OperationHistoryDataManagerStubTest {
+
+    @Test
+    public void test() {
+        OperationHistoryDataManagerStub mgr = new OperationHistoryDataManagerStub();
+
+        assertThatCode(() -> mgr.store(null, null, null)).doesNotThrowAnyException();
+        assertThatCode(() -> mgr.stop()).doesNotThrowAnyException();
+    }
+}
diff --git a/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/utils/ControlLoopUtilsTest.java b/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/utils/ControlLoopUtilsTest.java
index 2e48114..2f14954 100644
--- a/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/utils/ControlLoopUtilsTest.java
+++ b/controlloop/common/eventmanager/src/test/java/org/onap/policy/controlloop/utils/ControlLoopUtilsTest.java
@@ -19,11 +19,22 @@
 package org.onap.policy.controlloop.utils;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 
+import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Paths;
+import java.util.AbstractSet;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
 import org.junit.Test;
+import org.onap.policy.common.utils.coder.CoderException;
 import org.onap.policy.common.utils.coder.StandardCoder;
 import org.onap.policy.controlloop.drl.legacy.ControlLoopParams;
 import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
@@ -32,8 +43,7 @@
 
     @Test
     public void testToControlLoopParams() throws Exception {
-        String policy =
-            new String(Files.readAllBytes(Paths.get("src/test/resources/tosca-policy-legacy-vcpe.json")));
+        String policy = Files.readString(Paths.get("src/test/resources/tosca-policy-legacy-vcpe.json"));
         ToscaPolicy toscaPolicy = new StandardCoder().decode(policy, ToscaPolicy.class);
 
         ControlLoopParams params = ControlLoopUtils.toControlLoopParams(toscaPolicy);
@@ -42,5 +52,182 @@
         assertEquals(toscaPolicy.getVersion(), params.getPolicyVersion());
         assertEquals(toscaPolicy.getType() + ":" + toscaPolicy.getVersion(), params.getPolicyScope());
         assertSame(toscaPolicy, params.getToscaPolicy());
+
+        assertNull(ControlLoopUtils.toControlLoopParams(null));
     }
-}
\ No newline at end of file
+
+    @Test
+    public void testToObject() {
+        Map<String, String> map = Map.of("abc", "def", "ghi", "jkl");
+        Properties props = new Properties();
+        props.putAll(map);
+
+        // with empty prefix
+        Map<String, Object> result = ControlLoopUtils.toObject(props, "");
+        assertEquals(map, result);
+
+        // with dotted prefix - other items skipped
+        map = Map.of("pfx.abc", "def", "ghi", "jkl", "pfx.mno", "pqr", "differentpfx.stu", "vwx");
+        props.clear();
+        props.putAll(Map.of("pfx.abc", "def", "ghi", "jkl", "pfx.mno", "pqr", "differentpfx.stu", "vwx"));
+        result = ControlLoopUtils.toObject(props, "pfx.");
+        map = Map.of("abc", "def", "mno", "pqr");
+        assertEquals(map, result);
+
+        // undotted prefix - still skips other items
+        result = ControlLoopUtils.toObject(props, "pfx");
+        assertEquals(map, result);
+    }
+
+    @Test
+    public void testSetProperty() {
+        // one, two, and three components in the name, the last two with subscripts
+        Map<String, Object> map = Map.of("one", "one.abc", "two.def", "two.ghi", "three.jkl.mno[0]", "three.pqr",
+                        "three.jkl.mno[1]", "three.stu");
+        Properties props = new Properties();
+        props.putAll(map);
+
+        Map<String, Object> result = ControlLoopUtils.toObject(props, "");
+        // @formatter:off
+        map = Map.of(
+                "one", "one.abc",
+                "two", Map.of("def", "two.ghi"),
+                "three", Map.of("jkl",
+                            Map.of("mno",
+                                List.of("three.pqr", "three.stu"))));
+        // @formatter:on
+        assertEquals(map, result);
+    }
+
+    @Test
+    public void testGetNode() {
+        Map<String, Object> map = Map.of("abc[0].def", "node.ghi", "abc[0].jkl", "node.mno", "abc[1].def", "node.pqr");
+        Properties props = new Properties();
+        props.putAll(map);
+
+        Map<String, Object> result = ControlLoopUtils.toObject(props, "");
+        // @formatter:off
+        map = Map.of(
+                "abc",
+                    List.of(
+                        Map.of("def", "node.ghi", "jkl", "node.mno"),
+                        Map.of("def", "node.pqr")
+                    ));
+        // @formatter:on
+        assertEquals(map, result);
+
+    }
+
+    @Test
+    public void testExpand() {
+        // add subscripts out of order
+        Properties props = makeProperties("abc[2]", "expand.def", "abc[1]", "expand.ghi");
+
+        Map<String, Object> result = ControlLoopUtils.toObject(props, "");
+        // @formatter:off
+        Map<String,Object> map =
+            Map.of("abc",
+                Arrays.asList(null, "expand.ghi", "expand.def"));
+        // @formatter:on
+        assertEquals(map, result);
+
+    }
+
+    @Test
+    public void testGetObject() {
+        // first value is primitive, while second is a map
+        Properties props = makeProperties("object.abc", "object.def", "object.abc.ghi", "object.jkl");
+
+        Map<String, Object> result = ControlLoopUtils.toObject(props, "");
+        // @formatter:off
+        Map<String,Object> map =
+            Map.of("object",
+                Map.of("abc",
+                    Map.of("ghi", "object.jkl")));
+        // @formatter:on
+        assertEquals(map, result);
+    }
+
+    @Test
+    public void testGetArray() {
+        // first value is primitive, while second is an array
+        Properties props = makeProperties("array.abc", "array.def", "array.abc[0].ghi", "array.jkl");
+
+        Map<String, Object> result = ControlLoopUtils.toObject(props, "");
+        // @formatter:off
+        Map<String,Object> map =
+            Map.of("array",
+                Map.of("abc",
+                    List.of(
+                        Map.of("ghi", "array.jkl"))));
+        // @formatter:on
+        assertEquals(map, result);
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testCompressLists() throws IOException, CoderException {
+        assertEquals("plain-string", ControlLoopUtils.compressLists("plain-string").toString());
+
+        // @formatter:off
+        Map<String, Object> map =
+            Map.of(
+                "cmp.abc", "cmp.def",
+                "cmp.ghi",
+                    Arrays.asList(null, "cmp.list1", null, "cmp.list2",
+                        Map.of("cmp.map", Arrays.asList("cmp.map.list1", "cmp.map1.list2", null))));
+        // @formatter:on
+
+        // the data structure needs to be modifiable, so we'll encode/decode it
+        StandardCoder coder = new StandardCoder();
+        map = coder.decode(coder.encode(map), LinkedHashMap.class);
+
+        ControlLoopUtils.compressLists(map);
+
+        // @formatter:off
+        Map<String, Object> expected =
+            Map.of(
+                "cmp.abc", "cmp.def",
+                "cmp.ghi",
+                    Arrays.asList("cmp.list1", "cmp.list2",
+                        Map.of("cmp.map", Arrays.asList("cmp.map.list1", "cmp.map1.list2"))));
+        // @formatter:on
+        assertEquals(expected, map);
+    }
+
+    /**
+     * Makes properties containing the specified key/value pairs. The property set returns
+     * names in the order listed.
+     *
+     * @return a new properties containing the specified key/value pairs
+     */
+    private Properties makeProperties(String key1, String value1, String key2, String value2) {
+        // control the order in which the names are returned
+        List<String> keyList = List.of(key1, key2);
+
+        Set<String> keySet = new AbstractSet<>() {
+            @Override
+            public Iterator<String> iterator() {
+                return keyList.iterator();
+            }
+
+            @Override
+            public int size() {
+                return 2;
+            }
+        };
+
+        Properties props = new Properties() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public Set<String> stringPropertyNames() {
+                return keySet;
+            }
+        };
+
+        props.putAll(Map.of(key1, value1, key2, value2));
+
+        return props;
+    }
+}
diff --git a/controlloop/common/eventmanager/src/test/resources/META-INF/persistence.xml b/controlloop/common/eventmanager/src/test/resources/META-INF/persistence.xml
index 07dafec..4d47751 100644
--- a/controlloop/common/eventmanager/src/test/resources/META-INF/persistence.xml
+++ b/controlloop/common/eventmanager/src/test/resources/META-INF/persistence.xml
@@ -29,7 +29,7 @@
 

         <properties>

             <property name="eclipselink.ddl-generation" value="create-tables" />

-            <property name="eclipselink.logging.level" value="FINE" />

+            <property name="eclipselink.logging.level" value="INFO" />

             <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />

             <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />

             <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:testdb;DATABASE_TO_UPPER=FALSE" />

diff --git a/controlloop/common/eventmanager/src/test/resources/eventManager/event-mgr-multi.yaml b/controlloop/common/eventmanager/src/test/resources/eventManager/event-mgr-multi.yaml
new file mode 100644
index 0000000..7acf021
--- /dev/null
+++ b/controlloop/common/eventmanager/src/test/resources/eventManager/event-mgr-multi.yaml
@@ -0,0 +1,68 @@
+tosca_definitions_version: tosca_simple_yaml_1_0_0
+topology_template:
+    policies:
+    -   operational.activity:
+            type: onap.policies.controlloop.operational.common.Drools
+            type_version: 1.0.0
+            version: 1.0.0
+            metadata:
+                policy-id: operational.activity
+                policy-version: 1.0.0
+            properties:
+                id: ControlLoop-event-mgr
+                timeout: 1200
+                abatement: false
+                trigger: first-operation
+                operations:
+                -   id: first-operation
+                    description: First action
+                    operation:
+                        actor: First
+                        operation: OperationA
+                        target:
+                            targetType: VNF
+                            entityIds:
+                                resourceID: bbb3cefd-01c8-413c-9bdd-2b92f9ca3d38
+                    timeout: 300
+                    retries: 0
+                    success: second-operation
+                    failure: final_failure
+                    failure_timeout: final_failure_timeout
+                    failure_retries: final_failure_retries
+                    failure_exception: final_failure_exception
+                    failure_guard: final_failure_guard
+                -   id: second-operation
+                    description: Second action
+                    operation:
+                        actor: Second
+                        operation: OperationB
+                        target:
+                            targetType: VNF
+                            entityIds:
+                                resourceID: bbb3cefd-01c8-413c-9bdd-2b92f9ca3d38
+                    timeout: 300
+                    retries: 0
+                    success: third-operation
+                    failure: final_failure
+                    failure_timeout: final_failure_timeout
+                    failure_retries: final_failure_retries
+                    failure_exception: final_failure_exception
+                    failure_guard: final_failure_guard
+                -   id: third-operation
+                    description: Third action
+                    operation:
+                        actor: Third
+                        operation: OperationC
+                        target:
+                            targetType: VNF
+                            entityIds:
+                                resourceID: bbb3cefd-01c8-413c-9bdd-2b92f9ca3d38
+                    timeout: 300
+                    retries: 0
+                    success: final_success
+                    failure: final_failure
+                    failure_timeout: final_failure_timeout
+                    failure_retries: final_failure_retries
+                    failure_exception: final_failure_exception
+                    failure_guard: final_failure_guard
+                controllerName: usecases
\ No newline at end of file
diff --git a/controlloop/common/eventmanager/src/test/resources/eventManager/event-mgr-simple.yaml b/controlloop/common/eventmanager/src/test/resources/eventManager/event-mgr-simple.yaml
new file mode 100644
index 0000000..c8b6db0
--- /dev/null
+++ b/controlloop/common/eventmanager/src/test/resources/eventManager/event-mgr-simple.yaml
@@ -0,0 +1,34 @@
+tosca_definitions_version: tosca_simple_yaml_1_0_0
+topology_template:
+    policies:
+    -   operational.activity:
+            type: onap.policies.controlloop.operational.common.Drools
+            type_version: 1.0.0
+            version: 1.0.0
+            metadata:
+                policy-id: operational.activity
+                policy-version: 1.0.0
+            properties:
+                id: ControlLoop-event-mgr
+                timeout: 1200
+                abatement: false
+                trigger: first-operation
+                operations:
+                -   id: first-operation
+                    description: First action
+                    operation:
+                        actor: First
+                        operation: OperationA
+                        target:
+                            targetType: VNF
+                            entityIds:
+                                resourceID: bbb3cefd-01c8-413c-9bdd-2b92f9ca3d38
+                    timeout: 300
+                    retries: 0
+                    success: final_success
+                    failure: final_failure
+                    failure_timeout: final_failure_timeout
+                    failure_retries: final_failure_retries
+                    failure_exception: final_failure_exception
+                    failure_guard: final_openloop
+                controllerName: usecases
\ No newline at end of file
diff --git a/controlloop/common/eventmanager/src/test/resources/eventService/event-svc-guard-disabled.properties b/controlloop/common/eventmanager/src/test/resources/eventService/event-svc-guard-disabled.properties
new file mode 100644
index 0000000..65f6c0c
--- /dev/null
+++ b/controlloop/common/eventmanager/src/test/resources/eventService/event-svc-guard-disabled.properties
@@ -0,0 +1,23 @@
+#
+# ============LICENSE_START======================================================
+# ONAP
+# ===============================================================================
+# Copyright (C) 2020 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========================================================
+#
+
+actor.service.GUARD.disabled=true
+actor.service.GUARD.clientName=guard-client
+actor.service.GUARD.operations.Decision.path=decide
diff --git a/controlloop/common/eventmanager/src/test/resources/eventService/event-svc-http-client.properties b/controlloop/common/eventmanager/src/test/resources/eventService/event-svc-http-client.properties
new file mode 100644
index 0000000..a563afd
--- /dev/null
+++ b/controlloop/common/eventmanager/src/test/resources/eventService/event-svc-http-client.properties
@@ -0,0 +1,24 @@
+#
+# ============LICENSE_START======================================================
+# ONAP
+# ===============================================================================
+# Copyright (C) 2020 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========================================================
+#
+
+http.client.services=guard-client
+http.client.services.guard-client.host=localhost
+http.client.services.guard-client.port=80
+http.client.services.guard-client.managed=true
diff --git a/controlloop/common/eventmanager/src/test/resources/eventService/event-svc-invalid-db.properties b/controlloop/common/eventmanager/src/test/resources/eventService/event-svc-invalid-db.properties
new file mode 100644
index 0000000..59b0615
--- /dev/null
+++ b/controlloop/common/eventmanager/src/test/resources/eventService/event-svc-invalid-db.properties
@@ -0,0 +1,29 @@
+#
+# ============LICENSE_START======================================================
+# ONAP
+# ===============================================================================
+# Copyright (C) 2020 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========================================================
+#
+
+#actor.service.GUARD.disabled=true
+actor.service.GUARD.clientName=guard-client
+actor.service.GUARD.operations.Decision.path=decide
+
+# purposely missing the URL
+#operation.history.url=jdbc:h2:mem:EventManagerServicesTest
+
+operation.history.userName=sa
+operation.history.password=
diff --git a/controlloop/common/eventmanager/src/test/resources/eventService/event-svc-no-guard-actor.properties b/controlloop/common/eventmanager/src/test/resources/eventService/event-svc-no-guard-actor.properties
new file mode 100644
index 0000000..027f824
--- /dev/null
+++ b/controlloop/common/eventmanager/src/test/resources/eventService/event-svc-no-guard-actor.properties
@@ -0,0 +1,25 @@
+#
+# ============LICENSE_START======================================================
+# ONAP
+# ===============================================================================
+# Copyright (C) 2020 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========================================================
+#
+
+# all GUARD properties are commented out on purpose
+
+#actor.service.GUARD.disabled=true
+#actor.service.GUARD.clientName=guard-client
+#actor.service.GUARD.operations.Decision.path=decide
diff --git a/controlloop/common/eventmanager/src/test/resources/eventService/event-svc-with-db.properties b/controlloop/common/eventmanager/src/test/resources/eventService/event-svc-with-db.properties
new file mode 100644
index 0000000..6e003f6
--- /dev/null
+++ b/controlloop/common/eventmanager/src/test/resources/eventService/event-svc-with-db.properties
@@ -0,0 +1,27 @@
+#
+# ============LICENSE_START======================================================
+# ONAP
+# ===============================================================================
+# Copyright (C) 2020 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========================================================
+#
+
+#actor.service.GUARD.disabled=true
+actor.service.GUARD.clientName=guard-client
+actor.service.GUARD.operations.Decision.path=decide
+
+operation.history.url=jdbc:h2:mem:EventManagerServicesTest
+operation.history.userName=sa
+operation.history.password=
diff --git a/controlloop/common/feature-controlloop-frankfurt/pom.xml b/controlloop/common/feature-controlloop-frankfurt/pom.xml
new file mode 100644
index 0000000..f10cd2b
--- /dev/null
+++ b/controlloop/common/feature-controlloop-frankfurt/pom.xml
@@ -0,0 +1,113 @@
+<!--
+  ============LICENSE_START=======================================================
+  ONAP
+  ================================================================================
+  Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+  ================================================================================
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  ============LICENSE_END=========================================================
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.onap.policy.drools-applications.controlloop.common</groupId>
+        <artifactId>drools-applications-common</artifactId>
+        <version>1.6.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>feature-controlloop-frankfurt</artifactId>
+
+    <description>
+        Load Experimental Frankfurt Control Loop Controller as a feature.
+    </description>
+
+    <build>
+        <resources>
+            <resource>
+                <directory>src/main/feature</directory>
+                <filtering>true</filtering>
+            </resource>
+            <resource>
+                <directory>src/main/resources</directory>
+            </resource>
+        </resources>
+        <plugins>
+            <plugin>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>zipfile</id>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                        <phase>package</phase>
+                        <configuration>
+                            <attach>true</attach>
+                            <finalName>${project.artifactId}-${project.version}</finalName>
+                            <descriptors>
+                                <descriptor>src/assembly/assemble_zip.xml</descriptor>
+                            </descriptors>
+                            <appendAssemblyId>false</appendAssemblyId>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>copy-dependencies</id>
+                        <goals>
+                            <goal>copy-dependencies</goal>
+                        </goals>
+                        <phase>prepare-package</phase>
+                        <configuration>
+                            <outputDirectory>${project.build.directory}/assembly/lib</outputDirectory>
+                            <overWriteReleases>false</overWriteReleases>
+                            <overWriteSnapshots>true</overWriteSnapshots>
+                            <overWriteIfNewer>true</overWriteIfNewer>
+                            <useRepositoryLayout>false</useRepositoryLayout>
+                            <addParentPoms>false</addParentPoms>
+                            <copyPom>false</copyPom>
+                            <includeScope>runtime</includeScope>
+                            <excludeTransitive>true</excludeTransitive>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.onap.policy.drools-applications.controlloop.common</groupId>
+            <artifactId>controller-frankfurt</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.drools-pdp</groupId>
+            <artifactId>policy-management</artifactId>
+            <version>${version.policy.drools-pdp}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/controlloop/common/feature-controlloop-frankfurt/src/assembly/assemble_zip.xml b/controlloop/common/feature-controlloop-frankfurt/src/assembly/assemble_zip.xml
new file mode 100644
index 0000000..bee309b
--- /dev/null
+++ b/controlloop/common/feature-controlloop-frankfurt/src/assembly/assemble_zip.xml
@@ -0,0 +1,84 @@
+<!--
+  ============LICENSE_START=======================================================
+  ONAP
+  ================================================================================
+  Copyright (C) 2020 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-controlloop-frankfurt-package</id>
+    <formats>
+        <format>zip</format>
+    </formats>
+
+    <includeBaseDirectory>false</includeBaseDirectory>
+
+    <fileSets>
+        <fileSet>
+            <directory>target</directory>
+            <outputDirectory>lib/feature</outputDirectory>
+            <includes>
+                <include>feature-controlloop-frankfurt-${project.version}.jar</include>
+            </includes>
+        </fileSet>
+        <fileSet>
+            <directory>target/assembly/lib</directory>
+            <outputDirectory>artifacts</outputDirectory>
+            <includes>
+                <include>controller-frankfurt-${project.version}.jar</include>
+            </includes>
+        </fileSet>
+        <fileSet>
+            <directory>target/assembly/lib</directory>
+            <outputDirectory>lib/dependencies</outputDirectory>
+            <includes>
+                <include>*.jar</include>
+            </includes>
+            <excludes>
+                <exclude>controller-frankfurt-${project.version}.jar</exclude>
+            </excludes>
+        </fileSet>
+        <fileSet>
+            <directory>target/classes/config</directory>
+            <outputDirectory>config</outputDirectory>
+            <fileMode>0644</fileMode>
+            <excludes />
+        </fileSet>
+        <fileSet>
+            <directory>src/main/feature/bin</directory>
+            <outputDirectory>bin</outputDirectory>
+            <fileMode>0755</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>0755</fileMode>
+            <excludes />
+        </fileSet>
+    </fileSets>
+</assembly>
diff --git a/controlloop/common/feature-controlloop-frankfurt/src/main/feature/config/frankfurt-controller.properties b/controlloop/common/feature-controlloop-frankfurt/src/main/feature/config/frankfurt-controller.properties
new file mode 100644
index 0000000..2129f1b
--- /dev/null
+++ b/controlloop/common/feature-controlloop-frankfurt/src/main/feature/config/frankfurt-controller.properties
@@ -0,0 +1,63 @@
+###
+# ============LICENSE_START=======================================================
+# ONAP
+# ================================================================================
+# Copyright (C) 2020 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=========================================================
+###
+
+controller.name=frankfurt
+
+rules.groupId=${project.groupId}
+rules.artifactId=controlloop-frankfurt
+rules.version=${project.version}
+
+dmaap.source.topics=DCAE_TOPIC,APPC-CL,APPC-LCM-WRITE,SDNR-CL-RSP
+dmaap.sink.topics=APPC-CL,APPC-LCM-READ,POLICY-CL-MGT,SDNR-CL,DCAE_CL_RSP
+
+dmaap.source.topics.DCAE_TOPIC.events=\
+    org.onap.policy.controlloop.CanonicalOnset,org.onap.policy.controlloop.CanonicalAbated
+dmaap.source.topics.DCAE_TOPIC.events.org.onap.policy.controlloop.CanonicalOnset.filter=\
+    [?($.closedLoopEventStatus == 'ONSET')]
+dmaap.source.topics.DCAE_TOPIC.events.org.onap.policy.controlloop.CanonicalAbated.filter=\
+    [?($.closedLoopEventStatus == 'ABATED')]
+dmaap.source.topics.DCAE_TOPIC.events.custom.gson=org.onap.policy.controlloop.util.Serialization,gson
+
+dmaap.source.topics.APPC-CL.events=org.onap.policy.appc.Response
+dmaap.source.topics.APPC-CL.events.org.onap.policy.appc.Response.filter=[?($.CommonHeader && $.Status)]
+dmaap.source.topics.APPC-CL.events.custom.gson=org.onap.policy.appc.util.Serialization,gsonPretty
+
+dmaap.source.topics.APPC-LCM-WRITE.events=org.onap.policy.appclcm.AppcLcmDmaapWrapper
+dmaap.source.topics.APPC-LCM-WRITE.events.org.onap.policy.appclcm.AppcLcmDmaapWrapper.filter=[?($.type == 'response')]
+dmaap.source.topics.APPC-LCM-WRITE.events.custom.gson=org.onap.policy.appclcm.util.Serialization,gson
+
+dmaap.sink.topics.APPC-CL.events=org.onap.policy.appc.Request
+dmaap.sink.topics.APPC-CL.events.custom.gson=org.onap.policy.appc.util.Serialization,gsonPretty
+
+dmaap.sink.topics.APPC-LCM-READ.events=org.onap.policy.appclcm.AppcLcmDmaapWrapper
+dmaap.sink.topics.APPC-LCM-READ.events.custom.gson=org.onap.policy.appclcm.util.Serialization,gson
+
+dmaap.sink.topics.POLICY-CL-MGT.events=org.onap.policy.controlloop.VirtualControlLoopNotification
+dmaap.sink.topics.POLICY-CL-MGT.events.custom.gson=org.onap.policy.controlloop.util.Serialization,gsonPretty
+
+dmaap.sink.topics.SDNR-CL.events=org.onap.policy.sdnr.PciRequestWrapper
+dmaap.sink.topics.SDNR-CL.events.custom.gson=org.onap.policy.sdnr.util.Serialization,gson
+
+dmaap.sink.topics.DCAE_CL_RSP.events=org.onap.policy.controlloop.ControlLoopResponse
+dmaap.sink.topics.DCAE_CL_RSP.events.custom.gson=org.onap.policy.controlloop.util.Serialization,gsonPretty
+
+dmaap.source.topics.SDNR-CL-RSP.events=org.onap.policy.sdnr.PciResponseWrapper
+dmaap.source.topics.SDNR-CL-RSP.events.org.onap.policy.sdnr.PciResponseWrapper.filter=[?($.type == 'response')]
+dmaap.source.topics.SDNR-CL-RSP.events.custom.gson=org.onap.policy.sdnr.util.Serialization,gson
diff --git a/controlloop/common/feature-controlloop-frankfurt/src/main/feature/config/logback-include-frankfurt.xml b/controlloop/common/feature-controlloop-frankfurt/src/main/feature/config/logback-include-frankfurt.xml
new file mode 100644
index 0000000..080fb99
--- /dev/null
+++ b/controlloop/common/feature-controlloop-frankfurt/src/main/feature/config/logback-include-frankfurt.xml
@@ -0,0 +1,56 @@
+<!--
+  ============LICENSE_START=======================================================
+  feature-controlloop-frankfurt
+  ================================================================================
+  Copyright (C) 2020 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=========================================================
+  -->
+
+<!--
+  The logger configurations in this file are for each individual controller
+  to have their own network logger for topic traffic. This is an extension of
+  logback.xml or logback-eelf.xml.
+
+  NOTE: Each logger MUST contain the same name as the control loop's controller.
+-->
+<included>
+
+    <property name="logDir" value="${POLICY_LOGS}" />
+    <property name="frankfurtLog" value="frankfurt-network" />
+    <property name="networkPattern" value="[%d{yyyy-MM-dd'T'HH:mm:ss.SSS+00:00, UTC}|%t]%m%n" />
+
+    <!-- Frankfurt Network Logging Properties -->
+    <appender name="FrankfurtOut" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${logDir}/${frankfurtLog}.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${logDir}/${frankfurtLog}.%d{yyyy-MM-dd}.%i.log.zip</fileNamePattern>
+            <maxFileSize>50MB</maxFileSize>
+            <maxHistory>30</maxHistory>
+            <totalSizeCap>10GB</totalSizeCap>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${networkPattern}</pattern>
+        </encoder>
+    </appender>
+
+    <appender name="AsyncFrankfurtOut" class="ch.qos.logback.classic.AsyncAppender">
+        <appender-ref ref="FrankfurtOut" />
+    </appender>
+
+    <logger name="frankfurt" level="INFO" additivity="false">
+        <appender-ref ref="AsyncFrankfurtOut" />
+    </logger>
+
+</included>
diff --git a/controlloop/common/feature-controlloop-frankfurt/src/main/java/org/onap/policy/drools/apps/controlloop/feature/frankfurt/FrankfurtFeature.java b/controlloop/common/feature-controlloop-frankfurt/src/main/java/org/onap/policy/drools/apps/controlloop/feature/frankfurt/FrankfurtFeature.java
new file mode 100644
index 0000000..95d8b13
--- /dev/null
+++ b/controlloop/common/feature-controlloop-frankfurt/src/main/java/org/onap/policy/drools/apps/controlloop/feature/frankfurt/FrankfurtFeature.java
@@ -0,0 +1,46 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.drools.apps.controlloop.feature.frankfurt;
+
+import org.onap.policy.drools.features.PolicyEngineFeatureApi;
+
+/**
+ * Frankfurt Use Cases installation as a feature saves time loading the controller at
+ * runtime over the usual installation from nexus. It also reduces potential for errors in
+ * the pom.xml generated in the brmsgw side.
+ *
+ * <p/>
+ * There is no impact on other components as the brmsgw etc .. they will continue
+ * operating as usual.
+ *
+ * <p/>
+ * This class will be expanded in the future for additional functionality
+ *
+ */
+public class FrankfurtFeature implements PolicyEngineFeatureApi {
+
+    public static final int SEQNO = 1000;
+
+    @Override
+    public int getSequenceNumber() {
+        return SEQNO;
+    }
+}
diff --git a/controlloop/common/feature-controlloop-frankfurt/src/main/resources/META-INF/services/org.onap.policy.drools.features.PolicyEngineFeatureApi b/controlloop/common/feature-controlloop-frankfurt/src/main/resources/META-INF/services/org.onap.policy.drools.features.PolicyEngineFeatureApi
new file mode 100644
index 0000000..dfea34b
--- /dev/null
+++ b/controlloop/common/feature-controlloop-frankfurt/src/main/resources/META-INF/services/org.onap.policy.drools.features.PolicyEngineFeatureApi
@@ -0,0 +1 @@
+org.onap.policy.drools.apps.controlloop.feature.frankfurt.FrankfurtFeature
diff --git a/controlloop/common/feature-controlloop-frankfurt/src/test/java/org/onap/policy/drools/apps/controlloop/feature/frankfurt/FrankfurtFeatureTest.java b/controlloop/common/feature-controlloop-frankfurt/src/test/java/org/onap/policy/drools/apps/controlloop/feature/frankfurt/FrankfurtFeatureTest.java
new file mode 100644
index 0000000..2cb60d7
--- /dev/null
+++ b/controlloop/common/feature-controlloop-frankfurt/src/test/java/org/onap/policy/drools/apps/controlloop/feature/frankfurt/FrankfurtFeatureTest.java
@@ -0,0 +1,34 @@
+/*
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.drools.apps.controlloop.feature.frankfurt;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.onap.policy.drools.apps.controlloop.feature.frankfurt.FrankfurtFeature;
+
+public class FrankfurtFeatureTest {
+
+    @Test
+    public void testGetSequenceNumber() {
+        assertEquals(FrankfurtFeature.SEQNO, new FrankfurtFeature().getSequenceNumber());
+    }
+}
diff --git a/controlloop/common/feature-controlloop-management/pom.xml b/controlloop/common/feature-controlloop-management/pom.xml
index 8c68216..85981f6 100644
--- a/controlloop/common/feature-controlloop-management/pom.xml
+++ b/controlloop/common/feature-controlloop-management/pom.xml
@@ -172,6 +172,11 @@
         </dependency>
         <dependency>
             <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId>
+            <artifactId>actor.aai</artifactId>
+            <version>${policy.models.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId>
             <artifactId>actor.appc</artifactId>
             <version>${policy.models.version}</version>
             <exclusions>
@@ -198,6 +203,11 @@
         </dependency>
         <dependency>
             <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId>
+            <artifactId>actor.guard</artifactId>
+            <version>${policy.models.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId>
             <artifactId>actor.sdnr</artifactId>
             <version>${policy.models.version}</version>
             <exclusions>
diff --git a/controlloop/common/feature-controlloop-management/src/main/feature/config/AAI-http-client.properties b/controlloop/common/feature-controlloop-management/src/main/feature/config/AAI-http-client.properties
new file mode 100644
index 0000000..5a5f82f
--- /dev/null
+++ b/controlloop/common/feature-controlloop-management/src/main/feature/config/AAI-http-client.properties
@@ -0,0 +1,27 @@
+# ============LICENSE_START=======================================================
+# ONAP
+# ================================================================================
+# Copyright (C) 2020 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=========================================================
+
+http.client.services=AAI
+
+http.client.services.AAI.managed=true
+http.client.services.AAI.https=true
+http.client.services.AAI.host=${env:AAI_HOST}
+http.client.services.AAI.port=${env:AAI_PORT}
+http.client.services.AAI.username=${env:AAI_USERNAME}
+http.client.services.AAI.password=${env:AAI_PASSWORD}
+http.client.services.AAI.contextUriPath=
diff --git a/controlloop/common/feature-controlloop-management/src/main/feature/config/GUARD-http-client.properties b/controlloop/common/feature-controlloop-management/src/main/feature/config/GUARD-http-client.properties
new file mode 100644
index 0000000..e1d00bd
--- /dev/null
+++ b/controlloop/common/feature-controlloop-management/src/main/feature/config/GUARD-http-client.properties
@@ -0,0 +1,27 @@
+# ============LICENSE_START=======================================================
+# ONAP
+# ================================================================================
+# Copyright (C) 2020 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=========================================================
+
+http.client.services=GUARD
+
+http.client.services.GUARD.managed=true
+http.client.services.GUARD.https=true
+http.client.services.GUARD.host=${env:PDP_HOST}
+http.client.services.GUARD.port=${env:PDP_PORT}
+http.client.services.GUARD.username=${env:PDP_USERNAME}
+http.client.services.GUARD.password=${env:PDP_PASSWORD}
+http.client.services.GUARD.contextUriPath=policy/pdpx/v1
diff --git a/controlloop/common/feature-controlloop-management/src/main/feature/config/SDNC-http-client.properties b/controlloop/common/feature-controlloop-management/src/main/feature/config/SDNC-http-client.properties
new file mode 100644
index 0000000..ad11c3f
--- /dev/null
+++ b/controlloop/common/feature-controlloop-management/src/main/feature/config/SDNC-http-client.properties
@@ -0,0 +1,27 @@
+# ============LICENSE_START=======================================================
+# ONAP
+# ================================================================================
+# Copyright (C) 2020 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=========================================================
+
+http.client.services=SDNC
+
+http.client.services.SDNC.managed=true
+http.client.services.SDNC.https=true
+http.client.services.SDNC.host=${env:SDNC_HOST}
+http.client.services.SDNC.port=${env:SDNC_PORT}
+http.client.services.SDNC.username=${env:SDNC_USERNAME}
+http.client.services.SDNC.password=${env:SDNC_PASSWORD}
+http.client.services.SDNC.contextUriPath=
diff --git a/controlloop/common/feature-controlloop-management/src/main/feature/config/SO-http-client.properties b/controlloop/common/feature-controlloop-management/src/main/feature/config/SO-http-client.properties
new file mode 100644
index 0000000..45e77ad
--- /dev/null
+++ b/controlloop/common/feature-controlloop-management/src/main/feature/config/SO-http-client.properties
@@ -0,0 +1,27 @@
+# ============LICENSE_START=======================================================
+# ONAP
+# ================================================================================
+# Copyright (C) 2020 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=========================================================
+
+http.client.services=SO
+
+http.client.services.SO.managed=true
+http.client.services.SO.https=true
+http.client.services.SO.host=${env:SO_HOST}
+http.client.services.SO.port=${env:SO_PORT}
+http.client.services.SO.username=${env:SO_USERNAME}
+http.client.services.SO.password=${env:SO_PASSWORD}
+http.client.services.SO.contextUriPath=
diff --git a/controlloop/common/feature-controlloop-management/src/main/feature/config/VFC-http-client.properties b/controlloop/common/feature-controlloop-management/src/main/feature/config/VFC-http-client.properties
new file mode 100644
index 0000000..28a2a75
--- /dev/null
+++ b/controlloop/common/feature-controlloop-management/src/main/feature/config/VFC-http-client.properties
@@ -0,0 +1,27 @@
+# ============LICENSE_START=======================================================
+# ONAP
+# ================================================================================
+# Copyright (C) 2020 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=========================================================
+
+http.client.services=VFC
+
+http.client.services.VFC.managed=true
+http.client.services.VFC.https=true
+http.client.services.VFC.host=${env:VFC_HOST}
+http.client.services.VFC.port=${env:VFC_PORT}
+http.client.services.VFC.username=${env:VFC_USERNAME}
+http.client.services.VFC.password=${env:VFC_PASSWORD}
+http.client.services.VFC.contextUriPath=api/nslcm/v1
diff --git a/controlloop/common/feature-controlloop-management/src/main/feature/config/event-manager.properties b/controlloop/common/feature-controlloop-management/src/main/feature/config/event-manager.properties
new file mode 100644
index 0000000..d957e40
--- /dev/null
+++ b/controlloop/common/feature-controlloop-management/src/main/feature/config/event-manager.properties
@@ -0,0 +1,72 @@
+#
+# ============LICENSE_START======================================================
+# ONAP
+# ===============================================================================
+# Copyright (C) 2020 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========================================================
+#
+
+# DB parameters
+operation.history.url=jdbc:mariadb://${env:SQL_HOST}:3306/operationshistory
+operation.history.userName=${env:SQL_USER}
+operation.history.password=${env:SQL_PASSWORD}
+
+#
+# Actor parameters
+#
+# Note: every operation must have at least one entry, otherwise it will not be
+# configured and started.  Thus some of them have a "placeholder" property.
+#
+
+actor.service.GUARD.disabled=true
+actor.service.GUARD.clientName=GUARD
+actor.service.GUARD.operations.Decision.path=decision
+
+actor.service.AAI.clientName=AAI
+actor.service.AAI.operations.CustomQuery.path=aai/v16/query
+actor.service.AAI.operations.Tenant.path=aai/v16/search/nodes-query
+
+actor.service.APPC.sinkTopic=APPC-LCM-READ
+actor.service.APPC.sourceTopic=APPC-LCM-WRITE
+actor.service.APPC.operations.ConfigModify.placeholder=
+actor.service.APPC.operations.Migrate.placeholder=
+actor.service.APPC.operations.Restart.placeholder=
+actor.service.APPC.operations.Rebuild.placeholder=
+
+# legacy APPC - must specify sink and source for each operation
+actor.service.APPC.operations.ModifyConfig.sinkTopic=APPC-CL
+actor.service.APPC.operations.ModifyConfig.sourceTopic=APPC-CL
+
+actor.service.CDS.clientName=CDS
+actor.service.CDS.grpcHost=${envd:CDS_GRPC_HOST:}
+actor.service.CDS.grpcPort=${envd:CDS_GRPC_PORT:}
+actor.service.CDS.grpcUsername=${envd:CDS_GRPC_USERNAME:}
+actor.service.CDS.grpcPassword=${envd:CDS_GRPC_PASSWORD:}
+actor.service.CDS.grpcTimeout=10
+actor.service.CDS.operations.gRPC.placeholder=
+
+actor.service.SDNC.clientName=SDNC
+actor.service.SDNC.operations.Reroute.path=
+actor.service.SDNC.operations.BandwidthOnDemand.path=
+
+actor.service.SO.clientName=SO
+actor.service.SO.pathGet=orchestrationRequests/v5/
+actor.service.SO.maxGets=20
+actor.service.SO.waitSecGet=20
+actor.service.SO.operations.VF\ Module\ Create.path=serviceInstantiation/v7/serviceInstances
+actor.service.SO.operations.VF\ Module\ Delete.path=serviceInstances/v7
+
+actor.service.VFC.clientName=VFC
+actor.service.VFC.operations.Restart.path=
diff --git a/controlloop/common/pom.xml b/controlloop/common/pom.xml
index a50e3d4..e383707 100644
--- a/controlloop/common/pom.xml
+++ b/controlloop/common/pom.xml
@@ -38,11 +38,13 @@
     <module>coordination</module>
     <module>eventmanager</module>
     <module>domains</module>
+    <module>controller-frankfurt</module>
     <module>controller-usecases</module>
     <module>feature-controlloop-utils</module>
     <module>feature-controlloop-trans</module>
     <module>feature-controlloop-management</module>
     <module>feature-controlloop-amsterdam</module>
+    <module>feature-controlloop-frankfurt</module>
     <module>feature-controlloop-usecases</module>
   </modules>