From 4c4a452097c16debab0177e9e87a33ae17d28e44 Mon Sep 17 00:00:00 2001 From: PatrikBuhr Date: Thu, 1 Oct 2020 09:40:04 +0200 Subject: [PATCH] Added supervision of producers Change-Id: I69471f80d4951a076458b58542c653ff8d0e58a4 Issue-ID: NONRTRIC-173 Signed-off-by: PatrikBuhr --- enrichment-coordinator-service/docs/api.yaml | 215 +++++++++++++++++---- .../enrichment/clients/ProducerCallbacks.java | 46 ++--- .../enrichment/controllers/VoidResponse.java | 32 +++ .../controllers/consumer/ConsumerController.java | 14 +- .../controllers/producer/ProducerController.java | 90 ++++----- .../producer/ProducerRegistrationInfo.java | 8 +- .../controllers/producer/ProducerStatusInfo.java | 54 ++++++ .../oransc/enrichment/repository/EiProducer.java | 49 ++++- .../oransc/enrichment/repository/EiProducers.java | 31 ++- .../org/oransc/enrichment/repository/EiType.java | 4 +- .../org/oransc/enrichment/repository/EiTypes.java | 15 +- .../enrichment/tasks/ProducerSupervision.java | 98 ++++++++++ .../org/oransc/enrichment/ApplicationTest.java | 46 ++++- .../controller/ProducerSimulatorController.java | 35 +++- 14 files changed, 591 insertions(+), 146 deletions(-) create mode 100644 enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/VoidResponse.java create mode 100644 enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerStatusInfo.java create mode 100644 enrichment-coordinator-service/src/main/java/org/oransc/enrichment/tasks/ProducerSupervision.java diff --git a/enrichment-coordinator-service/docs/api.yaml b/enrichment-coordinator-service/docs/api.yaml index 570caee6..b643e8ec 100644 --- a/enrichment-coordinator-service/docs/api.yaml +++ b/enrichment-coordinator-service/docs/api.yaml @@ -3,7 +3,7 @@ info: description: This page lists all the rest apis for the service. version: '1.0' title: Enrichment Data service -host: 'localhost:8081' +host: 'localhost:8082' basePath: / tags: - name: A1-E Enrichment Data Consumer API @@ -53,7 +53,7 @@ paths: '200': description: EI type schema: - $ref: '#/definitions/ei_type_info' + $ref: '#/definitions/EiType' '401': description: Unauthorized '403': @@ -122,7 +122,7 @@ paths: '200': description: EI Job schema: - $ref: '#/definitions/ei_job_info' + $ref: '#/definitions/EiJob' '401': description: Unauthorized '403': @@ -152,7 +152,7 @@ paths: description: eiJobInfo required: true schema: - $ref: '#/definitions/ei_job_info' + $ref: '#/definitions/EiJob' - name: eiTypeId in: path description: eiTypeId @@ -161,12 +161,8 @@ paths: responses: '200': description: Job updated - schema: - type: object '201': description: Job created - schema: - type: object '401': description: Unauthorized '403': @@ -197,12 +193,8 @@ paths: responses: '200': description: Not used - schema: - type: object '204': description: Job deleted - schema: - type: object '401': description: Unauthorized '403': @@ -235,7 +227,7 @@ paths: '200': description: EI Job status schema: - $ref: '#/definitions/ei_job_status' + $ref: '#/definitions/EiJobStatus' '401': description: Unauthorized '403': @@ -319,12 +311,8 @@ paths: responses: '200': description: Producer updated - schema: - type: object '201': description: Producer created - schema: - type: object '401': description: Unauthorized '403': @@ -348,12 +336,8 @@ paths: responses: '200': description: Not used - schema: - type: object '204': description: Producer deleted - schema: - type: object '401': description: Unauthorized '403': @@ -394,6 +378,34 @@ paths: schema: $ref: '#/definitions/error_information' deprecated: false + '/ei-producer/v1/eiproducers/{eiProducerId}/status': + get: + tags: + - Enrichment Data Producer API + summary: EI producer status + operationId: getEiProducerStatusUsingGET + produces: + - application/json + parameters: + - name: eiProducerId + in: path + description: eiProducerId + required: true + type: string + responses: + '200': + description: EI jobs + schema: + $ref: '#/definitions/producer_status' + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: Enrichment Information producer is not found + schema: + $ref: '#/definitions/error_information' + deprecated: false /ei-producer/v1/eitypes: get: tags: @@ -464,8 +476,35 @@ paths: responses: '200': description: OK + '201': + description: Created + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: Not Found + deprecated: false + /producer_simulator/job_created_error: + post: + tags: + - Producer Simulator + summary: 'Callback for EI job creation, returns error' + operationId: jobCreatedCallbackReturnErrorUsingPOST + consumes: + - application/json + produces: + - application/json + parameters: + - in: body + name: request + description: request + required: true schema: - type: object + $ref: '#/definitions/producer_ei_job_request' + responses: + '200': + description: OK '201': description: Created '401': @@ -495,8 +534,35 @@ paths: responses: '200': description: OK + '201': + description: Created + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: Not Found + deprecated: false + /producer_simulator/job_deleted_error: + post: + tags: + - Producer Simulator + summary: 'Callback for EI job creation, returns error' + operationId: jobDeletedCallbackReturnErrorUsingPOST + consumes: + - application/json + produces: + - application/json + parameters: + - in: body + name: request + description: request + required: true schema: - type: object + $ref: '#/definitions/producer_ei_job_request' + responses: + '200': + description: OK '201': description: Created '401': @@ -506,31 +572,71 @@ paths: '404': description: Not Found deprecated: false + /producer_simulator/supervision: + get: + tags: + - Producer Simulator + summary: Producer supervision + operationId: producerSupervisionUsingGET + produces: + - application/json + responses: + '200': + description: OK + schema: + type: string + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: Not Found + deprecated: false + /producer_simulator/supervision_error: + get: + tags: + - Producer Simulator + summary: Producer supervision error + operationId: producerSupervisionErrorUsingGET + produces: + - application/json + responses: + '200': + description: OK + schema: + type: string + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: Not Found + deprecated: false definitions: - ei_job_info: + EiJob: type: object required: - - job_data - - job_owner - - target_uri + - jobOwner + - jobParameters + - targetUri properties: - job_data: - type: object - description: EI Type specific job data - job_owner: + jobOwner: type: string description: Identity of the owner of the job - target_uri: + jobParameters: + type: object + description: EI Type specific job data + targetUri: type: string description: The target of the EI data - title: ei_job_info - description: Information for a Enrichment Information Job - ei_job_status: + title: EiJob + description: Information for an Enrichment Information Job + EiJobStatus: type: object required: - - operational_state + - operationalState properties: - operational_state: + operationalState: type: string description: |- Operational state, values: @@ -539,16 +645,19 @@ definitions: enum: - ENABLED - DISABLED - title: ei_job_status + title: EiJobStatus description: Status for an EI Job - ei_type_info: + EiType: type: object properties: - job_data_schema: + eiJobParametersSchema: type: object description: Json schema for the job data - title: ei_type_info + title: EiType description: Information for an EI type + Mono«ResponseEntity«object»»: + type: object + title: Mono«ResponseEntity«object»» error_information: type: object properties: @@ -613,6 +722,7 @@ definitions: required: - ei_job_creation_callback_url - ei_job_deletion_callback_url + - ei_producer_supervision_callback_url - supported_ei_types properties: ei_job_creation_callback_url: @@ -621,6 +731,9 @@ definitions: ei_job_deletion_callback_url: type: string description: callback for job deletion + ei_producer_supervision_callback_url: + type: string + description: callback for producer supervision supported_ei_types: type: array description: Supported EI types @@ -628,4 +741,24 @@ definitions: $ref: '#/definitions/producer_ei_type_registration_info' title: producer_registration_info description: Information for an EI producer + producer_status: + type: object + required: + - operational_state + properties: + operational_state: + type: string + description: |- + Operational state, values: + ENABLED: TBD + DISABLED: TBD. + enum: + - ENABLED + - DISABLED + title: producer_status + description: Status for an EI Producer + void: + type: object + title: void + description: Void/empty diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/clients/ProducerCallbacks.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/clients/ProducerCallbacks.java index f42c6e34..7f28a12b 100644 --- a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/clients/ProducerCallbacks.java +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/clients/ProducerCallbacks.java @@ -26,8 +26,6 @@ import com.google.gson.GsonBuilder; import java.lang.invoke.MethodHandles; import org.oransc.enrichment.configuration.ApplicationConfig; -import org.oransc.enrichment.configuration.ImmutableWebClientConfig; -import org.oransc.enrichment.configuration.WebClientConfig; import org.oransc.enrichment.repository.EiJob; import org.oransc.enrichment.repository.EiProducer; import org.slf4j.Logger; @@ -52,16 +50,22 @@ public class ProducerCallbacks { ApplicationConfig applicationConfig; public void notifyProducersJobDeleted(EiJob eiJob) { - AsyncRestClient restClient = restClient(false); + AsyncRestClient restClient = restClient(); ProducerJobInfo request = new ProducerJobInfo(eiJob); String body = gson.toJson(request); for (EiProducer producer : eiJob.type().getProducers()) { - restClient.post(producer.jobDeletionCallbackUrl(), body) // - .subscribe(notUsed -> logger.debug("Job subscription started OK {}", producer.id()), // - throwable -> logger.warn("Job subscription failed {}", producer.id(), throwable.toString()), null); + restClient.post(producer.getJobDeletionCallbackUrl(), body) // + .subscribe(notUsed -> logger.debug("Job deleted OK {}", producer.getId()), // + throwable -> logger.warn("Job delete failed {}", producer.getId(), throwable.toString()), null); } } + /** + * Calls all producers for an EiJob activation. + * + * @param eiJob an EI job + * @return the number of producers that returned OK + */ public Mono notifyProducersJobStarted(EiJob eiJob) { return Flux.fromIterable(eiJob.type().getProducers()) // .flatMap(eiProducer -> notifyProducerJobStarted(eiProducer, eiJob)) // @@ -69,32 +73,28 @@ public class ProducerCallbacks { .flatMap(okResponses -> Mono.just(Integer.valueOf(okResponses.size()))); // } + /** + * Calls one producer for an EiJob activation. + * + * @param producer a producer + * @param eiJob an EI job + * @return the body of the response from the REST call + */ public Mono notifyProducerJobStarted(EiProducer producer, EiJob eiJob) { - AsyncRestClient restClient = restClient(false); + AsyncRestClient restClient = restClient(); ProducerJobInfo request = new ProducerJobInfo(eiJob); String body = gson.toJson(request); - return restClient.post(producer.jobCreationCallbackUrl(), body) - .doOnNext(resp -> logger.debug("Job subscription started OK {}", producer.id())) + return restClient.post(producer.getJobCreationCallbackUrl(), body) + .doOnNext(resp -> logger.debug("Job subscription started OK {}", producer.getId())) .onErrorResume(throwable -> { - logger.warn("Job subscription failed {}", producer.id(), throwable.toString()); + logger.warn("Job subscription failed {}", producer.getId(), throwable.toString()); return Mono.empty(); }); } - private AsyncRestClient restClient(boolean useTrustValidation) { - WebClientConfig config = this.applicationConfig.getWebClientConfig(); - config = ImmutableWebClientConfig.builder() // - .keyStoreType(config.keyStoreType()) // - .keyStorePassword(config.keyStorePassword()) // - .keyStore(config.keyStore()) // - .keyPassword(config.keyPassword()) // - .isTrustStoreUsed(useTrustValidation) // - .trustStore(config.trustStore()) // - .trustStorePassword(config.trustStorePassword()) // - .build(); - - return new AsyncRestClient("", config); + private AsyncRestClient restClient() { + return new AsyncRestClient("", this.applicationConfig.getWebClientConfig()); } } diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/VoidResponse.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/VoidResponse.java new file mode 100644 index 00000000..1933c2b9 --- /dev/null +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/VoidResponse.java @@ -0,0 +1,32 @@ +/*- + * ========================LICENSE_START================================= + * ONAP : ccsdk oran + * ====================================================================== + * Copyright (C) 2020 Nordix Foundation. 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.oransc.enrichment.controllers; + +import io.swagger.annotations.ApiModel; + +import org.immutables.gson.Gson; + +@Gson.TypeAdapters +@ApiModel(value = "void", description = "Void/empty") +public class VoidResponse { + private VoidResponse() { + } +} diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerController.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerController.java index b120d5fa..288421a1 100644 --- a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerController.java +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/consumer/ConsumerController.java @@ -30,7 +30,6 @@ import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; -import java.lang.invoke.MethodHandles; import java.util.ArrayList; import java.util.List; @@ -40,14 +39,13 @@ import org.json.JSONObject; import org.oransc.enrichment.clients.ProducerCallbacks; import org.oransc.enrichment.configuration.ApplicationConfig; import org.oransc.enrichment.controllers.ErrorResponse; +import org.oransc.enrichment.controllers.VoidResponse; import org.oransc.enrichment.exceptions.ServiceException; import org.oransc.enrichment.repository.EiJob; import org.oransc.enrichment.repository.EiJobs; import org.oransc.enrichment.repository.EiType; import org.oransc.enrichment.repository.EiTypes; import org.oransc.enrichment.repository.ImmutableEiJob; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -65,8 +63,6 @@ import reactor.core.publisher.Mono; @Api(tags = {ConsumerConsts.CONSUMER_API_NAME}) public class ConsumerController { - private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - @Autowired ApplicationConfig applicationConfig; @@ -214,8 +210,8 @@ public class ConsumerController { @ApiOperation(value = "Individual EI Job", notes = "") @ApiResponses( value = { // - @ApiResponse(code = 200, message = "Not used", response = void.class), - @ApiResponse(code = 204, message = "Job deleted", response = void.class), + @ApiResponse(code = 200, message = "Not used", response = VoidResponse.class), + @ApiResponse(code = 204, message = "Job deleted", response = VoidResponse.class), @ApiResponse( code = 404, message = "Enrichment Information type or job is not found", @@ -240,8 +236,8 @@ public class ConsumerController { @ApiOperation(value = "Individual EI Job", notes = "") @ApiResponses( value = { // - @ApiResponse(code = 201, message = "Job created", response = void.class), // - @ApiResponse(code = 200, message = "Job updated", response = void.class), // , + @ApiResponse(code = 201, message = "Job created", response = VoidResponse.class), // + @ApiResponse(code = 200, message = "Job updated", response = VoidResponse.class), // , @ApiResponse( code = 404, message = "Enrichment Information type is not found", diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerController.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerController.java index 7743bce7..8adff419 100644 --- a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerController.java +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerController.java @@ -36,6 +36,7 @@ import java.util.List; import org.oransc.enrichment.clients.ProducerCallbacks; import org.oransc.enrichment.clients.ProducerJobInfo; import org.oransc.enrichment.controllers.ErrorResponse; +import org.oransc.enrichment.controllers.VoidResponse; import org.oransc.enrichment.controllers.producer.ProducerRegistrationInfo.ProducerEiTypeRegistrationInfo; import org.oransc.enrichment.repository.EiJob; import org.oransc.enrichment.repository.EiJobs; @@ -43,7 +44,6 @@ import org.oransc.enrichment.repository.EiProducer; import org.oransc.enrichment.repository.EiProducers; import org.oransc.enrichment.repository.EiType; import org.oransc.enrichment.repository.EiTypes; -import org.oransc.enrichment.repository.ImmutableEiProducer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -134,7 +134,7 @@ public class ProducerController { ) { List result = new ArrayList<>(); for (EiProducer eiProducer : this.eiProducers.getAllProducers()) { - result.add(eiProducer.id()); + result.add(eiProducer.getId()); } return new ResponseEntity<>(gson.toJson(result), HttpStatus.OK); @@ -178,7 +178,7 @@ public class ProducerController { try { EiProducer producer = this.eiProducers.getProducer(eiProducerId); Collection producerJobs = new ArrayList<>(); - for (EiType type : producer.eiTypes()) { + for (EiType type : producer.getEiTypes()) { for (EiJob eiJob : this.eiJobs.getJobsForType(type)) { ProducerJobInfo request = new ProducerJobInfo(eiJob); producerJobs.add(request); @@ -191,14 +191,42 @@ public class ProducerController { } } + @GetMapping( + path = ProducerConsts.API_ROOT + "/eiproducers/{eiProducerId}/status", + produces = MediaType.APPLICATION_JSON_VALUE) + @ApiOperation(value = "EI producer status") + @ApiResponses( + value = { // + @ApiResponse(code = 200, message = "EI jobs", response = ProducerStatusInfo.class), // + @ApiResponse( + code = 404, + message = "Enrichment Information producer is not found", + response = ErrorResponse.ErrorInfo.class)}) + public ResponseEntity getEiProducerStatus( // + @PathVariable("eiProducerId") String eiProducerId) { + try { + EiProducer producer = this.eiProducers.getProducer(eiProducerId); + return new ResponseEntity<>(gson.toJson(producerStatusInfo(producer)), HttpStatus.OK); + } catch (Exception e) { + return ErrorResponse.create(e, HttpStatus.NOT_FOUND); + } + } + + private ProducerStatusInfo producerStatusInfo(EiProducer producer) { + ProducerStatusInfo.OperationalState opState = + producer.isAvailable() ? ProducerStatusInfo.OperationalState.ENABLED + : ProducerStatusInfo.OperationalState.DISABLED; + return new ProducerStatusInfo(opState); + } + @PutMapping( path = ProducerConsts.API_ROOT + "/eiproducers/{eiProducerId}", produces = MediaType.APPLICATION_JSON_VALUE) @ApiOperation(value = "Individual EI producer", notes = "") @ApiResponses( value = { // - @ApiResponse(code = 201, message = "Producer created", response = void.class), // - @ApiResponse(code = 200, message = "Producer updated", response = void.class)}// + @ApiResponse(code = 201, message = "Producer created", response = VoidResponse.class), // + @ApiResponse(code = 200, message = "Producer updated", response = VoidResponse.class)}// ) public ResponseEntity putEiProducer( // @PathVariable("eiProducerId") String eiProducerId, // @@ -206,14 +234,14 @@ public class ProducerController { try { EiProducer previousDefinition = this.eiProducers.get(eiProducerId); if (previousDefinition != null) { - for (EiType type : previousDefinition.eiTypes()) { + for (EiType type : previousDefinition.getEiTypes()) { type.removeProducer(previousDefinition); } } registerProducer(eiProducerId, registrationInfo); if (previousDefinition != null) { - purgeTypes(previousDefinition.eiTypes()); + purgeTypes(previousDefinition.getEiTypes()); } return new ResponseEntity<>(previousDefinition == null ? HttpStatus.CREATED : HttpStatus.OK); @@ -225,7 +253,7 @@ public class ProducerController { private void purgeTypes(Collection types) { for (EiType type : types) { if (type.getProducerIds().isEmpty()) { - this.deregisterType(type); + this.eiTypes.deregisterType(type, this.eiJobs); } } } @@ -236,13 +264,13 @@ public class ProducerController { @ApiOperation(value = "Individual EI producer", notes = "") @ApiResponses( value = { // - @ApiResponse(code = 200, message = "Not used", response = void.class), - @ApiResponse(code = 204, message = "Producer deleted", response = void.class), + @ApiResponse(code = 200, message = "Not used", response = VoidResponse.class), + @ApiResponse(code = 204, message = "Producer deleted", response = VoidResponse.class), @ApiResponse(code = 404, message = "Producer is not found", response = ErrorResponse.ErrorInfo.class)}) public ResponseEntity deleteEiProducer(@PathVariable("eiProducerId") String eiProducerId) { try { final EiProducer producer = this.eiProducers.getProducer(eiProducerId); - deregisterProducer(producer); + this.eiProducers.deregisterProducer(producer, this.eiTypes, this.eiJobs); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } catch (Exception e) { return ErrorResponse.create(e, HttpStatus.NOT_FOUND); @@ -256,16 +284,11 @@ public class ProducerController { this.eiTypes.put(type); } return type; - } EiProducer createProducer(Collection types, String producerId, ProducerRegistrationInfo registrationInfo) { - return ImmutableEiProducer.builder() // - .id(producerId) // - .eiTypes(types) // - .jobCreationCallbackUrl(registrationInfo.jobCreationCallbackUrl) // - .jobDeletionCallbackUrl(registrationInfo.jobDeletionCallbackUrl) // - .build(); + return new EiProducer(producerId, types, registrationInfo.jobCreationCallbackUrl, + registrationInfo.jobDeletionCallbackUrl, registrationInfo.producerSupervisionCallbackUrl); } private EiProducer registerProducer(String producerId, ProducerRegistrationInfo registrationInfo) { @@ -279,43 +302,20 @@ public class ProducerController { for (EiType type : types) { for (EiJob job : this.eiJobs.getJobsForType(type)) { this.producerCallbacks.notifyProducerJobStarted(producer, job) // - .subscribe(// - response -> logger.debug("Producer notified OK"), // - throwable -> logger.warn("Producer rejected job {}", throwable.getMessage()) // - ); + .subscribe(); } type.addProducer(producer); } return producer; } - private void deregisterType(EiType type) { - this.eiTypes.remove(type); - for (EiJob job : this.eiJobs.getJobsForType(type.getId())) { - this.eiJobs.remove(job); - this.logger.warn("Deleted job {} because no producers left", job.id()); - } - } - - private void deregisterProducer(EiProducer producer) { - this.eiProducers.remove(producer); - for (EiType type : producer.eiTypes()) { - boolean removed = type.removeProducer(producer) != null; - if (!removed) { - this.logger.error("Bug, no producer found"); - } - if (type.getProducerIds().isEmpty()) { - deregisterType(type); - } - } - } - ProducerRegistrationInfo toEiProducerRegistrationInfo(EiProducer p) { Collection types = new ArrayList<>(); - for (EiType type : p.eiTypes()) { + for (EiType type : p.getEiTypes()) { types.add(toEiTypeRegistrationInfo(type)); } - return new ProducerRegistrationInfo(types, p.jobCreationCallbackUrl(), p.jobDeletionCallbackUrl()); + return new ProducerRegistrationInfo(types, p.getJobCreationCallbackUrl(), p.getJobDeletionCallbackUrl(), + p.getProducerSupervisionCallbackUrl()); } private ProducerEiTypeRegistrationInfo toEiTypeRegistrationInfo(EiType type) { diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerRegistrationInfo.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerRegistrationInfo.java index 1ef2ed4c..19bb541e 100644 --- a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerRegistrationInfo.java +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerRegistrationInfo.java @@ -72,11 +72,17 @@ public class ProducerRegistrationInfo { @JsonProperty(value = "ei_job_deletion_callback_url", required = true) public String jobDeletionCallbackUrl; + @ApiModelProperty(value = "callback for producer supervision", required = true) + @SerializedName("ei_producer_supervision_callback_url") + @JsonProperty(value = "ei_producer_supervision_callback_url", required = true) + public String producerSupervisionCallbackUrl; + public ProducerRegistrationInfo(Collection types, String jobCreationCallbackUrl, - String jobDeletionCallbackUrl) { + String jobDeletionCallbackUrl, String producerSupervisionCallbackUrl) { this.types = types; this.jobCreationCallbackUrl = jobCreationCallbackUrl; this.jobDeletionCallbackUrl = jobDeletionCallbackUrl; + this.producerSupervisionCallbackUrl = producerSupervisionCallbackUrl; } public ProducerRegistrationInfo() { diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerStatusInfo.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerStatusInfo.java new file mode 100644 index 00000000..8f2e0d0e --- /dev/null +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/controllers/producer/ProducerStatusInfo.java @@ -0,0 +1,54 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2019 Nordix Foundation + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ + +package org.oransc.enrichment.controllers.producer; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import org.immutables.gson.Gson; + +@Gson.TypeAdapters +@ApiModel(value = "producer_status", description = "Status for an EI Producer") +public class ProducerStatusInfo { + + @Gson.TypeAdapters + @ApiModel(value = "producer_operational_state", description = "Represents the operational states") + public enum OperationalState { + ENABLED, DISABLED + } + + private static final String OPERATIONAL_STATE_DESCRIPTION = "Operational state, values:\n" // + + "ENABLED: TBD\n" // + + "DISABLED: TBD."; + + @ApiModelProperty(value = OPERATIONAL_STATE_DESCRIPTION, name = "operational_state", required = true) + @SerializedName("operational_state") + @JsonProperty(value = "operational_state", required = true) + public final OperationalState opState; + + public ProducerStatusInfo(OperationalState state) { + this.opState = state; + } + +} diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiProducer.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiProducer.java index 30e62de3..99932a72 100644 --- a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiProducer.java +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiProducer.java @@ -22,18 +22,49 @@ package org.oransc.enrichment.repository; import java.util.Collection; -import org.immutables.gson.Gson; -import org.immutables.value.Value; +import lombok.Getter; -@Value.Immutable -@Gson.TypeAdapters -public interface EiProducer { - public String id(); +public class EiProducer { + @Getter + private final String id; - public Collection eiTypes(); + @Getter + private final Collection eiTypes; - public String jobCreationCallbackUrl(); + @Getter + private final String jobCreationCallbackUrl; - public String jobDeletionCallbackUrl(); + @Getter + private final String jobDeletionCallbackUrl; + + @Getter + private final String producerSupervisionCallbackUrl; + + private int unresponsiveCounter = 0; + + public EiProducer(String id, Collection eiTypes, String jobCreationCallbackUrl, + String jobDeletionCallbackUrl, String producerSupervisionCallbackUrl) { + this.id = id; + this.eiTypes = eiTypes; + this.jobCreationCallbackUrl = jobCreationCallbackUrl; + this.jobDeletionCallbackUrl = jobDeletionCallbackUrl; + this.producerSupervisionCallbackUrl = producerSupervisionCallbackUrl; + } + + public synchronized void setAliveStatus(boolean isAlive) { + if (isAlive) { + unresponsiveCounter = 0; + } else { + unresponsiveCounter++; + } + } + + public synchronized boolean isDead() { + return this.unresponsiveCounter >= 3; + } + + public synchronized boolean isAvailable() { + return this.unresponsiveCounter == 0; + } } diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiProducers.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiProducers.java index 483850d9..b3cd8956 100644 --- a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiProducers.java +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiProducers.java @@ -20,21 +20,26 @@ package org.oransc.enrichment.repository; +import java.lang.invoke.MethodHandles; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Vector; import org.oransc.enrichment.exceptions.ServiceException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** - * Dynamic representation of all Rics in the system. + * Dynamic representation of all EiProducers. */ +@SuppressWarnings("squid:S2629") // Invoke method(s) only conditionally public class EiProducers { + private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private Map allEiProducers = new HashMap<>(); public synchronized void put(EiProducer producer) { - allEiProducers.put(producer.id(), producer); + allEiProducers.put(producer.getId(), producer); } @@ -58,10 +63,6 @@ public class EiProducers { this.allEiProducers.remove(id); } - public synchronized void remove(EiProducer producer) { - this.allEiProducers.remove(producer.id()); - } - public synchronized int size() { return allEiProducers.size(); } @@ -69,4 +70,22 @@ public class EiProducers { public synchronized void clear() { this.allEiProducers.clear(); } + + public void deregisterProducer(EiProducer producer, EiTypes eiTypes, EiJobs eiJobs) { + this.remove(producer); + for (EiType type : producer.getEiTypes()) { + boolean removed = type.removeProducer(producer) != null; + if (!removed) { + this.logger.error("Bug, no producer found"); + } + if (type.getProducerIds().isEmpty()) { + eiTypes.deregisterType(type, eiJobs); + } + } + } + + private synchronized void remove(EiProducer producer) { + this.allEiProducers.remove(producer.getId()); + } + } diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiType.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiType.java index 803fcba7..a354198a 100644 --- a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiType.java +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiType.java @@ -50,10 +50,10 @@ public class EiType { } public synchronized void addProducer(EiProducer producer) { - this.producers.put(producer.id(), producer); + this.producers.put(producer.getId(), producer); } public synchronized EiProducer removeProducer(EiProducer producer) { - return this.producers.remove(producer.id()); + return this.producers.remove(producer.getId()); } } diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiTypes.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiTypes.java index 5454e8a7..9b7b6405 100644 --- a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiTypes.java +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/repository/EiTypes.java @@ -20,18 +20,23 @@ package org.oransc.enrichment.repository; +import java.lang.invoke.MethodHandles; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Vector; import org.oransc.enrichment.exceptions.ServiceException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Dynamic representation of all EI Types in the system. */ +@SuppressWarnings("squid:S2629") // Invoke method(s) only conditionally public class EiTypes { - Map allEiTypes = new HashMap<>(); + private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private final Map allEiTypes = new HashMap<>(); public synchronized void put(EiType type) { allEiTypes.put(type.getId(), type); @@ -69,4 +74,12 @@ public class EiTypes { this.allEiTypes.clear(); } + public void deregisterType(EiType type, EiJobs eiJobs) { + this.remove(type); + for (EiJob job : eiJobs.getJobsForType(type.getId())) { + eiJobs.remove(job); + this.logger.warn("Deleted job {} because no producers left", job.id()); + } + } + } diff --git a/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/tasks/ProducerSupervision.java b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/tasks/ProducerSupervision.java new file mode 100644 index 00000000..3b62fa74 --- /dev/null +++ b/enrichment-coordinator-service/src/main/java/org/oransc/enrichment/tasks/ProducerSupervision.java @@ -0,0 +1,98 @@ +/*- + * ========================LICENSE_START================================= + * ONAP : ccsdk oran + * ====================================================================== + * Copyright (C) 2020 Nordix Foundation. 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.oransc.enrichment.tasks; + +import org.oransc.enrichment.clients.AsyncRestClient; +import org.oransc.enrichment.configuration.ApplicationConfig; +import org.oransc.enrichment.repository.EiJobs; +import org.oransc.enrichment.repository.EiProducer; +import org.oransc.enrichment.repository.EiProducers; +import org.oransc.enrichment.repository.EiTypes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * Regularly checks the availability of the EI Producers + */ +@Component +@EnableScheduling +@SuppressWarnings("squid:S2629") // Invoke method(s) only conditionally +public class ProducerSupervision { + private static final Logger logger = LoggerFactory.getLogger(ProducerSupervision.class); + + @Autowired + ApplicationConfig applicationConfig; + + @Autowired + EiProducers eiProducers; + + @Autowired + EiJobs eiJobs; + + @Autowired + EiTypes eiTypes; + + @Scheduled(fixedRate = 1000 * 60 * 5) + public void checkAllProducers() { + logger.debug("Checking producers starting"); + createTask().subscribe(null, null, () -> logger.debug("Checking all Producers completed")); + } + + public Flux createTask() { + return Flux.fromIterable(eiProducers.getAllProducers()) // + .flatMap(this::checkOneProducer); + } + + private Mono checkOneProducer(EiProducer producer) { + return restClient().get(producer.getProducerSupervisionCallbackUrl()) // + .onErrorResume(throwable -> { + handleNonRespondingProducer(throwable, producer); + return Mono.empty(); + })// + .doOnNext(response -> handleRespondingProducer(response, producer)) + .flatMap(response -> Mono.just(producer)); + } + + private void handleNonRespondingProducer(Throwable throwable, EiProducer producer) { + logger.warn("Unresponsive producer: {} exception: {}", producer.getId(), throwable.getMessage()); + producer.setAliveStatus(false); + if (producer.isDead()) { + this.eiProducers.deregisterProducer(producer, this.eiTypes, this.eiJobs); + } + } + + private void handleRespondingProducer(String response, EiProducer producer) { + logger.debug("{}", response); + producer.setAliveStatus(true); + } + + private AsyncRestClient restClient() { + return new AsyncRestClient("", this.applicationConfig.getWebClientConfig()); + } + +} diff --git a/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/ApplicationTest.java b/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/ApplicationTest.java index 9a731e6c..9cfe222f 100644 --- a/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/ApplicationTest.java +++ b/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/ApplicationTest.java @@ -49,12 +49,14 @@ import org.oransc.enrichment.controllers.consumer.ConsumerEiTypeInfo; import org.oransc.enrichment.controllers.producer.ProducerConsts; import org.oransc.enrichment.controllers.producer.ProducerRegistrationInfo; import org.oransc.enrichment.controllers.producer.ProducerRegistrationInfo.ProducerEiTypeRegistrationInfo; +import org.oransc.enrichment.controllers.producer.ProducerStatusInfo; import org.oransc.enrichment.exceptions.ServiceException; import org.oransc.enrichment.repository.EiJob; import org.oransc.enrichment.repository.EiJobs; import org.oransc.enrichment.repository.EiProducers; import org.oransc.enrichment.repository.EiType; import org.oransc.enrichment.repository.EiTypes; +import org.oransc.enrichment.tasks.ProducerSupervision; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; @@ -103,6 +105,9 @@ class ApplicationTest { @Autowired ProducerSimulatorController producerSimulator; + @Autowired + ProducerSupervision producerSupervision; + private static Gson gson = new GsonBuilder() // .serializeNulls() // .create(); // @@ -323,7 +328,7 @@ class ApplicationTest { EiType type = this.eiTypes.getType(EI_TYPE_ID); assertThat(type.getProducerIds()).contains("eiProducerId"); assertThat(this.eiProducers.size()).isEqualTo(1); - assertThat(this.eiProducers.get("eiProducerId").eiTypes().iterator().next().getId()).isEqualTo(EI_TYPE_ID); + assertThat(this.eiProducers.get("eiProducerId").getEiTypes().iterator().next().getId()).isEqualTo(EI_TYPE_ID); resp = restClient().putForEntity(url, body).block(); assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK); @@ -426,6 +431,33 @@ class ApplicationTest { assertThat(resp.getBody()).contains(EI_PRODUCER_ID); } + private void assertProducerOpState(String producerId, + ProducerStatusInfo.OperationalState expectedOperationalState) { + String statusUrl = ProducerConsts.API_ROOT + "/eiproducers/" + producerId + "/status"; + ResponseEntity resp = restClient().getForEntity(statusUrl).block(); + ProducerStatusInfo statusInfo = gson.fromJson(resp.getBody(), ProducerStatusInfo.class); + assertThat(statusInfo.opState).isEqualTo(expectedOperationalState); + } + + @Test + void testProducerSupervision() throws JsonMappingException, JsonProcessingException, ServiceException { + putEiProducerWithOneTypeRejecting("simulateProducerError", EI_TYPE_ID); + + assertThat(this.eiProducers.size()).isEqualTo(1); + assertThat(this.eiTypes.size()).isEqualTo(1); + assertProducerOpState("simulateProducerError", ProducerStatusInfo.OperationalState.ENABLED); + + this.producerSupervision.createTask().blockLast(); + this.producerSupervision.createTask().blockLast(); + assertThat(this.eiProducers.size()).isEqualTo(1); + assertProducerOpState("simulateProducerError", ProducerStatusInfo.OperationalState.DISABLED); + + // After 3 failed checks, the producer shall be deregisterred + this.producerSupervision.createTask().blockLast(); + assertThat(this.eiProducers.size()).isEqualTo(0); + assertThat(this.eiTypes.size()).isEqualTo(0); + } + ProducerEiTypeRegistrationInfo producerEiTypeRegistrationInfo(String typeId) throws JsonMappingException, JsonProcessingException { return new ProducerEiTypeRegistrationInfo(jsonSchemaObject(), typeId); @@ -435,16 +467,20 @@ class ApplicationTest { throws JsonMappingException, JsonProcessingException { Collection types = new ArrayList<>(); types.add(producerEiTypeRegistrationInfo(typeId)); - return new ProducerRegistrationInfo(types, baseUrl() + ProducerSimulatorController.JOB_CREATED_ERROR_URL, - baseUrl() + ProducerSimulatorController.JOB_DELETED_ERROR_URL); + return new ProducerRegistrationInfo(types, // + baseUrl() + ProducerSimulatorController.JOB_CREATED_ERROR_URL, + baseUrl() + ProducerSimulatorController.JOB_DELETED_ERROR_URL, + baseUrl() + ProducerSimulatorController.SUPERVISION_ERROR_URL); } ProducerRegistrationInfo producerEiRegistratioInfo(String typeId) throws JsonMappingException, JsonProcessingException { Collection types = new ArrayList<>(); types.add(producerEiTypeRegistrationInfo(typeId)); - return new ProducerRegistrationInfo(types, baseUrl() + ProducerSimulatorController.JOB_CREATED_URL, - baseUrl() + ProducerSimulatorController.JOB_DELETED_URL); + return new ProducerRegistrationInfo(types, // + baseUrl() + ProducerSimulatorController.JOB_CREATED_URL, + baseUrl() + ProducerSimulatorController.JOB_DELETED_URL, + baseUrl() + ProducerSimulatorController.SUPERVISION_URL); } ConsumerEiJobInfo eiJobInfo() throws JsonMappingException, JsonProcessingException { diff --git a/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/controller/ProducerSimulatorController.java b/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/controller/ProducerSimulatorController.java index c44a9ee7..479c1b2d 100644 --- a/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/controller/ProducerSimulatorController.java +++ b/enrichment-coordinator-service/src/test/java/org/oransc/enrichment/controller/ProducerSimulatorController.java @@ -34,11 +34,13 @@ import lombok.Getter; import org.oransc.enrichment.clients.ProducerJobInfo; import org.oransc.enrichment.controllers.ErrorResponse; +import org.oransc.enrichment.controllers.VoidResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @@ -54,6 +56,9 @@ public class ProducerSimulatorController { public static final String JOB_CREATED_ERROR_URL = "/producer_simulator/job_created_error"; public static final String JOB_DELETED_ERROR_URL = "/producer_simulator/job_deleted_error"; + public static final String SUPERVISION_URL = "/producer_simulator/supervision"; + public static final String SUPERVISION_ERROR_URL = "/producer_simulator/supervision_error"; + public static class TestResults { public List jobsStarted = Collections.synchronizedList(new ArrayList()); @@ -81,7 +86,7 @@ public class ProducerSimulatorController { @ApiOperation(value = "Callback for EI job creation", notes = "") @ApiResponses( value = { // - @ApiResponse(code = 200, message = "OK", response = void.class)}// + @ApiResponse(code = 200, message = "OK", response = VoidResponse.class)}// ) public ResponseEntity jobCreatedCallback( // @RequestBody ProducerJobInfo request) { @@ -102,7 +107,7 @@ public class ProducerSimulatorController { @ApiOperation(value = "Callback for EI job deletion", notes = "") @ApiResponses( value = { // - @ApiResponse(code = 200, message = "OK", response = void.class)}// + @ApiResponse(code = 200, message = "OK", response = VoidResponse.class)}// ) public ResponseEntity jobDeletedCallback( // @RequestBody ProducerJobInfo request) { @@ -119,7 +124,7 @@ public class ProducerSimulatorController { @ApiOperation(value = "Callback for EI job creation, returns error", notes = "") @ApiResponses( value = { // - @ApiResponse(code = 200, message = "OK", response = void.class)}// + @ApiResponse(code = 200, message = "OK", response = VoidResponse.class)}// ) public ResponseEntity jobCreatedCallbackReturnError( // @RequestBody ProducerJobInfo request) { @@ -132,7 +137,7 @@ public class ProducerSimulatorController { @ApiOperation(value = "Callback for EI job creation, returns error", notes = "") @ApiResponses( value = { // - @ApiResponse(code = 200, message = "OK", response = void.class)}// + @ApiResponse(code = 200, message = "OK", response = VoidResponse.class)}// ) public ResponseEntity jobDeletedCallbackReturnError( // @RequestBody ProducerJobInfo request) { @@ -141,4 +146,26 @@ public class ProducerSimulatorController { return ErrorResponse.create("Producer returns error on delete job", HttpStatus.NOT_FOUND); } + @GetMapping(path = SUPERVISION_URL, produces = MediaType.APPLICATION_JSON_VALUE) + @ApiOperation(value = "Producer supervision", notes = "") + @ApiResponses( + value = { // + @ApiResponse(code = 200, message = "OK", response = String.class)}// + ) + public ResponseEntity producerSupervision() { + logger.info("Producer supervision"); + return new ResponseEntity<>(HttpStatus.OK); + } + + @GetMapping(path = SUPERVISION_ERROR_URL, produces = MediaType.APPLICATION_JSON_VALUE) + @ApiOperation(value = "Producer supervision error", notes = "") + @ApiResponses( + value = { // + @ApiResponse(code = 200, message = "OK", response = String.class)}// + ) + public ResponseEntity producerSupervisionError() { + logger.info("Producer supervision error"); + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } -- 2.16.6