From 9be188dd7a5d0507a94e2ce321d3f3390d195a4e Mon Sep 17 00:00:00 2001 From: Bruno Sakoto Date: Wed, 16 Jun 2021 11:47:54 -0400 Subject: Support concurrent requests to create schema sets Issue-ID: CPS-466 Signed-off-by: Bruno Sakoto Change-Id: I2ecf98b9aa5a6097518e616c08f8bb2a2182a613 --- .../spi/impl/CpsModulePersistenceServiceImpl.java | 95 +++++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) (limited to 'cps-ri/src/main') diff --git a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceImpl.java index ae6543dab..eae1ef142 100755 --- a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceImpl.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceImpl.java @@ -1,7 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2020 Nordix Foundation - * Modifications Copyright (C) 2020 Bell Canada. All rights reserved. + * Modifications Copyright (C) 2020-2021 Bell Canada. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,10 +25,16 @@ import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.transaction.Transactional; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang3.StringUtils; +import org.hibernate.exception.ConstraintViolationException; import org.onap.cps.spi.CascadeDeleteAllowed; import org.onap.cps.spi.CpsAdminPersistenceService; import org.onap.cps.spi.CpsModulePersistenceService; @@ -36,6 +42,7 @@ import org.onap.cps.spi.entities.AnchorEntity; import org.onap.cps.spi.entities.SchemaSetEntity; import org.onap.cps.spi.entities.YangResourceEntity; import org.onap.cps.spi.exceptions.AlreadyDefinedException; +import org.onap.cps.spi.exceptions.DuplicatedYangResourceException; import org.onap.cps.spi.exceptions.SchemaSetInUseException; import org.onap.cps.spi.repository.AnchorRepository; import org.onap.cps.spi.repository.DataspaceRepository; @@ -44,12 +51,18 @@ import org.onap.cps.spi.repository.SchemaSetRepository; import org.onap.cps.spi.repository.YangResourceRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Component; @Component +@Slf4j public class CpsModulePersistenceServiceImpl implements CpsModulePersistenceService { + private static final String YANG_RESOURCE_CHECKSUM_CONSTRAINT_NAME = "yang_resource_checksum_key"; + private static final Pattern CHECKSUM_EXCEPTION_PATTERN = Pattern.compile(".*\\(checksum\\)=\\((\\w+)\\).*"); + @Autowired private YangResourceRepository yangResourceRepository; @@ -70,6 +83,9 @@ public class CpsModulePersistenceServiceImpl implements CpsModulePersistenceServ @Override @Transactional + // A retry is made to store the schema set if it fails because of duplicated yang resource exception that + // can occur in case of specific concurrent requests. + @Retryable(value = DuplicatedYangResourceException.class, maxAttempts = 2, backoff = @Backoff(delay = 500)) public void storeSchemaSet(final String dataspaceName, final String schemaSetName, final Map yangResourcesNameToContentMap) { @@ -107,7 +123,21 @@ public class CpsModulePersistenceServiceImpl implements CpsModulePersistenceServ final Collection newYangResourceEntities = checksumToEntityMap.values(); if (!newYangResourceEntities.isEmpty()) { - yangResourceRepository.saveAll(newYangResourceEntities); + try { + yangResourceRepository.saveAll(newYangResourceEntities); + } catch (final DataIntegrityViolationException dataIntegrityViolationException) { + // Throw a CPS duplicated Yang resource exception if the cause of the error is a yang checksum + // database constraint violation. + // If it is not, then throw the original exception + final Optional convertedException = + convertToDuplicatedYangResourceException( + dataIntegrityViolationException, newYangResourceEntities); + convertedException.ifPresent( + e -> log.warn( + "Cannot persist duplicated yang resource. " + + "A total of 2 attempts to store the schema set are planned.", e)); + throw convertedException.isPresent() ? convertedException.get() : dataIntegrityViolationException; + } } return ImmutableSet.builder() @@ -116,6 +146,67 @@ public class CpsModulePersistenceServiceImpl implements CpsModulePersistenceServ .build(); } + /** + * Convert the specified data integrity violation exception into a CPS duplicated Yang resource exception + * if the cause of the error is a yang checksum database constraint violation. + * @param originalException the original db exception. + * @param yangResourceEntities the collection of Yang resources involved in the db failure. + * @return an optional converted CPS duplicated Yang resource exception. The optional is empty if the original + * cause of the error is not a yang checksum database constraint violation. + */ + private Optional convertToDuplicatedYangResourceException( + final DataIntegrityViolationException originalException, + final Collection yangResourceEntities) { + + // The exception result + DuplicatedYangResourceException duplicatedYangResourceException = null; + + final Throwable cause = originalException.getCause(); + if (cause instanceof ConstraintViolationException) { + final ConstraintViolationException constraintException = (ConstraintViolationException) cause; + if (YANG_RESOURCE_CHECKSUM_CONSTRAINT_NAME.equals(constraintException.getConstraintName())) { + // Db constraint related to yang resource checksum uniqueness is not respected + final String checksumInError = getDuplicatedChecksumFromException(constraintException); + final String nameInError = getNameForChecksum(checksumInError, yangResourceEntities); + duplicatedYangResourceException = + new DuplicatedYangResourceException(nameInError, checksumInError, constraintException); + } + } + + return Optional.ofNullable(duplicatedYangResourceException); + + } + + /** + * Get the checksum that caused the constraint violation exception. + * @param exception the exception having the checksum in error. + * @return the checksum in error or null if not found. + */ + private String getDuplicatedChecksumFromException(final ConstraintViolationException exception) { + String checksum = null; + final Matcher matcher = CHECKSUM_EXCEPTION_PATTERN.matcher(exception.getSQLException().getMessage()); + if (matcher.find() && matcher.groupCount() == 1) { + checksum = matcher.group(1); + } + return checksum; + } + + /** + * Get the name of the yang resource having the specified checksum. + * @param checksum the checksum. Null is supported. + * @param yangResourceEntities the list of yang resources to search among. + * @return the name found or null if none. + */ + private String getNameForChecksum( + final String checksum, final Collection yangResourceEntities) { + return + yangResourceEntities.stream() + .filter(entity -> StringUtils.equals(checksum, (entity.getChecksum()))) + .findFirst() + .map(YangResourceEntity::getName) + .orElse(null); + } + @Override public Map getYangSchemaResources(final String dataspaceName, final String schemaSetName) { final var dataspaceEntity = dataspaceRepository.getByName(dataspaceName); -- cgit 1.2.3-korg