diff options
Diffstat (limited to 'models-interactions/model-actors')
83 files changed, 8588 insertions, 3366 deletions
diff --git a/models-interactions/model-actors/actor.aai/pom.xml b/models-interactions/model-actors/actor.aai/pom.xml new file mode 100644 index 000000000..4e932a11b --- /dev/null +++ b/models-interactions/model-actors/actor.aai/pom.xml @@ -0,0 +1,83 @@ +<?xml version="1.0"?> +<!-- + ============LICENSE_START======================================================= + Copyright (C) 2018 Huawei Intellectual Property. All rights reserved. + Modifications Copyright (C) 2019-2020 Nordix Foundation. + 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.models.policy-models-interactions.model-actors</groupId> + <artifactId>model-actors</artifactId> + <version>2.2.1-SNAPSHOT</version> + </parent> + + <artifactId>actor.aai</artifactId> + + <dependencies> + <dependency> + <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId> + <artifactId>actorServiceProvider</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId> + <artifactId>aai</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId> + <artifactId>events</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.common</groupId> + <artifactId>policy-endpoints</artifactId> + <version>${policy.common.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId> + <artifactId>actor.test</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.models.policy-models-interactions</groupId> + <artifactId>simulators</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.powermock</groupId> + <artifactId>powermock-api-mockito2</artifactId> + <scope>test</scope> + </dependency> + </dependencies> +</project> diff --git a/models-interactions/model-actors/actor.aai/src/main/java/org/onap/policy/controlloop/actor/aai/AaiActorServiceProvider.java b/models-interactions/model-actors/actor.aai/src/main/java/org/onap/policy/controlloop/actor/aai/AaiActorServiceProvider.java new file mode 100644 index 000000000..df427c32c --- /dev/null +++ b/models-interactions/model-actors/actor.aai/src/main/java/org/onap/policy/controlloop/actor/aai/AaiActorServiceProvider.java @@ -0,0 +1,47 @@ +/*- + * ============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.actor.aai; + +import org.onap.policy.aai.AaiConstants; +import org.onap.policy.controlloop.actorserviceprovider.impl.HttpActor; +import org.onap.policy.controlloop.actorserviceprovider.impl.HttpOperator; + +/** + * A&AI Actor. + */ +public class AaiActorServiceProvider extends HttpActor { + public static final String NAME = AaiConstants.ACTOR_NAME; + + /** + * Constructs the object. + */ + public AaiActorServiceProvider() { + super(NAME); + + addOperator(HttpOperator.makeOperator(NAME, AaiCustomQueryOperation.NAME, + AaiCustomQueryOperation::new)); + + // add all "get" operators + for (String operation : AaiGetOperation.OPERATIONS) { + addOperator(HttpOperator.makeOperator(NAME, operation, AaiGetOperation::new)); + } + } +} diff --git a/models-interactions/model-actors/actor.aai/src/main/java/org/onap/policy/controlloop/actor/aai/AaiCustomQueryOperation.java b/models-interactions/model-actors/actor.aai/src/main/java/org/onap/policy/controlloop/actor/aai/AaiCustomQueryOperation.java new file mode 100644 index 000000000..e32734b7d --- /dev/null +++ b/models-interactions/model-actors/actor.aai/src/main/java/org/onap/policy/controlloop/actor/aai/AaiCustomQueryOperation.java @@ -0,0 +1,132 @@ +/*- + * ============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.actor.aai; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.onap.policy.aai.AaiConstants; +import org.onap.policy.aai.AaiCqResponse; +import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure; +import org.onap.policy.common.endpoints.utils.NetLoggerUtil.EventType; +import org.onap.policy.common.utils.coder.StandardCoderObject; +import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome; +import org.onap.policy.controlloop.actorserviceprovider.impl.HttpOperation; +import org.onap.policy.controlloop.actorserviceprovider.impl.HttpOperator; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A&AI Custom Query. Stores the {@link AaiCqResponse} in the context. In addition, if the + * context does not contain the "tenant" data for the vserver, then it will request that, + * as well. + */ +public class AaiCustomQueryOperation extends HttpOperation<String> { + private static final Logger logger = LoggerFactory.getLogger(AaiCustomQueryOperation.class); + + public static final String NAME = "CustomQuery"; + + public static final String RESOURCE_LINK = "resource-link"; + public static final String RESULT_DATA = "result-data"; + + private static final String PREFIX = "/aai/v16"; + + /** + * Constructs the object. + * + * @param params operation parameters + * @param operator operator that created this operation + */ + public AaiCustomQueryOperation(ControlLoopOperationParams params, HttpOperator operator) { + super(params, operator, String.class); + } + + /** + * Queries the vserver, if necessary. + */ + @Override + protected CompletableFuture<OperationOutcome> startPreprocessorAsync() { + String vserver = params.getTargetEntity(); + + ControlLoopOperationParams tenantParams = params.toBuilder().actor(AaiConstants.ACTOR_NAME) + .operation(AaiGetOperation.TENANT).payload(null).retry(null).timeoutSec(null).build(); + + return params.getContext().obtain(AaiGetOperation.getTenantKey(vserver), tenantParams); + } + + @Override + protected CompletableFuture<OperationOutcome> startOperationAsync(int attempt, OperationOutcome outcome) { + + Map<String, String> request = makeRequest(); + + Entity<Map<String, String>> entity = Entity.entity(request, MediaType.APPLICATION_JSON); + + Map<String, Object> headers = makeHeaders(); + + headers.put("Accept", MediaType.APPLICATION_JSON); + String url = makeUrl(); + + logMessage(EventType.OUT, CommInfrastructure.REST, url, request); + + // @formatter:off + return handleResponse(outcome, url, + callback -> operator.getClient().put(callback, makePath(), entity, headers)); + // @formatter:on + } + + /** + * Constructs the custom query using the previously retrieved tenant data. + */ + private Map<String, String> makeRequest() { + String vserver = params.getTargetEntity(); + StandardCoderObject tenant = params.getContext().getProperty(AaiGetOperation.getTenantKey(vserver)); + + String resourceLink = tenant.getString(RESULT_DATA, 0, RESOURCE_LINK); + if (resourceLink == null) { + throw new IllegalArgumentException("cannot perform custom query - no resource-link"); + } + + resourceLink = resourceLink.replace(PREFIX, ""); + + return Map.of("start", resourceLink, "query", "query/closed-loop"); + } + + @Override + protected Map<String, Object> makeHeaders() { + return AaiUtil.makeHeaders(params); + } + + /** + * Injects the response into the context. + */ + @Override + protected CompletableFuture<OperationOutcome> postProcessResponse(OperationOutcome outcome, String url, + Response rawResponse, String response) { + + logger.info("{}: caching response for {}", getFullName(), params.getRequestId()); + params.getContext().setProperty(AaiCqResponse.CONTEXT_KEY, new AaiCqResponse(response)); + + return super.postProcessResponse(outcome, url, rawResponse, response); + } +} diff --git a/models-interactions/model-actors/actor.aai/src/main/java/org/onap/policy/controlloop/actor/aai/AaiGetOperation.java b/models-interactions/model-actors/actor.aai/src/main/java/org/onap/policy/controlloop/actor/aai/AaiGetOperation.java new file mode 100644 index 000000000..ee1c4612d --- /dev/null +++ b/models-interactions/model-actors/actor.aai/src/main/java/org/onap/policy/controlloop/actor/aai/AaiGetOperation.java @@ -0,0 +1,137 @@ +/*- + * ============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.actor.aai; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.onap.policy.aai.AaiConstants; +import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure; +import org.onap.policy.common.endpoints.utils.NetLoggerUtil.EventType; +import org.onap.policy.common.utils.coder.StandardCoderObject; +import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome; +import org.onap.policy.controlloop.actorserviceprovider.impl.HttpOperation; +import org.onap.policy.controlloop.actorserviceprovider.impl.HttpOperator; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Superclass of A&AI operators that use "get" to perform their request and store their + * response within the context as a {@link StandardCoderObject}. The property name under + * which they are stored is ${actor}.${operation}.${targetEntity}. + */ +public class AaiGetOperation extends HttpOperation<StandardCoderObject> { + private static final Logger logger = LoggerFactory.getLogger(AaiGetOperation.class); + + public static final int DEFAULT_RETRY = 3; + + // operation names + public static final String TENANT = "Tenant"; + + // property prefixes + private static final String TENANT_KEY_PREFIX = AaiConstants.CONTEXT_PREFIX + TENANT + "."; + + /** + * Operation names supported by this operator. + */ + public static final Set<String> OPERATIONS = Set.of(TENANT); + + + /** + * Responses that are retrieved from A&AI are placed in the operation context under + * the name "${propertyPrefix}.${targetEntity}". + */ + private final String propertyPrefix; + + /** + * Constructs the object. + * + * @param params operation parameters + * @param operator operator that created this operation + */ + public AaiGetOperation(ControlLoopOperationParams params, HttpOperator operator) { + super(params, operator, StandardCoderObject.class); + this.propertyPrefix = operator.getFullName() + "."; + } + + /** + * Gets the "context key" for the tenant query response associated with the given + * target entity. + * + * @param targetEntity target entity + * @return the "context key" for the response associated with the given target + */ + public static String getTenantKey(String targetEntity) { + return (TENANT_KEY_PREFIX + targetEntity); + } + + @Override + protected CompletableFuture<OperationOutcome> startOperationAsync(int attempt, OperationOutcome outcome) { + + Map<String, Object> headers = makeHeaders(); + + headers.put("Accept", MediaType.APPLICATION_JSON); + String url = makeUrl(); + + logMessage(EventType.OUT, CommInfrastructure.REST, url, null); + + // @formatter:off + return handleResponse(outcome, url, + callback -> operator.getClient().get(callback, makePath(), headers)); + // @formatter:on + } + + @Override + protected Map<String, Object> makeHeaders() { + return AaiUtil.makeHeaders(params); + } + + @Override + public String makePath() { + return (operator.getPath() + "/" + params.getTargetEntity()); + } + + /** + * Injects the response into the context. + */ + @Override + protected CompletableFuture<OperationOutcome> postProcessResponse(OperationOutcome outcome, String url, + Response rawResponse, StandardCoderObject response) { + String entity = params.getTargetEntity(); + + logger.info("{}: caching response of {} for {}", getFullName(), entity, params.getRequestId()); + + params.getContext().setProperty(propertyPrefix + entity, response); + + return super.postProcessResponse(outcome, url, rawResponse, response); + } + + /** + * Provides a default retry value, if none specified. + */ + @Override + protected int getRetry(Integer retry) { + return (retry == null ? DEFAULT_RETRY : retry); + } +} diff --git a/models-interactions/model-actors/actor.aai/src/main/java/org/onap/policy/controlloop/actor/aai/AaiUtil.java b/models-interactions/model-actors/actor.aai/src/main/java/org/onap/policy/controlloop/actor/aai/AaiUtil.java new file mode 100644 index 000000000..14edc3aa1 --- /dev/null +++ b/models-interactions/model-actors/actor.aai/src/main/java/org/onap/policy/controlloop/actor/aai/AaiUtil.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.actor.aai; + +import java.util.HashMap; +import java.util.Map; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; + +/** + * Utilities used by A&AI classes. + */ +public class AaiUtil { + + private AaiUtil() { + // do nothing + } + + /** + * Makes standard request headers for A&AI requests. + * + * @param params operation parameters + * @return new request headers + */ + public static Map<String, Object> makeHeaders(ControlLoopOperationParams params) { + Map<String, Object> headers = new HashMap<>(); + + headers.put("X-FromAppId", "POLICY"); + headers.put("X-TransactionId", params.getRequestId().toString()); + + return headers; + } +} diff --git a/models-interactions/model-actors/actor.aai/src/main/resources/META-INF/services/org.onap.policy.controlloop.actorServiceProvider.spi.Actor b/models-interactions/model-actors/actor.aai/src/main/resources/META-INF/services/org.onap.policy.controlloop.actorServiceProvider.spi.Actor new file mode 100644 index 000000000..6a52e3f17 --- /dev/null +++ b/models-interactions/model-actors/actor.aai/src/main/resources/META-INF/services/org.onap.policy.controlloop.actorServiceProvider.spi.Actor @@ -0,0 +1 @@ +org.onap.policy.controlloop.actor.aai.AaiActorServiceProvider diff --git a/models-interactions/model-actors/actor.aai/src/test/java/org/onap/policy/controlloop/actor/aai/AaiActorServiceProviderTest.java b/models-interactions/model-actors/actor.aai/src/test/java/org/onap/policy/controlloop/actor/aai/AaiActorServiceProviderTest.java new file mode 100644 index 000000000..513f339fb --- /dev/null +++ b/models-interactions/model-actors/actor.aai/src/test/java/org/onap/policy/controlloop/actor/aai/AaiActorServiceProviderTest.java @@ -0,0 +1,48 @@ +/*- + * ============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.actor.aai; + +import static org.junit.Assert.assertEquals; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.Test; + +public class AaiActorServiceProviderTest { + + @Test + public void testAaiActorServiceProvider() { + final AaiActorServiceProvider prov = new AaiActorServiceProvider(); + + // verify that it has the operators we expect + List<String> expected = new LinkedList<>(); + expected.add(AaiCustomQueryOperation.NAME); + expected.addAll(AaiGetOperation.OPERATIONS); + + Collections.sort(expected); + + var actual = prov.getOperationNames().stream().sorted().collect(Collectors.toList()); + + assertEquals(expected.toString(), actual.toString()); + } +} diff --git a/models-interactions/model-actors/actor.aai/src/test/java/org/onap/policy/controlloop/actor/aai/AaiCustomQueryOperationTest.java b/models-interactions/model-actors/actor.aai/src/test/java/org/onap/policy/controlloop/actor/aai/AaiCustomQueryOperationTest.java new file mode 100644 index 000000000..a93508757 --- /dev/null +++ b/models-interactions/model-actors/actor.aai/src/test/java/org/onap/policy/controlloop/actor/aai/AaiCustomQueryOperationTest.java @@ -0,0 +1,200 @@ +/*- + * ============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.actor.aai; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.onap.policy.aai.AaiConstants; +import org.onap.policy.aai.AaiCqResponse; +import org.onap.policy.common.endpoints.http.client.HttpClientFactory; +import org.onap.policy.common.utils.coder.StandardCoder; +import org.onap.policy.common.utils.coder.StandardCoderObject; +import org.onap.policy.controlloop.actorserviceprovider.Operation; +import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome; +import org.onap.policy.controlloop.actorserviceprovider.Util; +import org.onap.policy.controlloop.actorserviceprovider.impl.HttpOperator; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; +import org.onap.policy.controlloop.actorserviceprovider.parameters.HttpParams; +import org.onap.policy.controlloop.actorserviceprovider.spi.Actor; +import org.onap.policy.controlloop.policy.PolicyResult; + +public class AaiCustomQueryOperationTest extends BasicAaiOperation<Map<String, String>> { + private static final StandardCoder coder = new StandardCoder(); + + private static final String MY_LINK = "my-link"; + + @Mock + private Actor tenantActor; + + private AaiCustomQueryOperation oper; + + public AaiCustomQueryOperationTest() { + super(AaiConstants.ACTOR_NAME, AaiCustomQueryOperation.NAME); + } + + /** + * Sets up. + */ + @Before + public void setUp() throws Exception { + super.setUp(); + + MyTenantOperator tenantOperator = new MyTenantOperator(); + + when(service.getActor(AaiConstants.ACTOR_NAME)).thenReturn(tenantActor); + when(tenantActor.getOperator(AaiGetOperation.TENANT)).thenReturn(tenantOperator); + + oper = new AaiCustomQueryOperation(params, operator); + } + + @Test + public void testAaiCustomQueryOperation() { + assertEquals(AaiConstants.ACTOR_NAME, oper.getActorName()); + assertEquals(AaiCustomQueryOperation.NAME, oper.getName()); + } + + @Test + public void testStartOperationAsync_testStartPreprocessorAsync_testMakeRequest_testPostProcess() throws Exception { + // need two responses + when(rawResponse.readEntity(String.class)).thenReturn(makeTenantReply()).thenReturn(makeCqReply()); + when(client.get(any(), any(), any())).thenAnswer(provideResponse(rawResponse)); + when(client.put(any(), any(), any(), any())).thenAnswer(provideResponse(rawResponse)); + + CompletableFuture<OperationOutcome> future2 = oper.start(); + + assertEquals(PolicyResult.SUCCESS, getResult(future2)); + + // tenant response should have been cached within the context + assertNotNull(context.getProperty(AaiGetOperation.getTenantKey(TARGET_ENTITY))); + + // custom query response should have been cached within the context + AaiCqResponse cqData = context.getProperty(AaiCqResponse.CONTEXT_KEY); + assertNotNull(cqData); + } + + /** + * Tests when preprocessor step is not needed. + */ + @Test + public void testStartOperationAsync_testStartPreprocessorAsyncNotNeeded() throws Exception { + // pre-load the tenant data + final StandardCoderObject data = preloadTenantData(); + + // only need one response + when(rawResponse.readEntity(String.class)).thenReturn(makeCqReply()); + when(client.put(any(), any(), any(), any())).thenAnswer(provideResponse(rawResponse)); + + CompletableFuture<OperationOutcome> future2 = oper.start(); + + assertEquals(PolicyResult.SUCCESS, getResult(future2)); + + // should not have replaced tenant response + assertSame(data, context.getProperty(AaiGetOperation.getTenantKey(TARGET_ENTITY))); + + // custom query response should have been cached within the context + AaiCqResponse cqData = context.getProperty(AaiCqResponse.CONTEXT_KEY); + assertNotNull(cqData); + } + + @Test + public void testMakeHeaders() { + verifyHeaders(oper.makeHeaders()); + } + + @Test + public void testMakeRequestNoResourceLink() throws Exception { + // pre-load EMPTY tenant data + preloadTenantData(new StandardCoderObject()); + + when(rawResponse.readEntity(String.class)).thenReturn(makeCqReply()); + when(client.put(any(), any(), any(), any())).thenAnswer(provideResponse(rawResponse)); + + CompletableFuture<OperationOutcome> future2 = oper.start(); + + assertEquals(PolicyResult.FAILURE_EXCEPTION, getResult(future2)); + } + + private String makeTenantReply() throws Exception { + Map<String, String> links = Map.of(AaiCustomQueryOperation.RESOURCE_LINK, MY_LINK); + List<Map<String, String>> data = Arrays.asList(links); + + Map<String, Object> reply = Map.of(AaiCustomQueryOperation.RESULT_DATA, data); + return coder.encode(reply); + } + + private String makeCqReply() { + return "{}"; + } + + private StandardCoderObject preloadTenantData() throws Exception { + StandardCoderObject data = coder.decode(makeTenantReply(), StandardCoderObject.class); + preloadTenantData(data); + return data; + } + + private void preloadTenantData(StandardCoderObject data) { + context.setProperty(AaiGetOperation.getTenantKey(TARGET_ENTITY), data); + } + + private PolicyResult getResult(CompletableFuture<OperationOutcome> future2) + throws InterruptedException, ExecutionException, TimeoutException { + + executor.runAll(100); + assertTrue(future2.isDone()); + + return future2.get().getResult(); + } + + protected class MyTenantOperator extends HttpOperator { + public MyTenantOperator() { + super(AaiConstants.ACTOR_NAME, AaiGetOperation.TENANT); + + HttpParams http = HttpParams.builder().clientName(MY_CLIENT).path(PATH).timeoutSec(1).build(); + + configure(Util.translateToMap(AaiGetOperation.TENANT, http)); + start(); + } + + @Override + public Operation buildOperation(ControlLoopOperationParams params) { + return new AaiGetOperation(params, this); + } + + @Override + protected HttpClientFactory getClientFactory() { + return factory; + } + } +} diff --git a/models-interactions/model-actors/actor.aai/src/test/java/org/onap/policy/controlloop/actor/aai/AaiGetOperationTest.java b/models-interactions/model-actors/actor.aai/src/test/java/org/onap/policy/controlloop/actor/aai/AaiGetOperationTest.java new file mode 100644 index 000000000..654864246 --- /dev/null +++ b/models-interactions/model-actors/actor.aai/src/test/java/org/onap/policy/controlloop/actor/aai/AaiGetOperationTest.java @@ -0,0 +1,137 @@ +/*- + * ============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.actor.aai; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import org.junit.Before; +import org.junit.Test; +import org.onap.policy.aai.AaiConstants; +import org.onap.policy.common.utils.coder.StandardCoder; +import org.onap.policy.common.utils.coder.StandardCoderObject; +import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome; +import org.onap.policy.controlloop.policy.PolicyResult; + +public class AaiGetOperationTest extends BasicAaiOperation<Void> { + + private static final String INPUT_FIELD = "input"; + private static final String TEXT = "my-text"; + + private AaiGetOperation oper; + + public AaiGetOperationTest() { + super(AaiConstants.ACTOR_NAME, AaiGetOperation.TENANT); + } + + /** + * Sets up. + */ + @Before + public void setUp() throws Exception { + super.setUp(); + oper = new AaiGetOperation(params, operator); + } + + @Test + public void testGetRetry() { + // use default if null retry + assertEquals(AaiGetOperation.DEFAULT_RETRY, oper.getRetry(null)); + + // otherwise, use specified value + assertEquals(0, oper.getRetry(0)); + assertEquals(10, oper.getRetry(10)); + } + + @Test + public void testStartOperationAsync_testStartQueryAsync_testPostProcessResponse() throws Exception { + + // return a map in the reply + Map<String, String> reply = Map.of(INPUT_FIELD, TEXT); + when(rawResponse.readEntity(String.class)).thenReturn(new StandardCoder().encode(reply)); + + when(client.get(any(), any(), any())).thenAnswer(provideResponse(rawResponse)); + + CompletableFuture<OperationOutcome> future2 = oper.startOperationAsync(1, outcome); + assertFalse(future2.isDone()); + + executor.runAll(100); + assertTrue(future2.isDone()); + + assertEquals(PolicyResult.SUCCESS, future2.get().getResult()); + + // data should have been cached within the context + StandardCoderObject data = context.getProperty(AaiGetOperation.getTenantKey(TARGET_ENTITY)); + assertNotNull(data); + assertEquals(TEXT, data.getString(INPUT_FIELD)); + } + + /** + * Tests startOperationAsync() when there's a failure. + */ + @Test + public void testStartOperationAsyncFailure() throws Exception { + + when(rawResponse.getStatus()).thenReturn(500); + when(rawResponse.readEntity(String.class)).thenReturn(""); + + when(client.get(any(), any(), any())).thenAnswer(provideResponse(rawResponse)); + + CompletableFuture<OperationOutcome> future2 = oper.startOperationAsync(1, outcome); + assertFalse(future2.isDone()); + + executor.runAll(100); + assertTrue(future2.isDone()); + + assertEquals(PolicyResult.FAILURE, future2.get().getResult()); + + // data should NOT have been cached within the context + assertNull(context.getProperty(AaiGetOperation.getTenantKey(TARGET_ENTITY))); + } + + @Test + public void testMakeHeaders() { + verifyHeaders(oper.makeHeaders()); + } + + @Test + public void testMakePath() { + assertEquals(PATH + "/" + TARGET_ENTITY, oper.makePath()); + } + + @Test + public void testAaiGetOperator() { + assertEquals(AaiConstants.ACTOR_NAME, oper.getActorName()); + assertEquals(AaiGetOperation.TENANT, oper.getName()); + } + + @Test + public void testGetTenantKey() { + assertEquals("AAI.Tenant." + TARGET_ENTITY, AaiGetOperation.getTenantKey(TARGET_ENTITY)); + } +} diff --git a/models-interactions/model-actors/actor.aai/src/test/java/org/onap/policy/controlloop/actor/aai/AaiUtilTest.java b/models-interactions/model-actors/actor.aai/src/test/java/org/onap/policy/controlloop/actor/aai/AaiUtilTest.java new file mode 100644 index 000000000..ae38cca35 --- /dev/null +++ b/models-interactions/model-actors/actor.aai/src/test/java/org/onap/policy/controlloop/actor/aai/AaiUtilTest.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.actor.aai; + +import java.util.Map; +import org.junit.Test; + +public class AaiUtilTest extends BasicAaiOperation<Void> { + + @Test + public void testMakeHeaders() { + makeContext(); + + Map<String, Object> headers = AaiUtil.makeHeaders(params); + + verifyHeaders(headers); + } +} diff --git a/models-interactions/model-actors/actor.aai/src/test/java/org/onap/policy/controlloop/actor/aai/BasicAaiOperation.java b/models-interactions/model-actors/actor.aai/src/test/java/org/onap/policy/controlloop/actor/aai/BasicAaiOperation.java new file mode 100644 index 000000000..00485c935 --- /dev/null +++ b/models-interactions/model-actors/actor.aai/src/test/java/org/onap/policy/controlloop/actor/aai/BasicAaiOperation.java @@ -0,0 +1,54 @@ +/*- + * ============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.actor.aai; + +import static org.junit.Assert.assertEquals; + +import java.util.Map; +import org.onap.policy.controlloop.actor.test.BasicHttpOperation; + +/** + * Superclass for various operator tests. + */ +public abstract class BasicAaiOperation<Q> extends BasicHttpOperation<Q> { + + /** + * Constructs the object using a default actor and operation name. + */ + public BasicAaiOperation() { + super(); + } + + /** + * Constructs the object. + * + * @param actor actor name + * @param operation operation name + */ + public BasicAaiOperation(String actor, String operation) { + super(actor, operation); + } + + protected void verifyHeaders(Map<String, Object> headers) { + assertEquals("POLICY", headers.get("X-FromAppId").toString()); + assertEquals(params.getRequestId().toString(), headers.get("X-TransactionId")); + } +} diff --git a/models-interactions/model-actors/actor.appc/pom.xml b/models-interactions/model-actors/actor.appc/pom.xml index 74bff9aa9..0cc243c3f 100644 --- a/models-interactions/model-actors/actor.appc/pom.xml +++ b/models-interactions/model-actors/actor.appc/pom.xml @@ -18,57 +18,82 @@ ============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> +<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.models.policy-models-interactions.model-actors</groupId> - <artifactId>model-actors</artifactId> - <version>2.2.1-SNAPSHOT</version> - </parent> + <parent> + <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId> + <artifactId>model-actors</artifactId> + <version>2.2.1-SNAPSHOT</version> + </parent> - <artifactId>actor.appc</artifactId> + <artifactId>actor.appc</artifactId> - <dependencies> - <dependency> - <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId> - <artifactId>actorServiceProvider</artifactId> - <version>${project.version}</version> - <scope>provided</scope> - </dependency> - <dependency> - <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId> - <artifactId>appc</artifactId> - <version>${project.version}</version> - <scope>provided</scope> - </dependency> - <dependency> - <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId> - <artifactId>events</artifactId> - <version>${project.version}</version> - <scope>provided</scope> - </dependency> - <dependency> - <groupId>com.google.code.gson</groupId> - <artifactId>gson</artifactId> - <scope>test</scope> - </dependency> - <dependency> - <groupId>junit</groupId> - <artifactId>junit</artifactId> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.onap.policy.models.policy-models-interactions</groupId> - <artifactId>simulators</artifactId> - <version>${project.version}</version> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.onap.policy.common</groupId> - <artifactId>policy-endpoints</artifactId> - <version>${policy.common.version}</version> - <scope>provided</scope> - </dependency> - </dependencies> + <dependencies> + <dependency> + <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId> + <artifactId>actorServiceProvider</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId> + <artifactId>appc</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId> + <artifactId>aai</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId> + <artifactId>events</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId> + <artifactId>actor.aai</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.google.code.gson</groupId> + <artifactId>gson</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.models.policy-models-interactions</groupId> + <artifactId>simulators</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.common</groupId> + <artifactId>policy-endpoints</artifactId> + <version>${policy.common.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId> + <artifactId>actor.test</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.powermock</groupId> + <artifactId>powermock-api-mockito2</artifactId> + <scope>test</scope> + </dependency> + </dependencies> </project> diff --git a/models-interactions/model-actors/actor.appc/src/main/java/org/onap/policy/controlloop/actor/appc/AppcActorServiceProvider.java b/models-interactions/model-actors/actor.appc/src/main/java/org/onap/policy/controlloop/actor/appc/AppcActorServiceProvider.java index 0da1e2a27..2491c33a1 100644 --- a/models-interactions/model-actors/actor.appc/src/main/java/org/onap/policy/controlloop/actor/appc/AppcActorServiceProvider.java +++ b/models-interactions/model-actors/actor.appc/src/main/java/org/onap/policy/controlloop/actor/appc/AppcActorServiceProvider.java @@ -33,17 +33,19 @@ import org.onap.policy.common.utils.coder.CoderException; import org.onap.policy.common.utils.coder.StandardCoder; import org.onap.policy.controlloop.ControlLoopOperation; import org.onap.policy.controlloop.VirtualControlLoopEvent; -import org.onap.policy.controlloop.actorserviceprovider.impl.ActorImpl; +import org.onap.policy.controlloop.actorserviceprovider.impl.BidirectionalTopicActor; import org.onap.policy.controlloop.policy.Policy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class AppcActorServiceProvider extends ActorImpl { +public class AppcActorServiceProvider extends BidirectionalTopicActor { private static final String NAME = "APPC"; private static final Logger logger = LoggerFactory.getLogger(AppcActorServiceProvider.class); + // TODO old code: remove lines down to **HERE** + private static final StandardCoder coder = new StandardCoder(); // Strings for targets @@ -57,17 +59,26 @@ public class AppcActorServiceProvider extends ActorImpl { private static final String RECIPE_MODIFY = "ModifyConfig"; private static final ImmutableList<String> recipes = - ImmutableList.of(RECIPE_RESTART, RECIPE_REBUILD, RECIPE_MIGRATE, RECIPE_MODIFY); + ImmutableList.of(RECIPE_RESTART, RECIPE_REBUILD, RECIPE_MIGRATE, RECIPE_MODIFY); private static final ImmutableMap<String, List<String>> targets = new ImmutableMap.Builder<String, List<String>>() - .put(RECIPE_RESTART, ImmutableList.of(TARGET_VM)).put(RECIPE_REBUILD, ImmutableList.of(TARGET_VM)) - .put(RECIPE_MIGRATE, ImmutableList.of(TARGET_VM)).put(RECIPE_MODIFY, ImmutableList.of(TARGET_VNF)).build(); + .put(RECIPE_RESTART, ImmutableList.of(TARGET_VM)).put(RECIPE_REBUILD, ImmutableList.of(TARGET_VM)) + .put(RECIPE_MIGRATE, ImmutableList.of(TARGET_VM)).put(RECIPE_MODIFY, ImmutableList.of(TARGET_VNF)) + .build(); private static final ImmutableMap<String, List<String>> payloads = new ImmutableMap.Builder<String, List<String>>() - .put(RECIPE_MODIFY, ImmutableList.of("generic-vnf.vnf-id")).build(); + .put(RECIPE_MODIFY, ImmutableList.of("generic-vnf.vnf-id")).build(); + + // **HERE** + /** + * Constructs the object. + */ public AppcActorServiceProvider() { super(NAME); } + + // TODO old code: remove lines down to **HERE** + @Override public String actor() { return NAME; @@ -89,17 +100,19 @@ public class AppcActorServiceProvider extends ActorImpl { } /** - * Constructs an APPC request conforming to the legacy API. The legacy API will be deprecated in - * future releases as all legacy functionality is moved into the LCM API. + * Constructs an APPC request conforming to the legacy API. The legacy API will be + * deprecated in future releases as all legacy functionality is moved into the LCM + * API. * * @param onset the event that is reporting the alert for policy to perform an action - * @param operation the control loop operation specifying the actor, operation, target, etc. - * @param policy the policy the was specified from the yaml generated by CLAMP or through the - * Policy GUI/API + * @param operation the control loop operation specifying the actor, operation, + * target, etc. + * @param policy the policy the was specified from the yaml generated by CLAMP or + * through the Policy GUI/API * @return an APPC request conforming to the legacy API */ public static Request constructRequest(VirtualControlLoopEvent onset, ControlLoopOperation operation, Policy policy, - String targetVnf) { + String targetVnf) { /* * Construct an APPC request */ @@ -144,4 +157,5 @@ public class AppcActorServiceProvider extends ActorImpl { } } + // **HERE** } diff --git a/models-interactions/model-actors/actor.sdnc/pom.xml b/models-interactions/model-actors/actor.sdnc/pom.xml index 04040dbbf..4bb03ecec 100644 --- a/models-interactions/model-actors/actor.sdnc/pom.xml +++ b/models-interactions/model-actors/actor.sdnc/pom.xml @@ -19,58 +19,71 @@ ============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> +<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.models.policy-models-interactions.model-actors</groupId> - <artifactId>model-actors</artifactId> - <version>2.2.1-SNAPSHOT</version> - </parent> + <parent> + <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId> + <artifactId>model-actors</artifactId> + <version>2.2.1-SNAPSHOT</version> + </parent> - <artifactId>actor.sdnc</artifactId> + <artifactId>actor.sdnc</artifactId> - <dependencies> - <dependency> - <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId> - <artifactId>actorServiceProvider</artifactId> - <version>${project.version}</version> - <scope>provided</scope> - </dependency> - <dependency> - <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId> - <artifactId>sdnc</artifactId> - <version>${project.version}</version> - <scope>provided</scope> - </dependency> - <dependency> - <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId> - <artifactId>events</artifactId> - <version>${project.version}</version> - <scope>provided</scope> - </dependency> - <dependency> - <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId> - <artifactId>aai</artifactId> - <version>${project.version}</version> - <scope>provided</scope> - </dependency> - <dependency> - <groupId>org.onap.policy.common</groupId> - <artifactId>policy-endpoints</artifactId> - <version>${policy.common.version}</version> - <scope>provided</scope> - </dependency> - <dependency> - <groupId>junit</groupId> - <artifactId>junit</artifactId> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.onap.policy.models.policy-models-interactions</groupId> - <artifactId>simulators</artifactId> - <version>${project.version}</version> - <scope>test</scope> - </dependency> - </dependencies> + <dependencies> + <dependency> + <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId> + <artifactId>actorServiceProvider</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId> + <artifactId>sdnc</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId> + <artifactId>events</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId> + <artifactId>aai</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.common</groupId> + <artifactId>policy-endpoints</artifactId> + <version>${policy.common.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.models.policy-models-interactions.model-actors</groupId> + <artifactId>actor.test</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.models.policy-models-interactions</groupId> + <artifactId>simulators</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.powermock</groupId> + <artifactId>powermock-api-mockito2</artifactId> + <scope>test</scope> + </dependency> + </dependencies> </project> diff --git a/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/BandwidthOnDemandOperator.java b/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/BandwidthOnDemandOperation.java index 2927bd85b..26cdfada3 100644 --- a/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/BandwidthOnDemandOperator.java +++ b/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/BandwidthOnDemandOperation.java @@ -23,6 +23,8 @@ package org.onap.policy.controlloop.actor.sdnc; import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.onap.policy.controlloop.actorserviceprovider.controlloop.ControlLoopEventContext; +import org.onap.policy.controlloop.actorserviceprovider.impl.HttpOperator; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; import org.onap.policy.sdnc.SdncHealRequest; import org.onap.policy.sdnc.SdncHealRequestHeaderInfo; import org.onap.policy.sdnc.SdncHealRequestInfo; @@ -34,7 +36,7 @@ import org.onap.policy.sdnc.SdncHealVfModuleRequestInput; import org.onap.policy.sdnc.SdncHealVnfInfo; import org.onap.policy.sdnc.SdncRequest; -public class BandwidthOnDemandOperator extends SdncOperator { +public class BandwidthOnDemandOperation extends SdncOperation { public static final String NAME = "BandwidthOnDemand"; public static final String URI = "/GENERIC-RESOURCE-API:vf-module-topology-operation"; @@ -46,14 +48,17 @@ public class BandwidthOnDemandOperator extends SdncOperator { /** * Constructs the object. * - * @param actorName name of the actor with which this operator is associated + * @param params operation parameters + * @param operator operator that created this operation */ - public BandwidthOnDemandOperator(String actorName) { - super(actorName, NAME); + public BandwidthOnDemandOperation(ControlLoopOperationParams params, HttpOperator operator) { + super(params, operator); } @Override - protected SdncRequest constructRequest(ControlLoopEventContext context) { + protected SdncRequest makeRequest(int attempt) { + ControlLoopEventContext context = params.getContext(); + String serviceInstance = context.getEnrichment().get(SERVICE_ID_KEY); if (StringUtils.isBlank(serviceInstance)) { throw new IllegalArgumentException("missing enrichment data, " + SERVICE_ID_KEY); diff --git a/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/RerouteOperator.java b/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/RerouteOperation.java index da400f8eb..f255f3e84 100644 --- a/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/RerouteOperator.java +++ b/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/RerouteOperation.java @@ -23,6 +23,8 @@ package org.onap.policy.controlloop.actor.sdnc; import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.onap.policy.controlloop.actorserviceprovider.controlloop.ControlLoopEventContext; +import org.onap.policy.controlloop.actorserviceprovider.impl.HttpOperator; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; import org.onap.policy.sdnc.SdncHealNetworkInfo; import org.onap.policy.sdnc.SdncHealRequest; import org.onap.policy.sdnc.SdncHealRequestHeaderInfo; @@ -30,7 +32,7 @@ import org.onap.policy.sdnc.SdncHealRequestInfo; import org.onap.policy.sdnc.SdncHealServiceInfo; import org.onap.policy.sdnc.SdncRequest; -public class RerouteOperator extends SdncOperator { +public class RerouteOperation extends SdncOperation { public static final String NAME = "Reroute"; public static final String URI = "/GENERIC-RESOURCE-API:network-topology-operation"; @@ -42,14 +44,17 @@ public class RerouteOperator extends SdncOperator { /** * Constructs the object. * - * @param actorName name of the actor with which this operator is associated + * @param params operation parameters + * @param operator operator that created this operation */ - public RerouteOperator(String actorName) { - super(actorName, NAME); + public RerouteOperation(ControlLoopOperationParams params, HttpOperator operator) { + super(params, operator); } @Override - protected SdncRequest constructRequest(ControlLoopEventContext context) { + protected SdncRequest makeRequest(int attempt) { + ControlLoopEventContext context = params.getContext(); + String serviceInstance = context.getEnrichment().get(SERVICE_ID_KEY); if (StringUtils.isBlank(serviceInstance)) { throw new IllegalArgumentException("missing enrichment data, " + SERVICE_ID_KEY); diff --git a/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/SdncActorServiceProvider.java b/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/SdncActorServiceProvider.java index 8dc8ba50d..99a4fdadd 100644 --- a/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/SdncActorServiceProvider.java +++ b/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/SdncActorServiceProvider.java @@ -30,6 +30,7 @@ import java.util.UUID; import org.onap.policy.controlloop.ControlLoopOperation; import org.onap.policy.controlloop.VirtualControlLoopEvent; import org.onap.policy.controlloop.actorserviceprovider.impl.HttpActor; +import org.onap.policy.controlloop.actorserviceprovider.impl.HttpOperator; import org.onap.policy.controlloop.policy.Policy; import org.onap.policy.sdnc.SdncHealNetworkInfo; import org.onap.policy.sdnc.SdncHealRequest; @@ -76,8 +77,11 @@ public class SdncActorServiceProvider extends HttpActor { public SdncActorServiceProvider() { super(NAME); - addOperator(new RerouteOperator(NAME)); - addOperator(new BandwidthOnDemandOperator(NAME)); + addOperator(HttpOperator.makeOperator(NAME, RerouteOperation.NAME, + RerouteOperation::new)); + + addOperator(HttpOperator.makeOperator(NAME, BandwidthOnDemandOperation.NAME, + BandwidthOnDemandOperation::new)); } diff --git a/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/SdncOperation.java b/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/SdncOperation.java new file mode 100644 index 000000000..406722ef5 --- /dev/null +++ b/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/SdncOperation.java @@ -0,0 +1,95 @@ +/*- + * ============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.actor.sdnc; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure; +import org.onap.policy.common.endpoints.utils.NetLoggerUtil.EventType; +import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome; +import org.onap.policy.controlloop.actorserviceprovider.impl.HttpOperation; +import org.onap.policy.controlloop.actorserviceprovider.impl.HttpOperator; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; +import org.onap.policy.sdnc.SdncRequest; +import org.onap.policy.sdnc.SdncResponse; + +/** + * Superclass for SDNC Operators. + */ +public abstract class SdncOperation extends HttpOperation<SdncResponse> { + + /** + * Constructs the object. + * + * @param params operation parameters + * @param operator operator that created this operation + */ + public SdncOperation(ControlLoopOperationParams params, HttpOperator operator) { + super(params, operator, SdncResponse.class); + } + + /** + * Starts the GUARD. + */ + @Override + protected CompletableFuture<OperationOutcome> startPreprocessorAsync() { + return startGuardAsync(); + } + + @Override + protected CompletableFuture<OperationOutcome> startOperationAsync(int attempt, OperationOutcome outcome) { + + SdncRequest request = makeRequest(attempt); + + Entity<SdncRequest> entity = Entity.entity(request, MediaType.APPLICATION_JSON); + + Map<String, Object> headers = makeHeaders(); + + headers.put("Accept", MediaType.APPLICATION_JSON); + String url = makeUrl(); + + logMessage(EventType.OUT, CommInfrastructure.REST, url, request); + + // @formatter:off + return handleResponse(outcome, url, + callback -> operator.getClient().post(callback, makePath(), entity, headers)); + // @formatter:on + } + + /** + * Makes the request. + * + * @param attempt current attempt, starting with "1" + * @return a new request to be posted + */ + protected abstract SdncRequest makeRequest(int attempt); + + /** + * Checks that the response has an "output" and that the output indicates success. + */ + @Override + protected boolean isSuccess(Response rawResponse, SdncResponse response) { + return response.getResponseOutput() != null && "200".equals(response.getResponseOutput().getResponseCode()); + } +} diff --git a/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/SdncOperator.java b/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/SdncOperator.java deleted file mode 100644 index 479ee908d..000000000 --- a/models-interactions/model-actors/actor.sdnc/src/main/java/org/onap/policy/controlloop/actor/sdnc/SdncOperator.java +++ /dev/null @@ -1,148 +0,0 @@ -/*- - * ============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.actor.sdnc; - -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import org.onap.policy.common.endpoints.http.client.HttpClient; -import org.onap.policy.common.utils.coder.CoderException; -import org.onap.policy.common.utils.coder.StandardCoder; -import org.onap.policy.controlloop.actorserviceprovider.AsyncResponseHandler; -import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome; -import org.onap.policy.controlloop.actorserviceprovider.Util; -import org.onap.policy.controlloop.actorserviceprovider.controlloop.ControlLoopEventContext; -import org.onap.policy.controlloop.actorserviceprovider.impl.HttpOperator; -import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; -import org.onap.policy.controlloop.policy.PolicyResult; -import org.onap.policy.sdnc.SdncRequest; -import org.onap.policy.sdnc.SdncResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Superclass for SDNC Operators. - */ -public abstract class SdncOperator extends HttpOperator { - private static final Logger logger = LoggerFactory.getLogger(SdncOperator.class); - - /** - * Constructs the object. - * - * @param actorName name of the actor with which this operator is associated - * @param name operation name - */ - public SdncOperator(String actorName, String name) { - super(actorName, name); - } - - @Override - protected CompletableFuture<OperationOutcome> startOperationAsync(ControlLoopOperationParams params, int attempt, - OperationOutcome outcome) { - - SdncRequest request = constructRequest(params.getContext()); - return postRequest(params, outcome, request); - } - - /** - * Constructs the request. - * - * @param context associated event context - * @return a new request - */ - protected abstract SdncRequest constructRequest(ControlLoopEventContext context); - - /** - * Posts the request and and arranges to retrieve the response. - * - * @param params operation parameters - * @param outcome updated with the response - * @param sdncRequest request to be posted - * @return the result of the request - */ - private CompletableFuture<OperationOutcome> postRequest(ControlLoopOperationParams params, OperationOutcome outcome, - SdncRequest sdncRequest) { - Map<String, Object> headers = new HashMap<>(); - - headers.put("Accept", "application/json"); - String sdncUrl = getClient().getBaseUrl(); - - Util.logRestRequest(sdncUrl, sdncRequest); - - Entity<SdncRequest> entity = Entity.entity(sdncRequest, MediaType.APPLICATION_JSON); - - ResponseHandler handler = new ResponseHandler(params, outcome, sdncUrl); - return handler.handle(getClient().post(handler, getPath(), entity, headers)); - } - - private class ResponseHandler extends AsyncResponseHandler<Response> { - private final String sdncUrl; - - public ResponseHandler(ControlLoopOperationParams params, OperationOutcome outcome, String sdncUrl) { - super(params, outcome); - this.sdncUrl = sdncUrl; - } - - /** - * Handles the response. - */ - @Override - protected OperationOutcome doComplete(Response rawResponse) { - String strResponse = HttpClient.getBody(rawResponse, String.class); - - Util.logRestResponse(sdncUrl, strResponse); - - SdncResponse response; - try { - response = makeDecoder().decode(strResponse, SdncResponse.class); - } catch (CoderException e) { - logger.warn("Sdnc Heal cannot decode response with http error code {}", rawResponse.getStatus(), e); - return SdncOperator.this.setOutcome(getParams(), getOutcome(), PolicyResult.FAILURE_EXCEPTION); - } - - if (response.getResponseOutput() != null && "200".equals(response.getResponseOutput().getResponseCode())) { - return SdncOperator.this.setOutcome(getParams(), getOutcome(), PolicyResult.SUCCESS); - - } else { - logger.info("Sdnc Heal Restcall failed with http error code {}", rawResponse.getStatus()); - return SdncOperator.this.setOutcome(getParams(), getOutcome(), PolicyResult.FAILURE); - } - } - - /** - * Handles exceptions. - */ - @Override - protected OperationOutcome doFailed(Throwable thrown) { - logger.info("Sdnc Heal Restcall threw an exception", thrown); - return SdncOperator.this.setOutcome(getParams(), getOutcome(), PolicyResult.FAILURE_EXCEPTION); - } - } - - // these may be overridden by junit tests - - protected StandardCoder makeDecoder() { - return new StandardCoder(); - } -} diff --git a/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/BandwidthOnDemandOperatorTest.java b/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/BandwidthOnDemandOperationTest.java index 02931a4f1..42042da67 100644 --- a/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/BandwidthOnDemandOperatorTest.java +++ b/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/BandwidthOnDemandOperationTest.java @@ -26,45 +26,51 @@ import static org.junit.Assert.assertNotNull; import java.util.Map; import org.junit.Before; import org.junit.Test; -import org.onap.policy.common.utils.coder.CoderException; import org.onap.policy.sdnc.SdncRequest; -public class BandwidthOnDemandOperatorTest extends BasicOperator { +public class BandwidthOnDemandOperationTest extends BasicSdncOperation { - private BandwidthOnDemandOperator oper; + private BandwidthOnDemandOperation oper; + public BandwidthOnDemandOperationTest() { + super(DEFAULT_ACTOR, BandwidthOnDemandOperation.NAME); + } /** * Set up. */ @Before - public void setUp() { - makeContext(); - oper = new BandwidthOnDemandOperator(ACTOR); + public void setUp() throws Exception { + super.setUp(); + oper = new BandwidthOnDemandOperation(params, operator); } @Test public void testBandwidthOnDemandOperator() { - assertEquals(ACTOR, oper.getActorName()); - assertEquals(BandwidthOnDemandOperator.NAME, oper.getName()); + assertEquals(DEFAULT_ACTOR, oper.getActorName()); + assertEquals(BandwidthOnDemandOperation.NAME, oper.getName()); } @Test - public void testConstructRequest() throws CoderException { - SdncRequest request = oper.constructRequest(context); + public void testMakeRequest() throws Exception { + SdncRequest request = oper.makeRequest(1); assertEquals("my-service", request.getNsInstanceId()); assertEquals(REQ_ID, request.getRequestId()); - assertEquals(BandwidthOnDemandOperator.URI, request.getUrl()); + assertEquals(BandwidthOnDemandOperation.URI, request.getUrl()); assertNotNull(request.getHealRequest().getRequestHeaderInfo().getSvcRequestId()); verifyRequest("bod.json", request); - verifyMissing(oper, BandwidthOnDemandOperator.SERVICE_ID_KEY, "service"); + verifyMissing(BandwidthOnDemandOperation.SERVICE_ID_KEY, "service", BandwidthOnDemandOperation::new); + + // perform the operation + makeContext(); + verifyRequest("bod.json", verifyOperation(oper)); } @Override protected Map<String, String> makeEnrichment() { - return Map.of(BandwidthOnDemandOperator.SERVICE_ID_KEY, "my-service", BandwidthOnDemandOperator.VNF_ID, + return Map.of(BandwidthOnDemandOperation.SERVICE_ID_KEY, "my-service", BandwidthOnDemandOperation.VNF_ID, "my-vnf"); } } diff --git a/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/BasicOperator.java b/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/BasicOperator.java deleted file mode 100644 index b9028d462..000000000 --- a/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/BasicOperator.java +++ /dev/null @@ -1,94 +0,0 @@ -/*- - * ============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.actor.sdnc; - -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.junit.Assert.assertEquals; - -import java.util.Map; -import java.util.TreeMap; -import java.util.UUID; -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.VirtualControlLoopEvent; -import org.onap.policy.controlloop.actorserviceprovider.controlloop.ControlLoopEventContext; - -/** - * Superclass for various operator tests. - */ -public abstract class BasicOperator { - protected static final UUID REQ_ID = UUID.randomUUID(); - protected static final String ACTOR = "my-actor"; - - protected Map<String, String> enrichment; - protected VirtualControlLoopEvent event; - protected ControlLoopEventContext context; - - /** - * Pretty-prints a request and verifies that the result matches the expected JSON. - * - * @param <T> request type - * @param expectedJsonFile name of the file containing the expected JSON - * @param request request to verify - * @throws CoderException if the request cannot be pretty-printed - */ - protected <T> void verifyRequest(String expectedJsonFile, T request) throws CoderException { - String json = new StandardCoder().encode(request, true); - String expected = ResourceUtils.getResourceAsString(expectedJsonFile); - - // strip request id, because it changes each time - final String stripper = "svc-request-id[^,]*"; - json = json.replaceFirst(stripper, "").trim(); - expected = expected.replaceFirst(stripper, "").trim(); - - assertEquals(expected, json); - } - - /** - * Verifies that an exception is thrown if a field is missing from the enrichment - * data. - * - * @param oper operator to construct the request - * @param fieldName name of the field to be removed from the enrichment data - * @param expectedText text expected in the exception message - */ - protected void verifyMissing(SdncOperator oper, String fieldName, String expectedText) { - makeContext(); - enrichment.remove(fieldName); - - assertThatIllegalArgumentException().isThrownBy(() -> oper.constructRequest(context)) - .withMessageContaining("missing").withMessageContaining(expectedText); - } - - protected void makeContext() { - // need a mutable map, so make a copy - enrichment = new TreeMap<>(makeEnrichment()); - - event = new VirtualControlLoopEvent(); - event.setRequestId(REQ_ID); - event.setAai(enrichment); - - context = new ControlLoopEventContext(event); - } - - protected abstract Map<String, String> makeEnrichment(); -} diff --git a/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/BasicSdncOperation.java b/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/BasicSdncOperation.java new file mode 100644 index 000000000..db8751d26 --- /dev/null +++ b/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/BasicSdncOperation.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.actor.sdnc; + +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.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.function.BiFunction; +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.actor.test.BasicHttpOperation; +import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome; +import org.onap.policy.controlloop.actorserviceprovider.impl.HttpOperator; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; +import org.onap.policy.controlloop.policy.PolicyResult; +import org.onap.policy.sdnc.SdncRequest; +import org.onap.policy.sdnc.SdncResponse; +import org.onap.policy.sdnc.SdncResponseOutput; +import org.powermock.reflect.Whitebox; + +/** + * Superclass for various operator tests. + */ +public abstract class BasicSdncOperation extends BasicHttpOperation<SdncRequest> { + + protected SdncResponse response; + + /** + * Constructs the object using a default actor and operation name. + */ + public BasicSdncOperation() { + super(); + } + + /** + * Constructs the object. + * + * @param actor actor name + * @param operation operation name + */ + public BasicSdncOperation(String actor, String operation) { + super(actor, operation); + } + + /** + * Initializes mocks and sets up. + */ + public void setUp() throws Exception { + super.setUp(); + + response = new SdncResponse(); + + SdncResponseOutput output = new SdncResponseOutput(); + response.setResponseOutput(output); + output.setResponseCode("200"); + + when(rawResponse.readEntity(String.class)).thenReturn(new StandardCoder().encode(response)); + } + + /** + * Runs the operation and verifies that the response is successful. + * + * @param operation operation to run + * @return the request that was posted + */ + protected SdncRequest verifyOperation(SdncOperation operation) + throws InterruptedException, ExecutionException, TimeoutException { + + CompletableFuture<OperationOutcome> future2 = operation.start(); + executor.runAll(100); + assertFalse(future2.isDone()); + + verify(client).post(callbackCaptor.capture(), any(), requestCaptor.capture(), any()); + callbackCaptor.getValue().completed(rawResponse); + + executor.runAll(100); + assertTrue(future2.isDone()); + + assertEquals(PolicyResult.SUCCESS, future2.get().getResult()); + + return requestCaptor.getValue().getEntity(); + } + + /** + * Pretty-prints a request and verifies that the result matches the expected JSON. + * + * @param <T> request type + * @param expectedJsonFile name of the file containing the expected JSON + * @param request request to verify + * @throws CoderException if the request cannot be pretty-printed + */ + protected <T> void verifyRequest(String expectedJsonFile, T request) throws CoderException { + String json = new StandardCoder().encode(request, true); + String expected = ResourceUtils.getResourceAsString(expectedJsonFile); + + // strip request id, because it changes each time + final String stripper = "svc-request-id[^,]*"; + json = json.replaceFirst(stripper, "").trim(); + expected = expected.replaceFirst(stripper, "").trim(); + + assertEquals(expected, json); + } + + /** + * Verifies that an exception is thrown if a field is missing from the enrichment + * data. + * + * @param fieldName name of the field to be removed from the enrichment data + * @param expectedText text expected in the exception message + */ + protected void verifyMissing(String fieldName, String expectedText, + BiFunction<ControlLoopOperationParams,HttpOperator,SdncOperation> maker) { + + makeContext(); + enrichment.remove(fieldName); + + SdncOperation oper = maker.apply(params, operator); + + assertThatIllegalArgumentException().isThrownBy(() -> Whitebox.invokeMethod(oper, "makeRequest", 1)) + .withMessageContaining("missing").withMessageContaining(expectedText); + } + + protected abstract Map<String, String> makeEnrichment(); +} diff --git a/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/RerouteOperatorTest.java b/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/RerouteOperationTest.java index 0a7bcad6f..a98c38180 100644 --- a/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/RerouteOperatorTest.java +++ b/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/RerouteOperationTest.java @@ -26,45 +26,51 @@ import static org.junit.Assert.assertNotNull; import java.util.Map; import org.junit.Before; import org.junit.Test; -import org.onap.policy.common.utils.coder.CoderException; import org.onap.policy.sdnc.SdncRequest; -public class RerouteOperatorTest extends BasicOperator { +public class RerouteOperationTest extends BasicSdncOperation { - private RerouteOperator oper; + private RerouteOperation oper; + public RerouteOperationTest() { + super(DEFAULT_ACTOR, RerouteOperation.NAME); + } /** * Set up. */ @Before - public void setUp() { - makeContext(); - oper = new RerouteOperator(ACTOR); + public void setUp() throws Exception { + super.setUp(); + oper = new RerouteOperation(params, operator); } @Test public void testRerouteOperator() { - assertEquals(ACTOR, oper.getActorName()); - assertEquals(RerouteOperator.NAME, oper.getName()); + assertEquals(DEFAULT_ACTOR, oper.getActorName()); + assertEquals(RerouteOperation.NAME, oper.getName()); } @Test - public void testConstructRequest() throws CoderException { - SdncRequest request = oper.constructRequest(context); + public void testMakeRequest() throws Exception { + SdncRequest request = oper.makeRequest(1); assertEquals("my-service", request.getNsInstanceId()); assertEquals(REQ_ID, request.getRequestId()); - assertEquals(RerouteOperator.URI, request.getUrl()); + assertEquals(RerouteOperation.URI, request.getUrl()); assertNotNull(request.getHealRequest().getRequestHeaderInfo().getSvcRequestId()); verifyRequest("reroute.json", request); - verifyMissing(oper, RerouteOperator.SERVICE_ID_KEY, "service"); - verifyMissing(oper, RerouteOperator.NETWORK_ID_KEY, "network"); + verifyMissing(RerouteOperation.SERVICE_ID_KEY, "service", RerouteOperation::new); + verifyMissing(RerouteOperation.NETWORK_ID_KEY, "network", RerouteOperation::new); + + // perform the operation + makeContext(); + verifyRequest("reroute.json", verifyOperation(oper)); } @Override protected Map<String, String> makeEnrichment() { - return Map.of(RerouteOperator.SERVICE_ID_KEY, "my-service", RerouteOperator.NETWORK_ID_KEY, "my-network"); + return Map.of(RerouteOperation.SERVICE_ID_KEY, "my-service", RerouteOperation.NETWORK_ID_KEY, "my-network"); } } diff --git a/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/SdncActorServiceProviderTest.java b/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/SdncActorServiceProviderTest.java index 08655c349..ac81d49c9 100644 --- a/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/SdncActorServiceProviderTest.java +++ b/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/SdncActorServiceProviderTest.java @@ -41,7 +41,7 @@ import org.onap.policy.sdnc.SdncRequest; public class SdncActorServiceProviderTest { - private static final String REROUTE = RerouteOperator.NAME; + private static final String REROUTE = RerouteOperation.NAME; /** * Set up before test class. @@ -63,7 +63,7 @@ public class SdncActorServiceProviderTest { final SdncActorServiceProvider prov = new SdncActorServiceProvider(); // verify that it has the operators we expect - var expected = Arrays.asList(BandwidthOnDemandOperator.NAME, RerouteOperator.NAME).stream().sorted() + var expected = Arrays.asList(BandwidthOnDemandOperation.NAME, RerouteOperation.NAME).stream().sorted() .collect(Collectors.toList()); var actual = prov.getOperationNames().stream().sorted().collect(Collectors.toList()); diff --git a/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/SdncOperationTest.java b/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/SdncOperationTest.java new file mode 100644 index 000000000..e0825e13b --- /dev/null +++ b/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/SdncOperationTest.java @@ -0,0 +1,87 @@ +/*- + * ============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.actor.sdnc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Map; +import java.util.TreeMap; +import org.junit.Before; +import org.junit.Test; +import org.onap.policy.sdnc.SdncRequest; + +public class SdncOperationTest extends BasicSdncOperation { + + private SdncRequest request; + private SdncOperation oper; + + /** + * Sets up. + */ + @Before + public void setUp() throws Exception { + super.setUp(); + + oper = new SdncOperation(params, operator) { + @Override + protected SdncRequest makeRequest(int attempt) { + return request; + } + }; + } + + @Test + public void testSdncOperator() { + assertEquals(DEFAULT_ACTOR, oper.getActorName()); + assertEquals(DEFAULT_OPERATION, oper.getName()); + } + + @Test + public void testStartOperationAsync_testStartRequestAsync() throws Exception { + verifyOperation(oper); + } + + @Test + public void testIsSuccess() { + // success case + response.getResponseOutput().setResponseCode("200"); + assertTrue(oper.isSuccess(null, response)); + + // failure code + response.getResponseOutput().setResponseCode("555"); + assertFalse(oper.isSuccess(null, response)); + + // null code + response.getResponseOutput().setResponseCode(null); + assertFalse(oper.isSuccess(null, response)); + + // null output + response.setResponseOutput(null); + assertFalse(oper.isSuccess(null, response)); + } + + @Override + protected Map<String, String> makeEnrichment() { + return new TreeMap<>(); + } +} diff --git a/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/SdncOperatorTest.java b/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/SdncOperatorTest.java deleted file mode 100644 index 25d383eb8..000000000 --- a/models-interactions/model-actors/actor.sdnc/src/test/java/org/onap/policy/controlloop/actor/sdnc/SdncOperatorTest.java +++ /dev/null @@ -1,326 +0,0 @@ -/*- - * ============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.actor.sdnc; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -import java.util.Map; -import java.util.Properties; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import javax.ws.rs.Consumes; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; -import lombok.Setter; -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.endpoints.event.comm.bus.internal.BusTopicParams; -import org.onap.policy.common.endpoints.event.comm.bus.internal.BusTopicParams.TopicParamsBuilder; -import org.onap.policy.common.endpoints.http.client.HttpClient; -import org.onap.policy.common.endpoints.http.client.HttpClientFactoryInstance; -import org.onap.policy.common.endpoints.http.server.HttpServletServer; -import org.onap.policy.common.endpoints.http.server.HttpServletServerFactoryInstance; -import org.onap.policy.common.endpoints.properties.PolicyEndPointProperties; -import org.onap.policy.common.gson.GsonMessageBodyHandler; -import org.onap.policy.common.utils.coder.CoderException; -import org.onap.policy.common.utils.coder.StandardCoder; -import org.onap.policy.common.utils.network.NetworkUtil; -import org.onap.policy.controlloop.VirtualControlLoopEvent; -import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome; -import org.onap.policy.controlloop.actorserviceprovider.Util; -import org.onap.policy.controlloop.actorserviceprovider.controlloop.ControlLoopEventContext; -import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; -import org.onap.policy.controlloop.actorserviceprovider.parameters.HttpParams; -import org.onap.policy.controlloop.policy.PolicyResult; -import org.onap.policy.sdnc.SdncHealRequest; -import org.onap.policy.sdnc.SdncRequest; -import org.onap.policy.sdnc.SdncResponse; -import org.onap.policy.sdnc.SdncResponseOutput; - -public class SdncOperatorTest { - public static final String MEDIA_TYPE_APPLICATION_JSON = "application/json"; - private static final String EXPECTED_EXCEPTION = "expected exception"; - public static final String HTTP_CLIENT = "my-http-client"; - public static final String HTTP_NO_SERVER = "my-http-no-server-client"; - private static final String ACTOR = "my-actor"; - private static final String OPERATION = "my-operation"; - - /** - * Outcome to be added to the response. - */ - @Setter - private static SdncResponseOutput output; - - - private VirtualControlLoopEvent event; - private ControlLoopEventContext context; - private MyOper oper; - - /** - * Starts the SDNC simulator. - */ - @BeforeClass - public static void setUpBeforeClass() throws Exception { - // allocate a port - int port = NetworkUtil.allocPort(); - - /* - * Start the simulator. Must use "Properties" to configure it, otherwise the - * server will use the wrong serialization provider. - */ - Properties svrprops = getServerProperties("my-server", port); - HttpServletServerFactoryInstance.getServerFactory().build(svrprops).forEach(HttpServletServer::start); - - /* - * Start the clients, one to the server, and one to a non-existent server. - */ - TopicParamsBuilder builder = BusTopicParams.builder().managed(true).hostname("localhost").basePath("sdnc") - .serializationProvider(GsonMessageBodyHandler.class.getName()); - - HttpClientFactoryInstance.getClientFactory().build(builder.clientName(HTTP_CLIENT).port(port).build()); - - HttpClientFactoryInstance.getClientFactory() - .build(builder.clientName(HTTP_NO_SERVER).port(NetworkUtil.allocPort()).build()); - } - - @AfterClass - public static void tearDownAfterClass() { - HttpClientFactoryInstance.getClientFactory().destroy(); - HttpServletServerFactoryInstance.getServerFactory().destroy(); - } - - /** - * Initializes {@link #oper} and sets {@link #output} to a success code. - */ - @Before - public void setUp() { - event = new VirtualControlLoopEvent(); - context = new ControlLoopEventContext(event); - - initOper(HTTP_CLIENT); - - output = new SdncResponseOutput(); - output.setResponseCode("200"); - } - - @After - public void tearDown() { - oper.shutdown(); - } - - @Test - public void testSdncOperator() { - assertEquals(ACTOR, oper.getActorName()); - assertEquals(OPERATION, oper.getName()); - assertEquals(ACTOR + "." + OPERATION, oper.getFullName()); - } - - @Test - public void testGetClient() { - assertNotNull(oper.getTheClient()); - } - - @Test - public void testStartOperationAsync_testPostRequest() throws Exception { - OperationOutcome outcome = runOperation(); - assertNotNull(outcome); - assertEquals(PolicyResult.SUCCESS, outcome.getResult()); - } - - /** - * Tests postRequest() when decode() throws an exception. - */ - @Test - public void testPostRequestDecodeException() throws Exception { - - oper.setDecodeFailure(true); - - OperationOutcome outcome = runOperation(); - assertNotNull(outcome); - assertEquals(PolicyResult.FAILURE_EXCEPTION, outcome.getResult()); - } - - /** - * Tests postRequest() when there is no "output" field in the response. - */ - @Test - public void testPostRequestNoOutput() throws Exception { - - setOutput(null); - - OperationOutcome outcome = runOperation(); - assertNotNull(outcome); - assertEquals(PolicyResult.FAILURE, outcome.getResult()); - } - - /** - * Tests postRequest() when the output is not a success. - */ - @Test - public void testPostRequestOutputFailure() throws Exception { - - output.setResponseCode(null); - - OperationOutcome outcome = runOperation(); - assertNotNull(outcome); - assertEquals(PolicyResult.FAILURE, outcome.getResult()); - } - - /** - * Tests postRequest() when the post() request throws an exception retrieving the - * response. - */ - @Test - public void testPostRequestException() throws Exception { - - // reset "oper" to point to a non-existent server - oper.shutdown(); - initOper(HTTP_NO_SERVER); - - OperationOutcome outcome = runOperation(); - assertNotNull(outcome); - assertEquals(PolicyResult.FAILURE_EXCEPTION, outcome.getResult()); - } - - private static Properties getServerProperties(String name, int port) { - final Properties props = new Properties(); - props.setProperty(PolicyEndPointProperties.PROPERTY_HTTP_SERVER_SERVICES, name); - - final String svcpfx = PolicyEndPointProperties.PROPERTY_HTTP_SERVER_SERVICES + "." + name; - - props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_REST_CLASSES_SUFFIX, Server.class.getName()); - props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_HOST_SUFFIX, "localhost"); - props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_PORT_SUFFIX, String.valueOf(port)); - props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_MANAGED_SUFFIX, "true"); - props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_SWAGGER_SUFFIX, "false"); - - props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_SERIALIZATION_PROVIDER, - GsonMessageBodyHandler.class.getName()); - return props; - } - - /** - * Initializes {@link #oper}. - * - * @param clientName name of the client which it should use - */ - private void initOper(String clientName) { - oper = new MyOper(); - - HttpParams params = HttpParams.builder().clientName(clientName).path("request").build(); - Map<String, Object> mapParams = Util.translateToMap(OPERATION, params); - oper.configure(mapParams); - oper.start(); - } - - /** - * Runs the operation. - * - * @return the outcome of the operation, or {@code null} if it does not complete in - * time - */ - private OperationOutcome runOperation() throws InterruptedException, ExecutionException, TimeoutException { - ControlLoopOperationParams params = - ControlLoopOperationParams.builder().actor(ACTOR).operation(OPERATION).context(context).build(); - - CompletableFuture<OperationOutcome> future = oper.startOperationAsync(params, 1, params.makeOutcome()); - - return future.get(5, TimeUnit.SECONDS); - } - - - private class MyOper extends SdncOperator { - - /** - * Set to {@code true} to cause the decoder to throw an exception. - */ - @Setter - private boolean decodeFailure = false; - - public MyOper() { - super(ACTOR, OPERATION); - } - - protected HttpClient getTheClient() { - return getClient(); - } - - @Override - protected SdncRequest constructRequest(ControlLoopEventContext context) { - SdncRequest request = new SdncRequest(); - - SdncHealRequest heal = new SdncHealRequest(); - request.setHealRequest(heal); - - return request; - } - - @Override - protected StandardCoder makeDecoder() { - if (decodeFailure) { - // return a coder that throws exceptions when decode() is invoked - return new StandardCoder() { - @Override - public <T> T decode(String json, Class<T> clazz) throws CoderException { - throw new CoderException(EXPECTED_EXCEPTION); - } - }; - - } else { - return super.makeDecoder(); - } - } - } - - /** - * SDNC Simulator. - */ - @Path("/sdnc") - @Produces(MEDIA_TYPE_APPLICATION_JSON) - public static class Server { - - /** - * Generates a response. - * - * @param request incoming request - * @return resulting response - */ - @POST - @Path("/request") - @Consumes(value = {MEDIA_TYPE_APPLICATION_JSON}) - public Response postRequest(SdncRequest request) { - - SdncResponse response = new SdncResponse(); - response.setResponseOutput(output); - - return Response.status(Status.OK).entity(response).build(); - } - } -} diff --git a/models-interactions/model-actors/actor.test/pom.xml b/models-interactions/model-actors/actor.test/pom.xml new file mode 100644 index 000000000..3a10fa3d1 --- /dev/null +++ b/models-interactions/model-actors/actor.test/pom.xml @@ -0,0 +1,74 @@ +<?xml version="1.0"?> +<!-- + ============LICENSE_START======================================================= + 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.models.policy-models-interactions.model-actors</groupId> + <artifactId>model-actors</artifactId> + <version>2.2.1-SNAPSHOT</version> + </parent> + + <artifactId>actor.test</artifactId> + <description>Utilities for testing actors</description> + + <dependencies> + <dependency> + <groupId>org.onap.policy.models.policy-models-interactions.model-impl</groupId> + <artifactId>events</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>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.common</groupId> + <artifactId>policy-endpoints</artifactId> + <version>${policy.common.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.onap.policy.common</groupId> + <artifactId>utils-test</artifactId> + <version>${policy.common.version}</version> + </dependency> + <dependency> + <groupId>org.powermock</groupId> + <artifactId>powermock-api-mockito2</artifactId> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <scope>compile</scope> + </dependency> + </dependencies> +</project> diff --git a/models-interactions/model-actors/actor.test/src/main/java/org/onap/policy/controlloop/actor/test/BasicBidirectionalTopicOperation.java b/models-interactions/model-actors/actor.test/src/main/java/org/onap/policy/controlloop/actor/test/BasicBidirectionalTopicOperation.java new file mode 100644 index 000000000..14c7ef576 --- /dev/null +++ b/models-interactions/model-actors/actor.test/src/main/java/org/onap/policy/controlloop/actor/test/BasicBidirectionalTopicOperation.java @@ -0,0 +1,181 @@ +/*- + * ============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.actor.test; + +import static org.mockito.Mockito.when; + +import java.util.Map; +import java.util.TreeMap; +import java.util.UUID; +import java.util.function.BiConsumer; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +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.StandardCoder; +import org.onap.policy.common.utils.coder.StandardCoderObject; +import org.onap.policy.common.utils.time.PseudoExecutor; +import org.onap.policy.controlloop.VirtualControlLoopEvent; +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.actorserviceprovider.impl.BidirectionalTopicOperator; +import org.onap.policy.controlloop.actorserviceprovider.parameters.BidirectionalTopicParams; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; +import org.onap.policy.controlloop.actorserviceprovider.topic.BidirectionalTopicHandler; +import org.onap.policy.controlloop.actorserviceprovider.topic.Forwarder; + +/** + * Superclass for various BidirectionalTopicOperation tests. + */ +public class BasicBidirectionalTopicOperation { + protected static final UUID REQ_ID = UUID.randomUUID(); + protected static final String DEFAULT_ACTOR = "default-actor"; + protected static final String DEFAULT_OPERATION = "default-operation"; + protected static final String MY_SINK = "my-sink"; + protected static final String MY_SOURCE = "my-source"; + protected static final String TARGET_ENTITY = "my-target"; + protected static final Coder coder = new StandardCoder(); + protected static final int TIMEOUT = 10; + + protected final String actorName; + protected final String operationName; + + @Captor + protected ArgumentCaptor<BiConsumer<String, StandardCoderObject>> listenerCaptor; + + @Mock + protected ActorService service; + @Mock + protected BidirectionalTopicHandler topicHandler; + @Mock + protected Forwarder forwarder; + @Mock + protected BidirectionalTopicOperator operator; + + protected BidirectionalTopicParams topicParams; + protected ControlLoopOperationParams params; + protected Map<String, String> enrichment; + protected VirtualControlLoopEvent event; + protected ControlLoopEventContext context; + protected OperationOutcome outcome; + protected PseudoExecutor executor; + + /** + * Constructs the object using a default actor and operation name. + */ + public BasicBidirectionalTopicOperation() { + this.actorName = DEFAULT_ACTOR; + this.operationName = DEFAULT_OPERATION; + } + + /** + * Constructs the object. + * + * @param actor actor name + * @param operation operation name + */ + public BasicBidirectionalTopicOperation(String actor, String operation) { + this.actorName = actor; + this.operationName = operation; + } + + /** + * Initializes mocks and sets up. + */ + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + executor = new PseudoExecutor(); + + makeContext(); + + outcome = params.makeOutcome(); + topicParams = BidirectionalTopicParams.builder().sinkTopic(MY_SINK).sourceTopic(MY_SOURCE).timeoutSec(TIMEOUT) + .build(); + + initOperator(); + } + + /** + * Reinitializes {@link #enrichment}, {@link #event}, {@link #context}, and + * {@link #params}. + * <p/> + * Note: {@link #params} is configured to use {@link #executor}. + */ + protected void makeContext() { + enrichment = new TreeMap<>(makeEnrichment()); + + event = new VirtualControlLoopEvent(); + event.setRequestId(REQ_ID); + event.setAai(enrichment); + + context = new ControlLoopEventContext(event); + + params = ControlLoopOperationParams.builder().executor(executor).context(context).actorService(service) + .actor(actorName).operation(operationName).targetEntity(TARGET_ENTITY).payload(makePayload()) + .build(); + } + + protected Map<String, String> makePayload() { + return null; + } + + /** + * Initializes an operator so that it is "alive" and has the given names. + */ + protected void initOperator() { + when(operator.isAlive()).thenReturn(true); + when(operator.getFullName()).thenReturn(actorName + "." + operationName); + when(operator.getActorName()).thenReturn(actorName); + when(operator.getName()).thenReturn(operationName); + when(operator.getTopicHandler()).thenReturn(topicHandler); + when(operator.getForwarder()).thenReturn(forwarder); + when(operator.getParams()).thenReturn(topicParams); + } + + /** + * Makes enrichment data. + * + * @return enrichment data + */ + protected Map<String, String> makeEnrichment() { + return new TreeMap<>(); + } + + /** + * Provides a response to the topic {@link #listenerCaptor}. + * + * @param listener listener to which to provide the response + * @param response response to be provided + */ + protected void provideResponse(BiConsumer<String, StandardCoderObject> listener, String response) { + try { + StandardCoderObject sco = coder.decode(response, StandardCoderObject.class); + listener.accept(response, sco); + + } catch (CoderException e) { + throw new IllegalArgumentException("response is not a Map", e); + } + } +} diff --git a/models-interactions/model-actors/actor.test/src/main/java/org/onap/policy/controlloop/actor/test/BasicHttpOperation.java b/models-interactions/model-actors/actor.test/src/main/java/org/onap/policy/controlloop/actor/test/BasicHttpOperation.java new file mode 100644 index 000000000..492929296 --- /dev/null +++ b/models-interactions/model-actors/actor.test/src/main/java/org/onap/policy/controlloop/actor/test/BasicHttpOperation.java @@ -0,0 +1,190 @@ +/*- + * ============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.actor.test; + +import static org.mockito.Mockito.when; + +import java.util.Map; +import java.util.TreeMap; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.InvocationCallback; +import javax.ws.rs.core.Response; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.Answer; +import org.onap.policy.common.endpoints.http.client.HttpClient; +import org.onap.policy.common.endpoints.http.client.HttpClientFactory; +import org.onap.policy.common.utils.time.PseudoExecutor; +import org.onap.policy.controlloop.VirtualControlLoopEvent; +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.actorserviceprovider.impl.HttpOperator; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; + +/** + * Superclass for various HttpOperation tests. + * + * @param <Q> request type + */ +public class BasicHttpOperation<Q> { + protected static final UUID REQ_ID = UUID.randomUUID(); + protected static final String DEFAULT_ACTOR = "default-actor"; + protected static final String DEFAULT_OPERATION = "default-operation"; + protected static final String MY_CLIENT = "my-client"; + protected static final String BASE_URI = "/base-uri"; + protected static final String PATH = "/my-path"; + protected static final String TARGET_ENTITY = "my-target"; + + protected final String actorName; + protected final String operationName; + + @Captor + protected ArgumentCaptor<InvocationCallback<Response>> callbackCaptor; + + @Captor + protected ArgumentCaptor<Entity<Q>> requestCaptor; + + @Captor + protected ArgumentCaptor<Map<String, Object>> headerCaptor; + + @Mock + protected ActorService service; + + @Mock + protected HttpClient client; + + @Mock + protected HttpClientFactory factory; + + @Mock + protected Response rawResponse; + + @Mock + protected HttpOperator operator; + + protected CompletableFuture<Response> future; + protected ControlLoopOperationParams params; + protected Map<String, String> enrichment; + protected VirtualControlLoopEvent event; + protected ControlLoopEventContext context; + protected OperationOutcome outcome; + protected PseudoExecutor executor; + + /** + * Constructs the object using a default actor and operation name. + */ + public BasicHttpOperation() { + this.actorName = DEFAULT_ACTOR; + this.operationName = DEFAULT_OPERATION; + } + + /** + * Constructs the object. + * + * @param actor actor name + * @param operation operation name + */ + public BasicHttpOperation(String actor, String operation) { + this.actorName = actor; + this.operationName = operation; + } + + /** + * Initializes mocks and sets up. + */ + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + when(factory.get(MY_CLIENT)).thenReturn(client); + + when(rawResponse.getStatus()).thenReturn(200); + + future = new CompletableFuture<>(); + when(client.getBaseUrl()).thenReturn(BASE_URI); + + executor = new PseudoExecutor(); + + makeContext(); + + outcome = params.makeOutcome(); + + initOperator(); + } + + /** + * Reinitializes {@link #enrichment}, {@link #event}, {@link #context}, and + * {@link #params}. + * <p/> + * Note: {@link #params} is configured to use {@link #executor}. + */ + protected void makeContext() { + enrichment = new TreeMap<>(makeEnrichment()); + + event = new VirtualControlLoopEvent(); + event.setRequestId(REQ_ID); + event.setAai(enrichment); + + context = new ControlLoopEventContext(event); + + params = ControlLoopOperationParams.builder().executor(executor).context(context).actorService(service) + .actor(actorName).operation(operationName).targetEntity(TARGET_ENTITY).build(); + } + + /** + * Initializes an operator so that it is "alive" and has the given names. + */ + protected void initOperator() { + when(operator.isAlive()).thenReturn(true); + when(operator.getFullName()).thenReturn(actorName + "." + operationName); + when(operator.getActorName()).thenReturn(actorName); + when(operator.getName()).thenReturn(operationName); + when(operator.getClient()).thenReturn(client); + when(operator.getPath()).thenReturn(PATH); + } + + /** + * Makes enrichment data. + * + * @return enrichment data + */ + protected Map<String, String> makeEnrichment() { + return new TreeMap<>(); + } + + /** + * Provides a response to an asynchronous HttpClient call. + * + * @param response response to be provided to the call + * @return a function that provides the response to the call + */ + protected Answer<CompletableFuture<Response>> provideResponse(Response response) { + return args -> { + InvocationCallback<Response> cb = args.getArgument(0); + cb.completed(response); + return CompletableFuture.completedFuture(response); + }; + } +} diff --git a/models-interactions/model-actors/actor.test/src/test/java/org/onap/policy/controlloop/actor/test/BasicBidirectionalTopicOperationTest.java b/models-interactions/model-actors/actor.test/src/test/java/org/onap/policy/controlloop/actor/test/BasicBidirectionalTopicOperationTest.java new file mode 100644 index 000000000..4fd559101 --- /dev/null +++ b/models-interactions/model-actors/actor.test/src/test/java/org/onap/policy/controlloop/actor/test/BasicBidirectionalTopicOperationTest.java @@ -0,0 +1,140 @@ +/*- + * ============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.actor.test; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.junit.Assert.assertEquals; +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.eq; +import static org.mockito.Mockito.verify; + +import java.util.function.BiConsumer; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.onap.policy.common.utils.coder.StandardCoderObject; + +public class BasicBidirectionalTopicOperationTest { + private static final String ACTOR = "my-actor"; + private static final String OPERATION = "my-operation"; + + @Mock + private BiConsumer<String, StandardCoderObject> listener; + + private BasicBidirectionalTopicOperation oper; + + + /** + * Sets up. + */ + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + oper = new BasicBidirectionalTopicOperation(ACTOR, OPERATION); + oper.setUp(); + } + + @Test + public void testBasicBidirectionalTopicOperation() { + oper = new BasicBidirectionalTopicOperation(); + assertEquals(BasicHttpOperation.DEFAULT_ACTOR, oper.actorName); + assertEquals(BasicHttpOperation.DEFAULT_OPERATION, oper.operationName); + } + + @Test + public void testBasicBidirectionalTopicOperationStringString() { + assertEquals(ACTOR, oper.actorName); + assertEquals(OPERATION, oper.operationName); + } + + @Test + public void testSetUp() { + assertNotNull(oper.topicParams); + assertNotNull(oper.context); + assertNotNull(oper.outcome); + assertNotNull(oper.executor); + assertTrue(oper.operator.isAlive()); + } + + @Test + public void testMakeContext() { + oper.makeContext(); + + assertTrue(oper.enrichment.isEmpty()); + + assertSame(BasicBidirectionalTopicOperation.REQ_ID, oper.event.getRequestId()); + assertSame(oper.enrichment, oper.event.getAai()); + + assertSame(oper.event, oper.context.getEvent()); + + assertSame(oper.context, oper.params.getContext()); + assertSame(oper.service, oper.params.getActorService()); + assertSame(oper.executor, oper.params.getExecutor()); + assertEquals(ACTOR, oper.params.getActor()); + assertEquals(OPERATION, oper.params.getOperation()); + assertEquals(BasicBidirectionalTopicOperation.TARGET_ENTITY, oper.params.getTargetEntity()); + } + + @Test + public void testMakePayload() { + assertNull(oper.makePayload()); + } + + @Test + public void testInitOperator() { + oper.initOperator(); + + assertTrue(oper.operator.isAlive()); + assertEquals(ACTOR + "." + OPERATION, oper.operator.getFullName()); + assertEquals(ACTOR, oper.operator.getActorName()); + assertEquals(OPERATION, oper.operator.getName()); + assertSame(oper.topicHandler, oper.operator.getTopicHandler()); + assertSame(oper.forwarder, oper.operator.getForwarder()); + assertSame(oper.topicParams, oper.operator.getParams()); + } + + @Test + public void testMakeEnrichment() { + assertTrue(oper.makeEnrichment().isEmpty()); + } + + @Test + public void testProvideResponse() { + String response = "{\"input\": 10}"; + + oper.provideResponse(listener, response); + + ArgumentCaptor<StandardCoderObject> scoCaptor = ArgumentCaptor.forClass(StandardCoderObject.class); + verify(listener).accept(eq(response), scoCaptor.capture()); + + assertEquals("10", scoCaptor.getValue().getString("input")); + + // try with an invalid response + assertThatIllegalArgumentException().isThrownBy(() -> oper.provideResponse(listener, "{invalid json")) + .withMessage("response is not a Map"); + } +} diff --git a/models-interactions/model-actors/actor.test/src/test/java/org/onap/policy/controlloop/actor/test/BasicHttpOperationTest.java b/models-interactions/model-actors/actor.test/src/test/java/org/onap/policy/controlloop/actor/test/BasicHttpOperationTest.java new file mode 100644 index 000000000..096b8b80d --- /dev/null +++ b/models-interactions/model-actors/actor.test/src/test/java/org/onap/policy/controlloop/actor/test/BasicHttpOperationTest.java @@ -0,0 +1,129 @@ +/*- + * ============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.actor.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import javax.ws.rs.client.InvocationCallback; +import javax.ws.rs.core.Response; +import org.junit.Before; +import org.junit.Test; + +public class BasicHttpOperationTest { + private static final String ACTOR = "my-actor"; + private static final String OPERATION = "my-operation"; + + private BasicHttpOperation<String> oper; + + + @Before + public void setUp() throws Exception { + oper = new BasicHttpOperation<>(ACTOR, OPERATION); + oper.setUp(); + } + + @Test + public void testBasicHttpOperation() { + oper = new BasicHttpOperation<>(); + assertEquals(BasicHttpOperation.DEFAULT_ACTOR, oper.actorName); + assertEquals(BasicHttpOperation.DEFAULT_OPERATION, oper.operationName); + } + + @Test + public void testBasicHttpOperationStringString() { + assertEquals(ACTOR, oper.actorName); + assertEquals(OPERATION, oper.operationName); + } + + @Test + public void testSetUp() throws Exception { + assertNotNull(oper.client); + assertSame(oper.client, oper.factory.get(BasicHttpOperation.MY_CLIENT)); + assertEquals(200, oper.rawResponse.getStatus()); + assertNotNull(oper.future); + assertEquals(BasicHttpOperation.BASE_URI, oper.client.getBaseUrl()); + assertNotNull(oper.context); + assertNotNull(oper.outcome); + assertNotNull(oper.executor); + assertTrue(oper.operator.isAlive()); + } + + @Test + public void testMakeContext() { + oper.makeContext(); + + assertTrue(oper.enrichment.isEmpty()); + + assertSame(BasicHttpOperation.REQ_ID, oper.event.getRequestId()); + assertSame(oper.enrichment, oper.event.getAai()); + + assertSame(oper.event, oper.context.getEvent()); + + assertSame(oper.context, oper.params.getContext()); + assertSame(oper.service, oper.params.getActorService()); + assertSame(oper.executor, oper.params.getExecutor()); + assertEquals(ACTOR, oper.params.getActor()); + assertEquals(OPERATION, oper.params.getOperation()); + assertEquals(BasicHttpOperation.TARGET_ENTITY, oper.params.getTargetEntity()); + } + + @Test + public void testInitOperator() throws Exception { + oper.initOperator(); + + assertTrue(oper.operator.isAlive()); + assertEquals(ACTOR + "." + OPERATION, oper.operator.getFullName()); + assertEquals(ACTOR, oper.operator.getActorName()); + assertEquals(OPERATION, oper.operator.getName()); + assertSame(oper.client, oper.operator.getClient()); + assertEquals(BasicHttpOperation.PATH, oper.operator.getPath()); + } + + @Test + public void testMakeEnrichment() { + assertTrue(oper.makeEnrichment().isEmpty()); + } + + @Test + public void testProvideResponse() throws Exception { + InvocationCallback<Response> cb = new InvocationCallback<>() { + @Override + public void completed(Response response) { + // do nothing + } + + @Override + public void failed(Throwable throwable) { + // do nothing + } + }; + + + when(oper.client.get(any(), any(), any())).thenAnswer(oper.provideResponse(oper.rawResponse)); + + assertSame(oper.rawResponse, oper.client.get(cb, null, null).get()); + } +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/ActorService.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/ActorService.java index 2886b1feb..24c2cfc23 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/ActorService.java +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/ActorService.java @@ -40,7 +40,7 @@ import org.slf4j.LoggerFactory; * {@link #start()} to start all of the actors. When finished using the actor service, * invoke {@link #stop()} or {@link #shutdown()}. */ -public class ActorService extends StartConfigPartial<Map<String, Object>> { +public class ActorService extends StartConfigPartial<Map<String, Map<String, Object>>> { private static final Logger logger = LoggerFactory.getLogger(ActorService.class); private final Map<String, Actor> name2actor; @@ -116,14 +116,14 @@ public class ActorService extends StartConfigPartial<Map<String, Object>> { } @Override - protected void doConfigure(Map<String, Object> parameters) { + protected void doConfigure(Map<String, Map<String, Object>> parameters) { logger.info("configuring actors"); BeanValidationResult valres = new BeanValidationResult("ActorService", parameters); for (Actor actor : name2actor.values()) { String actorName = actor.getName(); - Map<String, Object> subparams = Util.translateToMap(actorName, parameters.get(actorName)); + Map<String, Object> subparams = parameters.get(actorName); if (subparams != null) { diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/AsyncResponseHandler.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/AsyncResponseHandler.java deleted file mode 100644 index d78403809..000000000 --- a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/AsyncResponseHandler.java +++ /dev/null @@ -1,119 +0,0 @@ -/*- - * ============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.actorserviceprovider; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Future; -import javax.ws.rs.client.InvocationCallback; -import lombok.AccessLevel; -import lombok.Getter; -import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; -import org.onap.policy.controlloop.actorserviceprovider.pipeline.PipelineControllerFuture; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Handler for a <i>single</i> asynchronous response. - * - * @param <T> response type - */ -@Getter -public abstract class AsyncResponseHandler<T> implements InvocationCallback<T> { - - private static final Logger logger = LoggerFactory.getLogger(AsyncResponseHandler.class); - - @Getter(AccessLevel.NONE) - private final PipelineControllerFuture<OperationOutcome> result = new PipelineControllerFuture<>(); - private final ControlLoopOperationParams params; - private final OperationOutcome outcome; - - /** - * Constructs the object. - * - * @param params operation parameters - * @param outcome outcome to be populated based on the response - */ - public AsyncResponseHandler(ControlLoopOperationParams params, OperationOutcome outcome) { - this.params = params; - this.outcome = outcome; - } - - /** - * Handles the given future, arranging to cancel it when the response is received. - * - * @param future future to be handled - * @return a future to be used to cancel or wait for the response - */ - public CompletableFuture<OperationOutcome> handle(Future<T> future) { - result.add(future); - return result; - } - - /** - * Invokes {@link #doComplete()} and then completes "this" with the returned value. - */ - @Override - public void completed(T rawResponse) { - try { - logger.trace("{}.{}: response completed for {}", params.getActor(), params.getOperation(), - params.getRequestId()); - result.complete(doComplete(rawResponse)); - - } catch (RuntimeException e) { - logger.trace("{}.{}: response handler threw an exception for {}", params.getActor(), params.getOperation(), - params.getRequestId()); - result.completeExceptionally(e); - } - } - - /** - * Invokes {@link #doFailed()} and then completes "this" with the returned value. - */ - @Override - public void failed(Throwable throwable) { - try { - logger.trace("{}.{}: response failure for {}", params.getActor(), params.getOperation(), - params.getRequestId()); - result.complete(doFailed(throwable)); - - } catch (RuntimeException e) { - logger.trace("{}.{}: response failure handler threw an exception for {}", params.getActor(), - params.getOperation(), params.getRequestId()); - result.completeExceptionally(e); - } - } - - /** - * Completes the processing of a response. - * - * @param rawResponse raw response that was received - * @return the outcome - */ - protected abstract OperationOutcome doComplete(T rawResponse); - - /** - * Handles a response exception. - * - * @param thrown exception that was thrown - * @return the outcome - */ - protected abstract OperationOutcome doFailed(Throwable thrown); -} diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/Operation.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/Operation.java new file mode 100644 index 000000000..39977fd41 --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/Operation.java @@ -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========================================================= + */ + +package org.onap.policy.controlloop.actorserviceprovider; + +import java.util.concurrent.CompletableFuture; + +/** + * This is the service interface for defining an Actor operation used in Control Loop + * Operational Policies for performing actions on runtime entities. + */ +public interface Operation { + + /** + * Gets the name of the associated actor. + * + * @return the name of the associated actor + */ + String getActorName(); + + /** + * Gets the name of the operation. + * + * @return the operation name + */ + String getName(); + + /** + * Called by enforcement PDP engine to start the operation. As part of the operation, + * it invokes the "start" and "complete" call-backs found within the parameters. + * + * @return a future that can be used to cancel or await the result of the operation + */ + CompletableFuture<OperationOutcome> start(); +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/Operator.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/Operator.java index c09460e34..24faafd40 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/Operator.java +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/Operator.java @@ -21,7 +21,6 @@ package org.onap.policy.controlloop.actorserviceprovider; import java.util.Map; -import java.util.concurrent.CompletableFuture; import org.onap.policy.common.capabilities.Configurable; import org.onap.policy.common.capabilities.Startable; import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; @@ -47,11 +46,10 @@ public interface Operator extends Startable, Configurable<Map<String, Object>> { String getName(); /** - * Called by enforcement PDP engine to start the operation. As part of the operation, - * it invokes the "start" and "complete" call-backs found within the parameters. + * Called by enforcement PDP engine to build the operation. * - * @param params parameters needed to start the operation - * @return a future that can be used to cancel or await the result of the operation + * @param params parameters needed by the operation + * @return a new operation */ - CompletableFuture<OperationOutcome> startOperation(ControlLoopOperationParams params); + Operation buildOperation(ControlLoopOperationParams params); } diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/Util.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/Util.java index c3ddd17f3..ba4785922 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/Util.java +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/Util.java @@ -23,9 +23,6 @@ package org.onap.policy.controlloop.actorserviceprovider; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Map; -import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure; -import org.onap.policy.common.endpoints.utils.NetLoggerUtil; -import org.onap.policy.common.endpoints.utils.NetLoggerUtil.EventType; import org.onap.policy.common.utils.coder.Coder; import org.onap.policy.common.utils.coder.CoderException; import org.onap.policy.common.utils.coder.StandardCoder; @@ -37,6 +34,7 @@ import org.slf4j.LoggerFactory; */ public class Util { private static final Logger logger = LoggerFactory.getLogger(Util.class); + private static final Coder coder = new StandardCoder(); private Util() { // do nothing @@ -56,82 +54,6 @@ public class Util { } /** - * Logs a REST request. If the request is not of type, String, then it attempts to - * pretty-print it into JSON before logging. - * - * @param url request URL - * @param request request to be logged - */ - public static <T> void logRestRequest(String url, T request) { - logRestRequest(new StandardCoder(), url, request); - } - - /** - * Logs a REST request. If the request is not of type, String, then it attempts to - * pretty-print it into JSON before logging. - * - * @param coder coder to be used to pretty-print the request - * @param url request URL - * @param request request to be logged - */ - protected static <T> void logRestRequest(Coder coder, String url, T request) { - String json; - try { - if (request instanceof String) { - json = request.toString(); - } else { - json = coder.encode(request, true); - } - - } catch (CoderException e) { - logger.warn("cannot pretty-print request", e); - json = request.toString(); - } - - NetLoggerUtil.log(EventType.OUT, CommInfrastructure.REST, url, json); - logger.info("[OUT|{}|{}|]{}{}", CommInfrastructure.REST, url, NetLoggerUtil.SYSTEM_LS, json); - } - - /** - * Logs a REST response. If the response is not of type, String, then it attempts to - * pretty-print it into JSON before logging. - * - * @param url request URL - * @param response response to be logged - */ - public static <T> void logRestResponse(String url, T response) { - logRestResponse(new StandardCoder(), url, response); - } - - /** - * Logs a REST response. If the request is not of type, String, then it attempts to - * pretty-print it into JSON before logging. - * - * @param coder coder to be used to pretty-print the response - * @param url request URL - * @param response response to be logged - */ - protected static <T> void logRestResponse(Coder coder, String url, T response) { - String json; - try { - if (response == null) { - json = null; - } else if (response instanceof String) { - json = response.toString(); - } else { - json = coder.encode(response, true); - } - - } catch (CoderException e) { - logger.warn("cannot pretty-print response", e); - json = response.toString(); - } - - NetLoggerUtil.log(EventType.IN, CommInfrastructure.REST, url, json); - logger.info("[IN|{}|{}|]{}{}", CommInfrastructure.REST, url, NetLoggerUtil.SYSTEM_LS, json); - } - - /** * Runs a function and logs a message if it throws an exception. Does <i>not</i> * re-throw the exception. * @@ -163,11 +85,8 @@ public class Util { * @return the translated object */ public static <T> T translate(String identifier, Object source, Class<T> clazz) { - Coder coder = new StandardCoder(); - try { - String json = coder.encode(source); - return coder.decode(json, clazz); + return coder.convert(source, clazz); } catch (CoderException | RuntimeException e) { throw new IllegalArgumentException("cannot translate parameters for " + identifier, e); @@ -184,10 +103,6 @@ public class Util { */ @SuppressWarnings("unchecked") public static Map<String, Object> translateToMap(String identifier, Object source) { - if (source == null) { - return null; - } - return translate(identifier, source, LinkedHashMap.class); } } diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/controlloop/ControlLoopEventContext.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/controlloop/ControlLoopEventContext.java index cd4d2570f..3e02da611 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/controlloop/ControlLoopEventContext.java +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/controlloop/ControlLoopEventContext.java @@ -23,12 +23,15 @@ package org.onap.policy.controlloop.actorserviceprovider.controlloop; import java.io.Serializable; import java.util.Map; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import lombok.AccessLevel; import lombok.Getter; import lombok.NonNull; import lombok.Setter; import org.onap.policy.controlloop.VirtualControlLoopEvent; +import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; /** * Context associated with a control loop event. @@ -47,11 +50,23 @@ public class ControlLoopEventContext implements Serializable { */ private final Map<String, String> enrichment; + /** + * Set of properties that have been stored in the context. + */ @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) private Map<String, Serializable> properties = new ConcurrentHashMap<>(); /** + * When {@link #obtain(String, ControlLoopOperationParams)} is invoked and the + * specified property is not found in {@link #properties}, it is retrieved. This holds + * the futures for the operations retrieving the properties. + */ + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private transient Map<String, CompletableFuture<OperationOutcome>> retrievers = new ConcurrentHashMap<>(); + + /** * Request ID extracted from the event, or a generated value if the event has no * request id; never {@code null}. */ @@ -100,4 +115,46 @@ public class ControlLoopEventContext implements Serializable { public void setProperty(String name, Serializable value) { properties.put(name, value); } + + /** + * Obtains the given property. + * + * @param name name of the desired property + * @param params parameters needed to perform the operation to retrieve the desired + * property + * @return a future for retrieving the property, {@code null} if the property has + * already been retrieved + */ + public CompletableFuture<OperationOutcome> obtain(String name, ControlLoopOperationParams params) { + if (properties.containsKey(name)) { + return null; + } + + /* + * Return any existing future, if it wasn't canceled. Otherwise, start a new + * request. + */ + + // @formatter:off + CompletableFuture<OperationOutcome> oldFuture = + retrievers.compute(name, (key, future) -> (future == null || future.isCancelled() ? null : future)); + // @formatter:on + + if (oldFuture != null) { + return oldFuture; + } + + /* + * Note: must NOT invoke params.start() within retrievers.compute(), as start() + * may invoke obtain() which would cause a recursive update to the retrievers map. + */ + CompletableFuture<OperationOutcome> future = params.start(); + + if ((oldFuture = retrievers.putIfAbsent(name, future)) != null) { + future.cancel(false); + return oldFuture; + } + + return future; + } } diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/ActorImpl.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/ActorImpl.java index d7f322e8a..0c88ebee2 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/ActorImpl.java +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/ActorImpl.java @@ -91,7 +91,7 @@ public class ActorImpl extends StartConfigPartial<Map<String, Object>> implement public Operator getOperator(String name) { Operator operator = name2operator.get(name); if (operator == null) { - throw new IllegalArgumentException("unknown operation " + getName() + "." + name); + throw new IllegalArgumentException("unknown operator " + getName() + "." + name); } return operator; diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/BidirectionalTopicActor.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/BidirectionalTopicActor.java new file mode 100644 index 000000000..1e44a170c --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/BidirectionalTopicActor.java @@ -0,0 +1,108 @@ +/*- + * ============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.actorserviceprovider.impl; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import org.apache.commons.lang3.tuple.Pair; +import org.onap.policy.common.endpoints.event.comm.client.BidirectionalTopicClientException; +import org.onap.policy.controlloop.actorserviceprovider.Util; +import org.onap.policy.controlloop.actorserviceprovider.parameters.BidirectionalTopicActorParams; +import org.onap.policy.controlloop.actorserviceprovider.topic.BidirectionalTopicHandler; +import org.onap.policy.controlloop.actorserviceprovider.topic.BidirectionalTopicManager; + +/** + * Actor that uses a bidirectional topic. The actor's parameters must be a + * {@link BidirectionalTopicActorParams}. + */ +public class BidirectionalTopicActor extends ActorImpl implements BidirectionalTopicManager { + + /** + * Maps a pair of sink and source topic names to their bidirectional topic. + */ + private final Map<Pair<String, String>, BidirectionalTopicHandler> params2topic = new ConcurrentHashMap<>(); + + + /** + * Constructs the object. + * + * @param name actor's name + */ + public BidirectionalTopicActor(String name) { + super(name); + } + + @Override + protected void doStart() { + params2topic.values().forEach(BidirectionalTopicHandler::start); + super.doStart(); + } + + @Override + protected void doStop() { + params2topic.values().forEach(BidirectionalTopicHandler::stop); + super.doStop(); + } + + @Override + protected void doShutdown() { + params2topic.values().forEach(BidirectionalTopicHandler::shutdown); + params2topic.clear(); + super.doShutdown(); + } + + @Override + public BidirectionalTopicHandler getTopicHandler(String sinkTopic, String sourceTopic) { + Pair<String, String> key = Pair.of(sinkTopic, sourceTopic); + + return params2topic.computeIfAbsent(key, pair -> { + try { + return makeTopicHandler(sinkTopic, sourceTopic); + } catch (BidirectionalTopicClientException e) { + throw new IllegalArgumentException(e); + } + }); + } + + /** + * Translates the parameters to a {@link BidirectionalTopicActorParams} and then + * creates a function that will extract operator-specific parameters. + */ + @Override + protected Function<String, Map<String, Object>> makeOperatorParameters(Map<String, Object> actorParameters) { + String actorName = getName(); + + // @formatter:off + return Util.translate(actorName, actorParameters, BidirectionalTopicActorParams.class) + .doValidation(actorName) + .makeOperationParameters(actorName); + // @formatter:on + } + + // may be overridden by junit tests + + protected BidirectionalTopicHandler makeTopicHandler(String sinkTopic, String sourceTopic) + throws BidirectionalTopicClientException { + + return new BidirectionalTopicHandler(sinkTopic, sourceTopic); + } +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/BidirectionalTopicOperation.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/BidirectionalTopicOperation.java new file mode 100644 index 000000000..d1e21f8fd --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/BidirectionalTopicOperation.java @@ -0,0 +1,265 @@ +/*- + * ============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.actorserviceprovider.impl; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import lombok.Getter; +import org.onap.policy.common.endpoints.utils.NetLoggerUtil.EventType; +import org.onap.policy.common.utils.coder.CoderException; +import org.onap.policy.common.utils.coder.StandardCoderObject; +import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome; +import org.onap.policy.controlloop.actorserviceprovider.parameters.BidirectionalTopicParams; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; +import org.onap.policy.controlloop.actorserviceprovider.pipeline.PipelineControllerFuture; +import org.onap.policy.controlloop.actorserviceprovider.topic.BidirectionalTopicHandler; +import org.onap.policy.controlloop.actorserviceprovider.topic.Forwarder; +import org.onap.policy.controlloop.policy.PolicyResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Operation that uses a bidirectional topic. + * + * @param <S> response type + */ +@Getter +public abstract class BidirectionalTopicOperation<Q, S> extends OperationPartial { + private static final Logger logger = LoggerFactory.getLogger(BidirectionalTopicOperation.class); + + /** + * Response status. + */ + public enum Status { + SUCCESS, FAILURE, STILL_WAITING + } + + // fields extracted from the operator + + private final BidirectionalTopicHandler topicHandler; + private final Forwarder forwarder; + private final BidirectionalTopicParams topicParams; + private final long timeoutMs; + + /** + * Response class. + */ + private final Class<S> responseClass; + + + /** + * Constructs the object. + * + * @param params operation parameters + * @param operator operator that created this operation + * @param clazz response class + */ + public BidirectionalTopicOperation(ControlLoopOperationParams params, BidirectionalTopicOperator operator, + Class<S> clazz) { + super(params, operator); + this.topicHandler = operator.getTopicHandler(); + this.forwarder = operator.getForwarder(); + this.topicParams = operator.getParams(); + this.responseClass = clazz; + this.timeoutMs = TimeUnit.MILLISECONDS.convert(topicParams.getTimeoutSec(), TimeUnit.SECONDS); + } + + /** + * If no timeout is specified, then it returns the default timeout. + */ + @Override + protected long getTimeoutMs(Integer timeoutSec) { + // TODO move this method to the superclass + return (timeoutSec == null || timeoutSec == 0 ? this.timeoutMs : super.getTimeoutMs(timeoutSec)); + } + + /** + * Publishes the request and arranges to receive the response. + */ + @Override + protected CompletableFuture<OperationOutcome> startOperationAsync(int attempt, OperationOutcome outcome) { + + final Q request = makeRequest(attempt); + final List<String> expectedKeyValues = getExpectedKeyValues(attempt, request); + + final PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>(); + final Executor executor = params.getExecutor(); + + // register a listener BEFORE publishing + + BiConsumer<String, StandardCoderObject> listener = (rawResponse, scoResponse) -> { + OperationOutcome latestOutcome = processResponse(outcome, rawResponse, scoResponse); + if (latestOutcome != null) { + // final response - complete the controller + controller.completeAsync(() -> latestOutcome, executor); + } + }; + + forwarder.register(expectedKeyValues, listener); + + // ensure listener is unregistered if the controller is canceled + controller.add(() -> forwarder.unregister(expectedKeyValues, listener)); + + // publish the request + try { + publishRequest(request); + } catch (RuntimeException e) { + logger.warn("{}: failed to publish request for {}", getFullName(), params.getRequestId()); + forwarder.unregister(expectedKeyValues, listener); + throw e; + } + + return controller; + } + + /** + * Makes the request. + * + * @param attempt operation attempt + * @return a new request + */ + protected abstract Q makeRequest(int attempt); + + /** + * Gets values, expected in the response, that should match the selector keys. + * + * @param attempt operation attempt + * @param request request to be published + * @return a list of the values to be matched by the selector keys + */ + protected abstract List<String> getExpectedKeyValues(int attempt, Q request); + + /** + * Publishes the request. Encodes the request, if it is not already a String. + * + * @param request request to be published + */ + protected void publishRequest(Q request) { + String json; + try { + if (request instanceof String) { + json = request.toString(); + } else { + json = makeCoder().encode(request); + } + } catch (CoderException e) { + throw new IllegalArgumentException("cannot encode request", e); + } + + if (!topicHandler.send(json)) { + throw new IllegalStateException("nothing published"); + } + + logMessage(EventType.OUT, topicHandler.getSinkTopicCommInfrastructure(), topicHandler.getSinkTopic(), request); + } + + /** + * Processes a response. + * + * @param infra communication infrastructure on which the response was received + * @param outcome outcome to be populated + * @param response raw response to process + * @param scoResponse response, as a {@link StandardCoderObject} + * @return the outcome, or {@code null} if still waiting for completion + */ + protected OperationOutcome processResponse(OperationOutcome outcome, String rawResponse, + StandardCoderObject scoResponse) { + + logger.info("{}.{}: response received for {}", params.getActor(), params.getOperation(), params.getRequestId()); + + logMessage(EventType.IN, topicHandler.getSourceTopicCommInfrastructure(), topicHandler.getSourceTopic(), + rawResponse); + + // decode the response + S response; + if (responseClass == String.class) { + response = responseClass.cast(rawResponse); + + } else if (responseClass == StandardCoderObject.class) { + response = responseClass.cast(scoResponse); + + } else { + try { + response = makeCoder().decode(rawResponse, responseClass); + } catch (CoderException e) { + logger.warn("{}.{} cannot decode response for {}", params.getActor(), params.getOperation(), + params.getRequestId()); + throw new IllegalArgumentException("cannot decode response", e); + } + } + + // check its status + switch (detmStatus(rawResponse, response)) { + case SUCCESS: + logger.info("{}.{} request succeeded for {}", params.getActor(), params.getOperation(), + params.getRequestId()); + setOutcome(outcome, PolicyResult.SUCCESS, response); + postProcessResponse(outcome, rawResponse, response); + return outcome; + + case FAILURE: + logger.info("{}.{} request failed for {}", params.getActor(), params.getOperation(), + params.getRequestId()); + return setOutcome(outcome, PolicyResult.FAILURE, response); + + case STILL_WAITING: + default: + logger.info("{}.{} request incomplete for {}", params.getActor(), params.getOperation(), + params.getRequestId()); + return null; + } + } + + /** + * Sets an operation's outcome and default message based on the result. + * + * @param outcome operation to be updated + * @param result result of the operation + * @param response response used to populate the outcome + * @return the updated operation + */ + public OperationOutcome setOutcome(OperationOutcome outcome, PolicyResult result, S response) { + return setOutcome(outcome, result); + } + + /** + * Processes a successful response. + * + * @param outcome outcome to be populated + * @param rawResponse raw response + * @param response decoded response + */ + protected void postProcessResponse(OperationOutcome outcome, String rawResponse, S response) { + // do nothing + } + + /** + * Determines the status of the response. + * + * @param rawResponse raw response + * @param response decoded response + * @return the status of the response + */ + protected abstract Status detmStatus(String rawResponse, S response); +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/BidirectionalTopicOperator.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/BidirectionalTopicOperator.java new file mode 100644 index 000000000..51689e49b --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/BidirectionalTopicOperator.java @@ -0,0 +1,160 @@ +/*- + * ============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.actorserviceprovider.impl; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import lombok.Getter; +import org.onap.policy.common.parameters.ValidationResult; +import org.onap.policy.controlloop.actorserviceprovider.Operation; +import org.onap.policy.controlloop.actorserviceprovider.Util; +import org.onap.policy.controlloop.actorserviceprovider.parameters.BidirectionalTopicParams; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ParameterValidationRuntimeException; +import org.onap.policy.controlloop.actorserviceprovider.topic.BidirectionalTopicHandler; +import org.onap.policy.controlloop.actorserviceprovider.topic.BidirectionalTopicManager; +import org.onap.policy.controlloop.actorserviceprovider.topic.Forwarder; +import org.onap.policy.controlloop.actorserviceprovider.topic.SelectorKey; + +/** + * Operator that uses a bidirectional topic. Topic operators may share a + * {@link BidirectionalTopicHandler}. + */ +public abstract class BidirectionalTopicOperator extends OperatorPartial { + + /** + * Manager from which to get the topic handlers. + */ + private final BidirectionalTopicManager topicManager; + + /** + * Keys used to extract the fields used to select responses for this operator. + */ + private final List<SelectorKey> selectorKeys; + + /* + * The remaining fields are initialized when configure() is invoked, thus they may + * change. + */ + + /** + * Current parameters. While {@link params} may change, the values contained within it + * will not, thus operations may copy it. + */ + @Getter + private BidirectionalTopicParams params; + + /** + * Topic handler associated with the parameters. + */ + @Getter + private BidirectionalTopicHandler topicHandler; + + /** + * Forwarder associated with the parameters. + */ + @Getter + private Forwarder forwarder; + + + /** + * Constructs the object. + * + * @param actorName name of the actor with which this operator is associated + * @param name operation name + * @param topicManager manager from which to get the topic handler + * @param selectorKeys keys used to extract the fields used to select responses for + * this operator + */ + public BidirectionalTopicOperator(String actorName, String name, BidirectionalTopicManager topicManager, + List<SelectorKey> selectorKeys) { + super(actorName, name); + this.topicManager = topicManager; + this.selectorKeys = selectorKeys; + } + + @Override + protected void doConfigure(Map<String, Object> parameters) { + params = Util.translate(getFullName(), parameters, BidirectionalTopicParams.class); + ValidationResult result = params.validate(getFullName()); + if (!result.isValid()) { + throw new ParameterValidationRuntimeException("invalid parameters", result); + } + + topicHandler = topicManager.getTopicHandler(params.getSinkTopic(), params.getSourceTopic()); + forwarder = topicHandler.addForwarder(selectorKeys); + } + + /** + * Makes an operator that will construct operations. + * + * @param <Q> request type + * @param <S> response type + * @param actorName actor name + * @param operation operation name + * @param topicManager manager from which to get the topic handler + * @param operationMaker function to make an operation + * @param keys keys used to extract the fields used to select responses for this + * operator + * @return a new operator + */ + // @formatter:off + public static <Q, S> BidirectionalTopicOperator makeOperator(String actorName, String operation, + BidirectionalTopicManager topicManager, + BiFunction<ControlLoopOperationParams, BidirectionalTopicOperator, + BidirectionalTopicOperation<Q, S>> operationMaker, + SelectorKey... keys) { + // @formatter:off + + return makeOperator(actorName, operation, topicManager, Arrays.asList(keys), operationMaker); + } + + /** + * Makes an operator that will construct operations. + * + * @param <Q> request type + * @param <S> response type + * @param actorName actor name + * @param operation operation name + * @param topicManager manager from which to get the topic handler + * @param keys keys used to extract the fields used to select responses for + * this operator + * @param operationMaker function to make an operation + * @return a new operator + */ + // @formatter:off + public static <Q,S> BidirectionalTopicOperator makeOperator(String actorName, String operation, + BidirectionalTopicManager topicManager, + List<SelectorKey> keys, + BiFunction<ControlLoopOperationParams, BidirectionalTopicOperator, + BidirectionalTopicOperation<Q,S>> operationMaker) { + // @formatter:on + + return new BidirectionalTopicOperator(actorName, operation, topicManager, keys) { + @Override + public synchronized Operation buildOperation(ControlLoopOperationParams params) { + return operationMaker.apply(params, this); + } + }; + } +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpOperation.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpOperation.java new file mode 100644 index 000000000..c3c0f6dc2 --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpOperation.java @@ -0,0 +1,245 @@ +/*- + * ============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.actorserviceprovider.impl; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; +import java.util.function.Function; +import javax.ws.rs.client.InvocationCallback; +import javax.ws.rs.core.Response; +import lombok.Getter; +import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure; +import org.onap.policy.common.endpoints.http.client.HttpClient; +import org.onap.policy.common.endpoints.utils.NetLoggerUtil; +import org.onap.policy.common.endpoints.utils.NetLoggerUtil.EventType; +import org.onap.policy.common.utils.coder.CoderException; +import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; +import org.onap.policy.controlloop.actorserviceprovider.parameters.HttpParams; +import org.onap.policy.controlloop.actorserviceprovider.pipeline.PipelineControllerFuture; +import org.onap.policy.controlloop.policy.PolicyResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Operator that uses HTTP. The operator's parameters must be an {@link HttpParams}. + * + * @param <T> response type + */ +@Getter +public abstract class HttpOperation<T> extends OperationPartial { + private static final Logger logger = LoggerFactory.getLogger(HttpOperation.class); + + /** + * Operator that created this operation. + */ + protected final HttpOperator operator; + + /** + * Response class. + */ + private final Class<T> responseClass; + + + /** + * Constructs the object. + * + * @param params operation parameters + * @param operator operator that created this operation + * @param clazz response class + */ + public HttpOperation(ControlLoopOperationParams params, HttpOperator operator, Class<T> clazz) { + super(params, operator); + this.operator = operator; + this.responseClass = clazz; + } + + /** + * If no timeout is specified, then it returns the operator's configured timeout. + */ + @Override + protected long getTimeoutMs(Integer timeoutSec) { + return (timeoutSec == null || timeoutSec == 0 ? operator.getTimeoutMs() : super.getTimeoutMs(timeoutSec)); + } + + /** + * Makes the request headers. This simply returns an empty map. + * + * @return request headers, a non-null, modifiable map + */ + protected Map<String, Object> makeHeaders() { + return new HashMap<>(); + } + + /** + * Gets the path to be used when performing the request; this is typically appended to + * the base URL. This method simply invokes {@link #getPath()}. + * + * @return the path URI suffix + */ + public String makePath() { + return operator.getPath(); + } + + /** + * Makes the URL to which the "get" request should be posted. This ir primarily used + * for logging purposes. This particular method returns the base URL appended with the + * return value from {@link #makePath()}. + * + * @return the URL to which from which to get + */ + public String makeUrl() { + return (operator.getClient().getBaseUrl() + makePath()); + } + + /** + * Arranges to handle a response. + * + * @param outcome outcome to be populate + * @param url URL to which to request was sent + * @param requester function to initiate the request and invoke the given callback + * when it completes + * @return a future for the response + */ + protected CompletableFuture<OperationOutcome> handleResponse(OperationOutcome outcome, String url, + Function<InvocationCallback<Response>, Future<Response>> requester) { + + final PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>(); + final CompletableFuture<Response> future = new CompletableFuture<>(); + final Executor executor = params.getExecutor(); + + // arrange for the callback to complete "future" + InvocationCallback<Response> callback = new InvocationCallback<>() { + @Override + public void completed(Response response) { + future.complete(response); + } + + @Override + public void failed(Throwable throwable) { + logger.warn("{}.{}: response failure for {}", params.getActor(), params.getOperation(), + params.getRequestId()); + future.completeExceptionally(throwable); + } + }; + + // start the request and arrange to cancel it if the controller is canceled + controller.add(requester.apply(callback)); + + // once "future" completes, process the response, and then complete the controller + future.thenComposeAsync(response -> processResponse(outcome, url, response), executor) + .whenCompleteAsync(controller.delayedComplete(), executor); + + return controller; + } + + /** + * Processes a response. This method decodes the response, sets the outcome based on + * the response, and then returns a completed future. + * + * @param outcome outcome to be populate + * @param url URL to which to request was sent + * @param response raw response to process + * @return a future to cancel or await the outcome + */ + protected CompletableFuture<OperationOutcome> processResponse(OperationOutcome outcome, String url, + Response rawResponse) { + + logger.info("{}.{}: response received for {}", params.getActor(), params.getOperation(), params.getRequestId()); + + String strResponse = HttpClient.getBody(rawResponse, String.class); + + logMessage(EventType.IN, CommInfrastructure.REST, url, strResponse); + + T response; + if (responseClass == String.class) { + response = responseClass.cast(strResponse); + } else { + try { + response = makeCoder().decode(strResponse, responseClass); + } catch (CoderException e) { + logger.warn("{}.{} cannot decode response for {}", params.getActor(), params.getOperation(), + params.getRequestId(), e); + throw new IllegalArgumentException("cannot decode response"); + } + } + + if (!isSuccess(rawResponse, response)) { + logger.info("{}.{} request failed with http error code {} for {}", params.getActor(), params.getOperation(), + rawResponse.getStatus(), params.getRequestId()); + return CompletableFuture.completedFuture(setOutcome(outcome, PolicyResult.FAILURE, response)); + } + + logger.info("{}.{} request succeeded for {}", params.getActor(), params.getOperation(), params.getRequestId()); + setOutcome(outcome, PolicyResult.SUCCESS, response); + return postProcessResponse(outcome, url, rawResponse, response); + } + + /** + * Sets an operation's outcome and default message based on the result. + * + * @param outcome operation to be updated + * @param result result of the operation + * @param response response used to populate the outcome + * @return the updated operation + */ + public OperationOutcome setOutcome(OperationOutcome outcome, PolicyResult result, T response) { + return setOutcome(outcome, result); + } + + /** + * Processes a successful response. This method simply returns the outcome wrapped in + * a completed future. + * + * @param outcome outcome to be populate + * @param url URL to which to request was sent + * @param rawResponse raw response + * @param response decoded response + * @return a future to cancel or await the outcome + */ + protected CompletableFuture<OperationOutcome> postProcessResponse(OperationOutcome outcome, String url, + Response rawResponse, T response) { + + return CompletableFuture.completedFuture(outcome); + } + + /** + * Determines if the response indicates success. This method simply checks the HTTP + * status code. + * + * @param rawResponse raw response + * @param response decoded response + * @return {@code true} if the response indicates success, {@code false} otherwise + */ + protected boolean isSuccess(Response rawResponse, T response) { + return (rawResponse.getStatus() == 200); + } + + @Override + public <Q> String logMessage(EventType direction, CommInfrastructure infra, String sink, Q request) { + String json = super.logMessage(direction, infra, sink, request); + NetLoggerUtil.log(direction, infra, sink, json); + return json; + } +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpOperator.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpOperator.java index 566492907..add74aa42 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpOperator.java +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpOperator.java @@ -21,31 +21,35 @@ package org.onap.policy.controlloop.actorserviceprovider.impl; import java.util.Map; -import lombok.AccessLevel; +import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; import lombok.Getter; import org.onap.policy.common.endpoints.http.client.HttpClient; import org.onap.policy.common.endpoints.http.client.HttpClientFactory; import org.onap.policy.common.endpoints.http.client.HttpClientFactoryInstance; import org.onap.policy.common.parameters.ValidationResult; +import org.onap.policy.controlloop.actorserviceprovider.Operation; import org.onap.policy.controlloop.actorserviceprovider.Util; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; import org.onap.policy.controlloop.actorserviceprovider.parameters.HttpParams; import org.onap.policy.controlloop.actorserviceprovider.parameters.ParameterValidationRuntimeException; /** - * Operator that uses HTTP. The operator's parameters must be a {@link HttpParams}. + * Operator that uses HTTP. The operator's parameters must be an {@link HttpParams}. */ -public class HttpOperator extends OperatorPartial { +@Getter +public abstract class HttpOperator extends OperatorPartial { - @Getter(AccessLevel.PROTECTED) private HttpClient client; - @Getter - private long timeoutSec; + /** + * Default timeout, in milliseconds, if none specified in the request. + */ + private long timeoutMs; /** - * URI path for this particular operation. + * URI path for this particular operation. Includes a leading "/". */ - @Getter private String path; @@ -60,6 +64,26 @@ public class HttpOperator extends OperatorPartial { } /** + * Makes an operator that will construct operations. + * + * @param <T> response type + * @param actorName actor name + * @param operation operation name + * @param operationMaker function to make an operation + * @return a new operator + */ + public static <T> HttpOperator makeOperator(String actorName, String operation, + BiFunction<ControlLoopOperationParams, HttpOperator, HttpOperation<T>> operationMaker) { + + return new HttpOperator(actorName, operation) { + @Override + public Operation buildOperation(ControlLoopOperationParams params) { + return operationMaker.apply(params, this); + } + }; + } + + /** * Translates the parameters to an {@link HttpParams} and then extracts the relevant * values. */ @@ -73,10 +97,10 @@ public class HttpOperator extends OperatorPartial { client = getClientFactory().get(params.getClientName()); path = params.getPath(); - timeoutSec = params.getTimeoutSec(); + timeoutMs = TimeUnit.MILLISECONDS.convert(params.getTimeoutSec(), TimeUnit.SECONDS); } - // these may be overridden by junits + // these may be overridden by junit tests protected HttpClientFactory getClientFactory() { return HttpClientFactoryInstance.getClientFactory(); diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/OperationPartial.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/OperationPartial.java new file mode 100644 index 000000000..680a56f89 --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/OperationPartial.java @@ -0,0 +1,983 @@ +/*- + * ============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.actorserviceprovider.impl; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; +import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure; +import org.onap.policy.common.endpoints.utils.NetLoggerUtil; +import org.onap.policy.common.endpoints.utils.NetLoggerUtil.EventType; +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.controlloop.ControlLoopOperation; +import org.onap.policy.controlloop.actorserviceprovider.CallbackManager; +import org.onap.policy.controlloop.actorserviceprovider.Operation; +import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; +import org.onap.policy.controlloop.actorserviceprovider.pipeline.PipelineControllerFuture; +import org.onap.policy.controlloop.policy.PolicyResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Partial implementation of an operator. In general, it's preferable that subclasses + * would override {@link #startOperationAsync(int, OperationOutcome) + * startOperationAsync()}. However, if that proves to be too difficult, then they can + * simply override {@link #doOperation(int, OperationOutcome) doOperation()}. In addition, + * if the operation requires any preprocessor steps, the subclass may choose to override + * {@link #startPreprocessorAsync()}. + * <p/> + * The futures returned by the methods within this class can be canceled, and will + * propagate the cancellation to any subtasks. Thus it is also expected that any futures + * returned by overridden methods will do the same. Of course, if a class overrides + * {@link #doOperation(int, OperationOutcome) doOperation()}, then there's little that can + * be done to cancel that particular operation. + */ +public abstract class OperationPartial implements Operation { + private static final Logger logger = LoggerFactory.getLogger(OperationPartial.class); + private static final Coder coder = new StandardCoder(); + + public static final long DEFAULT_RETRY_WAIT_MS = 1000L; + + // values extracted from the operator + + private final OperatorPartial operator; + + /** + * Operation parameters. + */ + protected final ControlLoopOperationParams params; + + + /** + * Constructs the object. + * + * @param params operation parameters + * @param operator operator that created this operation + */ + public OperationPartial(ControlLoopOperationParams params, OperatorPartial operator) { + this.params = params; + this.operator = operator; + } + + public Executor getBlockingExecutor() { + return operator.getBlockingExecutor(); + } + + public String getFullName() { + return operator.getFullName(); + } + + public String getActorName() { + return operator.getActorName(); + } + + public String getName() { + return operator.getName(); + } + + @Override + public final CompletableFuture<OperationOutcome> start() { + if (!operator.isAlive()) { + throw new IllegalStateException("operation is not running: " + getFullName()); + } + + // allocate a controller for the entire operation + final PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>(); + + CompletableFuture<OperationOutcome> preproc = startPreprocessorAsync(); + if (preproc == null) { + // no preprocessor required - just start the operation + return startOperationAttempt(controller, 1); + } + + /* + * Do preprocessor first and then, if successful, start the operation. Note: + * operations create their own outcome, ignoring the outcome from any previous + * steps. + * + * Wrap the preprocessor to ensure "stop" is propagated to it. + */ + // @formatter:off + controller.wrap(preproc) + .exceptionally(fromException("preprocessor of operation")) + .thenCompose(handlePreprocessorFailure(controller)) + .thenCompose(unusedOutcome -> startOperationAttempt(controller, 1)) + .whenCompleteAsync(controller.delayedComplete(), params.getExecutor()); + // @formatter:on + + return controller; + } + + /** + * Handles a failure in the preprocessor pipeline. If a failure occurred, then it + * invokes the call-backs, marks the controller complete, and returns an incomplete + * future, effectively halting the pipeline. Otherwise, it returns the outcome that it + * received. + * <p/> + * Assumes that no callbacks have been invoked yet. + * + * @param controller pipeline controller + * @return a function that checks the outcome status and continues, if successful, or + * indicates a failure otherwise + */ + private Function<OperationOutcome, CompletableFuture<OperationOutcome>> handlePreprocessorFailure( + PipelineControllerFuture<OperationOutcome> controller) { + + return outcome -> { + + if (outcome != null && isSuccess(outcome)) { + logger.info("{}: preprocessor succeeded for {}", getFullName(), params.getRequestId()); + return CompletableFuture.completedFuture(outcome); + } + + logger.warn("preprocessor failed, discontinuing operation {} for {}", getFullName(), params.getRequestId()); + + final Executor executor = params.getExecutor(); + final CallbackManager callbacks = new CallbackManager(); + + // propagate "stop" to the callbacks + controller.add(callbacks); + + final OperationOutcome outcome2 = params.makeOutcome(); + + // TODO need a FAILURE_MISSING_DATA (e.g., A&AI) + + outcome2.setResult(PolicyResult.FAILURE_GUARD); + outcome2.setMessage(outcome != null ? outcome.getMessage() : null); + + // @formatter:off + CompletableFuture.completedFuture(outcome2) + .whenCompleteAsync(callbackStarted(callbacks), executor) + .whenCompleteAsync(callbackCompleted(callbacks), executor) + .whenCompleteAsync(controller.delayedComplete(), executor); + // @formatter:on + + return new CompletableFuture<>(); + }; + } + + /** + * Invokes the operation's preprocessor step(s) as a "future". This method simply + * invokes {@link #startGuardAsync()}. + * <p/> + * This method assumes the following: + * <ul> + * <li>the operator is alive</li> + * <li>exceptions generated within the pipeline will be handled by the invoker</li> + * </ul> + * + * @return a function that will start the preprocessor and returns its outcome, or + * {@code null} if this operation needs no preprocessor + */ + protected CompletableFuture<OperationOutcome> startPreprocessorAsync() { + return startGuardAsync(); + } + + /** + * Invokes the operation's guard step(s) as a "future". This method simply returns + * {@code null}. + * <p/> + * This method assumes the following: + * <ul> + * <li>the operator is alive</li> + * <li>exceptions generated within the pipeline will be handled by the invoker</li> + * </ul> + * + * @return a function that will start the guard checks and returns its outcome, or + * {@code null} if this operation has no guard + */ + protected CompletableFuture<OperationOutcome> startGuardAsync() { + return null; + } + + /** + * Starts the operation attempt, with no preprocessor. When all retries complete, it + * will complete the controller. + * + * @param controller controller for all operation attempts + * @param attempt attempt number, typically starting with 1 + * @return a future that will return the final result of all attempts + */ + private CompletableFuture<OperationOutcome> startOperationAttempt( + PipelineControllerFuture<OperationOutcome> controller, int attempt) { + + // propagate "stop" to the operation attempt + controller.wrap(startAttemptWithoutRetries(attempt)).thenCompose(retryOnFailure(controller, attempt)) + .whenCompleteAsync(controller.delayedComplete(), params.getExecutor()); + + return controller; + } + + /** + * Starts the operation attempt, without doing any retries. + * + * @param params operation parameters + * @param attempt attempt number, typically starting with 1 + * @return a future that will return the result of a single operation attempt + */ + private CompletableFuture<OperationOutcome> startAttemptWithoutRetries(int attempt) { + + logger.info("{}: start operation attempt {} for {}", getFullName(), attempt, params.getRequestId()); + + final Executor executor = params.getExecutor(); + final OperationOutcome outcome = params.makeOutcome(); + final CallbackManager callbacks = new CallbackManager(); + + // this operation attempt gets its own controller + final PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>(); + + // propagate "stop" to the callbacks + controller.add(callbacks); + + // @formatter:off + CompletableFuture<OperationOutcome> future = CompletableFuture.completedFuture(outcome) + .whenCompleteAsync(callbackStarted(callbacks), executor) + .thenCompose(controller.wrap(outcome2 -> startOperationAsync(attempt, outcome2))); + // @formatter:on + + // handle timeouts, if specified + long timeoutMillis = getTimeoutMs(params.getTimeoutSec()); + if (timeoutMillis > 0) { + logger.info("{}: set timeout to {}ms for {}", getFullName(), timeoutMillis, params.getRequestId()); + future = future.orTimeout(timeoutMillis, TimeUnit.MILLISECONDS); + } + + /* + * Note: we re-invoke callbackStarted() just to be sure the callback is invoked + * before callbackCompleted() is invoked. + * + * Note: no need to remove "callbacks" from the pipeline, as we're going to stop + * the pipeline as the last step anyway. + */ + + // @formatter:off + future.exceptionally(fromException("operation")) + .thenApply(setRetryFlag(attempt)) + .whenCompleteAsync(callbackStarted(callbacks), executor) + .whenCompleteAsync(callbackCompleted(callbacks), executor) + .whenCompleteAsync(controller.delayedComplete(), executor); + // @formatter:on + + return controller; + } + + /** + * Determines if the outcome was successful. + * + * @param outcome outcome to examine + * @return {@code true} if the outcome was successful + */ + protected boolean isSuccess(OperationOutcome outcome) { + return (outcome.getResult() == PolicyResult.SUCCESS); + } + + /** + * Determines if the outcome was a failure for this operator. + * + * @param outcome outcome to examine, or {@code null} + * @return {@code true} if the outcome is not {@code null} and was a failure + * <i>and</i> was associated with this operator, {@code false} otherwise + */ + protected boolean isActorFailed(OperationOutcome outcome) { + return (isSameOperation(outcome) && outcome.getResult() == PolicyResult.FAILURE); + } + + /** + * Determines if the given outcome is for this operation. + * + * @param outcome outcome to examine + * @return {@code true} if the outcome is for this operation, {@code false} otherwise + */ + protected boolean isSameOperation(OperationOutcome outcome) { + return OperationOutcome.isFor(outcome, getActorName(), getName()); + } + + /** + * Invokes the operation as a "future". This method simply invokes + * {@link #doOperation()} using the {@link #blockingExecutor "blocking executor"}, + * returning the result via a "future". + * <p/> + * Note: if the operation uses blocking I/O, then it should <i>not</i> be run using + * the executor in the "params", as that may bring the background thread pool to a + * grinding halt. The {@link #blockingExecutor "blocking executor"} should be used + * instead. + * <p/> + * This method assumes the following: + * <ul> + * <li>the operator is alive</li> + * <li>verifyRunning() has been invoked</li> + * <li>callbackStarted() has been invoked</li> + * <li>the invoker will perform appropriate timeout checks</li> + * <li>exceptions generated within the pipeline will be handled by the invoker</li> + * </ul> + * + * @param attempt attempt number, typically starting with 1 + * @return a function that will start the operation and return its result when + * complete + */ + protected CompletableFuture<OperationOutcome> startOperationAsync(int attempt, OperationOutcome outcome) { + + return CompletableFuture.supplyAsync(() -> doOperation(attempt, outcome), getBlockingExecutor()); + } + + /** + * Low-level method that performs the operation. This can make the same assumptions + * that are made by {@link #doOperationAsFuture()}. This particular method simply + * throws an {@link UnsupportedOperationException}. + * + * @param attempt attempt number, typically starting with 1 + * @param operation the operation being performed + * @return the outcome of the operation + */ + protected OperationOutcome doOperation(int attempt, OperationOutcome operation) { + + throw new UnsupportedOperationException("start operation " + getFullName()); + } + + /** + * Sets the outcome status to FAILURE_RETRIES, if the current operation outcome is + * FAILURE, assuming the policy specifies retries and the retry count has been + * exhausted. + * + * @param attempt latest attempt number, starting with 1 + * @return a function to get the next future to execute + */ + private Function<OperationOutcome, OperationOutcome> setRetryFlag(int attempt) { + + return operation -> { + if (operation != null && !isActorFailed(operation)) { + /* + * wrong type or wrong operation - just leave it as is. No need to log + * anything here, as retryOnFailure() will log a message + */ + return operation; + } + + // get a non-null operation + OperationOutcome oper2; + if (operation != null) { + oper2 = operation; + } else { + oper2 = params.makeOutcome(); + oper2.setResult(PolicyResult.FAILURE); + } + + int retry = getRetry(params.getRetry()); + if (retry > 0 && attempt > retry) { + /* + * retries were specified and we've already tried them all - change to + * FAILURE_RETRIES + */ + logger.info("operation {} retries exhausted for {}", getFullName(), params.getRequestId()); + oper2.setResult(PolicyResult.FAILURE_RETRIES); + } + + return oper2; + }; + } + + /** + * Restarts the operation if it was a FAILURE. Assumes that {@link #setRetryFlag(int)} + * was previously invoked, and thus that the "operation" is not {@code null}. + * + * @param controller controller for all of the retries + * @param attempt latest attempt number, starting with 1 + * @return a function to get the next future to execute + */ + private Function<OperationOutcome, CompletableFuture<OperationOutcome>> retryOnFailure( + PipelineControllerFuture<OperationOutcome> controller, int attempt) { + + return operation -> { + if (!isActorFailed(operation)) { + // wrong type or wrong operation - just leave it as is + logger.info("not retrying operation {} for {}", getFullName(), params.getRequestId()); + controller.complete(operation); + return new CompletableFuture<>(); + } + + if (getRetry(params.getRetry()) <= 0) { + // no retries - already marked as FAILURE, so just return it + logger.info("operation {} no retries for {}", getFullName(), params.getRequestId()); + controller.complete(operation); + return new CompletableFuture<>(); + } + + /* + * Retry the operation. + */ + long waitMs = getRetryWaitMs(); + logger.info("retry operation {} in {}ms for {}", getFullName(), waitMs, params.getRequestId()); + + return sleep(waitMs, TimeUnit.MILLISECONDS) + .thenCompose(unused -> startOperationAttempt(controller, attempt + 1)); + }; + } + + /** + * Convenience method that starts a sleep(), running via a future. + * + * @param sleepTime time to sleep + * @param unit time unit + * @return a future that will complete when the sleep completes + */ + protected CompletableFuture<Void> sleep(long sleepTime, TimeUnit unit) { + if (sleepTime <= 0) { + return CompletableFuture.completedFuture(null); + } + + return new CompletableFuture<Void>().completeOnTimeout(null, sleepTime, unit); + } + + /** + * Converts an exception into an operation outcome, returning a copy of the outcome to + * prevent background jobs from changing it. + * + * @param type type of item throwing the exception + * @return a function that will convert an exception into an operation outcome + */ + private Function<Throwable, OperationOutcome> fromException(String type) { + + return thrown -> { + OperationOutcome outcome = params.makeOutcome(); + + logger.warn("exception throw by {} {}.{} for {}", type, outcome.getActor(), outcome.getOperation(), + params.getRequestId(), thrown); + + return setOutcome(outcome, thrown); + }; + } + + /** + * Similar to {@link CompletableFuture#anyOf(CompletableFuture...)}, but it cancels + * any outstanding futures when one completes. + * + * @param futureMakers function to make a future. If the function returns + * {@code null}, then no future is created for that function. On the other + * hand, if the function throws an exception, then the previously created + * functions are canceled and the exception is re-thrown + * @return a future to cancel or await an outcome, or {@code null} if no futures were + * created. If this future is canceled, then all of the futures will be + * canceled + */ + protected CompletableFuture<OperationOutcome> anyOf( + @SuppressWarnings("unchecked") Supplier<CompletableFuture<OperationOutcome>>... futureMakers) { + + return anyOf(Arrays.asList(futureMakers)); + } + + /** + * Similar to {@link CompletableFuture#anyOf(CompletableFuture...)}, but it cancels + * any outstanding futures when one completes. + * + * @param futureMakers function to make a future. If the function returns + * {@code null}, then no future is created for that function. On the other + * hand, if the function throws an exception, then the previously created + * functions are canceled and the exception is re-thrown + * @return a future to cancel or await an outcome, or {@code null} if no futures were + * created. If this future is canceled, then all of the futures will be + * canceled. Similarly, when this future completes, any incomplete futures + * will be canceled + */ + protected CompletableFuture<OperationOutcome> anyOf( + List<Supplier<CompletableFuture<OperationOutcome>>> futureMakers) { + + PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>(); + + CompletableFuture<OperationOutcome>[] futures = + attachFutures(controller, futureMakers, UnaryOperator.identity()); + + if (futures.length == 0) { + // no futures were started + return null; + } + + if (futures.length == 1) { + return futures[0]; + } + + CompletableFuture.anyOf(futures).thenApply(outcome -> (OperationOutcome) outcome) + .whenCompleteAsync(controller.delayedComplete(), params.getExecutor()); + + return controller; + } + + /** + * Similar to {@link CompletableFuture#allOf(CompletableFuture...)}. + * + * @param futureMakers function to make a future. If the function returns + * {@code null}, then no future is created for that function. On the other + * hand, if the function throws an exception, then the previously created + * functions are canceled and the exception is re-thrown + * @return a future to cancel or await an outcome, or {@code null} if no futures were + * created. If this future is canceled, then all of the futures will be + * canceled + */ + protected CompletableFuture<OperationOutcome> allOf( + @SuppressWarnings("unchecked") Supplier<CompletableFuture<OperationOutcome>>... futureMakers) { + + return allOf(Arrays.asList(futureMakers)); + } + + /** + * Similar to {@link CompletableFuture#allOf(CompletableFuture...)}. + * + * @param futureMakers function to make a future. If the function returns + * {@code null}, then no future is created for that function. On the other + * hand, if the function throws an exception, then the previously created + * functions are canceled and the exception is re-thrown + * @return a future to cancel or await an outcome, or {@code null} if no futures were + * created. If this future is canceled, then all of the futures will be + * canceled. Similarly, when this future completes, any incomplete futures + * will be canceled + */ + protected CompletableFuture<OperationOutcome> allOf( + List<Supplier<CompletableFuture<OperationOutcome>>> futureMakers) { + PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>(); + + Queue<OperationOutcome> outcomes = new LinkedList<>(); + + CompletableFuture<OperationOutcome>[] futures = + attachFutures(controller, futureMakers, future -> future.thenApply(outcome -> { + synchronized (outcomes) { + outcomes.add(outcome); + } + return outcome; + })); + + if (futures.length == 0) { + // no futures were started + return null; + } + + if (futures.length == 1) { + return futures[0]; + } + + // @formatter:off + CompletableFuture.allOf(futures) + .thenApply(unused -> combineOutcomes(outcomes)) + .whenCompleteAsync(controller.delayedComplete(), params.getExecutor()); + // @formatter:on + + return controller; + } + + /** + * Invokes the functions to create the futures and attaches them to the controller. + * + * @param controller master controller for all of the futures + * @param futureMakers futures to be attached to the controller + * @param adorn function that "adorns" the future, possible adding onto its pipeline. + * Returns the adorned future + * @return an array of futures, possibly zero-length. If the array is of size one, + * then that one item should be returned instead of the controller + */ + private CompletableFuture<OperationOutcome>[] attachFutures(PipelineControllerFuture<OperationOutcome> controller, + List<Supplier<CompletableFuture<OperationOutcome>>> futureMakers, + UnaryOperator<CompletableFuture<OperationOutcome>> adorn) { + + if (futureMakers.isEmpty()) { + @SuppressWarnings("unchecked") + CompletableFuture<OperationOutcome>[] result = new CompletableFuture[0]; + return result; + } + + // the last, unadorned future that is created + CompletableFuture<OperationOutcome> lastFuture = null; + + List<CompletableFuture<OperationOutcome>> futures = new ArrayList<>(futureMakers.size()); + + // make each future + for (var maker : futureMakers) { + try { + CompletableFuture<OperationOutcome> future = maker.get(); + if (future == null) { + continue; + } + + // propagate "stop" to the future + controller.add(future); + + futures.add(adorn.apply(future)); + + lastFuture = future; + + } catch (RuntimeException e) { + logger.warn("{}: exception creating 'future' for {}", getFullName(), params.getRequestId()); + controller.cancel(false); + throw e; + } + } + + @SuppressWarnings("unchecked") + CompletableFuture<OperationOutcome>[] result = new CompletableFuture[futures.size()]; + + if (result.length == 1) { + // special case - return the unadorned future + result[0] = lastFuture; + return result; + } + + return futures.toArray(result); + } + + /** + * Combines the outcomes from a set of tasks. + * + * @param outcomes outcomes to be examined + * @return the combined outcome + */ + private OperationOutcome combineOutcomes(Queue<OperationOutcome> outcomes) { + + // identify the outcome with the highest priority + OperationOutcome outcome = outcomes.remove(); + int priority = detmPriority(outcome); + + for (OperationOutcome outcome2 : outcomes) { + int priority2 = detmPriority(outcome2); + + if (priority2 > priority) { + outcome = outcome2; + priority = priority2; + } + } + + logger.info("{}: combined outcome of tasks is {} for {}", getFullName(), + (outcome == null ? null : outcome.getResult()), params.getRequestId()); + + return outcome; + } + + /** + * Determines the priority of an outcome based on its result. + * + * @param outcome outcome to examine, or {@code null} + * @return the outcome's priority + */ + protected int detmPriority(OperationOutcome outcome) { + if (outcome == null || outcome.getResult() == null) { + return 1; + } + + switch (outcome.getResult()) { + case SUCCESS: + return 0; + + case FAILURE_GUARD: + return 2; + + case FAILURE_RETRIES: + return 3; + + case FAILURE: + return 4; + + case FAILURE_TIMEOUT: + return 5; + + case FAILURE_EXCEPTION: + default: + return 6; + } + } + + /** + * Performs a sequence of tasks, stopping if a task fails. A given task's future is + * not created until the previous task completes. The pipeline returns the outcome of + * the last task executed. + * + * @param futureMakers functions to make the futures + * @return a future to cancel the sequence or await the outcome + */ + protected CompletableFuture<OperationOutcome> sequence( + @SuppressWarnings("unchecked") Supplier<CompletableFuture<OperationOutcome>>... futureMakers) { + + return sequence(Arrays.asList(futureMakers)); + } + + /** + * Performs a sequence of tasks, stopping if a task fails. A given task's future is + * not created until the previous task completes. The pipeline returns the outcome of + * the last task executed. + * + * @param futureMakers functions to make the futures + * @return a future to cancel the sequence or await the outcome, or {@code null} if + * there were no tasks to perform + */ + protected CompletableFuture<OperationOutcome> sequence( + List<Supplier<CompletableFuture<OperationOutcome>>> futureMakers) { + + Queue<Supplier<CompletableFuture<OperationOutcome>>> queue = new ArrayDeque<>(futureMakers); + + CompletableFuture<OperationOutcome> nextTask = getNextTask(queue); + if (nextTask == null) { + // no tasks + return null; + } + + if (queue.isEmpty()) { + // only one task - just return it rather than wrapping it in a controller + return nextTask; + } + + /* + * multiple tasks - need a controller to stop whichever task is currently + * executing + */ + final PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>(); + final Executor executor = params.getExecutor(); + + // @formatter:off + controller.wrap(nextTask) + .thenComposeAsync(nextTaskOnSuccess(controller, queue), executor) + .whenCompleteAsync(controller.delayedComplete(), executor); + // @formatter:on + + return controller; + } + + /** + * Executes the next task in the queue, if the previous outcome was successful. + * + * @param controller pipeline controller + * @param taskQueue queue of tasks to be performed + * @return a future to execute the remaining tasks, or the current outcome, if it's a + * failure, or if there are no more tasks + */ + private Function<OperationOutcome, CompletableFuture<OperationOutcome>> nextTaskOnSuccess( + PipelineControllerFuture<OperationOutcome> controller, + Queue<Supplier<CompletableFuture<OperationOutcome>>> taskQueue) { + + return outcome -> { + if (!isSuccess(outcome)) { + // return the failure + return CompletableFuture.completedFuture(outcome); + } + + CompletableFuture<OperationOutcome> nextTask = getNextTask(taskQueue); + if (nextTask == null) { + // no tasks - just return the success + return CompletableFuture.completedFuture(outcome); + } + + // @formatter:off + return controller + .wrap(nextTask) + .thenComposeAsync(nextTaskOnSuccess(controller, taskQueue), params.getExecutor()); + // @formatter:on + }; + } + + /** + * Gets the next task from the queue, skipping those that are {@code null}. + * + * @param taskQueue task queue + * @return the next task, or {@code null} if the queue is now empty + */ + private CompletableFuture<OperationOutcome> getNextTask( + Queue<Supplier<CompletableFuture<OperationOutcome>>> taskQueue) { + + Supplier<CompletableFuture<OperationOutcome>> maker; + + while ((maker = taskQueue.poll()) != null) { + CompletableFuture<OperationOutcome> future = maker.get(); + if (future != null) { + return future; + } + } + + return null; + } + + /** + * Sets the start time of the operation and invokes the callback to indicate that the + * operation has started. Does nothing if the pipeline has been stopped. + * <p/> + * This assumes that the "outcome" is not {@code null}. + * + * @param callbacks used to determine if the start callback can be invoked + * @return a function that sets the start time and invokes the callback + */ + private BiConsumer<OperationOutcome, Throwable> callbackStarted(CallbackManager callbacks) { + + return (outcome, thrown) -> { + + if (callbacks.canStart()) { + // haven't invoked "start" callback yet + outcome.setStart(callbacks.getStartTime()); + outcome.setEnd(null); + params.callbackStarted(outcome); + } + }; + } + + /** + * Sets the end time of the operation and invokes the callback to indicate that the + * operation has completed. Does nothing if the pipeline has been stopped. + * <p/> + * This assumes that the "outcome" is not {@code null}. + * <p/> + * Note: the start time must be a reference rather than a plain value, because it's + * value must be gotten on-demand, when the returned function is executed at a later + * time. + * + * @param callbacks used to determine if the end callback can be invoked + * @return a function that sets the end time and invokes the callback + */ + private BiConsumer<OperationOutcome, Throwable> callbackCompleted(CallbackManager callbacks) { + + return (outcome, thrown) -> { + + if (callbacks.canEnd()) { + outcome.setStart(callbacks.getStartTime()); + outcome.setEnd(callbacks.getEndTime()); + params.callbackCompleted(outcome); + } + }; + } + + /** + * Sets an operation's outcome and message, based on a throwable. + * + * @param operation operation to be updated + * @return the updated operation + */ + protected OperationOutcome setOutcome(OperationOutcome operation, Throwable thrown) { + PolicyResult result = (isTimeout(thrown) ? PolicyResult.FAILURE_TIMEOUT : PolicyResult.FAILURE_EXCEPTION); + return setOutcome(operation, result); + } + + /** + * Sets an operation's outcome and default message based on the result. + * + * @param operation operation to be updated + * @param result result of the operation + * @return the updated operation + */ + public OperationOutcome setOutcome(OperationOutcome operation, PolicyResult result) { + logger.trace("{}: set outcome {} for {}", getFullName(), result, params.getRequestId()); + operation.setResult(result); + operation.setMessage(result == PolicyResult.SUCCESS ? ControlLoopOperation.SUCCESS_MSG + : ControlLoopOperation.FAILED_MSG); + + return operation; + } + + /** + * Determines if a throwable is due to a timeout. + * + * @param thrown throwable of interest + * @return {@code true} if the throwable is due to a timeout, {@code false} otherwise + */ + protected boolean isTimeout(Throwable thrown) { + if (thrown instanceof CompletionException) { + thrown = thrown.getCause(); + } + + return (thrown instanceof TimeoutException); + } + + /** + * Logs a response. If the response is not of type, String, then it attempts to + * pretty-print it into JSON before logging. + * + * @param direction IN or OUT + * @param infra communication infrastructure on which it was published + * @param source source name (e.g., the URL or Topic name) + * @param response response to be logged + * @return the JSON text that was logged + */ + public <T> String logMessage(EventType direction, CommInfrastructure infra, String source, T response) { + String json; + try { + if (response == null) { + json = null; + } else if (response instanceof String) { + json = response.toString(); + } else { + json = makeCoder().encode(response, true); + } + + } catch (CoderException e) { + String type = (direction == EventType.IN ? "response" : "request"); + logger.warn("cannot pretty-print {}", type, e); + json = response.toString(); + } + + logger.info("[{}|{}|{}|]{}{}", direction, infra, source, NetLoggerUtil.SYSTEM_LS, json); + + return json; + } + + // these may be overridden by subclasses or junit tests + + /** + * Gets the retry count. + * + * @param retry retry, extracted from the parameters, or {@code null} + * @return the number of retries, or {@code 0} if no retries were specified + */ + protected int getRetry(Integer retry) { + return (retry == null ? 0 : retry); + } + + /** + * Gets the retry wait, in milliseconds. + * + * @return the retry wait, in milliseconds + */ + protected long getRetryWaitMs() { + return DEFAULT_RETRY_WAIT_MS; + } + + /** + * Gets the operation timeout. + * + * @param timeoutSec timeout, in seconds, extracted from the parameters, or + * {@code null} + * @return the operation timeout, in milliseconds, or {@code 0} if no timeout was + * specified + */ + protected long getTimeoutMs(Integer timeoutSec) { + return (timeoutSec == null ? 0 : TimeUnit.MILLISECONDS.convert(timeoutSec, TimeUnit.SECONDS)); + } + + // these may be overridden by junit tests + + protected Coder makeCoder() { + return coder; + } +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/OperatorPartial.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/OperatorPartial.java index df5258d71..3e15c1be4 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/OperatorPartial.java +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/impl/OperatorPartial.java @@ -20,57 +20,24 @@ package org.onap.policy.controlloop.actorserviceprovider.impl; -import java.util.List; import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; import java.util.concurrent.Executor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.function.BiConsumer; -import java.util.function.Function; -import lombok.AccessLevel; import lombok.Getter; -import lombok.Setter; -import org.onap.policy.controlloop.ControlLoopOperation; -import org.onap.policy.controlloop.actorserviceprovider.CallbackManager; -import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome; import org.onap.policy.controlloop.actorserviceprovider.Operator; -import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; -import org.onap.policy.controlloop.actorserviceprovider.pipeline.PipelineControllerFuture; -import org.onap.policy.controlloop.policy.PolicyResult; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** - * Partial implementation of an operator. In general, it's preferable that subclasses - * would override - * {@link #startOperationAsync(ControlLoopOperationParams, int, OperationOutcome) - * startOperationAsync()}. However, if that proves to be too difficult, then they can - * simply override {@link #doOperation(ControlLoopOperationParams, int, OperationOutcome) - * doOperation()}. In addition, if the operation requires any preprocessor steps, the - * subclass may choose to override - * {@link #startPreprocessorAsync(ControlLoopOperationParams) startPreprocessorAsync()}. - * <p/> - * The futures returned by the methods within this class can be canceled, and will - * propagate the cancellation to any subtasks. Thus it is also expected that any futures - * returned by overridden methods will do the same. Of course, if a class overrides - * {@link #doOperation(ControlLoopOperationParams, int, OperationOutcome) doOperation()}, - * then there's little that can be done to cancel that particular operation. + * Partial implementation of an operator. */ public abstract class OperatorPartial extends StartConfigPartial<Map<String, Object>> implements Operator { - private static final Logger logger = LoggerFactory.getLogger(OperatorPartial.class); - /** * Executor to be used for tasks that may perform blocking I/O. The default executor * simply launches a new thread for each command that is submitted to it. * <p/> - * May be overridden by junit tests. + * The "get" method may be overridden by junit tests. */ - @Getter(AccessLevel.PROTECTED) - @Setter(AccessLevel.PROTECTED) - private Executor blockingExecutor = command -> { + @Getter + private final Executor blockingExecutor = command -> { Thread thread = new Thread(command); thread.setDaemon(true); thread.start(); @@ -125,721 +92,4 @@ public abstract class OperatorPartial extends StartConfigPartial<Map<String, Obj protected void doShutdown() { // do nothing } - - @Override - public final CompletableFuture<OperationOutcome> startOperation(ControlLoopOperationParams params) { - if (!isAlive()) { - throw new IllegalStateException("operation is not running: " + getFullName()); - } - - // allocate a controller for the entire operation - final PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>(); - - CompletableFuture<OperationOutcome> preproc = startPreprocessorAsync(params); - if (preproc == null) { - // no preprocessor required - just start the operation - return startOperationAttempt(params, controller, 1); - } - - /* - * Do preprocessor first and then, if successful, start the operation. Note: - * operations create their own outcome, ignoring the outcome from any previous - * steps. - * - * Wrap the preprocessor to ensure "stop" is propagated to it. - */ - // @formatter:off - controller.wrap(preproc) - .exceptionally(fromException(params, "preprocessor of operation")) - .thenCompose(handlePreprocessorFailure(params, controller)) - .thenCompose(unusedOutcome -> startOperationAttempt(params, controller, 1)); - // @formatter:on - - return controller; - } - - /** - * Handles a failure in the preprocessor pipeline. If a failure occurred, then it - * invokes the call-backs, marks the controller complete, and returns an incomplete - * future, effectively halting the pipeline. Otherwise, it returns the outcome that it - * received. - * <p/> - * Assumes that no callbacks have been invoked yet. - * - * @param params operation parameters - * @param controller pipeline controller - * @return a function that checks the outcome status and continues, if successful, or - * indicates a failure otherwise - */ - private Function<OperationOutcome, CompletableFuture<OperationOutcome>> handlePreprocessorFailure( - ControlLoopOperationParams params, PipelineControllerFuture<OperationOutcome> controller) { - - return outcome -> { - - if (outcome != null && isSuccess(outcome)) { - logger.trace("{}: preprocessor succeeded for {}", getFullName(), params.getRequestId()); - return CompletableFuture.completedFuture(outcome); - } - - logger.warn("preprocessor failed, discontinuing operation {} for {}", getFullName(), params.getRequestId()); - - final Executor executor = params.getExecutor(); - final CallbackManager callbacks = new CallbackManager(); - - // propagate "stop" to the callbacks - controller.add(callbacks); - - final OperationOutcome outcome2 = params.makeOutcome(); - - // TODO need a FAILURE_MISSING_DATA (e.g., A&AI) - - outcome2.setResult(PolicyResult.FAILURE_GUARD); - outcome2.setMessage(outcome != null ? outcome.getMessage() : null); - - // @formatter:off - CompletableFuture.completedFuture(outcome2) - .whenCompleteAsync(callbackStarted(params, callbacks), executor) - .whenCompleteAsync(callbackCompleted(params, callbacks), executor) - .whenCompleteAsync(controller.delayedComplete(), executor); - // @formatter:on - - return new CompletableFuture<>(); - }; - } - - /** - * Invokes the operation's preprocessor step(s) as a "future". This method simply - * returns {@code null}. - * <p/> - * This method assumes the following: - * <ul> - * <li>the operator is alive</li> - * <li>exceptions generated within the pipeline will be handled by the invoker</li> - * </ul> - * - * @param params operation parameters - * @return a function that will start the preprocessor and returns its outcome, or - * {@code null} if this operation needs no preprocessor - */ - protected CompletableFuture<OperationOutcome> startPreprocessorAsync(ControlLoopOperationParams params) { - return null; - } - - /** - * Starts the operation attempt, with no preprocessor. When all retries complete, it - * will complete the controller. - * - * @param params operation parameters - * @param controller controller for all operation attempts - * @param attempt attempt number, typically starting with 1 - * @return a future that will return the final result of all attempts - */ - private CompletableFuture<OperationOutcome> startOperationAttempt(ControlLoopOperationParams params, - PipelineControllerFuture<OperationOutcome> controller, int attempt) { - - // propagate "stop" to the operation attempt - controller.wrap(startAttemptWithoutRetries(params, attempt)) - .thenCompose(retryOnFailure(params, controller, attempt)); - - return controller; - } - - /** - * Starts the operation attempt, without doing any retries. - * - * @param params operation parameters - * @param attempt attempt number, typically starting with 1 - * @return a future that will return the result of a single operation attempt - */ - private CompletableFuture<OperationOutcome> startAttemptWithoutRetries(ControlLoopOperationParams params, - int attempt) { - - logger.info("{}: start operation attempt {} for {}", getFullName(), attempt, params.getRequestId()); - - final Executor executor = params.getExecutor(); - final OperationOutcome outcome = params.makeOutcome(); - final CallbackManager callbacks = new CallbackManager(); - - // this operation attempt gets its own controller - final PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>(); - - // propagate "stop" to the callbacks - controller.add(callbacks); - - // @formatter:off - CompletableFuture<OperationOutcome> future = CompletableFuture.completedFuture(outcome) - .whenCompleteAsync(callbackStarted(params, callbacks), executor) - .thenCompose(controller.wrap(outcome2 -> startOperationAsync(params, attempt, outcome2))); - // @formatter:on - - // handle timeouts, if specified - long timeoutMillis = getTimeOutMillis(params.getTimeoutSec()); - if (timeoutMillis > 0) { - logger.info("{}: set timeout to {}ms for {}", getFullName(), timeoutMillis, params.getRequestId()); - future = future.orTimeout(timeoutMillis, TimeUnit.MILLISECONDS); - } - - /* - * Note: we re-invoke callbackStarted() just to be sure the callback is invoked - * before callbackCompleted() is invoked. - * - * Note: no need to remove "callbacks" from the pipeline, as we're going to stop - * the pipeline as the last step anyway. - */ - - // @formatter:off - future.exceptionally(fromException(params, "operation")) - .thenApply(setRetryFlag(params, attempt)) - .whenCompleteAsync(callbackStarted(params, callbacks), executor) - .whenCompleteAsync(callbackCompleted(params, callbacks), executor) - .whenCompleteAsync(controller.delayedComplete(), executor); - // @formatter:on - - return controller; - } - - /** - * Determines if the outcome was successful. - * - * @param outcome outcome to examine - * @return {@code true} if the outcome was successful - */ - protected boolean isSuccess(OperationOutcome outcome) { - return (outcome.getResult() == PolicyResult.SUCCESS); - } - - /** - * Determines if the outcome was a failure for this operator. - * - * @param outcome outcome to examine, or {@code null} - * @return {@code true} if the outcome is not {@code null} and was a failure - * <i>and</i> was associated with this operator, {@code false} otherwise - */ - protected boolean isActorFailed(OperationOutcome outcome) { - return (isSameOperation(outcome) && outcome.getResult() == PolicyResult.FAILURE); - } - - /** - * Determines if the given outcome is for this operation. - * - * @param outcome outcome to examine - * @return {@code true} if the outcome is for this operation, {@code false} otherwise - */ - protected boolean isSameOperation(OperationOutcome outcome) { - return OperationOutcome.isFor(outcome, getActorName(), getName()); - } - - /** - * Invokes the operation as a "future". This method simply invokes - * {@link #doOperation(ControlLoopOperationParams)} using the {@link #blockingExecutor - * "blocking executor"}, returning the result via a "future". - * <p/> - * Note: if the operation uses blocking I/O, then it should <i>not</i> be run using - * the executor in the "params", as that may bring the background thread pool to a - * grinding halt. The {@link #blockingExecutor "blocking executor"} should be used - * instead. - * <p/> - * This method assumes the following: - * <ul> - * <li>the operator is alive</li> - * <li>verifyRunning() has been invoked</li> - * <li>callbackStarted() has been invoked</li> - * <li>the invoker will perform appropriate timeout checks</li> - * <li>exceptions generated within the pipeline will be handled by the invoker</li> - * </ul> - * - * @param params operation parameters - * @param attempt attempt number, typically starting with 1 - * @return a function that will start the operation and return its result when - * complete - */ - protected CompletableFuture<OperationOutcome> startOperationAsync(ControlLoopOperationParams params, int attempt, - OperationOutcome outcome) { - - return CompletableFuture.supplyAsync(() -> doOperation(params, attempt, outcome), getBlockingExecutor()); - } - - /** - * Low-level method that performs the operation. This can make the same assumptions - * that are made by {@link #doOperationAsFuture(ControlLoopOperationParams)}. This - * particular method simply throws an {@link UnsupportedOperationException}. - * - * @param params operation parameters - * @param attempt attempt number, typically starting with 1 - * @param operation the operation being performed - * @return the outcome of the operation - */ - protected OperationOutcome doOperation(ControlLoopOperationParams params, int attempt, OperationOutcome operation) { - - throw new UnsupportedOperationException("start operation " + getFullName()); - } - - /** - * Sets the outcome status to FAILURE_RETRIES, if the current operation outcome is - * FAILURE, assuming the policy specifies retries and the retry count has been - * exhausted. - * - * @param params operation parameters - * @param attempt latest attempt number, starting with 1 - * @return a function to get the next future to execute - */ - private Function<OperationOutcome, OperationOutcome> setRetryFlag(ControlLoopOperationParams params, int attempt) { - - return operation -> { - if (operation != null && !isActorFailed(operation)) { - /* - * wrong type or wrong operation - just leave it as is. No need to log - * anything here, as retryOnFailure() will log a message - */ - return operation; - } - - // get a non-null operation - OperationOutcome oper2; - if (operation != null) { - oper2 = operation; - } else { - oper2 = params.makeOutcome(); - oper2.setResult(PolicyResult.FAILURE); - } - - Integer retry = params.getRetry(); - if (retry != null && retry > 0 && attempt > retry) { - /* - * retries were specified and we've already tried them all - change to - * FAILURE_RETRIES - */ - logger.info("operation {} retries exhausted for {}", getFullName(), params.getRequestId()); - oper2.setResult(PolicyResult.FAILURE_RETRIES); - } - - return oper2; - }; - } - - /** - * Restarts the operation if it was a FAILURE. Assumes that - * {@link #setRetryFlag(ControlLoopOperationParams, int)} was previously invoked, and - * thus that the "operation" is not {@code null}. - * - * @param params operation parameters - * @param controller controller for all of the retries - * @param attempt latest attempt number, starting with 1 - * @return a function to get the next future to execute - */ - private Function<OperationOutcome, CompletableFuture<OperationOutcome>> retryOnFailure( - ControlLoopOperationParams params, PipelineControllerFuture<OperationOutcome> controller, - int attempt) { - - return operation -> { - if (!isActorFailed(operation)) { - // wrong type or wrong operation - just leave it as is - logger.trace("not retrying operation {} for {}", getFullName(), params.getRequestId()); - controller.complete(operation); - return new CompletableFuture<>(); - } - - Integer retry = params.getRetry(); - if (retry == null || retry <= 0) { - // no retries - already marked as FAILURE, so just return it - logger.info("operation {} no retries for {}", getFullName(), params.getRequestId()); - controller.complete(operation); - return new CompletableFuture<>(); - } - - - /* - * Retry the operation. - */ - logger.info("retry operation {} for {}", getFullName(), params.getRequestId()); - - return startOperationAttempt(params, controller, attempt + 1); - }; - } - - /** - * Converts an exception into an operation outcome, returning a copy of the outcome to - * prevent background jobs from changing it. - * - * @param params operation parameters - * @param type type of item throwing the exception - * @return a function that will convert an exception into an operation outcome - */ - private Function<Throwable, OperationOutcome> fromException(ControlLoopOperationParams params, String type) { - - return thrown -> { - OperationOutcome outcome = params.makeOutcome(); - - logger.warn("exception throw by {} {}.{} for {}", type, outcome.getActor(), outcome.getOperation(), - params.getRequestId(), thrown); - - return setOutcome(params, outcome, thrown); - }; - } - - /** - * Similar to {@link CompletableFuture#anyOf(CompletableFuture...)}, but it cancels - * any outstanding futures when one completes. - * - * @param params operation parameters - * @param futures futures for which to wait - * @return a future to cancel or await an outcome. If this future is canceled, then - * all of the futures will be canceled - */ - protected CompletableFuture<OperationOutcome> anyOf(ControlLoopOperationParams params, - List<CompletableFuture<OperationOutcome>> futures) { - - // convert list to an array - @SuppressWarnings("rawtypes") - CompletableFuture[] arrFutures = futures.toArray(new CompletableFuture[futures.size()]); - - @SuppressWarnings("unchecked") - CompletableFuture<OperationOutcome> result = anyOf(params, arrFutures); - return result; - } - - /** - * Same as {@link CompletableFuture#anyOf(CompletableFuture...)}, but it cancels any - * outstanding futures when one completes. - * - * @param params operation parameters - * @param futures futures for which to wait - * @return a future to cancel or await an outcome. If this future is canceled, then - * all of the futures will be canceled - */ - protected CompletableFuture<OperationOutcome> anyOf(ControlLoopOperationParams params, - @SuppressWarnings("unchecked") CompletableFuture<OperationOutcome>... futures) { - - final Executor executor = params.getExecutor(); - final PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>(); - - attachFutures(controller, futures); - - // @formatter:off - CompletableFuture.anyOf(futures) - .thenApply(object -> (OperationOutcome) object) - .whenCompleteAsync(controller.delayedComplete(), executor); - // @formatter:on - - return controller; - } - - /** - * Similar to {@link CompletableFuture#allOf(CompletableFuture...)}, but it cancels - * the futures if returned future is canceled. The future returns the "worst" outcome, - * based on priority (see {@link #detmPriority(OperationOutcome)}). - * - * @param params operation parameters - * @param futures futures for which to wait - * @return a future to cancel or await an outcome. If this future is canceled, then - * all of the futures will be canceled - */ - protected CompletableFuture<OperationOutcome> allOf(ControlLoopOperationParams params, - List<CompletableFuture<OperationOutcome>> futures) { - - // convert list to an array - @SuppressWarnings("rawtypes") - CompletableFuture[] arrFutures = futures.toArray(new CompletableFuture[futures.size()]); - - @SuppressWarnings("unchecked") - CompletableFuture<OperationOutcome> result = allOf(params, arrFutures); - return result; - } - - /** - * Same as {@link CompletableFuture#allOf(CompletableFuture...)}, but it cancels the - * futures if returned future is canceled. The future returns the "worst" outcome, - * based on priority (see {@link #detmPriority(OperationOutcome)}). - * - * @param params operation parameters - * @param futures futures for which to wait - * @return a future to cancel or await an outcome. If this future is canceled, then - * all of the futures will be canceled - */ - protected CompletableFuture<OperationOutcome> allOf(ControlLoopOperationParams params, - @SuppressWarnings("unchecked") CompletableFuture<OperationOutcome>... futures) { - - final PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>(); - - attachFutures(controller, futures); - - OperationOutcome[] outcomes = new OperationOutcome[futures.length]; - - @SuppressWarnings("rawtypes") - CompletableFuture[] futures2 = new CompletableFuture[futures.length]; - - // record the outcomes of each future when it completes - for (int count = 0; count < futures2.length; ++count) { - final int count2 = count; - futures2[count] = futures[count].whenComplete((outcome2, thrown) -> outcomes[count2] = outcome2); - } - - CompletableFuture.allOf(futures2).whenComplete(combineOutcomes(params, controller, outcomes)); - - return controller; - } - - /** - * Attaches the given futures to the controller. - * - * @param controller master controller for all of the futures - * @param futures futures to be attached to the controller - */ - private void attachFutures(PipelineControllerFuture<OperationOutcome> controller, - @SuppressWarnings("unchecked") CompletableFuture<OperationOutcome>... futures) { - - // attach each task - for (CompletableFuture<OperationOutcome> future : futures) { - controller.add(future); - } - } - - /** - * Combines the outcomes from a set of tasks. - * - * @param params operation parameters - * @param future future to be completed with the combined result - * @param outcomes outcomes to be examined - */ - private BiConsumer<Void, Throwable> combineOutcomes(ControlLoopOperationParams params, - CompletableFuture<OperationOutcome> future, OperationOutcome[] outcomes) { - - return (unused, thrown) -> { - if (thrown != null) { - future.completeExceptionally(thrown); - return; - } - - // identify the outcome with the highest priority - OperationOutcome outcome = outcomes[0]; - int priority = detmPriority(outcome); - - // start with "1", as we've already dealt with "0" - for (int count = 1; count < outcomes.length; ++count) { - OperationOutcome outcome2 = outcomes[count]; - int priority2 = detmPriority(outcome2); - - if (priority2 > priority) { - outcome = outcome2; - priority = priority2; - } - } - - logger.trace("{}: combined outcome of tasks is {} for {}", getFullName(), - (outcome == null ? null : outcome.getResult()), params.getRequestId()); - - future.complete(outcome); - }; - } - - /** - * Determines the priority of an outcome based on its result. - * - * @param outcome outcome to examine, or {@code null} - * @return the outcome's priority - */ - protected int detmPriority(OperationOutcome outcome) { - if (outcome == null) { - return 1; - } - - switch (outcome.getResult()) { - case SUCCESS: - return 0; - - case FAILURE_GUARD: - return 2; - - case FAILURE_RETRIES: - return 3; - - case FAILURE: - return 4; - - case FAILURE_TIMEOUT: - return 5; - - case FAILURE_EXCEPTION: - default: - return 6; - } - } - - /** - * Performs a task, after verifying that the controller is still running. Also checks - * that the previous outcome was successful, if specified. - * - * @param params operation parameters - * @param controller overall pipeline controller - * @param checkSuccess {@code true} to check the previous outcome, {@code false} - * otherwise - * @param outcome outcome of the previous task - * @param tasks tasks to be performed - * @return a function to perform the task. If everything checks out, then it returns - * the task's future. Otherwise, it returns an incomplete future and completes - * the controller instead. - */ - // @formatter:off - protected CompletableFuture<OperationOutcome> doTask(ControlLoopOperationParams params, - PipelineControllerFuture<OperationOutcome> controller, - boolean checkSuccess, OperationOutcome outcome, - CompletableFuture<OperationOutcome> task) { - // @formatter:on - - if (checkSuccess && !isSuccess(outcome)) { - /* - * must complete before canceling so that cancel() doesn't cause controller to - * complete - */ - controller.complete(outcome); - task.cancel(false); - return new CompletableFuture<>(); - } - - return controller.wrap(task); - } - - /** - * Performs a task, after verifying that the controller is still running. Also checks - * that the previous outcome was successful, if specified. - * - * @param params operation parameters - * @param controller overall pipeline controller - * @param checkSuccess {@code true} to check the previous outcome, {@code false} - * otherwise - * @param tasks tasks to be performed - * @return a function to perform the task. If everything checks out, then it returns - * the task's future. Otherwise, it returns an incomplete future and completes - * the controller instead. - */ - // @formatter:off - protected Function<OperationOutcome, CompletableFuture<OperationOutcome>> doTask(ControlLoopOperationParams params, - PipelineControllerFuture<OperationOutcome> controller, - boolean checkSuccess, - Function<OperationOutcome, CompletableFuture<OperationOutcome>> task) { - // @formatter:on - - return outcome -> { - - if (!controller.isRunning()) { - return new CompletableFuture<>(); - } - - if (checkSuccess && !isSuccess(outcome)) { - controller.complete(outcome); - return new CompletableFuture<>(); - } - - return controller.wrap(task.apply(outcome)); - }; - } - - /** - * Sets the start time of the operation and invokes the callback to indicate that the - * operation has started. Does nothing if the pipeline has been stopped. - * <p/> - * This assumes that the "outcome" is not {@code null}. - * - * @param params operation parameters - * @param callbacks used to determine if the start callback can be invoked - * @return a function that sets the start time and invokes the callback - */ - private BiConsumer<OperationOutcome, Throwable> callbackStarted(ControlLoopOperationParams params, - CallbackManager callbacks) { - - return (outcome, thrown) -> { - - if (callbacks.canStart()) { - // haven't invoked "start" callback yet - outcome.setStart(callbacks.getStartTime()); - outcome.setEnd(null); - params.callbackStarted(outcome); - } - }; - } - - /** - * Sets the end time of the operation and invokes the callback to indicate that the - * operation has completed. Does nothing if the pipeline has been stopped. - * <p/> - * This assumes that the "outcome" is not {@code null}. - * <p/> - * Note: the start time must be a reference rather than a plain value, because it's - * value must be gotten on-demand, when the returned function is executed at a later - * time. - * - * @param params operation parameters - * @param callbacks used to determine if the end callback can be invoked - * @return a function that sets the end time and invokes the callback - */ - private BiConsumer<OperationOutcome, Throwable> callbackCompleted(ControlLoopOperationParams params, - CallbackManager callbacks) { - - return (outcome, thrown) -> { - - if (callbacks.canEnd()) { - outcome.setStart(callbacks.getStartTime()); - outcome.setEnd(callbacks.getEndTime()); - params.callbackCompleted(outcome); - } - }; - } - - /** - * Sets an operation's outcome and message, based on a throwable. - * - * @param params operation parameters - * @param operation operation to be updated - * @return the updated operation - */ - protected OperationOutcome setOutcome(ControlLoopOperationParams params, OperationOutcome operation, - Throwable thrown) { - PolicyResult result = (isTimeout(thrown) ? PolicyResult.FAILURE_TIMEOUT : PolicyResult.FAILURE_EXCEPTION); - return setOutcome(params, operation, result); - } - - /** - * Sets an operation's outcome and default message based on the result. - * - * @param params operation parameters - * @param operation operation to be updated - * @param result result of the operation - * @return the updated operation - */ - protected OperationOutcome setOutcome(ControlLoopOperationParams params, OperationOutcome operation, - PolicyResult result) { - logger.trace("{}: set outcome {} for {}", getFullName(), result, params.getRequestId()); - operation.setResult(result); - operation.setMessage(result == PolicyResult.SUCCESS ? ControlLoopOperation.SUCCESS_MSG - : ControlLoopOperation.FAILED_MSG); - - return operation; - } - - /** - * Determines if a throwable is due to a timeout. - * - * @param thrown throwable of interest - * @return {@code true} if the throwable is due to a timeout, {@code false} otherwise - */ - protected boolean isTimeout(Throwable thrown) { - if (thrown instanceof CompletionException) { - thrown = thrown.getCause(); - } - - return (thrown instanceof TimeoutException); - } - - // these may be overridden by junit tests - - /** - * Gets the operation timeout. Subclasses may override this method to obtain the - * timeout in some other way (e.g., through configuration properties). - * - * @param timeoutSec timeout, in seconds, or {@code null} - * @return the operation timeout, in milliseconds - */ - protected long getTimeOutMillis(Integer timeoutSec) { - return (timeoutSec == null ? 0 : TimeUnit.MILLISECONDS.convert(timeoutSec, TimeUnit.SECONDS)); - } } diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/BidirectionalTopicActorParams.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/BidirectionalTopicActorParams.java new file mode 100644 index 000000000..291aeeb23 --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/BidirectionalTopicActorParams.java @@ -0,0 +1,57 @@ +/*- + * ============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.actorserviceprovider.parameters; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.onap.policy.common.parameters.annotations.Min; + +/** + * Parameters used by Actors whose Operators use bidirectional topic. + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +public class BidirectionalTopicActorParams extends CommonActorParams { + + /* + * Optional, default values that are used if missing from the operation-specific + * parameters. + */ + + /** + * Sink topic name to which requests should be published. + */ + private String sinkTopic; + + /** + * Source topic name, from which to read responses. + */ + private String sourceTopic; + + /** + * Amount of time, in seconds, to wait for the HTTP request to complete. The default + * is 90 seconds. + */ + @Min(1) + private int timeoutSec = 90; +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/TopicParams.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/BidirectionalTopicParams.java index 9e6d8a15e..cafca1fa6 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/TopicParams.java +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/BidirectionalTopicParams.java @@ -29,34 +29,36 @@ import org.onap.policy.common.parameters.annotations.NotBlank; import org.onap.policy.common.parameters.annotations.NotNull; /** - * Parameters used by Operators that connect to a server via DMaaP. + * Parameters used by Operators that use a bidirectional topic. */ @NotNull @NotBlank @Data @Builder(toBuilder = true) -public class TopicParams { +public class BidirectionalTopicParams { /** - * Name of the target topic end point to which requests should be published. + * Sink topic name to which requests should be published. */ - private String target; + private String sinkTopic; /** - * Source topic end point, from which to read responses. + * Source topic name, from which to read responses. */ - private String source; + private String sourceTopic; /** - * Amount of time, in seconds to wait for the response, where zero indicates that it - * should wait forever. The default is zero. + * Amount of time, in seconds to wait for the response. + * <p/> + * Note: this should NOT have a default value, as it receives its default value from + * {@link BidirectionalTopicActorParams}. */ - @Min(0) - @Builder.Default - private long timeoutSec = 0; + @Min(1) + private int timeoutSec; + /** - * Validates both the publisher and the subscriber parameters. + * Validates the parameters. * * @param resultName name of the result * diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/CommonActorParams.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/CommonActorParams.java new file mode 100644 index 000000000..dc6f2b657 --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/CommonActorParams.java @@ -0,0 +1,102 @@ +/*- + * ============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.actorserviceprovider.parameters; + +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Function; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.onap.policy.common.parameters.BeanValidator; +import org.onap.policy.common.parameters.ValidationResult; +import org.onap.policy.common.parameters.annotations.NotNull; +import org.onap.policy.controlloop.actorserviceprovider.Util; + +/** + * Superclass for Actor parameters that have default values in "this" object, and + * operation-specific values in {@link #operation}. + */ +@Getter +@Setter +@EqualsAndHashCode +public class CommonActorParams { + + /** + * Maps the operation name to its parameters. + */ + @NotNull + protected Map<String, Map<String, Object>> operation; + + + /** + * Extracts a specific operation's parameters from "this". + * + * @param name name of the item containing "this" + * @return a function to extract an operation's parameters from "this". Note: the + * returned function is not thread-safe + */ + public Function<String, Map<String, Object>> makeOperationParameters(String name) { + + Map<String, Object> defaultParams = Util.translateToMap(name, this); + defaultParams.remove("operation"); + + return operationName -> { + Map<String, Object> specificParams = operation.get(operationName); + if (specificParams == null) { + return null; + } + + // start with a copy of defaults and overlay with specific + Map<String, Object> subparams = new TreeMap<>(defaultParams); + subparams.putAll(specificParams); + + return Util.translateToMap(name + "." + operationName, subparams); + }; + } + + /** + * Validates the parameters. + * + * @param name name of the object containing these parameters + * @return "this" + * @throws IllegalArgumentException if the parameters are invalid + */ + public CommonActorParams doValidation(String name) { + ValidationResult result = validate(name); + if (!result.isValid()) { + throw new ParameterValidationRuntimeException("invalid parameters", result); + } + + return this; + } + + /** + * 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/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/ControlLoopOperationParams.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/ControlLoopOperationParams.java index 57fce40d7..925916097 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/ControlLoopOperationParams.java +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/ControlLoopOperationParams.java @@ -148,7 +148,8 @@ public class ControlLoopOperationParams { return actorService .getActor(getActor()) .getOperator(getOperation()) - .startOperation(this); + .buildOperation(this) + .start(); // @formatter:on } diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpActorParams.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpActorParams.java index da4fb4f0c..d589e1d7e 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpActorParams.java +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpActorParams.java @@ -20,26 +20,23 @@ package org.onap.policy.controlloop.actorserviceprovider.parameters; -import java.util.Map; -import java.util.function.Function; -import lombok.Data; -import org.onap.policy.common.parameters.BeanValidationResult; -import org.onap.policy.common.parameters.BeanValidator; -import org.onap.policy.common.parameters.ValidationResult; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; import org.onap.policy.common.parameters.annotations.Min; -import org.onap.policy.common.parameters.annotations.NotBlank; -import org.onap.policy.common.parameters.annotations.NotNull; -import org.onap.policy.controlloop.actorserviceprovider.Util; /** - * Parameters used by Actors that connect to a server via HTTP. This contains the - * parameters that are common to all of the operations. Only the path changes for each - * operation, thus it includes a mapping from operation name to path. + * Parameters used by Actors that connect to a server via HTTP. */ -@Data -@NotNull -@NotBlank -public class HttpActorParams { +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +public class HttpActorParams extends CommonActorParams { + + /* + * Optional, default values that are used if missing from the operation-specific + * parameters. + */ /** * Name of the HttpClient, as found in the HttpClientFactory. @@ -47,66 +44,9 @@ public class HttpActorParams { private String clientName; /** - * Amount of time, in seconds to wait for the HTTP request to complete, where zero - * indicates that it should wait forever. The default is zero. - */ - @Min(0) - private long timeoutSec = 0; - - /** - * Maps the operation name to its URI path. - */ - private Map<String, String> path; - - /** - * Extracts a specific operation's parameters from "this". - * - * @param name name of the item containing "this" - * @return a function to extract an operation's parameters from "this". Note: the - * returned function is not thread-safe - */ - public Function<String, Map<String, Object>> makeOperationParameters(String name) { - HttpParams subparams = HttpParams.builder().clientName(getClientName()).timeoutSec(getTimeoutSec()).build(); - - return operation -> { - String subpath = path.get(operation); - if (subpath == null) { - return null; - } - - subparams.setPath(subpath); - return Util.translateToMap(name + "." + operation, subparams); - }; - } - - /** - * Validates the parameters. - * - * @param name name of the object containing these parameters - * @return "this" - * @throws IllegalArgumentException if the parameters are invalid + * Amount of time, in seconds, to wait for the HTTP request to complete. The default + * is 90 seconds. */ - public HttpActorParams doValidation(String name) { - ValidationResult result = validate(name); - if (!result.isValid()) { - throw new ParameterValidationRuntimeException("invalid parameters", result); - } - - return this; - } - - /** - * Validates the parameters. - * - * @param resultName name of the result - * - * @return the validation result - */ - public ValidationResult validate(String resultName) { - BeanValidationResult result = new BeanValidator().validateTop(resultName, this); - - result.validateMap("path", path, (result2, entry) -> result2.validateNotNull(entry.getKey(), entry.getValue())); - - return result; - } + @Min(1) + private int timeoutSec = 90; } diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpParams.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpParams.java index 695ffe4dd..2d3ab8b54 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpParams.java +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpParams.java @@ -48,12 +48,13 @@ public class HttpParams { private String path; /** - * Amount of time, in seconds to wait for the HTTP request to complete, where zero - * indicates that it should wait forever. The default is zero. + * Amount of time, in seconds, to wait for the HTTP request to complete. + * <p/> + * Note: this should NOT have a default value, as it receives its default value from + * {@link HttpActorParams}. */ - @Min(0) - @Builder.Default - private long timeoutSec = 0; + @Min(1) + private int timeoutSec; /** diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/spi/Actor.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/spi/Actor.java index 620950a3c..53bee5f00 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/spi/Actor.java +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/spi/Actor.java @@ -22,7 +22,6 @@ package org.onap.policy.controlloop.actorserviceprovider.spi; import java.util.Collection; - import java.util.List; import java.util.Map; import java.util.Set; diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/topic/BidirectionalTopicHandler.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/topic/BidirectionalTopicHandler.java new file mode 100644 index 000000000..30ee1e2d0 --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/topic/BidirectionalTopicHandler.java @@ -0,0 +1,79 @@ +/*- + * ============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.actorserviceprovider.topic; + +import java.util.List; +import org.onap.policy.common.endpoints.event.comm.client.BidirectionalTopicClient; +import org.onap.policy.common.endpoints.event.comm.client.BidirectionalTopicClientException; + +/** + * Handler for a bidirectional topic, supporting both publishing and forwarding of + * incoming messages. + */ +public class BidirectionalTopicHandler extends BidirectionalTopicClient { + + /** + * Listener that will be attached to the topic to receive responses. + */ + private final TopicListenerImpl listener = new TopicListenerImpl(); + + + /** + * Constructs the object. + * + * @param sinkTopic sink topic name + * @param sourceTopic source topic name + * @throws BidirectionalTopicClientException if an error occurs + */ + public BidirectionalTopicHandler(String sinkTopic, String sourceTopic) throws BidirectionalTopicClientException { + super(sinkTopic, sourceTopic); + } + + /** + * Starts listening on the source topic(s). + */ + public void start() { + getSource().register(listener); + } + + /** + * Stops listening on the source topic(s). + */ + public void stop() { + getSource().unregister(listener); + } + + /** + * Stops listening on the source topic(s) and clears all of the forwarders. + */ + public void shutdown() { + stop(); + listener.shutdown(); + } + + public Forwarder addForwarder(SelectorKey... keys) { + return listener.addForwarder(keys); + } + + public Forwarder addForwarder(List<SelectorKey> keys) { + return listener.addForwarder(keys); + } +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/topic/BidirectionalTopicManager.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/topic/BidirectionalTopicManager.java new file mode 100644 index 000000000..10411875a --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/topic/BidirectionalTopicManager.java @@ -0,0 +1,37 @@ +/*- + * ============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.actorserviceprovider.topic; + +/** + * Manages bidirectional topics. + */ +@FunctionalInterface +public interface BidirectionalTopicManager { + + /** + * Gets the topic handler for the given parameters, creating one if it does not exist. + * + * @param sinkTopic sink topic name + * @param sourceTopic source topic name + * @return the topic handler associated with the given sink and source topic names + */ + BidirectionalTopicHandler getTopicHandler(String sinkTopic, String sourceTopic); +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/topic/Forwarder.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/topic/Forwarder.java new file mode 100644 index 000000000..2d98b66fc --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/topic/Forwarder.java @@ -0,0 +1,138 @@ +/*- + * ============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.actorserviceprovider.topic; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import org.onap.policy.common.utils.coder.StandardCoderObject; +import org.onap.policy.controlloop.actorserviceprovider.Util; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Forwarder that selectively forwards message to listeners based on the content of the + * message. Each forwarder is associated with a single set of selector keys. Listeners are + * then registered with that forwarder for a particular set of values for the given keys. + */ +public class Forwarder { + private static final Logger logger = LoggerFactory.getLogger(Forwarder.class); + + /** + * Maps a set of field values to one or more listeners. + */ + // @formatter:off + private final Map<List<String>, Map<BiConsumer<String, StandardCoderObject>, String>> + values2listeners = new ConcurrentHashMap<>(); + // @formatter:on + + /** + * Keys used to extract the field values from the {@link StandardCoderObject}. + */ + private final List<SelectorKey> keys; + + /** + * Constructs the object. + * + * @param keys keys used to extract the field's value from the + * {@link StandardCoderObject} + */ + public Forwarder(List<SelectorKey> keys) { + this.keys = keys; + } + + /** + * Registers a listener for messages containing the given field values. + * + * @param values field values of interest, in one-to-one correspondence with the keys + * @param listener listener to register + */ + public void register(List<String> values, BiConsumer<String, StandardCoderObject> listener) { + if (keys.size() != values.size()) { + throw new IllegalArgumentException("key/value mismatch"); + } + + values2listeners.compute(values, (key, listeners) -> { + Map<BiConsumer<String, StandardCoderObject>, String> map = listeners; + if (map == null) { + map = new ConcurrentHashMap<>(); + } + + map.put(listener, ""); + return map; + }); + } + + /** + * Unregisters a listener for messages containing the given field values. + * + * @param values field values of interest, in one-to-one correspondence with the keys + * @param listener listener to unregister + */ + public void unregister(List<String> values, BiConsumer<String, StandardCoderObject> listener) { + values2listeners.computeIfPresent(values, (key, listeners) -> { + listeners.remove(listener); + return (listeners.isEmpty() ? null : listeners); + }); + } + + /** + * Processes a message, forwarding it to the appropriate listeners, if any. + * + * @param textMessage original text message that was received + * @param scoMessage decoded text message + */ + public void onMessage(String textMessage, StandardCoderObject scoMessage) { + // extract the key values from the message + List<String> values = new ArrayList<>(keys.size()); + for (SelectorKey key : keys) { + String value = key.extractField(scoMessage); + if (value == null) { + /* + * No value for this field, so this message is not relevant to this + * forwarder. + */ + return; + } + + values.add(value); + } + + // get the listeners for this set of values + Map<BiConsumer<String, StandardCoderObject>, String> listeners = values2listeners.get(values); + if (listeners == null) { + // no listeners for this particular list of values + return; + } + + + // forward the message to each listener + for (BiConsumer<String, StandardCoderObject> listener : listeners.keySet()) { + try { + listener.accept(textMessage, scoMessage); + } catch (RuntimeException e) { + logger.warn("exception thrown by listener {}", Util.ident(listener), e); + } + } + } +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/topic/SelectorKey.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/topic/SelectorKey.java new file mode 100644 index 000000000..fc5727395 --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/topic/SelectorKey.java @@ -0,0 +1,57 @@ +/*- + * ============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.actorserviceprovider.topic; + +import lombok.EqualsAndHashCode; +import org.onap.policy.common.utils.coder.StandardCoderObject; + +/** + * Selector key, which contains a hierarchical list of Strings and Integers that are used + * to extract the content of a field, typically from a {@link StandardCoderObject}. + */ +@EqualsAndHashCode +public class SelectorKey { + + /** + * Names and indices used to extract the field's value. + */ + private final Object[] fieldIdentifiers; + + /** + * Constructs the object. + * + * @param fieldIdentifiers names and indices used to extract the field's value + */ + public SelectorKey(Object... fieldIdentifiers) { + this.fieldIdentifiers = fieldIdentifiers; + } + + /** + * Extracts the given field from an object. + * + * @param object object from which to extract the field + * @return the extracted value, or {@code null} if the object does not contain the + * field + */ + public String extractField(StandardCoderObject object) { + return object.getString(fieldIdentifiers); + } +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/topic/TopicListenerImpl.java b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/topic/TopicListenerImpl.java new file mode 100644 index 000000000..93beab1cb --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/main/java/org/onap/policy/controlloop/actorserviceprovider/topic/TopicListenerImpl.java @@ -0,0 +1,104 @@ +/*- + * ============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.actorserviceprovider.topic; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure; +import org.onap.policy.common.endpoints.event.comm.TopicListener; +import org.onap.policy.common.utils.coder.CoderException; +import org.onap.policy.common.utils.coder.StandardCoder; +import org.onap.policy.common.utils.coder.StandardCoderObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A topic listener. When a message arrives on a topic, it is forwarded to listeners based + * on the content of fields found within the message. However, depending on the message + * type, the relevant fields might be found in different places within the message's + * object hierarchy. For each different list of keys, this class maintains a + * {@link Forwarder}, which is used to forward the message to all relevant listeners. + * <p/> + * Once a selector has been added, it is not removed until {@link #shutdown()} is invoked. + * As selectors are typically only added by Operators, and not by individual Operations, + * this should not pose a problem. + */ +public class TopicListenerImpl implements TopicListener { + private static final Logger logger = LoggerFactory.getLogger(TopicListenerImpl.class); + private static final StandardCoder coder = new StandardCoder(); + + /** + * Maps selector to a forwarder. + */ + private final Map<List<SelectorKey>, Forwarder> selector2forwarder = new ConcurrentHashMap<>(); + + + /** + * Removes all forwarders. + */ + public void shutdown() { + selector2forwarder.clear(); + } + + /** + * Adds a forwarder, if it doesn't already exist. + * + * @param keys the selector keys + * @return the forwarder associated with the given selector keys + */ + public Forwarder addForwarder(SelectorKey... keys) { + return addForwarder(Arrays.asList(keys)); + } + + /** + * Adds a forwarder, if it doesn't already exist. + * + * @param keys the selector keys + * @return the forwarder associated with the given selector keys + */ + public Forwarder addForwarder(List<SelectorKey> keys) { + return selector2forwarder.computeIfAbsent(keys, key -> new Forwarder(keys)); + } + + /** + * Decodes the message and then forwards it to each forwarder for processing. + */ + @Override + public void onTopicEvent(CommInfrastructure infra, String topic, String message) { + StandardCoderObject object; + try { + object = coder.decode(message, StandardCoderObject.class); + } catch (CoderException e) { + logger.warn("cannot decode message", e); + return; + } + + /* + * We don't know which selector is appropriate for the message, so we just let + * them all take a crack at it. + */ + for (Forwarder forwarder : selector2forwarder.values()) { + forwarder.onMessage(message, object); + } + } +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/ActorServiceTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/ActorServiceTest.java index 851a79129..efc7bb830 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/ActorServiceTest.java +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/ActorServiceTest.java @@ -65,7 +65,7 @@ public class ActorServiceTest { private Map<String, Object> sub2; private Map<String, Object> sub3; private Map<String, Object> sub4; - private Map<String, Object> params; + private Map<String, Map<String, Object>> params; private ActorService service; diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/AsyncResponseHandlerTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/AsyncResponseHandlerTest.java deleted file mode 100644 index 31c6d2077..000000000 --- a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/AsyncResponseHandlerTest.java +++ /dev/null @@ -1,172 +0,0 @@ -/*- - * ============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.actorserviceprovider; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; - -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; -import org.junit.Before; -import org.junit.Test; -import org.onap.policy.controlloop.VirtualControlLoopEvent; -import org.onap.policy.controlloop.actorserviceprovider.controlloop.ControlLoopEventContext; -import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; -import org.onap.policy.controlloop.policy.PolicyResult; - -public class AsyncResponseHandlerTest { - - private static final String ACTOR = "my-actor"; - private static final String OPERATION = "my-operation"; - private static final UUID REQ_ID = UUID.randomUUID(); - private static final String TEXT = "some text"; - - private VirtualControlLoopEvent event; - private ControlLoopEventContext context; - private ControlLoopOperationParams params; - private OperationOutcome outcome; - private MyHandler handler; - - /** - * Initializes all fields, including {@link #handler}. - */ - @Before - public void setUp() { - event = new VirtualControlLoopEvent(); - event.setRequestId(REQ_ID); - - context = new ControlLoopEventContext(event); - params = ControlLoopOperationParams.builder().actor(ACTOR).operation(OPERATION).context(context).build(); - outcome = params.makeOutcome(); - - handler = new MyHandler(params, outcome); - } - - @Test - public void testAsyncResponseHandler_testGetParams_testGetOutcome() { - assertSame(params, handler.getParams()); - assertSame(outcome, handler.getOutcome()); - } - - @Test - public void testHandle() { - CompletableFuture<String> future = new CompletableFuture<>(); - handler.handle(future).complete(outcome); - - assertTrue(future.isCancelled()); - } - - @Test - public void testCompleted() throws Exception { - CompletableFuture<OperationOutcome> result = handler.handle(new CompletableFuture<>()); - handler.completed(TEXT); - assertTrue(result.isDone()); - assertSame(outcome, result.get()); - assertEquals(PolicyResult.FAILURE_RETRIES, outcome.getResult()); - assertEquals(TEXT, outcome.getMessage()); - } - - /** - * Tests completed() when doCompleted() throws an exception. - */ - @Test - public void testCompletedException() throws Exception { - IllegalStateException except = new IllegalStateException(); - - outcome = params.makeOutcome(); - handler = new MyHandler(params, outcome) { - @Override - protected OperationOutcome doComplete(String rawResponse) { - throw except; - } - }; - - CompletableFuture<OperationOutcome> result = handler.handle(new CompletableFuture<>()); - handler.completed(TEXT); - assertTrue(result.isCompletedExceptionally()); - - AtomicReference<Throwable> thrown = new AtomicReference<>(); - result.whenComplete((unused, thrown2) -> thrown.set(thrown2)); - - assertSame(except, thrown.get()); - } - - @Test - public void testFailed() throws Exception { - IllegalStateException except = new IllegalStateException(); - - CompletableFuture<OperationOutcome> result = handler.handle(new CompletableFuture<>()); - handler.failed(except); - - assertTrue(result.isDone()); - assertSame(outcome, result.get()); - assertEquals(PolicyResult.FAILURE_GUARD, outcome.getResult()); - } - - /** - * Tests failed() when doFailed() throws an exception. - */ - @Test - public void testFailedException() throws Exception { - IllegalStateException except = new IllegalStateException(); - - outcome = params.makeOutcome(); - handler = new MyHandler(params, outcome) { - @Override - protected OperationOutcome doFailed(Throwable thrown) { - throw except; - } - }; - - CompletableFuture<OperationOutcome> result = handler.handle(new CompletableFuture<>()); - handler.failed(except); - assertTrue(result.isCompletedExceptionally()); - - AtomicReference<Throwable> thrown = new AtomicReference<>(); - result.whenComplete((unused, thrown2) -> thrown.set(thrown2)); - - assertSame(except, thrown.get()); - } - - private class MyHandler extends AsyncResponseHandler<String> { - - public MyHandler(ControlLoopOperationParams params, OperationOutcome outcome) { - super(params, outcome); - } - - @Override - protected OperationOutcome doComplete(String rawResponse) { - OperationOutcome outcome = getOutcome(); - outcome.setResult(PolicyResult.FAILURE_RETRIES); - outcome.setMessage(rawResponse); - return outcome; - } - - @Override - protected OperationOutcome doFailed(Throwable thrown) { - OperationOutcome outcome = getOutcome(); - outcome.setResult(PolicyResult.FAILURE_GUARD); - return outcome; - } - } -} diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/UtilTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/UtilTest.java index 4a3f321cf..0a2a5a90e 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/UtilTest.java +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/UtilTest.java @@ -39,16 +39,10 @@ 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.common.utils.coder.StandardCoder; import org.onap.policy.common.utils.test.log.logback.ExtractAppender; import org.slf4j.LoggerFactory; public class UtilTest { - private static final String MY_REQUEST = "my-request"; - private static final String URL = "my-url"; - private static final String OUT_URL = "OUT|REST|my-url"; - private static final String IN_URL = "IN|REST|my-url"; protected static final String EXPECTED_EXCEPTION = "expected exception"; /** @@ -89,82 +83,6 @@ public class UtilTest { } @Test - public void testLogRestRequest() throws CoderException { - // log structured data - appender.clearExtractions(); - Util.logRestRequest(URL, new Abc(10, null, null)); - List<String> output = appender.getExtracted(); - assertEquals(1, output.size()); - - assertThat(output.get(0)).contains(OUT_URL).contains("{\n \"intValue\": 10\n}"); - - // log a plain string - appender.clearExtractions(); - Util.logRestRequest(URL, MY_REQUEST); - output = appender.getExtracted(); - assertEquals(1, output.size()); - - assertThat(output.get(0)).contains(OUT_URL).contains(MY_REQUEST); - - // exception from coder - StandardCoder coder = new StandardCoder() { - @Override - public String encode(Object object, boolean pretty) throws CoderException { - throw new CoderException(EXPECTED_EXCEPTION); - } - }; - - appender.clearExtractions(); - Util.logRestRequest(coder, URL, new Abc(11, null, null)); - output = appender.getExtracted(); - assertEquals(2, output.size()); - assertThat(output.get(0)).contains("cannot pretty-print request"); - assertThat(output.get(1)).contains(OUT_URL); - } - - @Test - public void testLogRestResponse() throws CoderException { - // log structured data - appender.clearExtractions(); - Util.logRestResponse(URL, new Abc(10, null, null)); - List<String> output = appender.getExtracted(); - assertEquals(1, output.size()); - - assertThat(output.get(0)).contains(IN_URL).contains("{\n \"intValue\": 10\n}"); - - // log null response - appender.clearExtractions(); - Util.logRestResponse(URL, null); - output = appender.getExtracted(); - assertEquals(1, output.size()); - - assertThat(output.get(0)).contains(IN_URL).contains("null"); - - // log a plain string - appender.clearExtractions(); - Util.logRestResponse(URL, MY_REQUEST); - output = appender.getExtracted(); - assertEquals(1, output.size()); - - assertThat(output.get(0)).contains(IN_URL).contains(MY_REQUEST); - - // exception from coder - StandardCoder coder = new StandardCoder() { - @Override - public String encode(Object object, boolean pretty) throws CoderException { - throw new CoderException(EXPECTED_EXCEPTION); - } - }; - - appender.clearExtractions(); - Util.logRestResponse(coder, URL, new Abc(11, null, null)); - output = appender.getExtracted(); - assertEquals(2, output.size()); - assertThat(output.get(0)).contains("cannot pretty-print response"); - assertThat(output.get(1)).contains(IN_URL); - } - - @Test public void testRunFunction() { // no exception, no log AtomicInteger count = new AtomicInteger(); diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/controlloop/ControlLoopEventContextTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/controlloop/ControlLoopEventContextTest.java index 0d917ad3e..cf2426214 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/controlloop/ControlLoopEventContextTest.java +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/controlloop/ControlLoopEventContextTest.java @@ -24,16 +24,25 @@ 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.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.util.Map; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import org.junit.Before; import org.junit.Test; import org.onap.policy.controlloop.VirtualControlLoopEvent; +import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; public class ControlLoopEventContextTest { private static final UUID REQ_ID = UUID.randomUUID(); + private static final String ITEM_KEY = "obtain-C"; private Map<String, String> enrichment; private VirtualControlLoopEvent event; @@ -84,4 +93,54 @@ public class ControlLoopEventContextTest { int intValue = context.getProperty("def"); assertEquals(100, intValue); } + + @Test + public void testObtain() { + final ControlLoopOperationParams params = mock(ControlLoopOperationParams.class); + + // property is already loaded + context.setProperty("obtain-A", "value-A"); + assertNull(context.obtain("obtain-A", params)); + + // new property - should retrieve + CompletableFuture<OperationOutcome> future = new CompletableFuture<>(); + when(params.start()).thenReturn(future); + assertSame(future, context.obtain("obtain-B", params)); + + // repeat - should get the same future, without invoking start() again + assertSame(future, context.obtain("obtain-B", params)); + verify(params).start(); + + // arrange for another invoker to start while this one is starting + CompletableFuture<OperationOutcome> future2 = new CompletableFuture<>(); + + when(params.start()).thenAnswer(args -> { + + ControlLoopOperationParams params2 = mock(ControlLoopOperationParams.class); + when(params2.start()).thenReturn(future2); + + assertSame(future2, context.obtain(ITEM_KEY, params2)); + return future; + }); + + assertSame(future2, context.obtain(ITEM_KEY, params)); + + // should have canceled the interrupted future + assertTrue(future.isCancelled()); + + // return a new future next time start() is called + CompletableFuture<OperationOutcome> future3 = new CompletableFuture<>(); + when(params.start()).thenReturn(future3); + + // repeat - should get the same future + assertSame(future2, context.obtain(ITEM_KEY, params)); + assertSame(future2, context.obtain(ITEM_KEY, params)); + + // future2 should still be active + assertFalse(future2.isCancelled()); + + // cancel it - now we should get the new future + future2.cancel(false); + assertSame(future3, context.obtain(ITEM_KEY, params)); + } } diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/ActorImplTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/ActorImplTest.java index a209fb0d8..92cbbe774 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/ActorImplTest.java +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/ActorImplTest.java @@ -42,7 +42,9 @@ import org.junit.Before; import org.junit.Test; import org.onap.policy.common.parameters.ObjectValidationResult; import org.onap.policy.common.parameters.ValidationStatus; +import org.onap.policy.controlloop.actorserviceprovider.Operation; import org.onap.policy.controlloop.actorserviceprovider.Operator; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; import org.onap.policy.controlloop.actorserviceprovider.parameters.ParameterValidationRuntimeException; public class ActorImplTest { @@ -375,10 +377,15 @@ public class ActorImplTest { return actor; } - private static class MyOper extends OperatorPartial implements Operator { + private static class MyOper extends OperatorPartial { public MyOper(String name) { super(ACTOR_NAME, name); } + + @Override + public Operation buildOperation(ControlLoopOperationParams params) { + return null; + } } } diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/BidirectionalTopicActorTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/BidirectionalTopicActorTest.java new file mode 100644 index 000000000..e1606aeaf --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/BidirectionalTopicActorTest.java @@ -0,0 +1,242 @@ +/*- + * ============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.actorserviceprovider.impl; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import java.util.Map; +import java.util.Properties; +import java.util.TreeMap; +import java.util.function.Function; +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.common.endpoints.event.comm.TopicEndpointManager; +import org.onap.policy.common.endpoints.event.comm.client.BidirectionalTopicClientException; +import org.onap.policy.controlloop.actorserviceprovider.Util; +import org.onap.policy.controlloop.actorserviceprovider.parameters.BidirectionalTopicActorParams; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ParameterValidationRuntimeException; +import org.onap.policy.controlloop.actorserviceprovider.topic.BidirectionalTopicHandler; + +public class BidirectionalTopicActorTest { + + private static final String ACTOR = "my-actor"; + private static final String UNKNOWN = "unknown"; + private static final String MY_SINK = "my-sink"; + private static final String MY_SOURCE1 = "my-source-A"; + private static final String MY_SOURCE2 = "my-source-B"; + private static final int TIMEOUT = 10; + + @Mock + private BidirectionalTopicHandler handler1; + @Mock + private BidirectionalTopicHandler handler2; + + private BidirectionalTopicActor actor; + + + /** + * Configures the endpoints. + */ + @BeforeClass + public static void setUpBeforeClass() { + Properties props = new Properties(); + props.setProperty("noop.sink.topics", MY_SINK); + props.setProperty("noop.source.topics", MY_SOURCE1 + "," + MY_SOURCE2); + + // clear all topics and then configure one sink and two sources + TopicEndpointManager.getManager().shutdown(); + TopicEndpointManager.getManager().addTopicSinks(props); + TopicEndpointManager.getManager().addTopicSources(props); + } + + @AfterClass + public static void tearDownAfterClass() { + // clear all topics after the tests + TopicEndpointManager.getManager().shutdown(); + } + + /** + * Sets up. + */ + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + actor = new MyActor(); + actor.configure(Util.translateToMap(ACTOR, makeParams())); + } + + @Test + public void testDoStart() throws BidirectionalTopicClientException { + // allocate some handlers + actor.getTopicHandler(MY_SINK, MY_SOURCE1); + actor.getTopicHandler(MY_SINK, MY_SOURCE2); + + // start it + actor.start(); + + verify(handler1).start(); + verify(handler2).start(); + + verify(handler1, never()).stop(); + verify(handler2, never()).stop(); + + verify(handler1, never()).shutdown(); + verify(handler2, never()).shutdown(); + } + + @Test + public void testDoStop() throws BidirectionalTopicClientException { + // allocate some handlers + actor.getTopicHandler(MY_SINK, MY_SOURCE1); + actor.getTopicHandler(MY_SINK, MY_SOURCE2); + + // start it + actor.start(); + + // stop it + actor.stop(); + + verify(handler1).stop(); + verify(handler2).stop(); + + verify(handler1, never()).shutdown(); + verify(handler2, never()).shutdown(); + } + + @Test + public void testDoShutdown() { + // allocate some handlers + actor.getTopicHandler(MY_SINK, MY_SOURCE1); + actor.getTopicHandler(MY_SINK, MY_SOURCE2); + + // start it + actor.start(); + + // stop it + actor.shutdown(); + + verify(handler1).shutdown(); + verify(handler2).shutdown(); + + verify(handler1, never()).stop(); + verify(handler2, never()).stop(); + } + + @Test + public void testMakeOperatorParameters() { + BidirectionalTopicActorParams params = makeParams(); + + final BidirectionalTopicActor prov = new BidirectionalTopicActor(ACTOR); + Function<String, Map<String, Object>> maker = + prov.makeOperatorParameters(Util.translateToMap(prov.getName(), params)); + + assertNull(maker.apply(UNKNOWN)); + + // use a TreeMap to ensure the properties are sorted + assertEquals("{sinkTopic=my-sink, sourceTopic=my-source-A, timeoutSec=10}", + new TreeMap<>(maker.apply("operA")).toString()); + + assertEquals("{sinkTopic=my-sink, sourceTopic=topicB, timeoutSec=10}", + new TreeMap<>(maker.apply("operB")).toString()); + + // with invalid actor parameters + params.setOperation(null); + assertThatThrownBy(() -> prov.makeOperatorParameters(Util.translateToMap(prov.getName(), params))) + .isInstanceOf(ParameterValidationRuntimeException.class); + } + + @Test + public void testBidirectionalTopicActor() { + assertEquals(ACTOR, actor.getName()); + assertEquals(ACTOR, actor.getFullName()); + } + + @Test + public void testGetTopicHandler() { + assertSame(handler1, actor.getTopicHandler(MY_SINK, MY_SOURCE1)); + assertSame(handler2, actor.getTopicHandler(MY_SINK, MY_SOURCE2)); + + assertThatIllegalArgumentException().isThrownBy(() -> actor.getTopicHandler(UNKNOWN, MY_SOURCE1)); + } + + @Test + public void testMakeTopicHandler() { + // use a real actor + actor = new BidirectionalTopicActor(ACTOR); + + handler1 = actor.getTopicHandler(MY_SINK, MY_SOURCE1); + handler2 = actor.getTopicHandler(MY_SINK, MY_SOURCE2); + + assertNotNull(handler1); + assertNotNull(handler2); + assertNotSame(handler1, handler2); + } + + + private BidirectionalTopicActorParams makeParams() { + BidirectionalTopicActorParams params = new BidirectionalTopicActorParams(); + params.setSinkTopic(MY_SINK); + params.setSourceTopic(MY_SOURCE1); + params.setTimeoutSec(TIMEOUT); + + // @formatter:off + params.setOperation(Map.of( + "operA", Map.of(), + "operB", Map.of("sourceTopic", "topicB"))); + // @formatter:on + return params; + } + + private class MyActor extends BidirectionalTopicActor { + + public MyActor() { + super(ACTOR); + } + + @Override + protected BidirectionalTopicHandler makeTopicHandler(String sinkTopic, String sourceTopic) + throws BidirectionalTopicClientException { + + if (MY_SINK.equals(sinkTopic)) { + if (MY_SOURCE1.equals(sourceTopic)) { + return handler1; + } else if (MY_SOURCE2.equals(sourceTopic)) { + return handler2; + } + } + + throw new BidirectionalTopicClientException("no topic " + sinkTopic + "/" + sourceTopic); + } + } +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/BidirectionalTopicOperationTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/BidirectionalTopicOperationTest.java new file mode 100644 index 000000000..ceb63fe91 --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/BidirectionalTopicOperationTest.java @@ -0,0 +1,403 @@ +/*- + * ============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.actorserviceprovider.impl; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +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.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiConsumer; +import lombok.Getter; +import lombok.Setter; +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.policy.common.endpoints.event.comm.Topic.CommInfrastructure; +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.coder.StandardCoderObject; +import org.onap.policy.common.utils.time.PseudoExecutor; +import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome; +import org.onap.policy.controlloop.actorserviceprovider.parameters.BidirectionalTopicParams; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; +import org.onap.policy.controlloop.actorserviceprovider.topic.BidirectionalTopicHandler; +import org.onap.policy.controlloop.actorserviceprovider.topic.Forwarder; +import org.onap.policy.controlloop.policy.PolicyResult; + +public class BidirectionalTopicOperationTest { + private static final CommInfrastructure SINK_INFRA = CommInfrastructure.NOOP; + private static final IllegalStateException EXPECTED_EXCEPTION = new IllegalStateException("expected exception"); + private static final String ACTOR = "my-actor"; + private static final String OPERATION = "my-operation"; + private static final String REQ_ID = "my-request-id"; + private static final String MY_SINK = "my-sink"; + private static final String MY_SOURCE = "my-source"; + private static final String TEXT = "some text"; + private static final int TIMEOUT_SEC = 10; + private static final long TIMEOUT_MS = 1000 * TIMEOUT_SEC; + private static final int MAX_REQUESTS = 100; + + private static final StandardCoder coder = new StandardCoder(); + + @Mock + private BidirectionalTopicOperator operator; + @Mock + private BidirectionalTopicHandler handler; + @Mock + private Forwarder forwarder; + + @Captor + private ArgumentCaptor<BiConsumer<String, StandardCoderObject>> listenerCaptor; + + private ControlLoopOperationParams params; + private BidirectionalTopicParams topicParams; + private OperationOutcome outcome; + private StandardCoderObject stdResponse; + private String responseText; + private PseudoExecutor executor; + private int ntimes; + private BidirectionalTopicOperation<MyRequest, MyResponse> oper; + + /** + * Sets up. + */ + @Before + public void setUp() throws CoderException { + MockitoAnnotations.initMocks(this); + + topicParams = BidirectionalTopicParams.builder().sourceTopic(MY_SOURCE).sinkTopic(MY_SINK) + .timeoutSec(TIMEOUT_SEC).build(); + + when(operator.getActorName()).thenReturn(ACTOR); + when(operator.getName()).thenReturn(OPERATION); + when(operator.getTopicHandler()).thenReturn(handler); + when(operator.getForwarder()).thenReturn(forwarder); + when(operator.getParams()).thenReturn(topicParams); + when(operator.isAlive()).thenReturn(true); + + when(handler.send(any())).thenReturn(true); + when(handler.getSinkTopicCommInfrastructure()).thenReturn(SINK_INFRA); + + executor = new PseudoExecutor(); + + params = ControlLoopOperationParams.builder().actor(ACTOR).operation(OPERATION).executor(executor).build(); + outcome = params.makeOutcome(); + + responseText = coder.encode(new MyResponse()); + stdResponse = coder.decode(responseText, StandardCoderObject.class); + + ntimes = 1; + + oper = new MyOperation(); + } + + @Test + public void testConstructor_testGetTopicHandler_testGetForwarder_testGetTopicParams() { + assertEquals(ACTOR, oper.getActorName()); + assertEquals(OPERATION, oper.getName()); + assertSame(handler, oper.getTopicHandler()); + assertSame(forwarder, oper.getForwarder()); + assertSame(topicParams, oper.getTopicParams()); + assertEquals(TIMEOUT_MS, oper.getTimeoutMs()); + assertSame(MyResponse.class, oper.getResponseClass()); + } + + @Test + public void testStartOperationAsync() throws Exception { + + // tell it to expect three responses + ntimes = 3; + + CompletableFuture<OperationOutcome> future = oper.startOperationAsync(1, outcome); + assertFalse(future.isDone()); + + verify(forwarder).register(eq(Arrays.asList(REQ_ID)), listenerCaptor.capture()); + + verify(forwarder, never()).unregister(any(), any()); + + verify(handler).send(any()); + + // provide first response + listenerCaptor.getValue().accept(responseText, stdResponse); + assertTrue(executor.runAll(MAX_REQUESTS)); + assertFalse(future.isDone()); + + // provide second response + listenerCaptor.getValue().accept(responseText, stdResponse); + assertTrue(executor.runAll(MAX_REQUESTS)); + assertFalse(future.isDone()); + + // provide final response + listenerCaptor.getValue().accept(responseText, stdResponse); + assertTrue(executor.runAll(MAX_REQUESTS)); + assertTrue(future.isDone()); + + assertSame(outcome, future.get()); + assertEquals(PolicyResult.SUCCESS, outcome.getResult()); + + verify(forwarder).unregister(eq(Arrays.asList(REQ_ID)), eq(listenerCaptor.getValue())); + } + + /** + * Tests startOperationAsync() when the publisher throws an exception. + */ + @Test + public void testStartOperationAsyncException() throws Exception { + // indicate that nothing was published + when(handler.send(any())).thenReturn(false); + + assertThatIllegalStateException().isThrownBy(() -> oper.startOperationAsync(1, outcome)); + + verify(forwarder).register(eq(Arrays.asList(REQ_ID)), listenerCaptor.capture()); + + // must still unregister + verify(forwarder).unregister(eq(Arrays.asList(REQ_ID)), eq(listenerCaptor.getValue())); + } + + @Test + public void testGetTimeoutMsInteger() { + // use default + assertEquals(TIMEOUT_MS, oper.getTimeoutMs(null)); + assertEquals(TIMEOUT_MS, oper.getTimeoutMs(0)); + + // use provided value + assertEquals(5000, oper.getTimeoutMs(5)); + } + + @Test + public void testPublishRequest() { + assertThatCode(() -> oper.publishRequest(new MyRequest())).doesNotThrowAnyException(); + } + + /** + * Tests publishRequest() when nothing is published. + */ + @Test + public void testPublishRequestUnpublished() { + when(handler.send(any())).thenReturn(false); + assertThatIllegalStateException().isThrownBy(() -> oper.publishRequest(new MyRequest())); + } + + /** + * Tests publishRequest() when the request type is a String. + */ + @Test + public void testPublishRequestString() { + MyStringOperation oper2 = new MyStringOperation(); + assertThatCode(() -> oper2.publishRequest(TEXT)).doesNotThrowAnyException(); + } + + /** + * Tests publishRequest() when the coder throws an exception. + */ + @Test + public void testPublishRequestException() { + setOperCoderException(); + assertThatIllegalArgumentException().isThrownBy(() -> oper.publishRequest(new MyRequest())); + } + + /** + * Tests processResponse() when it's a success and the response type is a String. + */ + @Test + public void testProcessResponseSuccessString() { + MyStringOperation oper2 = new MyStringOperation(); + + assertSame(outcome, oper2.processResponse(outcome, TEXT, null)); + assertEquals(PolicyResult.SUCCESS, outcome.getResult()); + } + + /** + * Tests processResponse() when it's a success and the response type is a + * StandardCoderObject. + */ + @Test + public void testProcessResponseSuccessSco() { + MyScoOperation oper2 = new MyScoOperation(); + + assertSame(outcome, oper2.processResponse(outcome, responseText, stdResponse)); + assertEquals(PolicyResult.SUCCESS, outcome.getResult()); + } + + /** + * Tests processResponse() when it's a failure. + */ + @Test + public void testProcessResponseFailure() throws CoderException { + // indicate error in the response + MyResponse resp = new MyResponse(); + resp.setOutput("error"); + + responseText = coder.encode(resp); + stdResponse = coder.decode(responseText, StandardCoderObject.class); + + assertSame(outcome, oper.processResponse(outcome, responseText, stdResponse)); + assertEquals(PolicyResult.FAILURE, outcome.getResult()); + } + + /** + * Tests processResponse() when the decoder succeeds. + */ + @Test + public void testProcessResponseDecodeOk() throws CoderException { + assertSame(outcome, oper.processResponse(outcome, responseText, stdResponse)); + assertEquals(PolicyResult.SUCCESS, outcome.getResult()); + } + + /** + * Tests processResponse() when the decoder throws an exception. + */ + @Test + public void testProcessResponseDecodeExcept() throws CoderException { + // @formatter:off + assertThatIllegalArgumentException().isThrownBy( + () -> oper.processResponse(outcome, "{invalid json", stdResponse)); + // @formatter:on + } + + @Test + public void testPostProcessResponse() { + assertThatCode(() -> oper.postProcessResponse(outcome, null, null)).doesNotThrowAnyException(); + } + + @Test + public void testMakeCoder() { + assertNotNull(oper.makeCoder()); + } + + /** + * Creates a new {@link #oper} whose coder will throw an exception. + */ + private void setOperCoderException() { + oper = new MyOperation() { + @Override + protected Coder makeCoder() { + return new StandardCoder() { + @Override + public String encode(Object object, boolean pretty) throws CoderException { + throw new CoderException(EXPECTED_EXCEPTION); + } + }; + } + }; + } + + @Getter + @Setter + public static class MyRequest { + private String theRequestId = REQ_ID; + private String input; + } + + @Getter + @Setter + public static class MyResponse { + private String requestId = REQ_ID; + private String output; + } + + + private class MyStringOperation extends BidirectionalTopicOperation<String, String> { + public MyStringOperation() { + super(BidirectionalTopicOperationTest.this.params, operator, String.class); + } + + @Override + protected String makeRequest(int attempt) { + return TEXT; + } + + @Override + protected List<String> getExpectedKeyValues(int attempt, String request) { + return Arrays.asList(REQ_ID); + } + + @Override + protected Status detmStatus(String rawResponse, String response) { + return (response != null ? Status.SUCCESS : Status.FAILURE); + } + } + + + private class MyScoOperation extends BidirectionalTopicOperation<MyRequest, StandardCoderObject> { + public MyScoOperation() { + super(BidirectionalTopicOperationTest.this.params, operator, StandardCoderObject.class); + } + + @Override + protected MyRequest makeRequest(int attempt) { + return new MyRequest(); + } + + @Override + protected List<String> getExpectedKeyValues(int attempt, MyRequest request) { + return Arrays.asList(REQ_ID); + } + + @Override + protected Status detmStatus(String rawResponse, StandardCoderObject response) { + return (response.getString("output") == null ? Status.SUCCESS : Status.FAILURE); + } + } + + + private class MyOperation extends BidirectionalTopicOperation<MyRequest, MyResponse> { + public MyOperation() { + super(BidirectionalTopicOperationTest.this.params, operator, MyResponse.class); + } + + @Override + protected MyRequest makeRequest(int attempt) { + return new MyRequest(); + } + + @Override + protected List<String> getExpectedKeyValues(int attempt, MyRequest request) { + return Arrays.asList(REQ_ID); + } + + @Override + protected Status detmStatus(String rawResponse, MyResponse response) { + if (--ntimes <= 0) { + return (response.getOutput() == null ? Status.SUCCESS : Status.FAILURE); + } + + return Status.STILL_WAITING; + } + } +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/BidirectionalTopicOperatorTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/BidirectionalTopicOperatorTest.java new file mode 100644 index 000000000..4fae782bd --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/BidirectionalTopicOperatorTest.java @@ -0,0 +1,143 @@ +/*- + * ============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.actorserviceprovider.impl; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.onap.policy.controlloop.actorserviceprovider.Operation; +import org.onap.policy.controlloop.actorserviceprovider.Util; +import org.onap.policy.controlloop.actorserviceprovider.parameters.BidirectionalTopicParams; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ParameterValidationRuntimeException; +import org.onap.policy.controlloop.actorserviceprovider.topic.BidirectionalTopicHandler; +import org.onap.policy.controlloop.actorserviceprovider.topic.BidirectionalTopicManager; +import org.onap.policy.controlloop.actorserviceprovider.topic.Forwarder; +import org.onap.policy.controlloop.actorserviceprovider.topic.SelectorKey; + +public class BidirectionalTopicOperatorTest { + private static final String ACTOR = "my-actor"; + private static final String OPERATION = "my-operation"; + private static final String MY_SOURCE = "my-source"; + private static final String MY_SINK = "my-target"; + private static final int TIMEOUT_SEC = 10; + + @Mock + private BidirectionalTopicManager mgr; + @Mock + private BidirectionalTopicHandler handler; + @Mock + private Forwarder forwarder; + @Mock + private BidirectionalTopicOperation<String, Integer> operation; + + private List<SelectorKey> keys; + private BidirectionalTopicParams params; + private MyOperator oper; + + /** + * Sets up. + */ + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + keys = List.of(new SelectorKey("")); + + when(mgr.getTopicHandler(MY_SINK, MY_SOURCE)).thenReturn(handler); + when(handler.addForwarder(keys)).thenReturn(forwarder); + + oper = new MyOperator(keys); + + params = BidirectionalTopicParams.builder().sourceTopic(MY_SOURCE).sinkTopic(MY_SINK).timeoutSec(TIMEOUT_SEC) + .build(); + oper.configure(Util.translateToMap(OPERATION, params)); + oper.start(); + } + + @Test + public void testConstructor_testGetParams_testGetTopicHandler_testGetForwarder() { + assertEquals(ACTOR, oper.getActorName()); + assertEquals(OPERATION, oper.getName()); + assertEquals(params, oper.getParams()); + assertSame(handler, oper.getTopicHandler()); + assertSame(forwarder, oper.getForwarder()); + } + + @Test + public void testDoConfigure() { + oper.stop(); + + // invalid parameters + params.setSourceTopic(null); + assertThatThrownBy(() -> oper.configure(Util.translateToMap(OPERATION, params))) + .isInstanceOf(ParameterValidationRuntimeException.class); + } + + @Test + public void testMakeOperator() { + AtomicReference<ControlLoopOperationParams> paramsRef = new AtomicReference<>(); + AtomicReference<BidirectionalTopicOperator> operRef = new AtomicReference<>(); + + // @formatter:off + BiFunction<ControlLoopOperationParams, BidirectionalTopicOperator, + BidirectionalTopicOperation<String, Integer>> maker = + (params, operator) -> { + paramsRef.set(params); + operRef.set(operator); + return operation; + }; + // @formatter:on + + BidirectionalTopicOperator oper2 = + BidirectionalTopicOperator.makeOperator(ACTOR, OPERATION, mgr, maker, new SelectorKey("")); + + assertEquals(ACTOR, oper2.getActorName()); + assertEquals(OPERATION, oper2.getName()); + + ControlLoopOperationParams params2 = ControlLoopOperationParams.builder().build(); + + assertSame(operation, oper2.buildOperation(params2)); + assertSame(params2, paramsRef.get()); + assertSame(oper2, operRef.get()); + } + + + private class MyOperator extends BidirectionalTopicOperator { + public MyOperator(List<SelectorKey> selectorKeys) { + super(ACTOR, OPERATION, mgr, selectorKeys); + } + + @Override + public Operation buildOperation(ControlLoopOperationParams params) { + return null; + } + } +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpActorTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpActorTest.java index 2da789989..80b1d427a 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpActorTest.java +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpActorTest.java @@ -38,7 +38,7 @@ public class HttpActorTest { private static final String ACTOR = "my-actor"; private static final String UNKNOWN = "unknown"; private static final String CLIENT = "my-client"; - private static final long TIMEOUT = 10L; + private static final int TIMEOUT = 10; private HttpActor actor; @@ -52,7 +52,12 @@ public class HttpActorTest { HttpActorParams params = new HttpActorParams(); params.setClientName(CLIENT); params.setTimeoutSec(TIMEOUT); - params.setPath(Map.of("operA", "urlA", "operB", "urlB")); + + // @formatter:off + params.setOperation(Map.of( + "operA", Map.of("path", "urlA"), + "operB", Map.of("path", "urlB"))); + // @formatter:on final HttpActor prov = new HttpActor(ACTOR); Function<String, Map<String, Object>> maker = @@ -68,7 +73,7 @@ public class HttpActorTest { new TreeMap<>(maker.apply("operB")).toString()); // with invalid actor parameters - params.setClientName(null); + params.setOperation(null); assertThatThrownBy(() -> prov.makeOperatorParameters(Util.translateToMap(prov.getName(), params))) .isInstanceOf(ParameterValidationRuntimeException.class); } diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpOperationTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpOperationTest.java new file mode 100644 index 000000000..8189c74fe --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpOperationTest.java @@ -0,0 +1,674 @@ +/*- + * ============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.actorserviceprovider.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +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.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.Map; +import java.util.Properties; +import java.util.UUID; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.InvocationCallback; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import lombok.Getter; +import lombok.Setter; +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.common.endpoints.event.comm.Topic.CommInfrastructure; +import org.onap.policy.common.endpoints.event.comm.bus.internal.BusTopicParams; +import org.onap.policy.common.endpoints.event.comm.bus.internal.BusTopicParams.TopicParamsBuilder; +import org.onap.policy.common.endpoints.http.client.HttpClient; +import org.onap.policy.common.endpoints.http.client.HttpClientFactoryInstance; +import org.onap.policy.common.endpoints.http.server.HttpServletServer; +import org.onap.policy.common.endpoints.http.server.HttpServletServerFactoryInstance; +import org.onap.policy.common.endpoints.properties.PolicyEndPointProperties; +import org.onap.policy.common.endpoints.utils.NetLoggerUtil.EventType; +import org.onap.policy.common.gson.GsonMessageBodyHandler; +import org.onap.policy.common.utils.coder.CoderException; +import org.onap.policy.common.utils.network.NetworkUtil; +import org.onap.policy.controlloop.VirtualControlLoopEvent; +import org.onap.policy.controlloop.actorserviceprovider.Operation; +import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome; +import org.onap.policy.controlloop.actorserviceprovider.Util; +import org.onap.policy.controlloop.actorserviceprovider.controlloop.ControlLoopEventContext; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; +import org.onap.policy.controlloop.actorserviceprovider.parameters.HttpParams; +import org.onap.policy.controlloop.policy.PolicyResult; + +public class HttpOperationTest { + + private static final IllegalStateException EXPECTED_EXCEPTION = new IllegalStateException("expected exception"); + private static final String ACTOR = "my-actor"; + private static final String OPERATION = "my-name"; + private static final String HTTP_CLIENT = "my-client"; + private static final String HTTP_NO_SERVER = "my-http-no-server-client"; + private static final String MEDIA_TYPE_APPLICATION_JSON = "application/json"; + private static final String BASE_URI = "oper"; + private static final String PATH = "/my-path"; + private static final String TEXT = "my-text"; + private static final UUID REQ_ID = UUID.randomUUID(); + + /** + * {@code True} if the server should reject the request, {@code false} otherwise. + */ + private static boolean rejectRequest; + + // call counts of each method type in the server + private static int nget; + private static int npost; + private static int nput; + private static int ndelete; + + @Mock + private HttpClient client; + + @Mock + private Response response; + + private VirtualControlLoopEvent event; + private ControlLoopEventContext context; + private ControlLoopOperationParams params; + private OperationOutcome outcome; + private AtomicReference<InvocationCallback<Response>> callback; + private Future<Response> future; + private HttpOperator operator; + private MyGetOperation<String> oper; + + /** + * Starts the simulator. + */ + @BeforeClass + public static void setUpBeforeClass() throws Exception { + // allocate a port + int port = NetworkUtil.allocPort(); + + /* + * Start the simulator. Must use "Properties" to configure it, otherwise the + * server will use the wrong serialization provider. + */ + Properties svrprops = getServerProperties("my-server", port); + HttpServletServerFactoryInstance.getServerFactory().build(svrprops).forEach(HttpServletServer::start); + + if (!NetworkUtil.isTcpPortOpen("localhost", port, 100, 100)) { + HttpServletServerFactoryInstance.getServerFactory().destroy(); + throw new IllegalStateException("server is not running"); + } + + /* + * Start the clients, one to the server, and one to a non-existent server. + */ + TopicParamsBuilder builder = BusTopicParams.builder().managed(true).hostname("localhost").basePath(BASE_URI) + .serializationProvider(GsonMessageBodyHandler.class.getName()); + + HttpClientFactoryInstance.getClientFactory().build(builder.clientName(HTTP_CLIENT).port(port).build()); + + HttpClientFactoryInstance.getClientFactory() + .build(builder.clientName(HTTP_NO_SERVER).port(NetworkUtil.allocPort()).build()); + } + + /** + * Destroys the Http factories and stops the appender. + */ + @AfterClass + public static void tearDownAfterClass() { + HttpClientFactoryInstance.getClientFactory().destroy(); + HttpServletServerFactoryInstance.getServerFactory().destroy(); + } + + /** + * Initializes fields, including {@link #oper}, and resets the static fields used by + * the REST server. + */ + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + rejectRequest = false; + nget = 0; + npost = 0; + nput = 0; + ndelete = 0; + + when(response.readEntity(String.class)).thenReturn(TEXT); + when(response.getStatus()).thenReturn(200); + + event = new VirtualControlLoopEvent(); + event.setRequestId(REQ_ID); + + context = new ControlLoopEventContext(event); + params = ControlLoopOperationParams.builder().actor(ACTOR).operation(OPERATION).context(context).build(); + + outcome = params.makeOutcome(); + + callback = new AtomicReference<>(); + future = new CompletableFuture<>(); + + operator = new HttpOperator(ACTOR, OPERATION) { + @Override + public Operation buildOperation(ControlLoopOperationParams params) { + return null; + } + + @Override + public HttpClient getClient() { + return client; + } + }; + + initOper(operator, HTTP_CLIENT); + + oper = new MyGetOperation<>(String.class); + } + + @Test + public void testHttpOperator() { + assertEquals(ACTOR, oper.getActorName()); + assertEquals(OPERATION, oper.getName()); + assertEquals(ACTOR + "." + OPERATION, oper.getFullName()); + } + + @Test + public void testMakeHeaders() { + assertEquals(Collections.emptyMap(), oper.makeHeaders()); + } + + @Test + public void testMakePath() { + assertEquals(PATH, oper.makePath()); + } + + @Test + public void testMakeUrl() { + // use a real client + client = HttpClientFactoryInstance.getClientFactory().get(HTTP_CLIENT); + + assertThat(oper.makeUrl()).endsWith("/" + BASE_URI + PATH); + } + + @Test + public void testDoConfigureMapOfStringObject_testGetClient_testGetPath_testGetTimeoutMs() { + + // use value from operator + assertEquals(1000L, oper.getTimeoutMs(null)); + assertEquals(1000L, oper.getTimeoutMs(0)); + + // should use given value + assertEquals(20 * 1000L, oper.getTimeoutMs(20)); + + // indicate we have a timeout value + operator = spy(operator); + when(operator.getTimeoutMs()).thenReturn(30L); + + oper = new MyGetOperation<String>(String.class); + + // should use default + assertEquals(30L, oper.getTimeoutMs(null)); + assertEquals(30L, oper.getTimeoutMs(0)); + + // should use given value + assertEquals(40 * 1000L, oper.getTimeoutMs(40)); + } + + /** + * Tests handleResponse() when it completes. + */ + @Test + public void testHandleResponseComplete() throws Exception { + CompletableFuture<OperationOutcome> future2 = oper.handleResponse(outcome, PATH, cb -> { + callback.set(cb); + return future; + }); + + assertFalse(future2.isDone()); + assertNotNull(callback.get()); + callback.get().completed(response); + + assertSame(outcome, future2.get(5, TimeUnit.SECONDS)); + + assertEquals(PolicyResult.SUCCESS, outcome.getResult()); + } + + /** + * Tests handleResponse() when it fails. + */ + @Test + public void testHandleResponseFailed() throws Exception { + CompletableFuture<OperationOutcome> future2 = oper.handleResponse(outcome, PATH, cb -> { + callback.set(cb); + return future; + }); + + assertFalse(future2.isDone()); + assertNotNull(callback.get()); + callback.get().failed(EXPECTED_EXCEPTION); + + assertThatThrownBy(() -> future2.get(5, TimeUnit.SECONDS)).hasCause(EXPECTED_EXCEPTION); + + // future and future2 may be completed in parallel so we must wait again + assertThatThrownBy(() -> future.get(5, TimeUnit.SECONDS)).isInstanceOf(CancellationException.class); + assertTrue(future.isCancelled()); + } + + /** + * Tests processResponse() when it's a success and the response type is a String. + */ + @Test + public void testProcessResponseSuccessString() throws Exception { + CompletableFuture<OperationOutcome> result = oper.processResponse(outcome, PATH, response); + assertTrue(result.isDone()); + assertSame(outcome, result.get()); + assertEquals(PolicyResult.SUCCESS, outcome.getResult()); + } + + /** + * Tests processResponse() when it's a failure. + */ + @Test + public void testProcessResponseFailure() throws Exception { + when(response.getStatus()).thenReturn(555); + CompletableFuture<OperationOutcome> result = oper.processResponse(outcome, PATH, response); + assertTrue(result.isDone()); + assertSame(outcome, result.get()); + assertEquals(PolicyResult.FAILURE, outcome.getResult()); + } + + /** + * Tests processResponse() when the decoder succeeds. + */ + @Test + public void testProcessResponseDecodeOk() throws Exception { + when(response.readEntity(String.class)).thenReturn("10"); + + MyGetOperation<Integer> oper2 = new MyGetOperation<>(Integer.class); + + CompletableFuture<OperationOutcome> result = oper2.processResponse(outcome, PATH, response); + assertTrue(result.isDone()); + assertSame(outcome, result.get()); + assertEquals(PolicyResult.SUCCESS, outcome.getResult()); + } + + /** + * Tests processResponse() when the decoder throws an exception. + */ + @Test + public void testProcessResponseDecodeExcept() throws CoderException { + MyGetOperation<Integer> oper2 = new MyGetOperation<>(Integer.class); + + assertThatIllegalArgumentException().isThrownBy(() -> oper2.processResponse(outcome, PATH, response)); + } + + @Test + public void testPostProcessResponse() { + assertThatCode(() -> oper.postProcessResponse(outcome, PATH, null, null)).doesNotThrowAnyException(); + } + + @Test + public void testIsSuccess() { + when(response.getStatus()).thenReturn(200); + assertTrue(oper.isSuccess(response, null)); + + when(response.getStatus()).thenReturn(555); + assertFalse(oper.isSuccess(response, null)); + } + + /** + * Tests a GET. + */ + @Test + public void testGet() throws Exception { + // use a real client + client = HttpClientFactoryInstance.getClientFactory().get(HTTP_CLIENT); + + MyGetOperation<MyResponse> oper2 = new MyGetOperation<>(MyResponse.class); + + OperationOutcome outcome = runOperation(oper2); + assertNotNull(outcome); + assertEquals(1, nget); + assertEquals(PolicyResult.SUCCESS, outcome.getResult()); + } + + /** + * Tests a DELETE. + */ + @Test + public void testDelete() throws Exception { + // use a real client + client = HttpClientFactoryInstance.getClientFactory().get(HTTP_CLIENT); + + MyDeleteOperation oper2 = new MyDeleteOperation(); + + OperationOutcome outcome = runOperation(oper2); + assertNotNull(outcome); + assertEquals(1, ndelete); + assertEquals(PolicyResult.SUCCESS, outcome.getResult()); + } + + /** + * Tests a POST. + */ + @Test + public void testPost() throws Exception { + // use a real client + client = HttpClientFactoryInstance.getClientFactory().get(HTTP_CLIENT); + + MyPostOperation oper2 = new MyPostOperation(); + + OperationOutcome outcome = runOperation(oper2); + assertNotNull(outcome); + assertEquals(1, npost); + assertEquals(PolicyResult.SUCCESS, outcome.getResult()); + } + + /** + * Tests a PUT. + */ + @Test + public void testPut() throws Exception { + // use a real client + client = HttpClientFactoryInstance.getClientFactory().get(HTTP_CLIENT); + + MyPutOperation oper2 = new MyPutOperation(); + + OperationOutcome outcome = runOperation(oper2); + assertNotNull(outcome); + assertEquals(1, nput); + assertEquals(PolicyResult.SUCCESS, outcome.getResult()); + } + + @Test + public void testMakeDecoder() { + assertNotNull(oper.makeCoder()); + } + + /** + * Gets server properties. + * + * @param name server name + * @param port server port + * @return server properties + */ + private static Properties getServerProperties(String name, int port) { + final Properties props = new Properties(); + props.setProperty(PolicyEndPointProperties.PROPERTY_HTTP_SERVER_SERVICES, name); + + final String svcpfx = PolicyEndPointProperties.PROPERTY_HTTP_SERVER_SERVICES + "." + name; + + props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_REST_CLASSES_SUFFIX, Server.class.getName()); + props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_HOST_SUFFIX, "localhost"); + props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_PORT_SUFFIX, String.valueOf(port)); + props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_MANAGED_SUFFIX, "true"); + props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_SWAGGER_SUFFIX, "false"); + + props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_SERIALIZATION_PROVIDER, + GsonMessageBodyHandler.class.getName()); + return props; + } + + /** + * Initializes the given operator. + * + * @param operator operator to be initialized + * @param clientName name of the client which it should use + */ + private void initOper(HttpOperator operator, String clientName) { + operator.stop(); + + HttpParams params = HttpParams.builder().clientName(clientName).path(PATH).timeoutSec(1).build(); + Map<String, Object> mapParams = Util.translateToMap(OPERATION, params); + operator.configure(mapParams); + operator.start(); + } + + /** + * Runs the operation. + * + * @param operator operator on which to start the operation + * @return the outcome of the operation, or {@code null} if it does not complete in + * time + */ + private <T> OperationOutcome runOperation(HttpOperation<T> operator) + throws InterruptedException, ExecutionException, TimeoutException { + + CompletableFuture<OperationOutcome> future = operator.start(); + + return future.get(5, TimeUnit.SECONDS); + } + + @Getter + @Setter + public static class MyRequest { + private String input = "some input"; + } + + @Getter + @Setter + public static class MyResponse { + private String output = "some output"; + } + + private class MyGetOperation<T> extends HttpOperation<T> { + public MyGetOperation(Class<T> responseClass) { + super(HttpOperationTest.this.params, HttpOperationTest.this.operator, responseClass); + } + + @Override + protected CompletableFuture<OperationOutcome> startOperationAsync(int attempt, OperationOutcome outcome) { + Map<String, Object> headers = makeHeaders(); + + headers.put("Accept", MediaType.APPLICATION_JSON); + String url = makeUrl(); + + logMessage(EventType.OUT, CommInfrastructure.REST, url, null); + + // @formatter:off + return handleResponse(outcome, url, + callback -> operator.getClient().get(callback, makePath(), headers)); + // @formatter:on + } + } + + private class MyPostOperation extends HttpOperation<MyResponse> { + public MyPostOperation() { + super(HttpOperationTest.this.params, HttpOperationTest.this.operator, MyResponse.class); + } + + @Override + protected CompletableFuture<OperationOutcome> startOperationAsync(int attempt, OperationOutcome outcome) { + + MyRequest request = new MyRequest(); + + Entity<MyRequest> entity = Entity.entity(request, MediaType.APPLICATION_JSON); + + Map<String, Object> headers = makeHeaders(); + + headers.put("Accept", MediaType.APPLICATION_JSON); + String url = makeUrl(); + + logMessage(EventType.OUT, CommInfrastructure.REST, url, request); + + // @formatter:off + return handleResponse(outcome, url, + callback -> operator.getClient().post(callback, makePath(), entity, headers)); + // @formatter:on + } + } + + private class MyPutOperation extends HttpOperation<MyResponse> { + public MyPutOperation() { + super(HttpOperationTest.this.params, HttpOperationTest.this.operator, MyResponse.class); + } + + @Override + protected CompletableFuture<OperationOutcome> startOperationAsync(int attempt, OperationOutcome outcome) { + + MyRequest request = new MyRequest(); + + Entity<MyRequest> entity = Entity.entity(request, MediaType.APPLICATION_JSON); + + Map<String, Object> headers = makeHeaders(); + + headers.put("Accept", MediaType.APPLICATION_JSON); + String url = makeUrl(); + + logMessage(EventType.OUT, CommInfrastructure.REST, url, request); + + // @formatter:off + return handleResponse(outcome, url, + callback -> operator.getClient().put(callback, makePath(), entity, headers)); + // @formatter:on + } + } + + private class MyDeleteOperation extends HttpOperation<String> { + public MyDeleteOperation() { + super(HttpOperationTest.this.params, HttpOperationTest.this.operator, String.class); + } + + @Override + protected CompletableFuture<OperationOutcome> startOperationAsync(int attempt, OperationOutcome outcome) { + Map<String, Object> headers = makeHeaders(); + + headers.put("Accept", MediaType.APPLICATION_JSON); + String url = makeUrl(); + + logMessage(EventType.OUT, CommInfrastructure.REST, url, null); + + // @formatter:off + return handleResponse(outcome, url, + callback -> operator.getClient().delete(callback, makePath(), headers)); + // @formatter:on + } + } + + /** + * Simulator. + */ + @Path("/" + BASE_URI) + @Produces(MEDIA_TYPE_APPLICATION_JSON) + @Consumes(value = {MEDIA_TYPE_APPLICATION_JSON}) + public static class Server { + + /** + * Generates a response to a GET. + * + * @return resulting response + */ + @GET + @Path(PATH) + public Response getRequest() { + ++nget; + + if (rejectRequest) { + return Response.status(Status.BAD_REQUEST).build(); + + } else { + return Response.status(Status.OK).entity(new MyResponse()).build(); + } + } + + /** + * Generates a response to a POST. + * + * @param request incoming request + * @return resulting response + */ + @POST + @Path(PATH) + public Response postRequest(MyRequest request) { + ++npost; + + if (rejectRequest) { + return Response.status(Status.BAD_REQUEST).build(); + + } else { + return Response.status(Status.OK).entity(new MyResponse()).build(); + } + } + + /** + * Generates a response to a PUT. + * + * @param request incoming request + * @return resulting response + */ + @PUT + @Path(PATH) + public Response putRequest(MyRequest request) { + ++nput; + + if (rejectRequest) { + return Response.status(Status.BAD_REQUEST).build(); + + } else { + return Response.status(Status.OK).entity(new MyResponse()).build(); + } + } + + /** + * Generates a response to a DELETE. + * + * @return resulting response + */ + @DELETE + @Path(PATH) + public Response deleteRequest() { + ++ndelete; + + if (rejectRequest) { + return Response.status(Status.BAD_REQUEST).build(); + + } else { + return Response.status(Status.OK).entity(new MyResponse()).build(); + } + } + } +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpOperatorTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpOperatorTest.java index c006cf333..081bb346b 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpOperatorTest.java +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/HttpOperatorTest.java @@ -23,19 +23,25 @@ package org.onap.policy.controlloop.actorserviceprovider.impl; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.Map; +import java.util.concurrent.CompletableFuture; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.onap.policy.common.endpoints.http.client.HttpClient; import org.onap.policy.common.endpoints.http.client.HttpClientFactory; +import org.onap.policy.controlloop.VirtualControlLoopEvent; +import org.onap.policy.controlloop.actorserviceprovider.Operation; +import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome; import org.onap.policy.controlloop.actorserviceprovider.Util; +import org.onap.policy.controlloop.actorserviceprovider.controlloop.ControlLoopEventContext; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; import org.onap.policy.controlloop.actorserviceprovider.parameters.HttpParams; import org.onap.policy.controlloop.actorserviceprovider.parameters.ParameterValidationRuntimeException; @@ -43,62 +49,116 @@ public class HttpOperatorTest { private static final String ACTOR = "my-actor"; private static final String OPERATION = "my-name"; - private static final String CLIENT = "my-client"; - private static final String PATH = "my-path"; - private static final long TIMEOUT = 100; + private static final String HTTP_CLIENT = "my-client"; + private static final String PATH = "/my-path"; + private static final int TIMEOUT = 100; @Mock private HttpClient client; - private HttpOperator oper; + @Mock + private HttpClientFactory factory; + + private MyOperator oper; /** - * Initializes fields, including {@link #oper}. + * Initializes fields, including {@link #oper}, and resets the static fields used by + * the REST server. */ @Before public void setUp() { MockitoAnnotations.initMocks(this); - oper = new HttpOperator(ACTOR, OPERATION); + when(factory.get(HTTP_CLIENT)).thenReturn(client); + + oper = new MyOperator(); + + HttpParams params = HttpParams.builder().clientName(HTTP_CLIENT).path(PATH).timeoutSec(TIMEOUT).build(); + Map<String, Object> paramMap = Util.translateToMap(OPERATION, params); + oper.configure(paramMap); } @Test - public void testDoConfigureMapOfStringObject_testGetClient_testGetPath_testGetTimeoutSec() { + public void testHttpOperator() { + assertEquals(ACTOR, oper.getActorName()); + assertEquals(OPERATION, oper.getName()); + assertEquals(ACTOR + "." + OPERATION, oper.getFullName()); + } + + @Test + public void testGetClient() { + assertNotNull(oper.getClient()); + } + + @Test + public void testMakeOperator() { + HttpOperator oper2 = HttpOperator.makeOperator(ACTOR, OPERATION, MyOperation::new); + assertNotNull(oper2); + + VirtualControlLoopEvent event = new VirtualControlLoopEvent(); + ControlLoopEventContext context = new ControlLoopEventContext(event); + ControlLoopOperationParams params = + ControlLoopOperationParams.builder().actor(ACTOR).operation(OPERATION).context(context).build(); + + Operation operation1 = oper2.buildOperation(params); + assertNotNull(operation1); + + Operation operation2 = oper2.buildOperation(params); + assertNotNull(operation2); + assertNotSame(operation1, operation2); + } + + @Test + public void testDoConfigureMapOfStringObject_testGetClient_testGetPath_testGetTimeoutMs() { + // start with an UNCONFIGURED operator + oper.shutdown(); + oper = new MyOperator(); + assertNull(oper.getClient()); assertNull(oper.getPath()); - assertEquals(0L, oper.getTimeoutSec()); - - oper = new HttpOperator(ACTOR, OPERATION) { - @Override - protected HttpClientFactory getClientFactory() { - HttpClientFactory factory = mock(HttpClientFactory.class); - when(factory.get(CLIENT)).thenReturn(client); - return factory; - } - }; - - HttpParams params = HttpParams.builder().clientName(CLIENT).path(PATH).timeoutSec(TIMEOUT).build(); + + // no timeout yet + assertEquals(0L, oper.getTimeoutMs()); + + HttpParams params = HttpParams.builder().clientName(HTTP_CLIENT).path(PATH).timeoutSec(TIMEOUT).build(); Map<String, Object> paramMap = Util.translateToMap(OPERATION, params); oper.configure(paramMap); assertSame(client, oper.getClient()); assertEquals(PATH, oper.getPath()); - assertEquals(TIMEOUT, oper.getTimeoutSec()); + + // should use given value + assertEquals(TIMEOUT * 1000, oper.getTimeoutMs()); // test invalid parameters paramMap.remove("path"); assertThatThrownBy(() -> oper.configure(paramMap)).isInstanceOf(ParameterValidationRuntimeException.class); } - @Test - public void testHttpOperator() { - assertEquals(ACTOR, oper.getActorName()); - assertEquals(OPERATION, oper.getName()); - assertEquals(ACTOR + "." + OPERATION, oper.getFullName()); + private class MyOperator extends HttpOperator { + public MyOperator() { + super(ACTOR, OPERATION); + } + + @Override + public Operation buildOperation(ControlLoopOperationParams params) { + return null; + } + + @Override + protected HttpClientFactory getClientFactory() { + return factory; + } } - @Test - public void testGetClient() { - assertNotNull(oper.getClientFactory()); + private class MyOperation extends HttpOperation<String> { + public MyOperation(ControlLoopOperationParams params, HttpOperator operator) { + super(params, operator, String.class); + } + + @Override + protected CompletableFuture<OperationOutcome> startOperationAsync(int attempt, OperationOutcome outcome) { + return null; + } } } diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/OperationPartialTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/OperationPartialTest.java new file mode 100644 index 000000000..67ac27c8d --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/OperationPartialTest.java @@ -0,0 +1,1295 @@ +/*- + * ============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.actorserviceprovider.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +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 ch.qos.logback.classic.Logger; +import java.time.Instant; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import lombok.Getter; +import lombok.Setter; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure; +import org.onap.policy.common.endpoints.utils.NetLoggerUtil.EventType; +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.test.log.logback.ExtractAppender; +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.actorserviceprovider.Operation; +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.policy.PolicyResult; +import org.slf4j.LoggerFactory; + +public class OperationPartialTest { + private static final CommInfrastructure SINK_INFRA = CommInfrastructure.NOOP; + private static final CommInfrastructure SOURCE_INFRA = CommInfrastructure.UEB; + private static final int MAX_REQUESTS = 100; + private static final int MAX_PARALLEL = 10; + private static final String EXPECTED_EXCEPTION = "expected exception"; + private static final String ACTOR = "my-actor"; + private static final String OPERATION = "my-operation"; + private static final String MY_SINK = "my-sink"; + private static final String MY_SOURCE = "my-source"; + private static final String TEXT = "my-text"; + private static final int TIMEOUT = 1000; + private static final UUID REQ_ID = UUID.randomUUID(); + + private static final List<PolicyResult> FAILURE_RESULTS = Arrays.asList(PolicyResult.values()).stream() + .filter(result -> result != PolicyResult.SUCCESS).collect(Collectors.toList()); + + /** + * Used to attach an appender to the class' logger. + */ + private static final Logger logger = (Logger) LoggerFactory.getLogger(OperationPartial.class); + private static final ExtractAppender appender = new ExtractAppender(); + + private VirtualControlLoopEvent event; + private ControlLoopEventContext context; + private PseudoExecutor executor; + private ControlLoopOperationParams params; + + private MyOper oper; + + private int numStart; + private int numEnd; + + private Instant tstart; + + private OperationOutcome opstart; + private OperationOutcome opend; + + private OperatorPartial operator; + + /** + * Attaches the appender to the logger. + */ + @BeforeClass + public static void setUpBeforeClass() throws Exception { + /** + * Attach appender to the logger. + */ + appender.setContext(logger.getLoggerContext()); + appender.start(); + + logger.addAppender(appender); + } + + /** + * Stops the appender. + */ + @AfterClass + public static void tearDownAfterClass() { + appender.stop(); + } + + /** + * Initializes the fields, including {@link #oper}. + */ + @Before + public void setUp() { + event = new VirtualControlLoopEvent(); + event.setRequestId(REQ_ID); + + context = new ControlLoopEventContext(event); + executor = new PseudoExecutor(); + + params = ControlLoopOperationParams.builder().completeCallback(this::completer).context(context) + .executor(executor).actor(ACTOR).operation(OPERATION).timeoutSec(TIMEOUT) + .startCallback(this::starter).targetEntity(MY_SINK).build(); + + operator = new OperatorPartial(ACTOR, OPERATION) { + @Override + public Executor getBlockingExecutor() { + return executor; + } + + @Override + public Operation buildOperation(ControlLoopOperationParams params) { + return null; + } + }; + + operator.configure(null); + operator.start(); + + oper = new MyOper(); + + tstart = null; + + opstart = null; + opend = null; + } + + @Test + public void testOperatorPartial_testGetActorName_testGetName() { + assertEquals(ACTOR, oper.getActorName()); + assertEquals(OPERATION, oper.getName()); + assertEquals(ACTOR + "." + OPERATION, oper.getFullName()); + } + + @Test + public void testGetBlockingThread() throws Exception { + CompletableFuture<Void> future = new CompletableFuture<>(); + + // use the real executor + OperatorPartial oper2 = new OperatorPartial(ACTOR, OPERATION) { + @Override + public Operation buildOperation(ControlLoopOperationParams params) { + return null; + } + }; + + oper2.getBlockingExecutor().execute(() -> future.complete(null)); + + assertNull(future.get(5, TimeUnit.SECONDS)); + } + + /** + * Exercises the doXxx() methods. + */ + @Test + public void testDoXxx() { + assertThatCode(() -> operator.doConfigure(null)).doesNotThrowAnyException(); + assertThatCode(() -> operator.doStart()).doesNotThrowAnyException(); + assertThatCode(() -> operator.doStop()).doesNotThrowAnyException(); + assertThatCode(() -> operator.doShutdown()).doesNotThrowAnyException(); + + } + + @Test + public void testStart() { + verifyRun("testStart", 1, 1, PolicyResult.SUCCESS); + } + + /** + * Tests startOperation() when the operator is not running. + */ + @Test + public void testStartNotRunning() { + // stop the operator + operator.stop(); + + assertThatIllegalStateException().isThrownBy(() -> oper.start()); + } + + /** + * Tests startOperation() when the operation has a preprocessor. + */ + @Test + public void testStartWithPreprocessor() { + AtomicInteger count = new AtomicInteger(); + + CompletableFuture<OperationOutcome> preproc = CompletableFuture.supplyAsync(() -> { + count.incrementAndGet(); + return makeSuccess(); + }, executor); + + oper.setGuard(preproc); + + verifyRun("testStartWithPreprocessor_testStartPreprocessor", 1, 1, PolicyResult.SUCCESS); + + assertEquals(1, count.get()); + } + + /** + * Tests start() with multiple running requests. + */ + @Test + public void testStartMultiple() { + for (int count = 0; count < MAX_PARALLEL; ++count) { + oper.start(); + } + + assertTrue(executor.runAll(MAX_REQUESTS * MAX_PARALLEL)); + + assertNotNull(opstart); + assertNotNull(opend); + assertEquals(PolicyResult.SUCCESS, opend.getResult()); + + assertEquals(MAX_PARALLEL, numStart); + assertEquals(MAX_PARALLEL, oper.getCount()); + assertEquals(MAX_PARALLEL, numEnd); + } + + /** + * Tests startPreprocessor() when the preprocessor returns a failure. + */ + @Test + public void testStartPreprocessorFailure() { + oper.setGuard(CompletableFuture.completedFuture(makeFailure())); + + verifyRun("testStartPreprocessorFailure", 1, 0, PolicyResult.FAILURE_GUARD); + } + + /** + * Tests startPreprocessor() when the preprocessor throws an exception. + */ + @Test + public void testStartPreprocessorException() { + // arrange for the preprocessor to throw an exception + oper.setGuard(CompletableFuture.failedFuture(new IllegalStateException(EXPECTED_EXCEPTION))); + + verifyRun("testStartPreprocessorException", 1, 0, PolicyResult.FAILURE_GUARD); + } + + /** + * Tests startPreprocessor() when the pipeline is not running. + */ + @Test + public void testStartPreprocessorNotRunning() { + // arrange for the preprocessor to return success, which will be ignored + oper.setGuard(CompletableFuture.completedFuture(makeSuccess())); + + oper.start().cancel(false); + assertTrue(executor.runAll(MAX_REQUESTS)); + + assertNull(opstart); + assertNull(opend); + + assertEquals(0, numStart); + assertEquals(0, oper.getCount()); + assertEquals(0, numEnd); + } + + /** + * Tests startPreprocessor() when the preprocessor <b>builder</b> throws an exception. + */ + @Test + public void testStartPreprocessorBuilderException() { + oper = new MyOper() { + @Override + protected CompletableFuture<OperationOutcome> startPreprocessorAsync() { + throw new IllegalStateException(EXPECTED_EXCEPTION); + } + }; + + assertThatIllegalStateException().isThrownBy(() -> oper.start()); + + // should be nothing in the queue + assertEquals(0, executor.getQueueLength()); + } + + @Test + public void testStartPreprocessorAsync() { + assertNull(oper.startPreprocessorAsync()); + } + + @Test + public void testStartGuardAsync() { + assertNull(oper.startGuardAsync()); + } + + @Test + public void testStartOperationAsync() { + oper.start(); + assertTrue(executor.runAll(MAX_REQUESTS)); + + assertEquals(1, oper.getCount()); + } + + @Test + public void testIsSuccess() { + OperationOutcome outcome = new OperationOutcome(); + + outcome.setResult(PolicyResult.SUCCESS); + assertTrue(oper.isSuccess(outcome)); + + for (PolicyResult failure : FAILURE_RESULTS) { + outcome.setResult(failure); + assertFalse("testIsSuccess-" + failure, oper.isSuccess(outcome)); + } + } + + @Test + public void testIsActorFailed() { + assertFalse(oper.isActorFailed(null)); + + OperationOutcome outcome = params.makeOutcome(); + + // incorrect outcome + outcome.setResult(PolicyResult.SUCCESS); + assertFalse(oper.isActorFailed(outcome)); + + outcome.setResult(PolicyResult.FAILURE_RETRIES); + assertFalse(oper.isActorFailed(outcome)); + + // correct outcome + outcome.setResult(PolicyResult.FAILURE); + + // incorrect actor + outcome.setActor(MY_SINK); + assertFalse(oper.isActorFailed(outcome)); + outcome.setActor(null); + assertFalse(oper.isActorFailed(outcome)); + outcome.setActor(ACTOR); + + // incorrect operation + outcome.setOperation(MY_SINK); + assertFalse(oper.isActorFailed(outcome)); + outcome.setOperation(null); + assertFalse(oper.isActorFailed(outcome)); + outcome.setOperation(OPERATION); + + // correct values + assertTrue(oper.isActorFailed(outcome)); + } + + @Test + public void testDoOperation() { + /* + * Use an operation that doesn't override doOperation(). + */ + OperationPartial oper2 = new OperationPartial(params, operator) {}; + + oper2.start(); + assertTrue(executor.runAll(MAX_REQUESTS)); + + assertNotNull(opend); + assertEquals(PolicyResult.FAILURE_EXCEPTION, opend.getResult()); + } + + @Test + public void testTimeout() throws Exception { + + // use a real executor + params = params.toBuilder().executor(ForkJoinPool.commonPool()).build(); + + // trigger timeout very quickly + oper = new MyOper() { + @Override + protected long getTimeoutMs(Integer timeoutSec) { + return 1; + } + + @Override + protected CompletableFuture<OperationOutcome> startOperationAsync(int attempt, OperationOutcome outcome) { + + OperationOutcome outcome2 = params.makeOutcome(); + outcome2.setResult(PolicyResult.SUCCESS); + + /* + * Create an incomplete future that will timeout after the operation's + * timeout. If it fires before the other timer, then it will return a + * SUCCESS outcome. + */ + CompletableFuture<OperationOutcome> future = new CompletableFuture<>(); + future = future.orTimeout(1, TimeUnit.SECONDS).handleAsync((unused1, unused2) -> outcome, + params.getExecutor()); + + return future; + } + }; + + assertEquals(PolicyResult.FAILURE_TIMEOUT, oper.start().get().getResult()); + } + + /** + * Tests retry functions, when the count is set to zero and retries are exhausted. + */ + @Test + public void testSetRetryFlag_testRetryOnFailure_ZeroRetries_testStartOperationAttempt() { + params = params.toBuilder().retry(0).build(); + + // new params, thus need a new operation + oper = new MyOper(); + + oper.setMaxFailures(10); + + verifyRun("testSetRetryFlag_testRetryOnFailure_ZeroRetries", 1, 1, PolicyResult.FAILURE); + } + + /** + * Tests retry functions, when the count is null and retries are exhausted. + */ + @Test + public void testSetRetryFlag_testRetryOnFailure_NullRetries() { + params = params.toBuilder().retry(null).build(); + + // new params, thus need a new operation + oper = new MyOper(); + + oper.setMaxFailures(10); + + verifyRun("testSetRetryFlag_testRetryOnFailure_NullRetries", 1, 1, PolicyResult.FAILURE); + } + + /** + * Tests retry functions, when retries are exhausted. + */ + @Test + public void testSetRetryFlag_testRetryOnFailure_RetriesExhausted() { + final int maxRetries = 3; + params = params.toBuilder().retry(maxRetries).build(); + + // new params, thus need a new operation + oper = new MyOper(); + + oper.setMaxFailures(10); + + verifyRun("testSetRetryFlag_testRetryOnFailure_RetriesExhausted", maxRetries + 1, maxRetries + 1, + PolicyResult.FAILURE_RETRIES); + } + + /** + * Tests retry functions, when a success follows some retries. + */ + @Test + public void testSetRetryFlag_testRetryOnFailure_SuccessAfterRetries() { + params = params.toBuilder().retry(10).build(); + + // new params, thus need a new operation + oper = new MyOper(); + + final int maxFailures = 3; + oper.setMaxFailures(maxFailures); + + verifyRun("testSetRetryFlag_testRetryOnFailure_SuccessAfterRetries", maxFailures + 1, maxFailures + 1, + PolicyResult.SUCCESS); + } + + /** + * Tests retry functions, when the outcome is {@code null}. + */ + @Test + public void testSetRetryFlag_testRetryOnFailure_NullOutcome() { + + // arrange to return null from doOperation() + oper = new MyOper() { + @Override + protected OperationOutcome doOperation(int attempt, OperationOutcome operation) { + + // update counters + super.doOperation(attempt, operation); + return null; + } + }; + + verifyRun("testSetRetryFlag_testRetryOnFailure_NullOutcome", 1, 1, PolicyResult.FAILURE, null, noop()); + } + + @Test + public void testSleep() throws Exception { + CompletableFuture<Void> future = oper.sleep(-1, TimeUnit.SECONDS); + assertTrue(future.isDone()); + assertNull(future.get()); + + // edge case + future = oper.sleep(0, TimeUnit.SECONDS); + assertTrue(future.isDone()); + assertNull(future.get()); + + /* + * Start a second sleep we can use to check the first while it's running. + */ + tstart = Instant.now(); + future = oper.sleep(100, TimeUnit.MILLISECONDS); + + CompletableFuture<Void> future2 = oper.sleep(10, TimeUnit.MILLISECONDS); + + // wait for second to complete and verify that the first has not completed + future2.get(); + assertFalse(future.isDone()); + + // wait for second to complete + future.get(); + + long diff = Instant.now().toEpochMilli() - tstart.toEpochMilli(); + assertTrue(diff >= 99); + } + + @Test + public void testIsSameOperation() { + assertFalse(oper.isSameOperation(null)); + + OperationOutcome outcome = params.makeOutcome(); + + // wrong actor - should be false + outcome.setActor(null); + assertFalse(oper.isSameOperation(outcome)); + outcome.setActor(MY_SINK); + assertFalse(oper.isSameOperation(outcome)); + outcome.setActor(ACTOR); + + // wrong operation - should be null + outcome.setOperation(null); + assertFalse(oper.isSameOperation(outcome)); + outcome.setOperation(MY_SINK); + assertFalse(oper.isSameOperation(outcome)); + outcome.setOperation(OPERATION); + + assertTrue(oper.isSameOperation(outcome)); + } + + /** + * Tests handleFailure() when the outcome is a success. + */ + @Test + public void testHandlePreprocessorFailureTrue() { + oper.setGuard(CompletableFuture.completedFuture(makeSuccess())); + verifyRun("testHandlePreprocessorFailureTrue", 1, 1, PolicyResult.SUCCESS); + } + + /** + * Tests handleFailure() when the outcome is <i>not</i> a success. + */ + @Test + public void testHandlePreprocessorFailureFalse() throws Exception { + oper.setGuard(CompletableFuture.completedFuture(makeFailure())); + verifyRun("testHandlePreprocessorFailureFalse", 1, 0, PolicyResult.FAILURE_GUARD); + } + + /** + * Tests handleFailure() when the outcome is {@code null}. + */ + @Test + public void testHandlePreprocessorFailureNull() throws Exception { + // arrange to return null from the preprocessor + oper.setGuard(CompletableFuture.completedFuture(null)); + + verifyRun("testHandlePreprocessorFailureNull", 1, 0, PolicyResult.FAILURE_GUARD); + } + + @Test + public void testFromException() { + // arrange to generate an exception when operation runs + oper.setGenException(true); + + verifyRun("testFromException", 1, 1, PolicyResult.FAILURE_EXCEPTION); + } + + /** + * Tests fromException() when there is no exception. + */ + @Test + public void testFromExceptionNoExcept() { + verifyRun("testFromExceptionNoExcept", 1, 1, PolicyResult.SUCCESS); + } + + /** + * Tests both flavors of anyOf(), because one invokes the other. + */ + @Test + public void testAnyOf() throws Exception { + // first task completes, others do not + List<Supplier<CompletableFuture<OperationOutcome>>> tasks = new LinkedList<>(); + + final OperationOutcome outcome = params.makeOutcome(); + + tasks.add(() -> CompletableFuture.completedFuture(outcome)); + tasks.add(() -> new CompletableFuture<>()); + tasks.add(() -> null); + tasks.add(() -> new CompletableFuture<>()); + + CompletableFuture<OperationOutcome> result = oper.anyOf(tasks); + assertTrue(executor.runAll(MAX_REQUESTS)); + assertTrue(result.isDone()); + assertSame(outcome, result.get()); + + // repeat using array form + @SuppressWarnings("unchecked") + Supplier<CompletableFuture<OperationOutcome>>[] taskArray = new Supplier[tasks.size()]; + result = oper.anyOf(tasks.toArray(taskArray)); + assertTrue(executor.runAll(MAX_REQUESTS)); + assertTrue(result.isDone()); + assertSame(outcome, result.get()); + + // second task completes, others do not + tasks.clear(); + tasks.add(() -> new CompletableFuture<>()); + tasks.add(() -> CompletableFuture.completedFuture(outcome)); + tasks.add(() -> new CompletableFuture<>()); + + result = oper.anyOf(tasks); + assertTrue(executor.runAll(MAX_REQUESTS)); + assertTrue(result.isDone()); + assertSame(outcome, result.get()); + + // third task completes, others do not + tasks.clear(); + tasks.add(() -> new CompletableFuture<>()); + tasks.add(() -> new CompletableFuture<>()); + tasks.add(() -> CompletableFuture.completedFuture(outcome)); + + result = oper.anyOf(tasks); + assertTrue(executor.runAll(MAX_REQUESTS)); + assertTrue(result.isDone()); + assertSame(outcome, result.get()); + } + + /** + * Tests both flavors of anyOf(), for edge cases: zero items, and one item. + */ + @Test + @SuppressWarnings("unchecked") + public void testAnyOfEdge() throws Exception { + List<Supplier<CompletableFuture<OperationOutcome>>> tasks = new LinkedList<>(); + + // zero items: check both using a list and using an array + assertNull(oper.anyOf(tasks)); + assertNull(oper.anyOf()); + + // one item: : check both using a list and using an array + CompletableFuture<OperationOutcome> future1 = new CompletableFuture<>(); + tasks.add(() -> future1); + + assertSame(future1, oper.anyOf(tasks)); + assertSame(future1, oper.anyOf(() -> future1)); + } + + @Test + public void testAllOfArray() throws Exception { + final OperationOutcome outcome = params.makeOutcome(); + + CompletableFuture<OperationOutcome> future1 = new CompletableFuture<>(); + CompletableFuture<OperationOutcome> future2 = new CompletableFuture<>(); + CompletableFuture<OperationOutcome> future3 = new CompletableFuture<>(); + + @SuppressWarnings("unchecked") + CompletableFuture<OperationOutcome> result = + oper.allOf(() -> future1, () -> future2, () -> null, () -> future3); + + assertTrue(executor.runAll(MAX_REQUESTS)); + assertFalse(result.isDone()); + future1.complete(outcome); + + // complete 3 before 2 + assertTrue(executor.runAll(MAX_REQUESTS)); + assertFalse(result.isDone()); + future3.complete(outcome); + + assertTrue(executor.runAll(MAX_REQUESTS)); + assertFalse(result.isDone()); + future2.complete(outcome); + + // all of them are now done + assertTrue(executor.runAll(MAX_REQUESTS)); + assertTrue(result.isDone()); + assertSame(outcome, result.get()); + } + + @Test + public void testAllOfList() throws Exception { + final OperationOutcome outcome = params.makeOutcome(); + + CompletableFuture<OperationOutcome> future1 = new CompletableFuture<>(); + CompletableFuture<OperationOutcome> future2 = new CompletableFuture<>(); + CompletableFuture<OperationOutcome> future3 = new CompletableFuture<>(); + + List<Supplier<CompletableFuture<OperationOutcome>>> tasks = new LinkedList<>(); + tasks.add(() -> future1); + tasks.add(() -> future2); + tasks.add(() -> null); + tasks.add(() -> future3); + + CompletableFuture<OperationOutcome> result = oper.allOf(tasks); + + assertTrue(executor.runAll(MAX_REQUESTS)); + assertFalse(result.isDone()); + future1.complete(outcome); + + // complete 3 before 2 + assertTrue(executor.runAll(MAX_REQUESTS)); + assertFalse(result.isDone()); + future3.complete(outcome); + + assertTrue(executor.runAll(MAX_REQUESTS)); + assertFalse(result.isDone()); + future2.complete(outcome); + + // all of them are now done + assertTrue(executor.runAll(MAX_REQUESTS)); + assertTrue(result.isDone()); + assertSame(outcome, result.get()); + } + + /** + * Tests both flavors of allOf(), for edge cases: zero items, and one item. + */ + @Test + @SuppressWarnings("unchecked") + public void testAllOfEdge() throws Exception { + List<Supplier<CompletableFuture<OperationOutcome>>> tasks = new LinkedList<>(); + + // zero items: check both using a list and using an array + assertNull(oper.allOf(tasks)); + assertNull(oper.allOf()); + + // one item: : check both using a list and using an array + CompletableFuture<OperationOutcome> future1 = new CompletableFuture<>(); + tasks.add(() -> future1); + + assertSame(future1, oper.allOf(tasks)); + assertSame(future1, oper.allOf(() -> future1)); + } + + @Test + public void testAttachFutures() throws Exception { + List<Supplier<CompletableFuture<OperationOutcome>>> tasks = new LinkedList<>(); + + // third task throws an exception during construction + CompletableFuture<OperationOutcome> future1 = new CompletableFuture<>(); + CompletableFuture<OperationOutcome> future2 = new CompletableFuture<>(); + CompletableFuture<OperationOutcome> future3 = new CompletableFuture<>(); + tasks.add(() -> future1); + tasks.add(() -> future2); + tasks.add(() -> { + throw new IllegalStateException(EXPECTED_EXCEPTION); + }); + tasks.add(() -> future3); + + assertThatIllegalStateException().isThrownBy(() -> oper.anyOf(tasks)).withMessage(EXPECTED_EXCEPTION); + + // should have canceled the first two, but not the last + assertTrue(future1.isCancelled()); + assertTrue(future2.isCancelled()); + assertFalse(future3.isCancelled()); + } + + @Test + public void testCombineOutcomes() throws Exception { + // only one outcome + verifyOutcomes(0, PolicyResult.SUCCESS); + verifyOutcomes(0, PolicyResult.FAILURE_EXCEPTION); + + // maximum is in different positions + verifyOutcomes(0, PolicyResult.FAILURE, PolicyResult.SUCCESS, PolicyResult.FAILURE_GUARD); + verifyOutcomes(1, PolicyResult.SUCCESS, PolicyResult.FAILURE, PolicyResult.FAILURE_GUARD); + verifyOutcomes(2, PolicyResult.SUCCESS, PolicyResult.FAILURE_GUARD, PolicyResult.FAILURE); + + // null outcome - takes precedence over a success + List<Supplier<CompletableFuture<OperationOutcome>>> tasks = new LinkedList<>(); + tasks.add(() -> CompletableFuture.completedFuture(params.makeOutcome())); + tasks.add(() -> CompletableFuture.completedFuture(null)); + tasks.add(() -> CompletableFuture.completedFuture(params.makeOutcome())); + CompletableFuture<OperationOutcome> result = oper.allOf(tasks); + + assertTrue(executor.runAll(MAX_REQUESTS)); + assertTrue(result.isDone()); + assertNull(result.get()); + + // one throws an exception during execution + IllegalStateException except = new IllegalStateException(EXPECTED_EXCEPTION); + + tasks.clear(); + tasks.add(() -> CompletableFuture.completedFuture(params.makeOutcome())); + tasks.add(() -> CompletableFuture.failedFuture(except)); + tasks.add(() -> CompletableFuture.completedFuture(params.makeOutcome())); + result = oper.allOf(tasks); + + assertTrue(executor.runAll(MAX_REQUESTS)); + assertTrue(result.isCompletedExceptionally()); + result.whenComplete((unused, thrown) -> assertSame(except, thrown)); + } + + /** + * Tests both flavors of sequence(), because one invokes the other. + */ + @Test + public void testSequence() throws Exception { + final OperationOutcome outcome = params.makeOutcome(); + + List<Supplier<CompletableFuture<OperationOutcome>>> tasks = new LinkedList<>(); + tasks.add(() -> CompletableFuture.completedFuture(outcome)); + tasks.add(() -> null); + tasks.add(() -> CompletableFuture.completedFuture(outcome)); + tasks.add(() -> CompletableFuture.completedFuture(outcome)); + + CompletableFuture<OperationOutcome> result = oper.sequence(tasks); + assertTrue(executor.runAll(MAX_REQUESTS)); + assertTrue(result.isDone()); + assertSame(outcome, result.get()); + + // repeat using array form + @SuppressWarnings("unchecked") + Supplier<CompletableFuture<OperationOutcome>>[] taskArray = new Supplier[tasks.size()]; + result = oper.sequence(tasks.toArray(taskArray)); + assertTrue(executor.runAll(MAX_REQUESTS)); + assertTrue(result.isDone()); + assertSame(outcome, result.get()); + + // second task fails, third should not run + OperationOutcome failure = params.makeOutcome(); + failure.setResult(PolicyResult.FAILURE); + tasks.clear(); + tasks.add(() -> CompletableFuture.completedFuture(outcome)); + tasks.add(() -> CompletableFuture.completedFuture(failure)); + tasks.add(() -> CompletableFuture.completedFuture(outcome)); + + result = oper.sequence(tasks); + assertTrue(executor.runAll(MAX_REQUESTS)); + assertTrue(result.isDone()); + assertSame(failure, result.get()); + } + + /** + * Tests both flavors of sequence(), for edge cases: zero items, and one item. + */ + @Test + @SuppressWarnings("unchecked") + public void testSequenceEdge() throws Exception { + List<Supplier<CompletableFuture<OperationOutcome>>> tasks = new LinkedList<>(); + + // zero items: check both using a list and using an array + assertNull(oper.sequence(tasks)); + assertNull(oper.sequence()); + + // one item: : check both using a list and using an array + CompletableFuture<OperationOutcome> future1 = new CompletableFuture<>(); + tasks.add(() -> future1); + + assertSame(future1, oper.sequence(tasks)); + assertSame(future1, oper.sequence(() -> future1)); + } + + private void verifyOutcomes(int expected, PolicyResult... results) throws Exception { + List<Supplier<CompletableFuture<OperationOutcome>>> tasks = new LinkedList<>(); + + OperationOutcome expectedOutcome = null; + + for (int count = 0; count < results.length; ++count) { + OperationOutcome outcome = params.makeOutcome(); + outcome.setResult(results[count]); + tasks.add(() -> CompletableFuture.completedFuture(outcome)); + + if (count == expected) { + expectedOutcome = outcome; + } + } + + CompletableFuture<OperationOutcome> result = oper.allOf(tasks); + + assertTrue(executor.runAll(MAX_REQUESTS)); + assertTrue(result.isDone()); + assertSame(expectedOutcome, result.get()); + } + + @Test + public void testDetmPriority() throws CoderException { + assertEquals(1, oper.detmPriority(null)); + + OperationOutcome outcome = params.makeOutcome(); + + Map<PolicyResult, Integer> map = Map.of(PolicyResult.SUCCESS, 0, PolicyResult.FAILURE_GUARD, 2, + PolicyResult.FAILURE_RETRIES, 3, PolicyResult.FAILURE, 4, PolicyResult.FAILURE_TIMEOUT, 5, + PolicyResult.FAILURE_EXCEPTION, 6); + + for (Entry<PolicyResult, Integer> ent : map.entrySet()) { + outcome.setResult(ent.getKey()); + assertEquals(ent.getKey().toString(), ent.getValue().intValue(), oper.detmPriority(outcome)); + } + + /* + * Test null result. We can't actually set it to null, because the set() method + * won't allow it. Instead, we decode it from a structure. + */ + outcome = new StandardCoder().decode("{\"result\":null}", OperationOutcome.class); + assertEquals(1, oper.detmPriority(outcome)); + } + + /** + * Tests callbackStarted() when the pipeline has already been stopped. + */ + @Test + public void testCallbackStartedNotRunning() { + AtomicReference<Future<OperationOutcome>> future = new AtomicReference<>(); + + /* + * arrange to stop the controller when the start-callback is invoked, but capture + * the outcome + */ + params = params.toBuilder().startCallback(oper -> { + starter(oper); + future.get().cancel(false); + }).build(); + + // new params, thus need a new operation + oper = new MyOper(); + + future.set(oper.start()); + assertTrue(executor.runAll(MAX_REQUESTS)); + + // should have only run once + assertEquals(1, numStart); + } + + /** + * Tests callbackCompleted() when the pipeline has already been stopped. + */ + @Test + public void testCallbackCompletedNotRunning() { + AtomicReference<Future<OperationOutcome>> future = new AtomicReference<>(); + + // arrange to stop the controller when the start-callback is invoked + params = params.toBuilder().startCallback(oper -> { + future.get().cancel(false); + }).build(); + + // new params, thus need a new operation + oper = new MyOper(); + + future.set(oper.start()); + assertTrue(executor.runAll(MAX_REQUESTS)); + + // should not have been set + assertNull(opend); + assertEquals(0, numEnd); + } + + @Test + public void testSetOutcomeControlLoopOperationOutcomeThrowable() { + final CompletionException timex = new CompletionException(new TimeoutException(EXPECTED_EXCEPTION)); + + OperationOutcome outcome; + + outcome = new OperationOutcome(); + oper.setOutcome(outcome, timex); + assertEquals(ControlLoopOperation.FAILED_MSG, outcome.getMessage()); + assertEquals(PolicyResult.FAILURE_TIMEOUT, outcome.getResult()); + + outcome = new OperationOutcome(); + oper.setOutcome(outcome, new IllegalStateException(EXPECTED_EXCEPTION)); + assertEquals(ControlLoopOperation.FAILED_MSG, outcome.getMessage()); + assertEquals(PolicyResult.FAILURE_EXCEPTION, outcome.getResult()); + } + + @Test + public void testSetOutcomeControlLoopOperationOutcomePolicyResult() { + OperationOutcome outcome; + + outcome = new OperationOutcome(); + oper.setOutcome(outcome, PolicyResult.SUCCESS); + assertEquals(ControlLoopOperation.SUCCESS_MSG, outcome.getMessage()); + assertEquals(PolicyResult.SUCCESS, outcome.getResult()); + + for (PolicyResult result : FAILURE_RESULTS) { + outcome = new OperationOutcome(); + oper.setOutcome(outcome, result); + assertEquals(result.toString(), ControlLoopOperation.FAILED_MSG, outcome.getMessage()); + assertEquals(result.toString(), result, outcome.getResult()); + } + } + + @Test + public void testIsTimeout() { + final TimeoutException timex = new TimeoutException(EXPECTED_EXCEPTION); + + assertFalse(oper.isTimeout(new IllegalStateException(EXPECTED_EXCEPTION))); + assertFalse(oper.isTimeout(new IllegalStateException(timex))); + assertFalse(oper.isTimeout(new CompletionException(new IllegalStateException(timex)))); + assertFalse(oper.isTimeout(new CompletionException(null))); + assertFalse(oper.isTimeout(new CompletionException(new CompletionException(timex)))); + + assertTrue(oper.isTimeout(timex)); + assertTrue(oper.isTimeout(new CompletionException(timex))); + } + + @Test + public void testLogMessage() { + final String infraStr = SINK_INFRA.toString(); + + // log structured data + appender.clearExtractions(); + oper.logMessage(EventType.OUT, SINK_INFRA, MY_SINK, new MyData()); + List<String> output = appender.getExtracted(); + assertEquals(1, output.size()); + + assertThat(output.get(0)).contains(infraStr).contains(MY_SINK).contains("OUT") + .contains("{\n \"text\": \"my-text\"\n}"); + + // repeat with a response + appender.clearExtractions(); + oper.logMessage(EventType.IN, SOURCE_INFRA, MY_SOURCE, new MyData()); + output = appender.getExtracted(); + assertEquals(1, output.size()); + + assertThat(output.get(0)).contains(SOURCE_INFRA.toString()).contains(MY_SOURCE).contains("IN") + .contains("{\n \"text\": \"my-text\"\n}"); + + // log a plain string + appender.clearExtractions(); + oper.logMessage(EventType.OUT, SINK_INFRA, MY_SINK, TEXT); + output = appender.getExtracted(); + assertEquals(1, output.size()); + assertThat(output.get(0)).contains(infraStr).contains(MY_SINK).contains(TEXT); + + // log a null request + appender.clearExtractions(); + oper.logMessage(EventType.OUT, SINK_INFRA, MY_SINK, null); + output = appender.getExtracted(); + assertEquals(1, output.size()); + + assertThat(output.get(0)).contains(infraStr).contains(MY_SINK).contains("null"); + + // generate exception from coder + setOperCoderException(); + + appender.clearExtractions(); + oper.logMessage(EventType.OUT, SINK_INFRA, MY_SINK, new MyData()); + output = appender.getExtracted(); + assertEquals(2, output.size()); + assertThat(output.get(0)).contains("cannot pretty-print request"); + assertThat(output.get(1)).contains(infraStr).contains(MY_SINK); + + // repeat with a response + appender.clearExtractions(); + oper.logMessage(EventType.IN, SOURCE_INFRA, MY_SOURCE, new MyData()); + output = appender.getExtracted(); + assertEquals(2, output.size()); + assertThat(output.get(0)).contains("cannot pretty-print response"); + assertThat(output.get(1)).contains(MY_SOURCE); + } + + @Test + public void testGetRetry() { + assertEquals(0, oper.getRetry(null)); + assertEquals(10, oper.getRetry(10)); + } + + @Test + public void testGetRetryWait() { + // need an operator that doesn't override the retry time + OperationPartial oper2 = new OperationPartial(params, operator) {}; + assertEquals(OperationPartial.DEFAULT_RETRY_WAIT_MS, oper2.getRetryWaitMs()); + } + + @Test + public void testGetTimeOutMs() { + assertEquals(TIMEOUT * 1000, oper.getTimeoutMs(params.getTimeoutSec())); + + params = params.toBuilder().timeoutSec(null).build(); + + // new params, thus need a new operation + oper = new MyOper(); + + assertEquals(0, oper.getTimeoutMs(params.getTimeoutSec())); + } + + private void starter(OperationOutcome oper) { + ++numStart; + tstart = oper.getStart(); + opstart = oper; + } + + private void completer(OperationOutcome oper) { + ++numEnd; + opend = oper; + } + + /** + * Gets a function that does nothing. + * + * @param <T> type of input parameter expected by the function + * @return a function that does nothing + */ + private <T> Consumer<T> noop() { + return unused -> { + }; + } + + private OperationOutcome makeSuccess() { + OperationOutcome outcome = params.makeOutcome(); + outcome.setResult(PolicyResult.SUCCESS); + + return outcome; + } + + private OperationOutcome makeFailure() { + OperationOutcome outcome = params.makeOutcome(); + outcome.setResult(PolicyResult.FAILURE); + + return outcome; + } + + /** + * Verifies a run. + * + * @param testName test name + * @param expectedCallbacks number of callbacks expected + * @param expectedOperations number of operation invocations expected + * @param expectedResult expected outcome + */ + private void verifyRun(String testName, int expectedCallbacks, int expectedOperations, + PolicyResult expectedResult) { + + String expectedSubRequestId = + (expectedResult == PolicyResult.FAILURE_EXCEPTION ? null : String.valueOf(expectedOperations)); + + verifyRun(testName, expectedCallbacks, expectedOperations, expectedResult, expectedSubRequestId, noop()); + } + + /** + * Verifies a run. + * + * @param testName test name + * @param expectedCallbacks number of callbacks expected + * @param expectedOperations number of operation invocations expected + * @param expectedResult expected outcome + * @param expectedSubRequestId expected sub request ID + * @param manipulator function to modify the future returned by + * {@link OperationPartial#start(ControlLoopOperationParams)} before the tasks + * in the executor are run + */ + private void verifyRun(String testName, int expectedCallbacks, int expectedOperations, PolicyResult expectedResult, + String expectedSubRequestId, Consumer<CompletableFuture<OperationOutcome>> manipulator) { + + CompletableFuture<OperationOutcome> future = oper.start(); + + manipulator.accept(future); + + assertTrue(testName, executor.runAll(MAX_REQUESTS)); + + assertEquals(testName, expectedCallbacks, numStart); + assertEquals(testName, expectedCallbacks, numEnd); + + if (expectedCallbacks > 0) { + assertNotNull(testName, opstart); + assertNotNull(testName, opend); + assertEquals(testName, expectedResult, opend.getResult()); + + assertSame(testName, tstart, opstart.getStart()); + assertSame(testName, tstart, opend.getStart()); + + try { + assertTrue(future.isDone()); + assertSame(testName, opend, future.get()); + + } catch (InterruptedException | ExecutionException e) { + throw new IllegalStateException(e); + } + + if (expectedOperations > 0) { + assertEquals(testName, expectedSubRequestId, opend.getSubRequestId()); + } + } + + assertEquals(testName, expectedOperations, oper.getCount()); + } + + /** + * Creates a new {@link #oper} whose coder will throw an exception. + */ + private void setOperCoderException() { + oper = new MyOper() { + @Override + protected Coder makeCoder() { + return new StandardCoder() { + @Override + public String encode(Object object, boolean pretty) throws CoderException { + throw new CoderException(EXPECTED_EXCEPTION); + } + }; + } + }; + } + + + @Getter + public static class MyData { + private String text = TEXT; + } + + + private class MyOper extends OperationPartial { + @Getter + private int count = 0; + + @Setter + private boolean genException; + + @Setter + private int maxFailures = 0; + + @Setter + private CompletableFuture<OperationOutcome> guard; + + + public MyOper() { + super(OperationPartialTest.this.params, operator); + } + + @Override + protected OperationOutcome doOperation(int attempt, OperationOutcome operation) { + ++count; + if (genException) { + throw new IllegalStateException(EXPECTED_EXCEPTION); + } + + operation.setSubRequestId(String.valueOf(attempt)); + + if (count > maxFailures) { + operation.setResult(PolicyResult.SUCCESS); + } else { + operation.setResult(PolicyResult.FAILURE); + } + + return operation; + } + + @Override + protected CompletableFuture<OperationOutcome> startGuardAsync() { + return (guard != null ? guard : super.startGuardAsync()); + } + + @Override + protected long getRetryWaitMs() { + /* + * Sleep timers run in the background, but we want to control things via the + * "executor", thus we avoid sleep timers altogether by simply returning 0. + */ + return 0L; + } + } +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/OperatorPartialTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/OperatorPartialTest.java index 21bc656f2..370426fd4 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/OperatorPartialTest.java +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/impl/OperatorPartialTest.java @@ -20,1271 +20,88 @@ package org.onap.policy.controlloop.actorserviceprovider.impl; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; 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.Mockito.never; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import java.time.Instant; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.Queue; import java.util.TreeMap; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; -import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Collectors; -import lombok.Getter; -import lombok.Setter; import org.junit.Before; import org.junit.Test; -import org.onap.policy.controlloop.ControlLoopOperation; -import org.onap.policy.controlloop.VirtualControlLoopEvent; -import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome; -import org.onap.policy.controlloop.actorserviceprovider.controlloop.ControlLoopEventContext; +import org.onap.policy.controlloop.actorserviceprovider.Operation; import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; -import org.onap.policy.controlloop.actorserviceprovider.pipeline.PipelineControllerFuture; -import org.onap.policy.controlloop.policy.PolicyResult; public class OperatorPartialTest { - private static final int MAX_PARALLEL_REQUESTS = 10; - private static final String EXPECTED_EXCEPTION = "expected exception"; private static final String ACTOR = "my-actor"; - private static final String OPERATOR = "my-operator"; - private static final String TARGET = "my-target"; - private static final int TIMEOUT = 1000; - private static final UUID REQ_ID = UUID.randomUUID(); + private static final String OPERATION = "my-name"; - private static final List<PolicyResult> FAILURE_RESULTS = Arrays.asList(PolicyResult.values()).stream() - .filter(result -> result != PolicyResult.SUCCESS).collect(Collectors.toList()); - - private VirtualControlLoopEvent event; - private Map<String, Object> config; - private ControlLoopEventContext context; - private MyExec executor; - private ControlLoopOperationParams params; - - private MyOper oper; - - private int numStart; - private int numEnd; - - private Instant tstart; - - private OperationOutcome opstart; - private OperationOutcome opend; + private OperatorPartial operator; /** - * Initializes the fields, including {@link #oper}. + * Initializes {@link #operator}. */ @Before public void setUp() { - event = new VirtualControlLoopEvent(); - event.setRequestId(REQ_ID); - - config = new TreeMap<>(); - context = new ControlLoopEventContext(event); - executor = new MyExec(); - - params = ControlLoopOperationParams.builder().completeCallback(this::completer).context(context) - .executor(executor).actor(ACTOR).operation(OPERATOR).timeoutSec(TIMEOUT) - .startCallback(this::starter).targetEntity(TARGET).build(); - - oper = new MyOper(); - oper.configure(new TreeMap<>()); - oper.start(); - - tstart = null; - - opstart = null; - opend = null; - } - - @Test - public void testOperatorPartial_testGetActorName_testGetName() { - assertEquals(ACTOR, oper.getActorName()); - assertEquals(OPERATOR, oper.getName()); - assertEquals(ACTOR + "." + OPERATOR, oper.getFullName()); - } - - @Test - public void testGetBlockingExecutor() throws InterruptedException { - CountDownLatch latch = new CountDownLatch(1); - - /* - * Use an operator that doesn't override getBlockingExecutor(). - */ - OperatorPartial oper2 = new OperatorPartial(ACTOR, OPERATOR) {}; - oper2.getBlockingExecutor().execute(() -> latch.countDown()); - - assertTrue(latch.await(5, TimeUnit.SECONDS)); - } - - @Test - public void testDoConfigure() { - oper = spy(new MyOper()); - - oper.configure(config); - verify(oper).configure(config); - - // repeat - SHOULD be run again - oper.configure(config); - verify(oper, times(2)).configure(config); - } - - @Test - public void testDoStart() { - oper = spy(new MyOper()); - - oper.configure(config); - oper.start(); - - verify(oper).doStart(); - - // others should not have been invoked - verify(oper, never()).doStop(); - verify(oper, never()).doShutdown(); - } - - @Test - public void testDoStop() { - oper = spy(new MyOper()); - - oper.configure(config); - oper.start(); - oper.stop(); - - verify(oper).doStop(); - - // should not have been re-invoked - verify(oper).doStart(); - - // others should not have been invoked - verify(oper, never()).doShutdown(); - } - - @Test - public void testDoShutdown() { - oper = spy(new MyOper()); - - oper.configure(config); - oper.start(); - oper.shutdown(); - - verify(oper).doShutdown(); - - // should not have been re-invoked - verify(oper).doStart(); - - // others should not have been invoked - verify(oper, never()).doStop(); - } - - @Test - public void testStartOperation() { - verifyRun("testStartOperation", 1, 1, PolicyResult.SUCCESS); - } - - /** - * Tests startOperation() when the operator is not running. - */ - @Test - public void testStartOperationNotRunning() { - // use a new operator, one that hasn't been started yet - oper = new MyOper(); - oper.configure(new TreeMap<>()); - - assertThatIllegalStateException().isThrownBy(() -> oper.startOperation(params)); - } - - /** - * Tests startOperation() when the operation has a preprocessor. - */ - @Test - public void testStartOperationWithPreprocessor() { - AtomicInteger count = new AtomicInteger(); - - CompletableFuture<OperationOutcome> preproc = CompletableFuture.supplyAsync(() -> { - count.incrementAndGet(); - return makeSuccess(); - }, executor); - - oper.setPreProcessor(preproc); - - verifyRun("testStartOperationWithPreprocessor_testStartPreprocessor", 1, 1, PolicyResult.SUCCESS); - - assertEquals(1, count.get()); - } - - /** - * Tests startOperation() with multiple running requests. - */ - @Test - public void testStartOperationMultiple() { - for (int count = 0; count < MAX_PARALLEL_REQUESTS; ++count) { - oper.startOperation(params); - } - - assertTrue(executor.runAll()); - - assertNotNull(opstart); - assertNotNull(opend); - assertEquals(PolicyResult.SUCCESS, opend.getResult()); - - assertEquals(MAX_PARALLEL_REQUESTS, numStart); - assertEquals(MAX_PARALLEL_REQUESTS, oper.getCount()); - assertEquals(MAX_PARALLEL_REQUESTS, numEnd); - } - - /** - * Tests startPreprocessor() when the preprocessor returns a failure. - */ - @Test - public void testStartPreprocessorFailure() { - oper.setPreProcessor(CompletableFuture.completedFuture(makeFailure())); - - verifyRun("testStartPreprocessorFailure", 1, 0, PolicyResult.FAILURE_GUARD); - } - - /** - * Tests startPreprocessor() when the preprocessor throws an exception. - */ - @Test - public void testStartPreprocessorException() { - // arrange for the preprocessor to throw an exception - oper.setPreProcessor(CompletableFuture.failedFuture(new IllegalStateException(EXPECTED_EXCEPTION))); - - verifyRun("testStartPreprocessorException", 1, 0, PolicyResult.FAILURE_GUARD); - } - - /** - * Tests startPreprocessor() when the pipeline is not running. - */ - @Test - public void testStartPreprocessorNotRunning() { - // arrange for the preprocessor to return success, which will be ignored - oper.setPreProcessor(CompletableFuture.completedFuture(makeSuccess())); - - oper.startOperation(params).cancel(false); - assertTrue(executor.runAll()); - - assertNull(opstart); - assertNull(opend); - - assertEquals(0, numStart); - assertEquals(0, oper.getCount()); - assertEquals(0, numEnd); - } - - /** - * Tests startPreprocessor() when the preprocessor <b>builder</b> throws an exception. - */ - @Test - public void testStartPreprocessorBuilderException() { - oper = new MyOper() { - @Override - protected CompletableFuture<OperationOutcome> startPreprocessorAsync(ControlLoopOperationParams params) { - throw new IllegalStateException(EXPECTED_EXCEPTION); - } - }; - - oper.configure(new TreeMap<>()); - oper.start(); - - assertThatIllegalStateException().isThrownBy(() -> oper.startOperation(params)); - - // should be nothing in the queue - assertEquals(0, executor.getQueueLength()); - } - - @Test - public void testStartPreprocessorAsync() { - assertNull(oper.startPreprocessorAsync(params)); - } - - @Test - public void testStartOperationAsync() { - oper.startOperation(params); - assertTrue(executor.runAll()); - - assertEquals(1, oper.getCount()); - } - - @Test - public void testIsSuccess() { - OperationOutcome outcome = new OperationOutcome(); - - outcome.setResult(PolicyResult.SUCCESS); - assertTrue(oper.isSuccess(outcome)); - - for (PolicyResult failure : FAILURE_RESULTS) { - outcome.setResult(failure); - assertFalse("testIsSuccess-" + failure, oper.isSuccess(outcome)); - } - } - - @Test - public void testIsActorFailed() { - assertFalse(oper.isActorFailed(null)); - - OperationOutcome outcome = params.makeOutcome(); - - // incorrect outcome - outcome.setResult(PolicyResult.SUCCESS); - assertFalse(oper.isActorFailed(outcome)); - - outcome.setResult(PolicyResult.FAILURE_RETRIES); - assertFalse(oper.isActorFailed(outcome)); - - // correct outcome - outcome.setResult(PolicyResult.FAILURE); - - // incorrect actor - outcome.setActor(TARGET); - assertFalse(oper.isActorFailed(outcome)); - outcome.setActor(null); - assertFalse(oper.isActorFailed(outcome)); - outcome.setActor(ACTOR); - - // incorrect operation - outcome.setOperation(TARGET); - assertFalse(oper.isActorFailed(outcome)); - outcome.setOperation(null); - assertFalse(oper.isActorFailed(outcome)); - outcome.setOperation(OPERATOR); - - // correct values - assertTrue(oper.isActorFailed(outcome)); - } - - @Test - public void testDoOperation() { - /* - * Use an operator that doesn't override doOperation(). - */ - OperatorPartial oper2 = new OperatorPartial(ACTOR, OPERATOR) { - @Override - protected Executor getBlockingExecutor() { - return executor; - } - }; - - oper2.configure(new TreeMap<>()); - oper2.start(); - - oper2.startOperation(params); - assertTrue(executor.runAll()); - - assertNotNull(opend); - assertEquals(PolicyResult.FAILURE_EXCEPTION, opend.getResult()); - } - - @Test - public void testTimeout() throws Exception { - - // use a real executor - params = params.toBuilder().executor(ForkJoinPool.commonPool()).build(); - - // trigger timeout very quickly - oper = new MyOper() { - @Override - protected long getTimeOutMillis(Integer timeoutSec) { - return 1; - } - - @Override - protected CompletableFuture<OperationOutcome> startOperationAsync(ControlLoopOperationParams params, - int attempt, OperationOutcome outcome) { - - OperationOutcome outcome2 = params.makeOutcome(); - outcome2.setResult(PolicyResult.SUCCESS); - - /* - * Create an incomplete future that will timeout after the operation's - * timeout. If it fires before the other timer, then it will return a - * SUCCESS outcome. - */ - CompletableFuture<OperationOutcome> future = new CompletableFuture<>(); - future = future.orTimeout(1, TimeUnit.SECONDS).handleAsync((unused1, unused2) -> outcome, - params.getExecutor()); - - return future; - } - }; - - oper.configure(new TreeMap<>()); - oper.start(); - - assertEquals(PolicyResult.FAILURE_TIMEOUT, oper.startOperation(params).get().getResult()); - } - - /** - * Verifies that the timer doesn't encompass the preprocessor and doesn't stop the - * operation once the preprocessor completes. - */ - @Test - public void testTimeoutInPreprocessor() throws Exception { - - // use a real executor - params = params.toBuilder().executor(ForkJoinPool.commonPool()).build(); - - // trigger timeout very quickly - oper = new MyOper() { + operator = new OperatorPartial(ACTOR, OPERATION) { @Override - protected long getTimeOutMillis(Integer timeoutSec) { - return 10; - } - - @Override - protected Executor getBlockingExecutor() { - return command -> { - Thread thread = new Thread(command); - thread.start(); - }; - } - - @Override - protected CompletableFuture<OperationOutcome> startPreprocessorAsync(ControlLoopOperationParams params) { - - OperationOutcome outcome = makeSuccess(); - - /* - * Create an incomplete future that will timeout after the operation's - * timeout. If it fires before the other timer, then it will return a - * SUCCESS outcome. - */ - CompletableFuture<OperationOutcome> future = new CompletableFuture<>(); - future = future.orTimeout(200, TimeUnit.MILLISECONDS).handleAsync((unused1, unused2) -> outcome, - params.getExecutor()); - - return future; - } - }; - - oper.configure(new TreeMap<>()); - oper.start(); - - OperationOutcome result = oper.startOperation(params).get(); - assertEquals(PolicyResult.SUCCESS, result.getResult()); - - assertNotNull(opstart); - assertNotNull(opend); - assertEquals(PolicyResult.SUCCESS, opend.getResult()); - - assertEquals(1, numStart); - assertEquals(1, oper.getCount()); - assertEquals(1, numEnd); - } - - /** - * Tests retry functions, when the count is set to zero and retries are exhausted. - */ - @Test - public void testSetRetryFlag_testRetryOnFailure_ZeroRetries_testStartOperationAttempt() { - params = params.toBuilder().retry(0).build(); - oper.setMaxFailures(10); - - verifyRun("testSetRetryFlag_testRetryOnFailure_ZeroRetries", 1, 1, PolicyResult.FAILURE); - } - - /** - * Tests retry functions, when the count is null and retries are exhausted. - */ - @Test - public void testSetRetryFlag_testRetryOnFailure_NullRetries() { - params = params.toBuilder().retry(null).build(); - oper.setMaxFailures(10); - - verifyRun("testSetRetryFlag_testRetryOnFailure_NullRetries", 1, 1, PolicyResult.FAILURE); - } - - /** - * Tests retry functions, when retries are exhausted. - */ - @Test - public void testSetRetryFlag_testRetryOnFailure_RetriesExhausted() { - final int maxRetries = 3; - params = params.toBuilder().retry(maxRetries).build(); - oper.setMaxFailures(10); - - verifyRun("testSetRetryFlag_testRetryOnFailure_RetriesExhausted", maxRetries + 1, maxRetries + 1, - PolicyResult.FAILURE_RETRIES); - } - - /** - * Tests retry functions, when a success follows some retries. - */ - @Test - public void testSetRetryFlag_testRetryOnFailure_SuccessAfterRetries() { - params = params.toBuilder().retry(10).build(); - - final int maxFailures = 3; - oper.setMaxFailures(maxFailures); - - verifyRun("testSetRetryFlag_testRetryOnFailure_SuccessAfterRetries", maxFailures + 1, maxFailures + 1, - PolicyResult.SUCCESS); - } - - /** - * Tests retry functions, when the outcome is {@code null}. - */ - @Test - public void testSetRetryFlag_testRetryOnFailure_NullOutcome() { - - // arrange to return null from doOperation() - oper = new MyOper() { - @Override - protected OperationOutcome doOperation(ControlLoopOperationParams params, int attempt, - OperationOutcome operation) { - - // update counters - super.doOperation(params, attempt, operation); + public Operation buildOperation(ControlLoopOperationParams params) { return null; } }; - - oper.configure(new TreeMap<>()); - oper.start(); - - verifyRun("testSetRetryFlag_testRetryOnFailure_NullOutcome", 1, 1, PolicyResult.FAILURE, null, noop()); - } - - @Test - public void testIsSameOperation() { - assertFalse(oper.isSameOperation(null)); - - OperationOutcome outcome = params.makeOutcome(); - - // wrong actor - should be false - outcome.setActor(null); - assertFalse(oper.isSameOperation(outcome)); - outcome.setActor(TARGET); - assertFalse(oper.isSameOperation(outcome)); - outcome.setActor(ACTOR); - - // wrong operation - should be null - outcome.setOperation(null); - assertFalse(oper.isSameOperation(outcome)); - outcome.setOperation(TARGET); - assertFalse(oper.isSameOperation(outcome)); - outcome.setOperation(OPERATOR); - - assertTrue(oper.isSameOperation(outcome)); - } - - /** - * Tests handleFailure() when the outcome is a success. - */ - @Test - public void testHandlePreprocessorFailureTrue() { - oper.setPreProcessor(CompletableFuture.completedFuture(makeSuccess())); - verifyRun("testHandlePreprocessorFailureTrue", 1, 1, PolicyResult.SUCCESS); - } - - /** - * Tests handleFailure() when the outcome is <i>not</i> a success. - */ - @Test - public void testHandlePreprocessorFailureFalse() throws Exception { - oper.setPreProcessor(CompletableFuture.completedFuture(makeFailure())); - verifyRun("testHandlePreprocessorFailureFalse", 1, 0, PolicyResult.FAILURE_GUARD); - } - - /** - * Tests handleFailure() when the outcome is {@code null}. - */ - @Test - public void testHandlePreprocessorFailureNull() throws Exception { - // arrange to return null from the preprocessor - oper.setPreProcessor(CompletableFuture.completedFuture(null)); - - verifyRun("testHandlePreprocessorFailureNull", 1, 0, PolicyResult.FAILURE_GUARD); - } - - @Test - public void testFromException() { - // arrange to generate an exception when operation runs - oper.setGenException(true); - - verifyRun("testFromException", 1, 1, PolicyResult.FAILURE_EXCEPTION); - } - - /** - * Tests fromException() when there is no exception. - */ - @Test - public void testFromExceptionNoExcept() { - verifyRun("testFromExceptionNoExcept", 1, 1, PolicyResult.SUCCESS); - } - - /** - * Tests both flavors of anyOf(), because one invokes the other. - */ - @Test - public void testAnyOf() throws Exception { - // first task completes, others do not - List<CompletableFuture<OperationOutcome>> tasks = new LinkedList<>(); - - final OperationOutcome outcome = params.makeOutcome(); - - tasks.add(CompletableFuture.completedFuture(outcome)); - tasks.add(new CompletableFuture<>()); - tasks.add(new CompletableFuture<>()); - - CompletableFuture<OperationOutcome> result = oper.anyOf(params, tasks); - assertTrue(executor.runAll()); - - assertTrue(result.isDone()); - assertSame(outcome, result.get()); - - // second task completes, others do not - tasks = new LinkedList<>(); - - tasks.add(new CompletableFuture<>()); - tasks.add(CompletableFuture.completedFuture(outcome)); - tasks.add(new CompletableFuture<>()); - - result = oper.anyOf(params, tasks); - assertTrue(executor.runAll()); - - assertTrue(result.isDone()); - assertSame(outcome, result.get()); - - // third task completes, others do not - tasks = new LinkedList<>(); - - tasks.add(new CompletableFuture<>()); - tasks.add(new CompletableFuture<>()); - tasks.add(CompletableFuture.completedFuture(outcome)); - - result = oper.anyOf(params, tasks); - assertTrue(executor.runAll()); - - assertTrue(result.isDone()); - assertSame(outcome, result.get()); - } - - /** - * Tests both flavors of allOf(), because one invokes the other. - */ - @Test - public void testAllOf() throws Exception { - List<CompletableFuture<OperationOutcome>> tasks = new LinkedList<>(); - - final OperationOutcome outcome = params.makeOutcome(); - - CompletableFuture<OperationOutcome> future1 = new CompletableFuture<>(); - CompletableFuture<OperationOutcome> future2 = new CompletableFuture<>(); - CompletableFuture<OperationOutcome> future3 = new CompletableFuture<>(); - - tasks.add(future1); - tasks.add(future2); - tasks.add(future3); - - CompletableFuture<OperationOutcome> result = oper.allOf(params, tasks); - - assertTrue(executor.runAll()); - assertFalse(result.isDone()); - future1.complete(outcome); - - // complete 3 before 2 - assertTrue(executor.runAll()); - assertFalse(result.isDone()); - future3.complete(outcome); - - assertTrue(executor.runAll()); - assertFalse(result.isDone()); - future2.complete(outcome); - - // all of them are now done - assertTrue(executor.runAll()); - assertTrue(result.isDone()); - assertSame(outcome, result.get()); - } - - @Test - public void testCombineOutcomes() throws Exception { - // only one outcome - verifyOutcomes(0, PolicyResult.SUCCESS); - verifyOutcomes(0, PolicyResult.FAILURE_EXCEPTION); - - // maximum is in different positions - verifyOutcomes(0, PolicyResult.FAILURE, PolicyResult.SUCCESS, PolicyResult.FAILURE_GUARD); - verifyOutcomes(1, PolicyResult.SUCCESS, PolicyResult.FAILURE, PolicyResult.FAILURE_GUARD); - verifyOutcomes(2, PolicyResult.SUCCESS, PolicyResult.FAILURE_GUARD, PolicyResult.FAILURE); - - // null outcome - final List<CompletableFuture<OperationOutcome>> tasks = new LinkedList<>(); - tasks.add(CompletableFuture.completedFuture(null)); - CompletableFuture<OperationOutcome> result = oper.allOf(params, tasks); - - assertTrue(executor.runAll()); - assertTrue(result.isDone()); - assertNull(result.get()); - - // one throws an exception during execution - IllegalStateException except = new IllegalStateException(EXPECTED_EXCEPTION); - - tasks.clear(); - tasks.add(CompletableFuture.completedFuture(params.makeOutcome())); - tasks.add(CompletableFuture.failedFuture(except)); - tasks.add(CompletableFuture.completedFuture(params.makeOutcome())); - result = oper.allOf(params, tasks); - - assertTrue(executor.runAll()); - assertTrue(result.isCompletedExceptionally()); - result.whenComplete((unused, thrown) -> assertSame(except, thrown)); - } - - private void verifyOutcomes(int expected, PolicyResult... results) throws Exception { - List<CompletableFuture<OperationOutcome>> tasks = new LinkedList<>(); - - - OperationOutcome expectedOutcome = null; - - for (int count = 0; count < results.length; ++count) { - OperationOutcome outcome = params.makeOutcome(); - outcome.setResult(results[count]); - tasks.add(CompletableFuture.completedFuture(outcome)); - - if (count == expected) { - expectedOutcome = outcome; - } - } - - CompletableFuture<OperationOutcome> result = oper.allOf(params, tasks); - - assertTrue(executor.runAll()); - assertTrue(result.isDone()); - assertSame(expectedOutcome, result.get()); - } - - private Function<OperationOutcome, CompletableFuture<OperationOutcome>> makeTask( - final OperationOutcome taskOutcome) { - - return outcome -> CompletableFuture.completedFuture(taskOutcome); - } - - @Test - public void testDetmPriority() { - assertEquals(1, oper.detmPriority(null)); - - OperationOutcome outcome = params.makeOutcome(); - - Map<PolicyResult, Integer> map = Map.of(PolicyResult.SUCCESS, 0, PolicyResult.FAILURE_GUARD, 2, - PolicyResult.FAILURE_RETRIES, 3, PolicyResult.FAILURE, 4, PolicyResult.FAILURE_TIMEOUT, 5, - PolicyResult.FAILURE_EXCEPTION, 6); - - for (Entry<PolicyResult, Integer> ent : map.entrySet()) { - outcome.setResult(ent.getKey()); - assertEquals(ent.getKey().toString(), ent.getValue().intValue(), oper.detmPriority(outcome)); - } - } - - /** - * Tests doTask(Future) when the controller is not running. - */ - @Test - public void testDoTaskFutureNotRunning() throws Exception { - CompletableFuture<OperationOutcome> taskFuture = new CompletableFuture<>(); - - PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>(); - controller.complete(params.makeOutcome()); - - CompletableFuture<OperationOutcome> future = - oper.doTask(params, controller, false, params.makeOutcome(), taskFuture); - assertFalse(future.isDone()); - assertTrue(executor.runAll()); - - // should not have run the task - assertFalse(future.isDone()); - - // should have canceled the task future - assertTrue(taskFuture.isCancelled()); - } - - /** - * Tests doTask(Future) when the previous outcome was successful. - */ - @Test - public void testDoTaskFutureSuccess() throws Exception { - CompletableFuture<OperationOutcome> taskFuture = new CompletableFuture<>(); - final OperationOutcome taskOutcome = params.makeOutcome(); - - PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>(); - - CompletableFuture<OperationOutcome> future = - oper.doTask(params, controller, true, params.makeOutcome(), taskFuture); - - taskFuture.complete(taskOutcome); - assertTrue(executor.runAll()); - - assertTrue(future.isDone()); - assertSame(taskOutcome, future.get()); - - // controller should not be done yet - assertFalse(controller.isDone()); - } - - /** - * Tests doTask(Future) when the previous outcome was failed. - */ - @Test - public void testDoTaskFutureFailure() throws Exception { - CompletableFuture<OperationOutcome> taskFuture = new CompletableFuture<>(); - final OperationOutcome failedOutcome = params.makeOutcome(); - failedOutcome.setResult(PolicyResult.FAILURE); - - PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>(); - - CompletableFuture<OperationOutcome> future = oper.doTask(params, controller, true, failedOutcome, taskFuture); - assertFalse(future.isDone()); - assertTrue(executor.runAll()); - - // should not have run the task - assertFalse(future.isDone()); - - // should have canceled the task future - assertTrue(taskFuture.isCancelled()); - - // controller SHOULD be done now - assertTrue(controller.isDone()); - assertSame(failedOutcome, controller.get()); - } - - /** - * Tests doTask(Future) when the previous outcome was failed, but not checking - * success. - */ - @Test - public void testDoTaskFutureUncheckedFailure() throws Exception { - CompletableFuture<OperationOutcome> taskFuture = new CompletableFuture<>(); - final OperationOutcome failedOutcome = params.makeOutcome(); - failedOutcome.setResult(PolicyResult.FAILURE); - - PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>(); - - CompletableFuture<OperationOutcome> future = oper.doTask(params, controller, false, failedOutcome, taskFuture); - assertFalse(future.isDone()); - - // complete the task - OperationOutcome taskOutcome = params.makeOutcome(); - taskFuture.complete(taskOutcome); - - assertTrue(executor.runAll()); - - // should have run the task - assertTrue(future.isDone()); - - assertTrue(future.isDone()); - assertSame(taskOutcome, future.get()); - - // controller should not be done yet - assertFalse(controller.isDone()); - } - - /** - * Tests doTask(Function) when the controller is not running. - */ - @Test - public void testDoTaskFunctionNotRunning() throws Exception { - AtomicBoolean invoked = new AtomicBoolean(); - - Function<OperationOutcome, CompletableFuture<OperationOutcome>> task = outcome -> { - invoked.set(true); - return CompletableFuture.completedFuture(params.makeOutcome()); - }; - - PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>(); - controller.complete(params.makeOutcome()); - - CompletableFuture<OperationOutcome> future = - oper.doTask(params, controller, false, task).apply(params.makeOutcome()); - assertFalse(future.isDone()); - assertTrue(executor.runAll()); - - // should not have run the task - assertFalse(future.isDone()); - - // should not have even invoked the task - assertFalse(invoked.get()); - } - - /** - * Tests doTask(Function) when the previous outcome was successful. - */ - @Test - public void testDoTaskFunctionSuccess() throws Exception { - final OperationOutcome taskOutcome = params.makeOutcome(); - - final OperationOutcome failedOutcome = params.makeOutcome(); - - Function<OperationOutcome, CompletableFuture<OperationOutcome>> task = makeTask(taskOutcome); - - PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>(); - - CompletableFuture<OperationOutcome> future = oper.doTask(params, controller, true, task).apply(failedOutcome); - - assertTrue(future.isDone()); - assertSame(taskOutcome, future.get()); - - // controller should not be done yet - assertFalse(controller.isDone()); - } - - /** - * Tests doTask(Function) when the previous outcome was failed. - */ - @Test - public void testDoTaskFunctionFailure() throws Exception { - final OperationOutcome failedOutcome = params.makeOutcome(); - failedOutcome.setResult(PolicyResult.FAILURE); - - AtomicBoolean invoked = new AtomicBoolean(); - - Function<OperationOutcome, CompletableFuture<OperationOutcome>> task = outcome -> { - invoked.set(true); - return CompletableFuture.completedFuture(params.makeOutcome()); - }; - - PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>(); - - CompletableFuture<OperationOutcome> future = oper.doTask(params, controller, true, task).apply(failedOutcome); - assertFalse(future.isDone()); - assertTrue(executor.runAll()); - - // should not have run the task - assertFalse(future.isDone()); - - // should not have even invoked the task - assertFalse(invoked.get()); - - // controller should have the failed task - assertTrue(controller.isDone()); - assertSame(failedOutcome, controller.get()); - } - - /** - * Tests doTask(Function) when the previous outcome was failed, but not checking - * success. - */ - @Test - public void testDoTaskFunctionUncheckedFailure() throws Exception { - final OperationOutcome taskOutcome = params.makeOutcome(); - - final OperationOutcome failedOutcome = params.makeOutcome(); - failedOutcome.setResult(PolicyResult.FAILURE); - - Function<OperationOutcome, CompletableFuture<OperationOutcome>> task = makeTask(taskOutcome); - - PipelineControllerFuture<OperationOutcome> controller = new PipelineControllerFuture<>(); - - CompletableFuture<OperationOutcome> future = oper.doTask(params, controller, false, task).apply(failedOutcome); - - assertTrue(future.isDone()); - assertSame(taskOutcome, future.get()); - - // controller should not be done yet - assertFalse(controller.isDone()); } - /** - * Tests callbackStarted() when the pipeline has already been stopped. - */ @Test - public void testCallbackStartedNotRunning() { - AtomicReference<Future<OperationOutcome>> future = new AtomicReference<>(); - - /* - * arrange to stop the controller when the start-callback is invoked, but capture - * the outcome - */ - params = params.toBuilder().startCallback(oper -> { - starter(oper); - future.get().cancel(false); - }).build(); - - future.set(oper.startOperation(params)); - assertTrue(executor.runAll()); - - // should have only run once - assertEquals(1, numStart); + public void testOperatorPartial_testGetActorName_testGetName() { + assertEquals(ACTOR, operator.getActorName()); + assertEquals(OPERATION, operator.getName()); + assertEquals(ACTOR + "." + OPERATION, operator.getFullName()); } - /** - * Tests callbackCompleted() when the pipeline has already been stopped. - */ @Test - public void testCallbackCompletedNotRunning() { - AtomicReference<Future<OperationOutcome>> future = new AtomicReference<>(); - - // arrange to stop the controller when the start-callback is invoked - params = params.toBuilder().startCallback(oper -> { - future.get().cancel(false); - }).build(); + public void testDoStart() { + operator.configure(null); - future.set(oper.startOperation(params)); - assertTrue(executor.runAll()); + operator = spy(operator); + operator.start(); - // should not have been set - assertNull(opend); - assertEquals(0, numEnd); + verify(operator).doStart(); } @Test - public void testSetOutcomeControlLoopOperationOutcomeThrowable() { - final CompletionException timex = new CompletionException(new TimeoutException(EXPECTED_EXCEPTION)); - - OperationOutcome outcome; + public void testDoStop() { + operator.configure(null); + operator.start(); - outcome = new OperationOutcome(); - oper.setOutcome(params, outcome, timex); - assertEquals(ControlLoopOperation.FAILED_MSG, outcome.getMessage()); - assertEquals(PolicyResult.FAILURE_TIMEOUT, outcome.getResult()); + operator = spy(operator); + operator.stop(); - outcome = new OperationOutcome(); - oper.setOutcome(params, outcome, new IllegalStateException(EXPECTED_EXCEPTION)); - assertEquals(ControlLoopOperation.FAILED_MSG, outcome.getMessage()); - assertEquals(PolicyResult.FAILURE_EXCEPTION, outcome.getResult()); + verify(operator).doStop(); } @Test - public void testSetOutcomeControlLoopOperationOutcomePolicyResult() { - OperationOutcome outcome; + public void testDoShutdown() { + operator.configure(null); + operator.start(); - outcome = new OperationOutcome(); - oper.setOutcome(params, outcome, PolicyResult.SUCCESS); - assertEquals(ControlLoopOperation.SUCCESS_MSG, outcome.getMessage()); - assertEquals(PolicyResult.SUCCESS, outcome.getResult()); + operator = spy(operator); + operator.shutdown(); - for (PolicyResult result : FAILURE_RESULTS) { - outcome = new OperationOutcome(); - oper.setOutcome(params, outcome, result); - assertEquals(result.toString(), ControlLoopOperation.FAILED_MSG, outcome.getMessage()); - assertEquals(result.toString(), result, outcome.getResult()); - } + verify(operator).doShutdown(); } @Test - public void testIsTimeout() { - final TimeoutException timex = new TimeoutException(EXPECTED_EXCEPTION); + public void testDoConfigureMapOfStringObject() { + operator = spy(operator); - assertFalse(oper.isTimeout(new IllegalStateException(EXPECTED_EXCEPTION))); - assertFalse(oper.isTimeout(new IllegalStateException(timex))); - assertFalse(oper.isTimeout(new CompletionException(new IllegalStateException(timex)))); - assertFalse(oper.isTimeout(new CompletionException(null))); - assertFalse(oper.isTimeout(new CompletionException(new CompletionException(timex)))); + Map<String, Object> params = new TreeMap<>(); + operator.configure(params); - assertTrue(oper.isTimeout(timex)); - assertTrue(oper.isTimeout(new CompletionException(timex))); + verify(operator).doConfigure(params); } @Test - public void testGetTimeOutMillis() { - assertEquals(TIMEOUT * 1000, oper.getTimeOutMillis(params.getTimeoutSec())); - - params = params.toBuilder().timeoutSec(null).build(); - assertEquals(0, oper.getTimeOutMillis(params.getTimeoutSec())); - } - - private void starter(OperationOutcome oper) { - ++numStart; - tstart = oper.getStart(); - opstart = oper; - } - - private void completer(OperationOutcome oper) { - ++numEnd; - opend = oper; - } - - /** - * Gets a function that does nothing. - * - * @param <T> type of input parameter expected by the function - * @return a function that does nothing - */ - private <T> Consumer<T> noop() { - return unused -> { - }; - } - - private OperationOutcome makeSuccess() { - OperationOutcome outcome = params.makeOutcome(); - outcome.setResult(PolicyResult.SUCCESS); - - return outcome; - } - - private OperationOutcome makeFailure() { - OperationOutcome outcome = params.makeOutcome(); - outcome.setResult(PolicyResult.FAILURE); - - return outcome; - } - - /** - * Verifies a run. - * - * @param testName test name - * @param expectedCallbacks number of callbacks expected - * @param expectedOperations number of operation invocations expected - * @param expectedResult expected outcome - */ - private void verifyRun(String testName, int expectedCallbacks, int expectedOperations, - PolicyResult expectedResult) { - - String expectedSubRequestId = - (expectedResult == PolicyResult.FAILURE_EXCEPTION ? null : String.valueOf(expectedOperations)); - - verifyRun(testName, expectedCallbacks, expectedOperations, expectedResult, expectedSubRequestId, noop()); - } - - /** - * Verifies a run. - * - * @param testName test name - * @param expectedCallbacks number of callbacks expected - * @param expectedOperations number of operation invocations expected - * @param expectedResult expected outcome - * @param expectedSubRequestId expected sub request ID - * @param manipulator function to modify the future returned by - * {@link OperatorPartial#startOperation(ControlLoopOperationParams)} before - * the tasks in the executor are run - */ - private void verifyRun(String testName, int expectedCallbacks, int expectedOperations, PolicyResult expectedResult, - String expectedSubRequestId, Consumer<CompletableFuture<OperationOutcome>> manipulator) { - - CompletableFuture<OperationOutcome> future = oper.startOperation(params); - - manipulator.accept(future); - - assertTrue(testName, executor.runAll()); - - assertEquals(testName, expectedCallbacks, numStart); - assertEquals(testName, expectedCallbacks, numEnd); - - if (expectedCallbacks > 0) { - assertNotNull(testName, opstart); - assertNotNull(testName, opend); - assertEquals(testName, expectedResult, opend.getResult()); - - assertSame(testName, tstart, opstart.getStart()); - assertSame(testName, tstart, opend.getStart()); - - try { - assertTrue(future.isDone()); - assertSame(testName, opend, future.get()); - - } catch (InterruptedException | ExecutionException e) { - throw new IllegalStateException(e); - } - - if (expectedOperations > 0) { - assertEquals(testName, expectedSubRequestId, opend.getSubRequestId()); - } - } - - assertEquals(testName, expectedOperations, oper.getCount()); - } - - private class MyOper extends OperatorPartial { - @Getter - private int count = 0; - - @Setter - private boolean genException; - - @Setter - private int maxFailures = 0; - - @Setter - private CompletableFuture<OperationOutcome> preProcessor; - - public MyOper() { - super(ACTOR, OPERATOR); - } - - @Override - protected OperationOutcome doOperation(ControlLoopOperationParams params, int attempt, - OperationOutcome operation) { - ++count; - if (genException) { - throw new IllegalStateException(EXPECTED_EXCEPTION); - } - - operation.setSubRequestId(String.valueOf(attempt)); - - if (count > maxFailures) { - operation.setResult(PolicyResult.SUCCESS); - } else { - operation.setResult(PolicyResult.FAILURE); - } - - return operation; - } - - @Override - protected CompletableFuture<OperationOutcome> startPreprocessorAsync(ControlLoopOperationParams params) { - return (preProcessor != null ? preProcessor : super.startPreprocessorAsync(params)); - } - - @Override - protected Executor getBlockingExecutor() { - return executor; - } - } - - /** - * Executor that will run tasks until the queue is empty or a maximum number of tasks - * have been executed. - */ - private static class MyExec implements Executor { - private static final int MAX_TASKS = MAX_PARALLEL_REQUESTS * 100; - - private Queue<Runnable> commands = new LinkedList<>(); - - public MyExec() { - // do nothing - } - - public int getQueueLength() { - return commands.size(); - } - - @Override - public void execute(Runnable command) { - commands.add(command); - } - - public boolean runAll() { - for (int count = 0; count < MAX_TASKS && !commands.isEmpty(); ++count) { - commands.remove().run(); - } - - return commands.isEmpty(); - } + public void testGetBlockingExecutor() { + assertNotNull(operator.getBlockingExecutor()); } } diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/BidirectionalTopicActorParamsTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/BidirectionalTopicActorParamsTest.java new file mode 100644 index 000000000..1f38ad371 --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/BidirectionalTopicActorParamsTest.java @@ -0,0 +1,118 @@ +/*- + * ============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.actorserviceprovider.parameters; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Collections; +import java.util.Map; +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.actorserviceprovider.Util; + +public class BidirectionalTopicActorParamsTest { + private static final String CONTAINER = "my-container"; + + private static final String DFLT_SOURCE = "default-source"; + private static final String DFLT_SINK = "default-target"; + private static final int DFLT_TIMEOUT = 10; + + private static final String OPER1_NAME = "oper A"; + private static final String OPER1_SOURCE = "source A"; + private static final String OPER1_SINK = "target A"; + private static final int OPER1_TIMEOUT = 20; + + // oper2 uses some default values + private static final String OPER2_NAME = "oper B"; + private static final String OPER2_SOURCE = "source B"; + + // oper3 uses default values for everything + private static final String OPER3_NAME = "oper C"; + + private Map<String, Map<String, Object>> operMap; + private BidirectionalTopicActorParams params; + + + /** + * Sets up. + */ + @Before + public void setUp() { + BidirectionalTopicParams oper1 = BidirectionalTopicParams.builder().sourceTopic(OPER1_SOURCE) + .sinkTopic(OPER1_SINK).timeoutSec(OPER1_TIMEOUT).build(); + + Map<String, Object> oper1Map = Util.translateToMap(OPER1_NAME, oper1); + Map<String, Object> oper2Map = Map.of("source", OPER2_SOURCE); + Map<String, Object> oper3Map = Collections.emptyMap(); + operMap = Map.of(OPER1_NAME, oper1Map, OPER2_NAME, oper2Map, OPER3_NAME, oper3Map); + + params = makeBidirectionalTopicActorParams(); + } + + @Test + public void testValidate() { + assertTrue(params.validate(CONTAINER).isValid()); + + // only a few fields are required + BidirectionalTopicActorParams sparse = Util.translate(CONTAINER, Map.of("operation", operMap, "timeoutSec", 1), + BidirectionalTopicActorParams.class); + assertTrue(sparse.validate(CONTAINER).isValid()); + + testValidateField("operation", "null", params2 -> params2.setOperation(null)); + testValidateField("timeoutSec", "minimum", params2 -> params2.setTimeoutSec(-1)); + + // check edge cases + params.setTimeoutSec(0); + assertFalse(params.validate(CONTAINER).isValid()); + + params.setTimeoutSec(1); + assertTrue(params.validate(CONTAINER).isValid()); + } + + private void testValidateField(String fieldName, String expected, + Consumer<BidirectionalTopicActorParams> makeInvalid) { + + // original params should be valid + ValidationResult result = params.validate(CONTAINER); + assertTrue(fieldName, result.isValid()); + + // make invalid params + BidirectionalTopicActorParams params2 = makeBidirectionalTopicActorParams(); + makeInvalid.accept(params2); + result = params2.validate(CONTAINER); + assertFalse(fieldName, result.isValid()); + assertThat(result.getResult()).contains(CONTAINER).contains(fieldName).contains(expected); + } + + private BidirectionalTopicActorParams makeBidirectionalTopicActorParams() { + BidirectionalTopicActorParams params2 = new BidirectionalTopicActorParams(); + params2.setSinkTopic(DFLT_SINK); + params2.setSourceTopic(DFLT_SOURCE); + params2.setTimeoutSec(DFLT_TIMEOUT); + params2.setOperation(operMap); + + return params2; + } +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/TopicParamsTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/BidirectionalTopicParamsTest.java index 4834c98d2..7e44fa2e1 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/TopicParamsTest.java +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/BidirectionalTopicParamsTest.java @@ -29,44 +29,46 @@ import java.util.function.Function; import org.junit.Before; import org.junit.Test; import org.onap.policy.common.parameters.ValidationResult; -import org.onap.policy.controlloop.actorserviceprovider.parameters.TopicParams.TopicParamsBuilder; +import org.onap.policy.controlloop.actorserviceprovider.parameters.BidirectionalTopicParams.BidirectionalTopicParamsBuilder; -public class TopicParamsTest { +public class BidirectionalTopicParamsTest { private static final String CONTAINER = "my-container"; - private static final String TARGET = "my-target"; + private static final String SINK = "my-sink"; private static final String SOURCE = "my-source"; - private static final long TIMEOUT = 10; + private static final int TIMEOUT = 10; - private TopicParams params; + private BidirectionalTopicParams params; @Before public void setUp() { - params = TopicParams.builder().target(TARGET).source(SOURCE).timeoutSec(TIMEOUT).build(); + params = BidirectionalTopicParams.builder().sinkTopic(SINK).sourceTopic(SOURCE).timeoutSec(TIMEOUT).build(); } @Test public void testValidate() { - testValidateField("target", "null", bldr -> bldr.target(null)); - testValidateField("source", "null", bldr -> bldr.source(null)); + assertTrue(params.validate(CONTAINER).isValid()); + + testValidateField("sink", "null", bldr -> bldr.sinkTopic(null)); + testValidateField("source", "null", bldr -> bldr.sourceTopic(null)); testValidateField("timeoutSec", "minimum", bldr -> bldr.timeoutSec(-1)); // check edge cases - assertTrue(params.toBuilder().timeoutSec(0).build().validate(CONTAINER).isValid()); + assertFalse(params.toBuilder().timeoutSec(0).build().validate(CONTAINER).isValid()); assertTrue(params.toBuilder().timeoutSec(1).build().validate(CONTAINER).isValid()); } @Test public void testBuilder_testToBuilder() { - assertEquals(TARGET, params.getTarget()); - assertEquals(SOURCE, params.getSource()); + assertEquals(SINK, params.getSinkTopic()); + assertEquals(SOURCE, params.getSourceTopic()); assertEquals(TIMEOUT, params.getTimeoutSec()); assertEquals(params, params.toBuilder().build()); } private void testValidateField(String fieldName, String expected, - Function<TopicParamsBuilder, TopicParamsBuilder> makeInvalid) { + Function<BidirectionalTopicParamsBuilder, BidirectionalTopicParamsBuilder> makeInvalid) { // original params should be valid ValidationResult result = params.validate(CONTAINER); diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/CommonActorParamsTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/CommonActorParamsTest.java new file mode 100644 index 000000000..901420346 --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/CommonActorParamsTest.java @@ -0,0 +1,137 @@ +/*- + * ============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.actorserviceprovider.parameters; + +import static org.assertj.core.api.Assertions.assertThat; +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.assertTrue; + +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Consumer; +import java.util.function.Function; +import lombok.Setter; +import org.junit.Before; +import org.junit.Test; +import org.onap.policy.common.parameters.ValidationResult; +import org.onap.policy.controlloop.actorserviceprovider.Util; + +public class CommonActorParamsTest { + + private static final String CONTAINER = "my-container"; + + private static final String PATH1 = "path #1"; + private static final String PATH2 = "path #2"; + private static final String URI1 = "uri #1"; + private static final String URI2 = "uri #2"; + private static final String TEXT1 = "hello"; + private static final String TEXT2 = "world"; + private static final String TEXT2B = "bye"; + + private Map<String, Map<String, Object>> operations; + private CommonActorParams params; + + /** + * Initializes {@link #operations} with two items and {@link params} with a fully + * populated object. + */ + @Before + public void setUp() { + operations = new TreeMap<>(); + operations.put(PATH1, Map.of("path", URI1)); + operations.put(PATH2, Map.of("path", URI2, "text2", TEXT2B)); + + params = makeCommonActorParams(); + } + + @Test + public void testMakeOperationParameters() { + Function<String, Map<String, Object>> maker = params.makeOperationParameters(CONTAINER); + assertNull(maker.apply("unknown-operation")); + + Map<String, Object> subparam = maker.apply(PATH1); + assertNotNull(subparam); + assertEquals("{path=uri #1, text1=hello, text2=world}", new TreeMap<>(subparam).toString()); + + subparam = maker.apply(PATH2); + assertNotNull(subparam); + assertEquals("{path=uri #2, text1=hello, text2=bye}", new TreeMap<>(subparam).toString()); + } + + @Test + public void testDoValidation() { + assertThatCode(() -> params.doValidation(CONTAINER)).doesNotThrowAnyException(); + + // invalid param + params.setOperation(null); + assertThatThrownBy(() -> params.doValidation(CONTAINER)) + .isInstanceOf(ParameterValidationRuntimeException.class); + } + + @Test + public void testValidate() { + assertTrue(params.validate(CONTAINER).isValid()); + + // only a few fields are required + CommonActorParams sparse = Util.translate(CONTAINER, Map.of("operation", operations, "timeoutSec", 1), + CommonActorParams.class); + assertTrue(sparse.validate(CONTAINER).isValid()); + + testValidateField("operation", "null", params2 -> params2.setOperation(null)); + } + + private void testValidateField(String fieldName, String expected, Consumer<CommonActorParams> makeInvalid) { + + // original params should be valid + ValidationResult result = params.validate(CONTAINER); + assertTrue(fieldName, result.isValid()); + + // make invalid params + CommonActorParams params2 = makeCommonActorParams(); + makeInvalid.accept(params2); + result = params2.validate(CONTAINER); + assertFalse(fieldName, result.isValid()); + assertThat(result.getResult()).contains(CONTAINER).contains(fieldName).contains(expected); + } + + private CommonActorParams makeCommonActorParams() { + MyParams params2 = new MyParams(); + params2.setOperation(operations); + params2.setText1(TEXT1); + params2.setText2(TEXT2); + + return params2; + } + + @Setter + public static class MyParams extends CommonActorParams { + @SuppressWarnings("unused") + private String text1; + + @SuppressWarnings("unused") + private String text2; + } +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/ControlLoopOperationParamsTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/ControlLoopOperationParamsTest.java index 9dd19d548..a5215a48f 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/ControlLoopOperationParamsTest.java +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/ControlLoopOperationParamsTest.java @@ -51,6 +51,7 @@ import org.mockito.MockitoAnnotations; import org.onap.policy.common.parameters.BeanValidationResult; import org.onap.policy.controlloop.VirtualControlLoopEvent; 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; @@ -87,12 +88,15 @@ public class ControlLoopOperationParamsTest { private Executor executor; @Mock - private CompletableFuture<OperationOutcome> operation; + private CompletableFuture<OperationOutcome> operFuture; @Mock private Operator operator; @Mock + private Operation operation; + + @Mock private Consumer<OperationOutcome> starter; private Map<String, String> payload; @@ -110,7 +114,8 @@ public class ControlLoopOperationParamsTest { when(actorService.getActor(ACTOR)).thenReturn(actor); when(actor.getOperator(OPERATION)).thenReturn(operator); - when(operator.startOperation(any())).thenReturn(operation); + when(operator.buildOperation(any())).thenReturn(operation); + when(operation.start()).thenReturn(operFuture); when(event.getRequestId()).thenReturn(REQ_ID); @@ -128,7 +133,7 @@ public class ControlLoopOperationParamsTest { @Test public void testStart() { - assertSame(operation, params.start()); + assertSame(operFuture, params.start()); assertThatIllegalArgumentException().isThrownBy(() -> params.toBuilder().context(null).build().start()); } diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpActorParamsTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpActorParamsTest.java index 6c1f538ec..9e708535f 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpActorParamsTest.java +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpActorParamsTest.java @@ -21,90 +21,62 @@ package org.onap.policy.controlloop.actorserviceprovider.parameters; import static org.assertj.core.api.Assertions.assertThat; -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.assertTrue; import java.util.Map; import java.util.TreeMap; import java.util.function.Consumer; -import java.util.function.Function; import org.junit.Before; import org.junit.Test; import org.onap.policy.common.parameters.ValidationResult; +import org.onap.policy.controlloop.actorserviceprovider.Util; public class HttpActorParamsTest { private static final String CONTAINER = "my-container"; private static final String CLIENT = "my-client"; - private static final long TIMEOUT = 10; + private static final int TIMEOUT = 10; private static final String PATH1 = "path #1"; private static final String PATH2 = "path #2"; private static final String URI1 = "uri #1"; private static final String URI2 = "uri #2"; - private Map<String, String> paths; + private Map<String, Map<String, Object>> operations; private HttpActorParams params; /** - * Initializes {@link #paths} with two items and {@link params} with a fully populated - * object. + * Initializes {@link #operations} with two items and {@link params} with a fully + * populated object. */ @Before public void setUp() { - paths = new TreeMap<>(); - paths.put(PATH1, URI1); - paths.put(PATH2, URI2); + operations = new TreeMap<>(); + operations.put(PATH1, Map.of("path", URI1)); + operations.put(PATH2, Map.of("path", URI2)); params = makeHttpActorParams(); } @Test - public void testMakeOperationParameters() { - Function<String, Map<String, Object>> maker = params.makeOperationParameters(CONTAINER); - assertNull(maker.apply("unknown-operation")); - - Map<String, Object> subparam = maker.apply(PATH1); - assertNotNull(subparam); - assertEquals("{clientName=my-client, path=uri #1, timeoutSec=10}", new TreeMap<>(subparam).toString()); - - subparam = maker.apply(PATH2); - assertNotNull(subparam); - assertEquals("{clientName=my-client, path=uri #2, timeoutSec=10}", new TreeMap<>(subparam).toString()); - } - - @Test - public void testDoValidation() { - assertThatCode(() -> params.doValidation(CONTAINER)).doesNotThrowAnyException(); - - // invalid param - params.setClientName(null); - assertThatThrownBy(() -> params.doValidation(CONTAINER)) - .isInstanceOf(ParameterValidationRuntimeException.class); - } - - @Test public void testValidate() { assertTrue(params.validate(CONTAINER).isValid()); - testValidateField("clientName", "null", params2 -> params2.setClientName(null)); - testValidateField("path", "null", params2 -> params2.setPath(null)); + // only a few fields are required + HttpActorParams sparse = Util.translate(CONTAINER, Map.of("operation", operations, "timeoutSec", 1), + HttpActorParams.class); + assertTrue(sparse.validate(CONTAINER).isValid()); + + testValidateField("operation", "null", params2 -> params2.setOperation(null)); testValidateField("timeoutSec", "minimum", params2 -> params2.setTimeoutSec(-1)); // check edge cases params.setTimeoutSec(0); - assertTrue(params.validate(CONTAINER).isValid()); + assertFalse(params.validate(CONTAINER).isValid()); params.setTimeoutSec(1); assertTrue(params.validate(CONTAINER).isValid()); - - // one path value is null - testValidateField(PATH2, "null", params2 -> paths.put(PATH2, null)); } private void testValidateField(String fieldName, String expected, Consumer<HttpActorParams> makeInvalid) { @@ -125,7 +97,7 @@ public class HttpActorParamsTest { HttpActorParams params2 = new HttpActorParams(); params2.setClientName(CLIENT); params2.setTimeoutSec(TIMEOUT); - params2.setPath(paths); + params2.setOperation(operations); return params2; } diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpParamsTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpParamsTest.java index 6cf7328ca..fdfb4b495 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpParamsTest.java +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/parameters/HttpParamsTest.java @@ -36,7 +36,7 @@ public class HttpParamsTest { private static final String CONTAINER = "my-container"; private static final String CLIENT = "my-client"; private static final String PATH = "my-path"; - private static final long TIMEOUT = 10; + private static final int TIMEOUT = 10; private HttpParams params; @@ -54,7 +54,7 @@ public class HttpParamsTest { testValidateField("timeoutSec", "minimum", bldr -> bldr.timeoutSec(-1)); // check edge cases - assertTrue(params.toBuilder().timeoutSec(0).build().validate(CONTAINER).isValid()); + assertFalse(params.toBuilder().timeoutSec(0).build().validate(CONTAINER).isValid()); assertTrue(params.toBuilder().timeoutSec(1).build().validate(CONTAINER).isValid()); } diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/pipeline/PipelineControllerFutureTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/pipeline/PipelineControllerFutureTest.java index a6b11ef65..4a00c065e 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/pipeline/PipelineControllerFutureTest.java +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/pipeline/PipelineControllerFutureTest.java @@ -424,7 +424,7 @@ public class PipelineControllerFutureTest { } /** - * Tests add(Function) when the controller is canceled after the future is added. + * Tests wrap(Function) when the controller is canceled after the future is added. */ @Test public void testWrapFunctionCancel() throws Exception { @@ -442,7 +442,7 @@ public class PipelineControllerFutureTest { } /** - * Tests add(Function) when the controller is not running. + * Tests wrap(Function) when the controller is not running. */ @Test public void testWrapFunctionNotRunning() { diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/topic/BidirectionalTopicHandlerTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/topic/BidirectionalTopicHandlerTest.java new file mode 100644 index 000000000..54d56de53 --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/topic/BidirectionalTopicHandlerTest.java @@ -0,0 +1,144 @@ +/*- + * ============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.actorserviceprovider.topic; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure; +import org.onap.policy.common.endpoints.event.comm.TopicEndpoint; +import org.onap.policy.common.endpoints.event.comm.TopicSink; +import org.onap.policy.common.endpoints.event.comm.TopicSource; +import org.onap.policy.common.endpoints.event.comm.client.BidirectionalTopicClientException; + +public class BidirectionalTopicHandlerTest { + private static final String UNKNOWN = "unknown"; + private static final String MY_SOURCE = "my-source"; + private static final String MY_SINK = "my-sink"; + private static final String KEY1 = "requestId"; + private static final String KEY2 = "subRequestId"; + + @Mock + private TopicSink publisher; + + @Mock + private TopicSource subscriber; + + @Mock + private TopicEndpoint mgr; + + private MyTopicHandler handler; + + + /** + * Sets up. + */ + @Before + public void setUp() throws BidirectionalTopicClientException { + MockitoAnnotations.initMocks(this); + + when(mgr.getTopicSinks(MY_SINK)).thenReturn(Arrays.asList(publisher)); + when(mgr.getTopicSources(eq(Arrays.asList(MY_SOURCE)))).thenReturn(Arrays.asList(subscriber)); + + when(publisher.getTopicCommInfrastructure()).thenReturn(CommInfrastructure.NOOP); + + handler = new MyTopicHandler(MY_SINK, MY_SOURCE); + + handler.start(); + } + + @Test + public void testBidirectionalTopicHandler_testGetSource_testGetTarget() { + assertEquals(MY_SOURCE, handler.getSourceTopic()); + assertEquals(MY_SINK, handler.getSinkTopic()); + + verify(mgr).getTopicSinks(anyString()); + verify(mgr).getTopicSources(any()); + + // source not found + assertThatThrownBy(() -> new MyTopicHandler(MY_SINK, UNKNOWN)) + .isInstanceOf(BidirectionalTopicClientException.class).hasMessageContaining("sources") + .hasMessageContaining(UNKNOWN); + + // target not found + assertThatThrownBy(() -> new MyTopicHandler(UNKNOWN, MY_SOURCE)) + .isInstanceOf(BidirectionalTopicClientException.class).hasMessageContaining("sinks") + .hasMessageContaining(UNKNOWN); + } + + @Test + public void testShutdown() { + handler.shutdown(); + verify(subscriber).unregister(any()); + } + + @Test + public void testStart() { + verify(subscriber).register(any()); + } + + @Test + public void testStop() { + handler.stop(); + verify(subscriber).unregister(any()); + } + + @Test + public void testAddForwarder() { + // array form + Forwarder forwarder = handler.addForwarder(new SelectorKey(KEY1), new SelectorKey(KEY2)); + assertNotNull(forwarder); + + // repeat using list form + assertSame(forwarder, handler.addForwarder(Arrays.asList(new SelectorKey(KEY1), new SelectorKey(KEY2)))); + } + + @Test + public void testGetTopicEndpointManager() { + // setting "mgr" to null should cause it to use the superclass' method + mgr = null; + assertNotNull(handler.getTopicEndpointManager()); + } + + + private class MyTopicHandler extends BidirectionalTopicHandler { + public MyTopicHandler(String sinkTopic, String sourceTopic) throws BidirectionalTopicClientException { + super(sinkTopic, sourceTopic); + } + + @Override + protected TopicEndpoint getTopicEndpointManager() { + return (mgr != null ? mgr : super.getTopicEndpointManager()); + } + } +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/topic/ForwarderTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/topic/ForwarderTest.java new file mode 100644 index 000000000..a01159bc2 --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/topic/ForwarderTest.java @@ -0,0 +1,199 @@ +/*- + * ============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.actorserviceprovider.topic; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.BiConsumer; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.onap.policy.common.utils.coder.StandardCoderObject; +import org.onap.policy.controlloop.actorserviceprovider.Util; + +public class ForwarderTest { + private static final String TEXT = "some text"; + + private static final String KEY1 = "requestId"; + private static final String KEY2 = "container"; + private static final String SUBKEY = "subRequestId"; + + private static final String VALUEA_REQID = "hello"; + private static final String VALUEA_SUBREQID = "world"; + + // request id is shared with value A + private static final String VALUEB_REQID = "hello"; + private static final String VALUEB_SUBREQID = "another world"; + + // unique values + private static final String VALUEC_REQID = "bye"; + private static final String VALUEC_SUBREQID = "bye-bye"; + + @Mock + private BiConsumer<String, StandardCoderObject> listener1; + + @Mock + private BiConsumer<String, StandardCoderObject> listener1b; + + @Mock + private BiConsumer<String, StandardCoderObject> listener2; + + @Mock + private BiConsumer<String, StandardCoderObject> listener3; + + private Forwarder forwarder; + + + /** + * Sets up. + */ + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + forwarder = new Forwarder(Arrays.asList(new SelectorKey(KEY1), new SelectorKey(KEY2, SUBKEY))); + + forwarder.register(Arrays.asList(VALUEA_REQID, VALUEA_SUBREQID), listener1); + forwarder.register(Arrays.asList(VALUEA_REQID, VALUEA_SUBREQID), listener1b); + forwarder.register(Arrays.asList(VALUEB_REQID, VALUEB_SUBREQID), listener2); + forwarder.register(Arrays.asList(VALUEC_REQID, VALUEC_SUBREQID), listener3); + } + + @Test + public void testRegister() { + // key size mismatches + assertThatIllegalArgumentException().isThrownBy(() -> forwarder.register(Arrays.asList(), listener1)) + .withMessage("key/value mismatch"); + assertThatIllegalArgumentException() + .isThrownBy(() -> forwarder.register(Arrays.asList(VALUEA_REQID), listener1)) + .withMessage("key/value mismatch"); + } + + @Test + public void testUnregister() { + // remove listener1b + forwarder.unregister(Arrays.asList(VALUEA_REQID, VALUEA_SUBREQID), listener1b); + + StandardCoderObject sco = makeMessage(Map.of(KEY1, VALUEA_REQID, KEY2, Map.of(SUBKEY, VALUEA_SUBREQID))); + forwarder.onMessage(TEXT, sco); + + verify(listener1).accept(TEXT, sco); + verify(listener1b, never()).accept(any(), any()); + + // remove listener1 + forwarder.unregister(Arrays.asList(VALUEA_REQID, VALUEA_SUBREQID), listener1); + forwarder.onMessage(TEXT, sco); + + // route a message to listener2 + sco = makeMessage(Map.of(KEY1, VALUEB_REQID, KEY2, Map.of(SUBKEY, VALUEB_SUBREQID))); + forwarder.onMessage(TEXT, sco); + verify(listener2).accept(TEXT, sco); + + // no more messages to listener1 or 1b + verify(listener1).accept(any(), any()); + verify(listener1b, never()).accept(any(), any()); + } + + @Test + public void testOnMessage() { + StandardCoderObject sco = makeMessage(Map.of(KEY1, VALUEA_REQID, KEY2, Map.of(SUBKEY, VALUEA_SUBREQID))); + forwarder.onMessage(TEXT, sco); + + verify(listener1).accept(TEXT, sco); + verify(listener1b).accept(TEXT, sco); + + // repeat - counts should increment + forwarder.onMessage(TEXT, sco); + + verify(listener1, times(2)).accept(TEXT, sco); + verify(listener1b, times(2)).accept(TEXT, sco); + + // should not have been invoked + verify(listener2, never()).accept(any(), any()); + verify(listener3, never()).accept(any(), any()); + + // try other listeners now + sco = makeMessage(Map.of(KEY1, VALUEB_REQID, KEY2, Map.of(SUBKEY, VALUEB_SUBREQID))); + forwarder.onMessage(TEXT, sco); + verify(listener2).accept(TEXT, sco); + + sco = makeMessage(Map.of(KEY1, VALUEC_REQID, KEY2, Map.of(SUBKEY, VALUEC_SUBREQID))); + forwarder.onMessage(TEXT, sco); + verify(listener3).accept(TEXT, sco); + + // message has no listeners + sco = makeMessage(Map.of(KEY1, "xyzzy", KEY2, Map.of(SUBKEY, VALUEB_SUBREQID))); + forwarder.onMessage(TEXT, sco); + + // message doesn't have both keys + sco = makeMessage(Map.of(KEY1, VALUEA_REQID)); + forwarder.onMessage(TEXT, sco); + + // counts should not have incremented + verify(listener1, times(2)).accept(any(), any()); + verify(listener1b, times(2)).accept(any(), any()); + verify(listener2).accept(any(), any()); + verify(listener3).accept(any(), any()); + + // listener throws an exception + doThrow(new IllegalStateException("expected exception")).when(listener1).accept(any(), any()); + } + + /* + * Tests onMessage() when listener1 throws an exception. + */ + @Test + public void testOnMessageListenerException1() { + doThrow(new IllegalStateException("expected exception")).when(listener1).accept(any(), any()); + + StandardCoderObject sco = makeMessage(Map.of(KEY1, VALUEA_REQID, KEY2, Map.of(SUBKEY, VALUEA_SUBREQID))); + forwarder.onMessage(TEXT, sco); + + verify(listener1b).accept(TEXT, sco); + } + + /* + * Tests onMessage() when listener1b throws an exception. + */ + @Test + public void testOnMessageListenerException1b() { + doThrow(new IllegalStateException("expected exception")).when(listener1b).accept(any(), any()); + + StandardCoderObject sco = makeMessage(Map.of(KEY1, VALUEA_REQID, KEY2, Map.of(SUBKEY, VALUEA_SUBREQID))); + forwarder.onMessage(TEXT, sco); + + verify(listener1).accept(TEXT, sco); + } + + /** + * Makes a message from a map. + */ + private StandardCoderObject makeMessage(Map<String, Object> map) { + return Util.translate("", map, StandardCoderObject.class); + } +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/topic/SelectorKeyTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/topic/SelectorKeyTest.java new file mode 100644 index 000000000..19df9c2d8 --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/topic/SelectorKeyTest.java @@ -0,0 +1,93 @@ +/*- + * ============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.actorserviceprovider.topic; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.util.Map; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.junit.Before; +import org.junit.Test; +import org.onap.policy.common.utils.coder.StandardCoderObject; +import org.onap.policy.controlloop.actorserviceprovider.Util; + +public class SelectorKeyTest { + private static final String FIELD1 = "map"; + private static final String FIELD2 = "abc"; + private static final String FIELDX = "abd"; + + private SelectorKey key; + + @Before + public void setUp() { + key = new SelectorKey(FIELD1, FIELD2); + } + + @Test + public void testHashCode_testEquals() { + SelectorKey key2 = new SelectorKey(FIELD1, FIELD2); + assertEquals(key, key2); + assertEquals(key.hashCode(), key2.hashCode()); + + key2 = new SelectorKey(FIELD1, FIELDX); + assertNotEquals(key, key2); + assertNotEquals(key.hashCode(), key2.hashCode()); + + // test empty key + key = new SelectorKey(); + key2 = new SelectorKey(); + assertEquals(key, key2); + assertEquals(key.hashCode(), key2.hashCode()); + } + + @Test + public void testExtractField() { + Map<String, Object> map = Map.of("hello", "world", FIELD1, Map.of("another", "", FIELD2, "value B")); + StandardCoderObject sco = Util.translate("", map, StandardCoderObject.class); + + String result = key.extractField(sco); + assertNotNull(result); + assertEquals("value B", result); + + // shorter key + assertEquals("world", new SelectorKey("hello").extractField(sco)); + assertNull(new SelectorKey("bye").extractField(sco)); + + // not found + assertNull(new SelectorKey(FIELD1, "not field 2").extractField(sco)); + + // test with empty key + assertNull(new SelectorKey().extractField(sco)); + } + + @Getter + @Setter + @Builder + protected static class Data { + private String text; + private Map<String, String> map; + } +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/topic/TopicListenerImplTest.java b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/topic/TopicListenerImplTest.java new file mode 100644 index 000000000..3012ff6af --- /dev/null +++ b/models-interactions/model-actors/actorServiceProvider/src/test/java/org/onap/policy/controlloop/actorserviceprovider/topic/TopicListenerImplTest.java @@ -0,0 +1,154 @@ +/*- + * ============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.actorserviceprovider.topic; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.BiConsumer; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure; +import org.onap.policy.common.utils.coder.CoderException; +import org.onap.policy.common.utils.coder.StandardCoder; +import org.onap.policy.common.utils.coder.StandardCoderObject; + +public class TopicListenerImplTest { + private static final StandardCoder coder = new StandardCoder(); + private static final CommInfrastructure INFRA = CommInfrastructure.NOOP; + private static final String MY_TOPIC = "my-topic"; + private static final String KEY1 = "requestId"; + private static final String KEY2 = "container"; + private static final String SUBKEY = "subRequestId"; + + private static final String VALUEA_REQID = "hello"; + private static final String VALUEA_SUBREQID = "world"; + + private static final String VALUEB_REQID = "bye"; + + private Forwarder forwarder1; + private Forwarder forwarder2; + private TopicListenerImpl topic; + + @Mock + private BiConsumer<String, StandardCoderObject> listener1; + + @Mock + private BiConsumer<String, StandardCoderObject> listener1b; + + @Mock + private BiConsumer<String, StandardCoderObject> listener2; + + + /** + * Sets up. + */ + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + topic = new TopicListenerImpl(); + + forwarder1 = topic.addForwarder(new SelectorKey(KEY1)); + forwarder2 = topic.addForwarder(new SelectorKey(KEY1), new SelectorKey(KEY2, SUBKEY)); + + assertNotNull(forwarder1); + assertNotNull(forwarder2); + assertNotSame(forwarder1, forwarder2); + + forwarder1.register(Arrays.asList(VALUEA_REQID), listener1); + forwarder1.register(Arrays.asList(VALUEB_REQID), listener1b); + forwarder2.register(Arrays.asList(VALUEA_REQID, VALUEA_SUBREQID), listener2); + } + + @Test + public void testShutdown() { + // shut it down, which should clear all forwarders + topic.shutdown(); + + // should get a new forwarder now + Forwarder forwarder = topic.addForwarder(new SelectorKey(KEY1)); + assertNotSame(forwarder1, forwarder); + assertNotSame(forwarder2, forwarder); + + // new forwarder should be unchanged + assertSame(forwarder, topic.addForwarder(new SelectorKey(KEY1))); + } + + @Test + public void testAddForwarder() { + assertSame(forwarder1, topic.addForwarder(new SelectorKey(KEY1))); + assertSame(forwarder2, topic.addForwarder(new SelectorKey(KEY1), new SelectorKey(KEY2, SUBKEY))); + } + + @Test + public void testOnTopicEvent() { + /* + * send a message that should go to listener1 on forwarder1 and listener2 on + * forwarder2 + */ + String msg = makeMessage(Map.of(KEY1, VALUEA_REQID, KEY2, Map.of(SUBKEY, VALUEA_SUBREQID))); + topic.onTopicEvent(INFRA, MY_TOPIC, msg); + + verify(listener1).accept(eq(msg), any()); + verify(listener2).accept(eq(msg), any()); + + // not to listener1b + verify(listener1b, never()).accept(any(), any()); + + /* + * now send a message that should only go to listener1b on forwarder1 + */ + msg = makeMessage(Map.of(KEY1, VALUEB_REQID, KEY2, Map.of(SUBKEY, VALUEA_SUBREQID))); + topic.onTopicEvent(INFRA, MY_TOPIC, msg); + + // should route to listener1 on forwarder1 and listener2 on forwarder2 + verify(listener1b).accept(eq(msg), any()); + + // try one where the coder throws an exception + topic.onTopicEvent(INFRA, MY_TOPIC, "{invalid-json"); + + // no extra invocations + verify(listener1).accept(any(), any()); + verify(listener1b).accept(any(), any()); + verify(listener2).accept(any(), any()); + } + + /** + * Makes a message from a map. + */ + private String makeMessage(Map<String, Object> map) { + try { + return coder.encode(map); + } catch (CoderException e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/models-interactions/model-actors/actorServiceProvider/src/test/resources/logback-test.xml b/models-interactions/model-actors/actorServiceProvider/src/test/resources/logback-test.xml index c7fe46e47..7b5b9fc32 100644 --- a/models-interactions/model-actors/actorServiceProvider/src/test/resources/logback-test.xml +++ b/models-interactions/model-actors/actorServiceProvider/src/test/resources/logback-test.xml @@ -39,4 +39,11 @@ <logger name="org.onap.policy.controlloop.actorserviceprovider.Util" level="info" additivity="false"> <appender-ref ref="STDOUT" /> </logger> + + <!-- this is required for OperationPartialTest --> + <logger + name="org.onap.policy.controlloop.actorserviceprovider.impl.OperationPartial" + level="info" additivity="false"> + <appender-ref ref="STDOUT" /> + </logger> </configuration> diff --git a/models-interactions/model-actors/pom.xml b/models-interactions/model-actors/pom.xml index 8765eb44b..30891142d 100644 --- a/models-interactions/model-actors/pom.xml +++ b/models-interactions/model-actors/pom.xml @@ -36,6 +36,8 @@ <modules> <module>actorServiceProvider</module> + <module>actor.test</module> + <module>actor.aai</module> <module>actor.appc</module> <module>actor.vfc</module> <module>actor.sdnc</module> |