Skip to content

Commit

Permalink
P4ADEV-508 configuring mongo (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
antonioT90 authored Jun 24, 2024
1 parent 8ee12c8 commit 50bd17e
Show file tree
Hide file tree
Showing 13 changed files with 253 additions and 6 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-data-redis")
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:$springDocOpenApiVersion")
implementation("org.codehaus.janino:janino:$janinoVersion")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
Expand Down
5 changes: 5 additions & 0 deletions gradle.lockfile
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ org.apache.tomcat.embed:tomcat-embed-websocket:10.1.20=compileClasspath
org.bouncycastle:bcprov-jdk18on:1.78.1=compileClasspath
org.codehaus.janino:commons-compiler:3.1.12=compileClasspath
org.codehaus.janino:janino:3.1.12=compileClasspath
org.mongodb:bson:4.11.2=compileClasspath
org.mongodb:mongodb-driver-core:4.11.2=compileClasspath
org.mongodb:mongodb-driver-sync:4.11.2=compileClasspath
org.openapitools:jackson-databind-nullable:0.2.6=compileClasspath
org.projectlombok:lombok:1.18.32=compileClasspath
org.reactivestreams:reactive-streams:1.0.4=compileClasspath
Expand All @@ -58,6 +61,7 @@ org.springframework.boot:spring-boot-actuator-autoconfigure:3.2.5=compileClasspa
org.springframework.boot:spring-boot-actuator:3.2.5=compileClasspath
org.springframework.boot:spring-boot-autoconfigure:3.2.5=compileClasspath
org.springframework.boot:spring-boot-starter-actuator:3.2.5=compileClasspath
org.springframework.boot:spring-boot-starter-data-mongodb:3.2.5=compileClasspath
org.springframework.boot:spring-boot-starter-data-redis:3.2.5=compileClasspath
org.springframework.boot:spring-boot-starter-json:3.2.5=compileClasspath
org.springframework.boot:spring-boot-starter-logging:3.2.5=compileClasspath
Expand All @@ -67,6 +71,7 @@ org.springframework.boot:spring-boot-starter:3.2.5=compileClasspath
org.springframework.boot:spring-boot:3.2.5=compileClasspath
org.springframework.data:spring-data-commons:3.2.5=compileClasspath
org.springframework.data:spring-data-keyvalue:3.2.5=compileClasspath
org.springframework.data:spring-data-mongodb:4.2.5=compileClasspath
org.springframework.data:spring-data-redis:3.2.5=compileClasspath
org.springframework:spring-aop:6.1.6=compileClasspath
org.springframework:spring-beans:6.1.6=compileClasspath
Expand Down
1 change: 1 addition & 0 deletions helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ microservice-chart:

REDIS_HOST: cache-standalone-host
REDIS_PASSWORD: cache-password
MONGODB_URI: mongodb-connection-string

# nodeSelector: {}

Expand Down
4 changes: 4 additions & 0 deletions openapi/p4pa-auth.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ paths:
description: Invalid request
'401':
description: Authentication failed
'429':
description: Too Many Requests
'500':
description: Server ERROR
content:
Expand All @@ -88,6 +90,8 @@ paths:
$ref: '#/components/schemas/UserInfo'
'401':
description: Unauthorized
'429':
description: Too Many Requests
'500':
description: Server ERROR
content:
Expand Down
46 changes: 46 additions & 0 deletions src/main/java/it/gov/pagopa/payhub/auth/config/MongoConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package it.gov.pagopa.payhub.auth.config;

import lombok.Setter;
import org.springframework.boot.autoconfigure.mongo.MongoClientSettingsBuilderCustomizer;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

import java.util.concurrent.TimeUnit;

@Configuration
@EnableMongoRepositories(basePackages = "it.gov.pagopa.payhub.auth.repository")
public class MongoConfig {

@Configuration
@ConfigurationProperties(prefix = "spring.data.mongodb.config")
@Setter
public static class MongoDbCustomProperties {
private ConnectionPoolSettings connectionPool;

static class ConnectionPoolSettings {
int maxSize;
int minSize;
long maxWaitTimeMS;
long maxConnectionLifeTimeMS;
long maxConnectionIdleTimeMS;
int maxConnecting;
}

}

@Bean
public MongoClientSettingsBuilderCustomizer customizer(MongoDbCustomProperties mongoDbCustomProperties) {
return builder -> builder.applyToConnectionPoolSettings(
connectionPool -> {
connectionPool.maxSize(mongoDbCustomProperties.connectionPool.maxSize);
connectionPool.minSize(mongoDbCustomProperties.connectionPool.minSize);
connectionPool.maxWaitTime(mongoDbCustomProperties.connectionPool.maxWaitTimeMS, TimeUnit.MILLISECONDS);
connectionPool.maxConnectionLifeTime(mongoDbCustomProperties.connectionPool.maxConnectionLifeTimeMS, TimeUnit.MILLISECONDS);
connectionPool.maxConnectionIdleTime(mongoDbCustomProperties.connectionPool.maxConnectionIdleTimeMS, TimeUnit.MILLISECONDS);
connectionPool.maxConnecting(mongoDbCustomProperties.connectionPool.maxConnecting);
});
}
}

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package it.gov.pagopa.payhub.auth.configuration;
package it.gov.pagopa.payhub.auth.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import it.gov.pagopa.payhub.model.generated.UserInfo;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package it.gov.pagopa.payhub.auth.configuration;
package it.gov.pagopa.payhub.auth.config;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public AuthErrorDTO handleMissingServletRequestParameterException(
return new AuthErrorDTO(AuthErrorDTO.ErrorEnum.INVALID_REQUEST, message);
}

private static ResponseEntity<AuthErrorDTO> handleAuthErrorException(RuntimeException ex, HttpServletRequest request, HttpStatus httpStatus, AuthErrorDTO.ErrorEnum errorEnum) {
static ResponseEntity<AuthErrorDTO> handleAuthErrorException(RuntimeException ex, HttpServletRequest request, HttpStatus httpStatus, AuthErrorDTO.ErrorEnum errorEnum) {
String message = ex.getMessage();
log.info("A {} occurred handling request {}: HttpStatus {} - {}",
ex.getClass(),
Expand All @@ -70,7 +70,7 @@ private static ResponseEntity<AuthErrorDTO> handleAuthErrorException(RuntimeExce
.body(new AuthErrorDTO(errorEnum, message));
}

private static String getRequestDetails(HttpServletRequest request) {
static String getRequestDetails(HttpServletRequest request) {
return "%s %s".formatted(request.getMethod(), request.getRequestURI());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package it.gov.pagopa.payhub.auth.exception;

import it.gov.pagopa.payhub.model.generated.AuthErrorDTO;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

@RestControllerAdvice
@Slf4j
public class MongoTooManyRequestsExceptionHandler {
private static final Pattern RETRY_AFTER_MS_PATTERN = Pattern.compile("RetryAfterMs=(\\d+)");

@ExceptionHandler(DataAccessException.class)
protected ResponseEntity<AuthErrorDTO> handleDataAccessException(
DataAccessException ex, HttpServletRequest request) {

if (isRequestRateTooLargeException(ex)) {
Long retryAfterMs = getRetryAfterMs(ex);
return handleRequestRateTooLargeException(ex, request, retryAfterMs);
} else {
return AuthExceptionHandler.handleAuthErrorException(ex, request, HttpStatus.INTERNAL_SERVER_ERROR, AuthErrorDTO.ErrorEnum.AUTH_GENERIC_ERROR);
}
}

private ResponseEntity<AuthErrorDTO> handleRequestRateTooLargeException(Exception ex, HttpServletRequest request, Long retryAfterMs) {
String message = ex.getMessage();

log.info(
"A MongoQueryException (RequestRateTooLarge) occurred handling request {}: HttpStatus 429 - {}",
AuthExceptionHandler.getRequestDetails(request), message);

final ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON);

if (retryAfterMs != null) {
long retryAfter = (long) Math.ceil((double) retryAfterMs / 1000);
responseBuilder.header(HttpHeaders.RETRY_AFTER, String.valueOf(retryAfter))
.header("Retry-After-Ms", String.valueOf(retryAfterMs));
}

return responseBuilder.build();
}

public static Long getRetryAfterMs(DataAccessException ex) {
Matcher matcher = RETRY_AFTER_MS_PATTERN.matcher(ex.getMessage());
if (matcher.find()) {
return Long.parseLong(matcher.group(1));
}
return null;
}

public static boolean isRequestRateTooLargeException(DataAccessException ex) {
return ex.getMessage().contains("TooManyRequests") || ex.getMessage().contains("Error=16500,");
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package it.gov.pagopa.payhub.auth.service;

import it.gov.pagopa.payhub.auth.configuration.RedisConfig;
import it.gov.pagopa.payhub.auth.config.RedisConfig;
import it.gov.pagopa.payhub.model.generated.UserInfo;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
Expand Down
12 changes: 12 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ spring:
port: "\${REDIS_PORT:6380}"
password: "\${REDIS_PASSWORD:}"
ssl.enabled: "\${REDIS_SSL_ENABLED:true}"
mongodb:
uri: "\${MONGODB_URI:mongodb://localhost:27017}"
database: "\${MONGODB_DBNAME:payhub}"
# custom configured properties
config:
connectionPool:
maxSize: "\${MONGODB_CONNECTIONPOOL_MAX_SIZE:100}"
minSize: "\${MONGODB_CONNECTIONPOOL_MIN_SIZE:0}"
maxWaitTimeMS: "\${MONGODB_CONNECTIONPOOL_MAX_WAIT_MS:120000}"
maxConnectionLifeTimeMS: "\${MONGODB_CONNECTIONPOOL_MAX_CONNECTION_LIFE_MS:0}"
maxConnectionIdleTimeMS: "\${MONGODB_CONNECTIONPOOL_MAX_CONNECTION_IDLE_MS:120000}"
maxConnecting: "\${MONGODB_CONNECTIONPOOL_MAX_CONNECTING:2}"


jwt:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package it.gov.pagopa.payhub.auth.configuration;
package it.gov.pagopa.payhub.auth.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Assertions;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package it.gov.pagopa.payhub.auth.exception;

import com.mongodb.MongoQueryException;
import com.mongodb.MongoWriteException;
import com.mongodb.ServerAddress;
import com.mongodb.WriteError;
import org.bson.BsonDocument;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.mongodb.UncategorizedMongoDbException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import static org.mockito.Mockito.doThrow;

@WebMvcTest(value = {
MongoTooManyRequestsExceptionHandler.class,
AuthExceptionHandlerTest.TestController.class})
@ContextConfiguration(classes = {MongoTooManyRequestsExceptionHandler.class,
AuthExceptionHandlerTest.TestController.class})
class MongoTooManyRequestsExceptionHandlerTest {
@Autowired
private MockMvc mockMvc;

@SpyBean
private AuthExceptionHandlerTest.TestController testControllerSpy;

@Test
void handleUncategorizedMongoDbException() throws Exception {

String mongoFullErrorResponse = """
{"ok": 0.0, "errmsg": "Error=16500, RetryAfterMs=34,\s
Details='Response status code does not indicate success: TooManyRequests (429) Substatus: 3200 ActivityId: 46ba3855-bc3b-4670-8609-17e1c2c87778 Reason:\s
(\\r\\nErrors : [\\r\\n \\"Request rate is large. More Request Units may be needed, so no changes were made. Please retry this request later. Learn more:
http://aka.ms/cosmosdb-error-429\\"\\r\\n]\\r\\n) ", "code": 16500, "codeName": "RequestRateTooLarge"}
""";

final MongoQueryException mongoQueryException = new MongoQueryException(
BsonDocument.parse(mongoFullErrorResponse), new ServerAddress());
doThrow(
new UncategorizedMongoDbException(mongoQueryException.getMessage(), mongoQueryException))
.when(testControllerSpy).testEndpoint("DATA");

mockMvc.perform(MockMvcRequestBuilders.get("/test")
.param(AuthExceptionHandlerTest.DATA, "DATA")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isTooManyRequests())
.andExpect(MockMvcResultMatchers.header().exists(HttpHeaders.RETRY_AFTER))
.andExpect(MockMvcResultMatchers.header().string(HttpHeaders.RETRY_AFTER, "1"))
.andExpect(MockMvcResultMatchers.header().string("Retry-After-Ms", "34"));
}

@Test
void handleWriteDbWithoutTooManyRequestsException() throws Exception {

String writeErrorMessage = """
Error=16500, Substatus: 3200; ActivityId: 822d212d-5aac-4f5d-a2d4-76d6da7b619e; Reason: (
Errors : [
"Request rate is large. More Request Units may be needed, so no changes were made. Please retry this request later. Learn more: http://aka.ms/cosmosdb-error-429"
]
);
""";

handleMongoWriteException(writeErrorMessage);
}

@Test
void handleTooManyRequestsWriteDbException() throws Exception {

String writeErrorMessage = """
RetryAfterMs=34, Details='Response status code does not indicate success: TooManyRequests (429); Substatus: 3200; ActivityId: 822d212d-5aac-4f5d-a2d4-76d6da7b619e; Reason: (
Errors : [
"Request rate is large. More Request Units may be needed, so no changes were made. Please retry this request later. Learn more: http://aka.ms/cosmosdb-error-429"
]
);
""";

handleMongoWriteException(writeErrorMessage);
}

@Test
void handleUncategorizedMongoDbExceptionNotRequestRateTooLarge() throws Exception {

doThrow(new UncategorizedMongoDbException("DUMMY", new Exception()))
.when(testControllerSpy).testEndpoint("DATA");

mockMvc.perform(MockMvcRequestBuilders.get("/test")
.param(AuthExceptionHandlerTest.DATA, "DATA")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isInternalServerError())
.andExpect(MockMvcResultMatchers.content().json("{\"error_description\":\"DUMMY\"}", false));
}


private void handleMongoWriteException(String writeErrorMessage) throws Exception {
final MongoWriteException mongoWriteException = new MongoWriteException(
new WriteError(16500, writeErrorMessage, BsonDocument.parse("{}")), new ServerAddress());
doThrow(
new DataIntegrityViolationException(mongoWriteException.getMessage(), mongoWriteException))
.when(testControllerSpy).testEndpoint("DATA");

mockMvc.perform(MockMvcRequestBuilders.get("/test")
.param(AuthExceptionHandlerTest.DATA, "DATA")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isTooManyRequests());
}
}

0 comments on commit 50bd17e

Please sign in to comment.