diff --git a/adr/017-connection-pooling.md b/adr/017-connection-pooling.md new file mode 100644 index 000000000..2ca6749ac --- /dev/null +++ b/adr/017-connection-pooling.md @@ -0,0 +1,19 @@ +# 17. Connection Pooling + +Date: 2024-02-02 + +## Decision + +We will use HikariCP for connection pooling because of the quality of documentation, and the significant margin in performance above the other available options. It is also still receiving active support and updates from the owners. + +## Status + +Accepted + +## Context +Connection pooling is vital to the project for many reasons including resource management and conservation, performance improvement, latency reduction, scalability, and most importantly here managing connections. + + +### Related Issues + +- 753 diff --git a/app/src/main/java/gov/hhs/cdc/trustedintermediary/external/javalin/App.java b/app/src/main/java/gov/hhs/cdc/trustedintermediary/external/javalin/App.java index ae2952487..9980ca764 100644 --- a/app/src/main/java/gov/hhs/cdc/trustedintermediary/external/javalin/App.java +++ b/app/src/main/java/gov/hhs/cdc/trustedintermediary/external/javalin/App.java @@ -6,13 +6,16 @@ import gov.hhs.cdc.trustedintermediary.domainconnector.DomainConnectorConstructionException; import gov.hhs.cdc.trustedintermediary.domainconnector.DomainResponseHelper; import gov.hhs.cdc.trustedintermediary.domainconnector.UnableToReadOpenApiSpecificationException; +import gov.hhs.cdc.trustedintermediary.external.HikariConnectionPool; import gov.hhs.cdc.trustedintermediary.external.apache.ApacheClient; +import gov.hhs.cdc.trustedintermediary.external.azure.AzureDatabaseCredentialsProvider; import gov.hhs.cdc.trustedintermediary.external.azure.AzureSecrets; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiFhirImplementation; import gov.hhs.cdc.trustedintermediary.external.inmemory.KeyCache; import gov.hhs.cdc.trustedintermediary.external.inmemory.LoggingMetricMetadata; import gov.hhs.cdc.trustedintermediary.external.jackson.Jackson; import gov.hhs.cdc.trustedintermediary.external.jjwt.JjwtEngine; +import gov.hhs.cdc.trustedintermediary.external.localfile.EnvironmentDatabaseCredentialsProvider; import gov.hhs.cdc.trustedintermediary.external.localfile.LocalSecrets; import gov.hhs.cdc.trustedintermediary.external.slf4j.DeployedLogger; import gov.hhs.cdc.trustedintermediary.external.slf4j.LocalLogger; @@ -25,6 +28,8 @@ import gov.hhs.cdc.trustedintermediary.wrappers.MetricMetadata; import gov.hhs.cdc.trustedintermediary.wrappers.Secrets; import gov.hhs.cdc.trustedintermediary.wrappers.YamlCombiner; +import gov.hhs.cdc.trustedintermediary.wrappers.database.ConnectionPool; +import gov.hhs.cdc.trustedintermediary.wrappers.database.DatabaseCredentialsProvider; import gov.hhs.cdc.trustedintermediary.wrappers.formatter.Formatter; import io.javalin.Javalin; import java.util.Set; @@ -87,5 +92,17 @@ private static void registerClasses() { ApplicationContext.register( OrganizationsSettings.class, OrganizationsSettings.getInstance()); ApplicationContext.register(MetricMetadata.class, LoggingMetricMetadata.getInstance()); + if (ApplicationContext.getProperty("DB_URL") != null) { + if (ApplicationContext.getEnvironment().equalsIgnoreCase("local")) { + ApplicationContext.register( + DatabaseCredentialsProvider.class, + EnvironmentDatabaseCredentialsProvider.getInstance()); + } else { + ApplicationContext.register( + DatabaseCredentialsProvider.class, + AzureDatabaseCredentialsProvider.getInstance()); + } + ApplicationContext.register(ConnectionPool.class, HikariConnectionPool.getInstance()); + } } } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistration.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistration.java index 21b7da355..82e2a7c53 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistration.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/EtorDomainRegistration.java @@ -28,16 +28,10 @@ import gov.hhs.cdc.trustedintermediary.etor.results.ResultResponse; import gov.hhs.cdc.trustedintermediary.etor.results.ResultSender; import gov.hhs.cdc.trustedintermediary.etor.results.SendResultUseCase; -import gov.hhs.cdc.trustedintermediary.external.azure.AzureClient; -import gov.hhs.cdc.trustedintermediary.external.azure.AzureDatabaseCredentialsProvider; -import gov.hhs.cdc.trustedintermediary.external.azure.AzureStorageAccountPartnerMetadataStorage; -import gov.hhs.cdc.trustedintermediary.external.database.DatabaseCredentialsProvider; import gov.hhs.cdc.trustedintermediary.external.database.DatabasePartnerMetadataStorage; import gov.hhs.cdc.trustedintermediary.external.database.DbDao; -import gov.hhs.cdc.trustedintermediary.external.database.EtorSqlDriverManager; import gov.hhs.cdc.trustedintermediary.external.database.PostgresDao; import gov.hhs.cdc.trustedintermediary.external.hapi.HapiOrderConverter; -import gov.hhs.cdc.trustedintermediary.external.localfile.EnvironmentDatabaseCredentialsProvider; import gov.hhs.cdc.trustedintermediary.external.localfile.FilePartnerMetadataStorage; import gov.hhs.cdc.trustedintermediary.external.localfile.MockRSEndpointClient; import gov.hhs.cdc.trustedintermediary.external.reportstream.ReportStreamEndpointClient; @@ -46,7 +40,6 @@ import gov.hhs.cdc.trustedintermediary.wrappers.FhirParseException; import gov.hhs.cdc.trustedintermediary.wrappers.HapiFhir; import gov.hhs.cdc.trustedintermediary.wrappers.Logger; -import gov.hhs.cdc.trustedintermediary.wrappers.SqlDriverManager; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -113,36 +106,20 @@ public Map> domainRegistra ApplicationContext.register(SendResultUseCase.class, SendResultUseCase.getInstance()); if (ApplicationContext.getProperty("DB_URL") != null) { - ApplicationContext.register(SqlDriverManager.class, EtorSqlDriverManager.getInstance()); ApplicationContext.register(DbDao.class, PostgresDao.getInstance()); ApplicationContext.register( PartnerMetadataStorage.class, DatabasePartnerMetadataStorage.getInstance()); - if (ApplicationContext.getEnvironment().equalsIgnoreCase("local")) { - ApplicationContext.register( - DatabaseCredentialsProvider.class, - EnvironmentDatabaseCredentialsProvider.getInstance()); - } else { - ApplicationContext.register( - DatabaseCredentialsProvider.class, - AzureDatabaseCredentialsProvider.getInstance()); - } } else if (ApplicationContext.getEnvironment().equalsIgnoreCase("local")) { ApplicationContext.register( PartnerMetadataStorage.class, FilePartnerMetadataStorage.getInstance()); - } else { - ApplicationContext.register( - PartnerMetadataStorage.class, - AzureStorageAccountPartnerMetadataStorage.getInstance()); } - if (ApplicationContext.getEnvironment().equalsIgnoreCase("local")) { ApplicationContext.register(RSEndpointClient.class, MockRSEndpointClient.getInstance()); } else { ApplicationContext.register( RSEndpointClient.class, ReportStreamEndpointClient.getInstance()); - - ApplicationContext.register(AzureClient.class, AzureClient.getInstance()); } + return endpoints; } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/azure/AzureClient.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/azure/AzureClient.java deleted file mode 100644 index fed814be2..000000000 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/azure/AzureClient.java +++ /dev/null @@ -1,51 +0,0 @@ -package gov.hhs.cdc.trustedintermediary.external.azure; - -import com.azure.core.credential.TokenRequestContext; -import com.azure.identity.DefaultAzureCredentialBuilder; -import com.azure.storage.blob.BlobClient; -import com.azure.storage.blob.BlobContainerClient; -import com.azure.storage.blob.BlobServiceClientBuilder; -import gov.hhs.cdc.trustedintermediary.context.ApplicationContext; - -/** This class represents a client for interacting with Azure Blob Storage. */ -public class AzureClient { - - private static final String STORAGE_ACCOUNT_BLOB_ENDPOINT = - ApplicationContext.getProperty("STORAGE_ACCOUNT_BLOB_ENDPOINT"); - private static final String METADATA_CONTAINER_NAME = - ApplicationContext.getProperty("METADATA_CONTAINER_NAME"); - - private static final AzureClient INSTANCE = new AzureClient(); - - private static BlobContainerClient BLOB_CONTAINER_CLIENT; - - private AzureClient() {} - - public static AzureClient getInstance() { - - /* - BLOB_CONTAINER_CLIENT is initialized here inside the getInstance method instead of the static context above - to ensure that it is not created until it is needed. This prevents an exception being thrown in the unit - test context where `STORAGE_ACCOUNT_BLOB_ENDPOINT` is empty. - */ - BLOB_CONTAINER_CLIENT = - new BlobServiceClientBuilder() - .endpoint(STORAGE_ACCOUNT_BLOB_ENDPOINT) - .credential(new DefaultAzureCredentialBuilder().build()) - .buildClient() - .getBlobContainerClient(METADATA_CONTAINER_NAME); - - return INSTANCE; - } - - public BlobClient getBlobClient(String blobName) { - return BLOB_CONTAINER_CLIENT.getBlobClient(blobName); - } - - public String getScopedToken(String scope) { - return new DefaultAzureCredentialBuilder() - .build() - .getTokenSync(new TokenRequestContext().addScopes(scope)) - .getToken(); - } -} diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/azure/AzureStorageAccountPartnerMetadataStorage.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/azure/AzureStorageAccountPartnerMetadataStorage.java deleted file mode 100644 index 453a89d69..000000000 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/azure/AzureStorageAccountPartnerMetadataStorage.java +++ /dev/null @@ -1,78 +0,0 @@ -package gov.hhs.cdc.trustedintermediary.external.azure; - -import com.azure.core.exception.AzureException; -import com.azure.core.util.BinaryData; -import com.azure.storage.blob.BlobClient; -import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadata; -import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataException; -import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataStorage; -import gov.hhs.cdc.trustedintermediary.wrappers.Logger; -import gov.hhs.cdc.trustedintermediary.wrappers.formatter.Formatter; -import gov.hhs.cdc.trustedintermediary.wrappers.formatter.FormatterProcessingException; -import gov.hhs.cdc.trustedintermediary.wrappers.formatter.TypeReference; -import java.util.Optional; -import java.util.Set; -import javax.inject.Inject; - -/** Implements the {@link PartnerMetadataStorage} using files stored in an Azure Storage Account. */ -public class AzureStorageAccountPartnerMetadataStorage implements PartnerMetadataStorage { - - private static final AzureStorageAccountPartnerMetadataStorage INSTANCE = - new AzureStorageAccountPartnerMetadataStorage(); - - @Inject Formatter formatter; - @Inject Logger logger; - @Inject AzureClient client; - - private AzureStorageAccountPartnerMetadataStorage() {} - - public static AzureStorageAccountPartnerMetadataStorage getInstance() { - return INSTANCE; - } - - @Override - public Optional readMetadata(final String uniqueId) - throws PartnerMetadataException { - String metadataFileName = getMetadataFileName(uniqueId); - try { - BlobClient blobClient = client.getBlobClient(metadataFileName); - String blobUrl = blobClient.getBlobUrl(); - logger.logInfo("Reading metadata from " + blobUrl); - if (!blobClient.exists()) { - logger.logWarning("Metadata blob not found: {}", blobUrl); - return Optional.empty(); - } - String content = blobClient.downloadContent().toString(); - PartnerMetadata metadata = - formatter.convertJsonToObject(content, new TypeReference<>() {}); - return Optional.ofNullable(metadata); - } catch (AzureException | FormatterProcessingException e) { - throw new PartnerMetadataException( - "Failed to download metadata file " + metadataFileName, e); - } - } - - @Override - public void saveMetadata(final PartnerMetadata metadata) throws PartnerMetadataException { - String metadataFileName = getMetadataFileName(metadata.receivedSubmissionId()); - try { - BlobClient blobClient = client.getBlobClient(metadataFileName); - String content = formatter.convertToJsonString(metadata); - blobClient.upload(BinaryData.fromString(content), true); - logger.logInfo("Saved metadata to " + blobClient.getBlobUrl()); - } catch (AzureException | FormatterProcessingException e) { - throw new PartnerMetadataException( - "Failed to upload metadata file " + metadataFileName, e); - } - } - - @Override - public Set readMetadataForSender(String sender) - throws PartnerMetadataException { - return null; - } - - public static String getMetadataFileName(String uniqueId) { - return uniqueId + ".json"; - } -} diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/EtorSqlDriverManager.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/EtorSqlDriverManager.java deleted file mode 100644 index d881d4f27..000000000 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/EtorSqlDriverManager.java +++ /dev/null @@ -1,24 +0,0 @@ -package gov.hhs.cdc.trustedintermediary.external.database; - -import gov.hhs.cdc.trustedintermediary.wrappers.SqlDriverManager; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; -import java.util.Properties; - -/** Wrapper class for SqlDriverManager */ -public class EtorSqlDriverManager implements SqlDriverManager { - - private static final EtorSqlDriverManager INSTANCE = new EtorSqlDriverManager(); - - private EtorSqlDriverManager() {} - - @Override - public Connection getConnection(String url, Properties props) throws SQLException { - return DriverManager.getConnection(url, props); - } - - public static EtorSqlDriverManager getInstance() { - return INSTANCE; - } -} diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/PostgresDao.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/PostgresDao.java index 4a603ee0f..dfc59ba89 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/PostgresDao.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/PostgresDao.java @@ -1,10 +1,8 @@ package gov.hhs.cdc.trustedintermediary.external.database; -import gov.hhs.cdc.trustedintermediary.context.ApplicationContext; import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadata; import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataStatus; -import gov.hhs.cdc.trustedintermediary.wrappers.Logger; -import gov.hhs.cdc.trustedintermediary.wrappers.SqlDriverManager; +import gov.hhs.cdc.trustedintermediary.wrappers.database.ConnectionPool; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -13,7 +11,6 @@ import java.sql.Types; import java.time.Instant; import java.util.HashSet; -import java.util.Properties; import java.util.Set; import javax.inject.Inject; @@ -22,56 +19,16 @@ public class PostgresDao implements DbDao { private static final PostgresDao INSTANCE = new PostgresDao(); - @Inject Logger logger; - @Inject SqlDriverManager driverManager; - @Inject DatabaseCredentialsProvider credentialsProvider; + @Inject ConnectionPool connectionPool; private PostgresDao() {} - protected Connection connect() throws SQLException { - Connection conn; - String url = - "jdbc:postgresql://" - + ApplicationContext.getProperty("DB_URL") - + ":" - + ApplicationContext.getProperty("DB_PORT") - + "/" - + ApplicationContext.getProperty("DB_NAME"); - - logger.logInfo("going to connect to db url {}", url); - - // Ternaries prevent NullPointerException during testing since we decided not to mock env - // vars. - String user = - ApplicationContext.getProperty("DB_USER") == null - ? "" - : ApplicationContext.getProperty("DB_USER"); - - String pass = credentialsProvider.getPassword(); - - String ssl = - ApplicationContext.getProperty("DB_SSL") == null - ? "" - : ApplicationContext.getProperty("DB_SSL"); - - Properties props = new Properties(); - props.setProperty("user", user); - props.setProperty("password", pass); - - // If the below prop isn't set to require and we just set ssl=true it will expect a CA cert - // in azure which breaks it - props.setProperty("ssl", ssl); - conn = driverManager.getConnection(url, props); - logger.logInfo("DB Connected Successfully"); - return conn; - } - public static PostgresDao getInstance() { return INSTANCE; } @Override - public synchronized void upsertMetadata( + public void upsertMetadata( String receivedSubmissionId, String sentSubmissionId, String sender, @@ -83,7 +40,7 @@ public synchronized void upsertMetadata( String failureReason) throws SQLException { - try (Connection conn = connect(); + try (Connection conn = connectionPool.getConnection(); PreparedStatement statement = conn.prepareStatement( """ @@ -124,10 +81,9 @@ ON CONFLICT (received_message_id) DO UPDATE SET receiver = EXCLUDED.receiver, se } @Override - public synchronized Set fetchMetadataForSender(String sender) - throws SQLException { + public Set fetchMetadataForSender(String sender) throws SQLException { - try (Connection conn = connect(); + try (Connection conn = connectionPool.getConnection(); PreparedStatement statement = conn.prepareStatement("SELECT * FROM metadata WHERE sender = ?")) { statement.setString(1, sender); @@ -144,8 +100,8 @@ public synchronized Set fetchMetadataForSender(String sender) } @Override - public synchronized PartnerMetadata fetchMetadata(String submissionId) throws SQLException { - try (Connection conn = connect(); + public PartnerMetadata fetchMetadata(String submissionId) throws SQLException { + try (Connection conn = connectionPool.getConnection(); PreparedStatement statement = conn.prepareStatement( "SELECT * FROM metadata where received_message_id = ? OR sent_message_id = ?")) { diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/azure/AzureStorageAccountPartnerMetadataStorageTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/azure/AzureStorageAccountPartnerMetadataStorageTest.groovy deleted file mode 100644 index 864ef607b..000000000 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/azure/AzureStorageAccountPartnerMetadataStorageTest.groovy +++ /dev/null @@ -1,146 +0,0 @@ -package gov.hhs.cdc.trustedintermediary.external.azure - -import com.azure.core.exception.AzureException -import com.azure.core.util.BinaryData -import com.azure.storage.blob.BlobClient -import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext -import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadata -import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataException -import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataStatus -import gov.hhs.cdc.trustedintermediary.external.jackson.Jackson -import gov.hhs.cdc.trustedintermediary.wrappers.formatter.Formatter -import java.time.Instant -import spock.lang.Specification - -class AzureStorageAccountPartnerMetadataStorageTest extends Specification { - - def setup() { - TestApplicationContext.reset() - TestApplicationContext.init() - TestApplicationContext.register(AzureStorageAccountPartnerMetadataStorage, AzureStorageAccountPartnerMetadataStorage.getInstance()) - } - - def "successfully read metadata"() { - given: - def expectedReceivedSubmissionId = "receivedSubmissionId" - def expectedSentSubmissionId = "sentSubmissionId" - def expectedSender = "sender" - def expectedReceiver = "receiver" - def expectedTimestamp = Instant.parse("2023-12-04T18:51:48.941875Z") - def expectedHash = "abcd" - PartnerMetadataStatus status = PartnerMetadataStatus.PENDING - - PartnerMetadata expectedMetadata = new PartnerMetadata(expectedReceivedSubmissionId, expectedSentSubmissionId, expectedSender, expectedReceiver, expectedTimestamp, null, expectedHash, status, null) - - String simulatedMetadataJson = """{ - "receivedSubmissionId": "${expectedReceivedSubmissionId}", - "sentSubmissionId": "${expectedSentSubmissionId}", - "sender": "${expectedSender}", - "receiver": "${expectedReceiver}", - "timeReceived": "${expectedTimestamp}", - "hash": "${expectedHash}", - "deliveryStatus": "${status}" - }""" - - def mockBlobClient = Mock(BlobClient) - mockBlobClient.exists() >> true - mockBlobClient.downloadContent() >> BinaryData.fromString(simulatedMetadataJson) - - def azureClient = Mock(AzureClient) - azureClient.getBlobClient(_ as String) >> mockBlobClient - - TestApplicationContext.register(AzureClient, azureClient) - TestApplicationContext.register(Formatter, Jackson.getInstance()) - TestApplicationContext.injectRegisteredImplementations() - - when: - def actualMetadata = AzureStorageAccountPartnerMetadataStorage.getInstance().readMetadata(expectedReceivedSubmissionId) - - then: - actualMetadata.get() == expectedMetadata - } - - def "readMetadata returns empty when blob does not exist"() { - given: - def mockBlobClient = Mock(BlobClient) - mockBlobClient.exists() >> false - - def azureClient = Mock(AzureClient) - azureClient.getBlobClient(_ as String) >> mockBlobClient - - TestApplicationContext.register(AzureClient, azureClient) - TestApplicationContext.injectRegisteredImplementations() - - when: - def actualMetadata = AzureStorageAccountPartnerMetadataStorage.getInstance().readMetadata("nonexistentId") - - then: - actualMetadata.isEmpty() - } - - def "exception path while reading metadata"() { - given: - String expectedUniqueId = "receivedSubmissionId" - def mockBlobClient = Mock(BlobClient) - mockBlobClient.exists() >> true - mockBlobClient.downloadContent() >> { throw new AzureException("Download error") } - - def azureClient = Mock(AzureClient) - azureClient.getBlobClient(_ as String) >> mockBlobClient - - TestApplicationContext.register(AzureClient, azureClient) - TestApplicationContext.injectRegisteredImplementations() - - when: - AzureStorageAccountPartnerMetadataStorage.getInstance().readMetadata(expectedUniqueId) - - then: - thrown(PartnerMetadataException) - } - - def "successfully save metadata"() { - given: - PartnerMetadata partnerMetadata = new PartnerMetadata("receivedSubmissionId", "sentSubmissionId", "sender", "receiver", Instant.now(), null, "abcd", PartnerMetadataStatus.DELIVERED, null) - - def mockBlobClient = Mock(BlobClient) - def azureClient = Mock(AzureClient) - azureClient.getBlobClient(_ as String) >> mockBlobClient - - def mockFormatter = Mock(Formatter) - mockFormatter.convertToJsonString(partnerMetadata) >> "DogCow" - - TestApplicationContext.register(AzureClient, azureClient) - TestApplicationContext.register(Formatter, mockFormatter) - TestApplicationContext.injectRegisteredImplementations() - - when: - AzureStorageAccountPartnerMetadataStorage.getInstance().saveMetadata(partnerMetadata) - - then: - 1 * mockBlobClient.upload(_ as BinaryData, true) - } - - def "failed to save metadata"() { - given: - PartnerMetadata partnerMetadata = new PartnerMetadata("receivedSubmissionId", "sentSubmissionId", "sender", "receiver", Instant.now(), null, "abcd", PartnerMetadataStatus.DELIVERED, null) - - def mockBlobClient = Mock(BlobClient) - mockBlobClient.upload(_ as BinaryData, true) >> { throw new AzureException("upload failed") } - - def azureClient = Mock(AzureClient) - azureClient.getBlobClient(_ as String) >> mockBlobClient - - def mockFormatter = Mock(Formatter) - mockFormatter.convertToJsonString(partnerMetadata) >> "DogCow" - - TestApplicationContext.register(AzureClient, azureClient) - TestApplicationContext.register(Formatter, mockFormatter) - TestApplicationContext.injectRegisteredImplementations() - - when: - AzureStorageAccountPartnerMetadataStorage.getInstance().saveMetadata(partnerMetadata) - - then: - thrown(PartnerMetadataException) - } -} diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/database/PostgresDaoTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/database/PostgresDaoTest.groovy index 10ebfa331..7664de567 100644 --- a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/database/PostgresDaoTest.groovy +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/database/PostgresDaoTest.groovy @@ -3,7 +3,8 @@ package gov.hhs.cdc.trustedintermediary.external.database import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadata import gov.hhs.cdc.trustedintermediary.etor.metadata.partner.PartnerMetadataStatus -import gov.hhs.cdc.trustedintermediary.wrappers.SqlDriverManager +import gov.hhs.cdc.trustedintermediary.wrappers.database.ConnectionPool +import gov.hhs.cdc.trustedintermediary.wrappers.database.DatabaseCredentialsProvider import java.sql.Connection import java.sql.PreparedStatement import java.sql.ResultSet @@ -15,7 +16,7 @@ import spock.lang.Specification class PostgresDaoTest extends Specification { - private SqlDriverManager mockDriver + private ConnectionPool mockConnPool private Connection mockConn private PreparedStatement mockPreparedStatement private ResultSet mockResultSet @@ -24,7 +25,7 @@ class PostgresDaoTest extends Specification { TestApplicationContext.reset() TestApplicationContext.init() - mockDriver = Mock(SqlDriverManager) + mockConnPool = Mock(ConnectionPool) mockConn = Mock(Connection) mockPreparedStatement = Mock(PreparedStatement) mockResultSet = Mock(ResultSet) @@ -35,32 +36,6 @@ class PostgresDaoTest extends Specification { TestApplicationContext.register(PostgresDao, PostgresDao.getInstance()) } - def "connect happy path works"() { - given: - mockDriver.getConnection(_ as String, _ as Properties) >> {mockConn} - - TestApplicationContext.register(SqlDriverManager, mockDriver) - TestApplicationContext.injectRegisteredImplementations() - - when: - def conn = PostgresDao.getInstance().connect() - - then: - conn == mockConn - } - - def "connect unhappy path throws exception"() { - given: - mockDriver.getConnection(_ as String, _ as Properties) >> {throw new SQLException()} - TestApplicationContext.register(SqlDriverManager, mockDriver) - TestApplicationContext.injectRegisteredImplementations() - - when: - PostgresDao.getInstance().connect() - - then: - thrown(SQLException) - } def "upsertMetadata works"() { given: @@ -73,10 +48,10 @@ class PostgresDaoTest extends Specification { def status = PartnerMetadataStatus.PENDING def failureReason = "failure reason" - mockDriver.getConnection(_ as String, _ as Properties) >> mockConn + mockConnPool.getConnection() >> mockConn mockConn.prepareStatement(_ as String) >> mockPreparedStatement - TestApplicationContext.register(SqlDriverManager, mockDriver) + TestApplicationContext.register(ConnectionPool, mockConnPool) TestApplicationContext.injectRegisteredImplementations() when: @@ -97,10 +72,10 @@ class PostgresDaoTest extends Specification { def "upsertMetadata unhappy path throws exception"() { given: - mockDriver.getConnection(_ as String, _ as Properties) >> mockConn + mockConnPool.getConnection() >> mockConn mockConn.prepareStatement(_ as String) >> { throw new SQLException() } - TestApplicationContext.register(SqlDriverManager, mockDriver) + TestApplicationContext.register(ConnectionPool, mockConnPool) TestApplicationContext.injectRegisteredImplementations() when: @@ -112,10 +87,10 @@ class PostgresDaoTest extends Specification { def "upsertMetadata writes null timestamp"() { given: - mockDriver.getConnection(_ as String, _ as Properties) >> mockConn + mockConnPool.getConnection() >> mockConn mockConn.prepareStatement(_ as String) >> mockPreparedStatement - TestApplicationContext.register(SqlDriverManager, mockDriver) + TestApplicationContext.register(ConnectionPool, mockConnPool) TestApplicationContext.injectRegisteredImplementations() when: @@ -129,13 +104,13 @@ class PostgresDaoTest extends Specification { def "select metadata retrieves data"(){ given: - mockDriver.getConnection(_ as String, _ as Properties) >> mockConn + mockConnPool.getConnection() >> mockConn mockConn.prepareStatement(_ as String) >> mockPreparedStatement mockPreparedStatement.executeQuery() >> mockResultSet mockResultSet.next() >> true mockResultSet.getTimestamp(_ as String) >> Timestamp.from(Instant.now()) mockResultSet.getString("delivery_status") >> "DELIVERED" - TestApplicationContext.register(SqlDriverManager, mockDriver) + TestApplicationContext.register(ConnectionPool, mockConnPool) TestApplicationContext.injectRegisteredImplementations() when: @@ -147,10 +122,10 @@ class PostgresDaoTest extends Specification { def "fetchMetadata unhappy path throws exception"() { given: - mockDriver.getConnection(_ as String, _ as Properties) >> mockConn + mockConnPool.getConnection() >> mockConn mockConn.prepareStatement(_ as String) >> { throw new SQLException() } - TestApplicationContext.register(SqlDriverManager, mockDriver) + TestApplicationContext.register(ConnectionPool, mockConnPool) TestApplicationContext.injectRegisteredImplementations() when: @@ -162,12 +137,12 @@ class PostgresDaoTest extends Specification { def "fetchMetadata returns null when rows do not exist"() { given: - mockDriver.getConnection(_ as String, _ as Properties) >> mockConn + mockConnPool.getConnection() >> mockConn mockConn.prepareStatement(_ as String) >> mockPreparedStatement mockResultSet.next() >> false mockPreparedStatement.executeQuery() >> mockResultSet - TestApplicationContext.register(SqlDriverManager, mockDriver) + TestApplicationContext.register(ConnectionPool, mockConnPool) TestApplicationContext.injectRegisteredImplementations() when: @@ -192,7 +167,7 @@ class PostgresDaoTest extends Specification { def reason = "It done Goofed" def expected = new PartnerMetadata(receivedMessageId, sentMessageId, sender, receiver, timeReceived, timeDelivered, hash, status, reason) - mockDriver.getConnection(_ as String, _ as Properties) >> mockConn + mockConnPool.getConnection() >> mockConn mockConn.prepareStatement(_ as String) >> mockPreparedStatement mockResultSet.next() >> true mockResultSet.getString("received_message_id") >> receivedMessageId @@ -206,7 +181,7 @@ class PostgresDaoTest extends Specification { mockResultSet.getString("failure_reason") >> reason mockPreparedStatement.executeQuery() >> mockResultSet - TestApplicationContext.register(SqlDriverManager, mockDriver) + TestApplicationContext.register(ConnectionPool, mockConnPool) TestApplicationContext.injectRegisteredImplementations() when: @@ -218,14 +193,14 @@ class PostgresDaoTest extends Specification { def "fetchMetadata successfully sets the received timestamp to null"() { given: - mockDriver.getConnection(_ as String, _ as Properties) >> mockConn + mockConnPool.getConnection() >> mockConn mockConn.prepareStatement(_ as String) >> mockPreparedStatement mockPreparedStatement.executeQuery() >> mockResultSet mockResultSet.next() >> true mockResultSet.getTimestamp("time_received") >> null mockResultSet.getString("delivery_status") >> "DELIVERED" mockResultSet.getString("failure_reason") >> "Your time is up" - TestApplicationContext.register(SqlDriverManager, mockDriver) + TestApplicationContext.register(ConnectionPool, mockConnPool) TestApplicationContext.injectRegisteredImplementations() when: @@ -241,7 +216,7 @@ class PostgresDaoTest extends Specification { def expected1 = new PartnerMetadata("12345", "7890", sender, "You'll get your just reward", Instant.parse("2024-01-03T15:45:33.30Z"),Instant.parse("2024-01-03T15:45:33.30Z"), sender.hashCode().toString(), PartnerMetadataStatus.PENDING, "It done Goofed") def expected2 = new PartnerMetadata("doreyme", "fasole", sender, "receiver", Instant.now(), Instant.now(), "gobeltygoook", PartnerMetadataStatus.DELIVERED, "cause I said so") - mockDriver.getConnection(_ as String, _ as Properties) >> mockConn + mockConnPool.getConnection() >> mockConn mockConn.prepareStatement(_ as String) >> mockPreparedStatement mockResultSet.next() >>> [true, true, false] mockResultSet.getString("received_message_id") >>> [ @@ -282,7 +257,7 @@ class PostgresDaoTest extends Specification { ] mockPreparedStatement.executeQuery() >> mockResultSet - TestApplicationContext.register(SqlDriverManager, mockDriver) + TestApplicationContext.register(ConnectionPool, mockConnPool) TestApplicationContext.injectRegisteredImplementations() diff --git a/operations/locustfile.py b/operations/locustfile.py index 772a48360..6ee4f6361 100644 --- a/operations/locustfile.py +++ b/operations/locustfile.py @@ -84,13 +84,13 @@ def get_v1_etor_metadata(self): headers={"Authorization": self.access_token}, ) - # @task(1) - # def get_v1_metadata_consolidated(self): - # if self.orders_api_called: - # self.client.get( - # f"{CONSOLIDATED_ENDPOINT}/{self.sender}", - # headers={"Authorization": self.access_token}, - # ) + @task(1) + def get_v1_metadata_consolidated(self): + if self.orders_api_called: + self.client.get( + f"{CONSOLIDATED_ENDPOINT}/{self.sender}", + headers={"Authorization": self.access_token}, + ) @events.test_start.add_listener diff --git a/shared/build.gradle b/shared/build.gradle index ebedd5671..06e2807c1 100644 --- a/shared/build.gradle +++ b/shared/build.gradle @@ -58,6 +58,9 @@ dependencies { // postgres implementation 'org.postgresql:postgresql:42.7.1' + + // hikari connection pool + implementation 'com.zaxxer:HikariCP:5.1.0' } jacocoTestCoverageVerification { diff --git a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/context/ApplicationContext.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/context/ApplicationContext.java index 0473341db..86a73a923 100644 --- a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/context/ApplicationContext.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/context/ApplicationContext.java @@ -22,6 +22,7 @@ public class ApplicationContext { protected static final Map, Object> OBJECT_MAP = new ConcurrentHashMap<>(); + protected static final Map TEST_ENV_VARS = new ConcurrentHashMap<>(); protected static final Set IMPLEMENTATIONS = new HashSet<>(); protected ApplicationContext() {} @@ -147,10 +148,16 @@ private static Object getDeclaringClassImplementation( } public static String getProperty(String key) { + if (!TEST_ENV_VARS.isEmpty()) { + return TEST_ENV_VARS.get(key); + } return DotEnv.get(key); } public static String getProperty(String key, String defaultValue) { + if (!TEST_ENV_VARS.isEmpty()) { + return TEST_ENV_VARS.getOrDefault(key, defaultValue); + } return DotEnv.get(key, defaultValue); } diff --git a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/HikariConnectionPool.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/HikariConnectionPool.java new file mode 100644 index 000000000..f67ccea19 --- /dev/null +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/HikariConnectionPool.java @@ -0,0 +1,61 @@ +package gov.hhs.cdc.trustedintermediary.external; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import gov.hhs.cdc.trustedintermediary.context.ApplicationContext; +import gov.hhs.cdc.trustedintermediary.wrappers.database.ConnectionPool; +import gov.hhs.cdc.trustedintermediary.wrappers.database.DatabaseCredentialsProvider; +import java.sql.Connection; +import java.sql.SQLException; + +/** + * A shared connection pool for connecting to the database. The class currently spins up 10 + * connections on server startup. This is the default Hikari behavior. For troubleshooting or tuning + * help please check the HikariCP repo for info on how to tune the connection performance should any + * issues arise. + */ +public class HikariConnectionPool implements ConnectionPool { + + private static HikariConnectionPool INSTANCE; + + public final HikariDataSource ds; + + private HikariConnectionPool() { + HikariConfig config = constructHikariConfig(); + ds = new HikariDataSource(config); + } + + public static synchronized HikariConnectionPool getInstance() { + if (INSTANCE == null) { + INSTANCE = new HikariConnectionPool(); + } + return INSTANCE; + } + + static HikariConfig constructHikariConfig() { + String user = ApplicationContext.getProperty("DB_USER", ""); + DatabaseCredentialsProvider credProvider = + ApplicationContext.getImplementation(DatabaseCredentialsProvider.class); + + String pass = credProvider.getPassword(); + String serverName = ApplicationContext.getProperty("DB_URL", ""); + String dbName = ApplicationContext.getProperty("DB_NAME", ""); + String dbPort = ApplicationContext.getProperty("DB_PORT", ""); + + HikariConfig config = new HikariDataSource(); + + config.setDataSourceClassName("org.postgresql.ds.PGSimpleDataSource"); + config.addDataSourceProperty("user", user); + config.addDataSourceProperty("password", pass); + config.addDataSourceProperty("serverName", serverName); + config.addDataSourceProperty("databaseName", dbName); + config.addDataSourceProperty("portNumber", dbPort); + + return config; + } + + @Override + public Connection getConnection() throws SQLException { + return ds.getConnection(); + } +} diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/azure/AzureDatabaseCredentialsProvider.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/azure/AzureDatabaseCredentialsProvider.java similarity index 57% rename from etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/azure/AzureDatabaseCredentialsProvider.java rename to shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/azure/AzureDatabaseCredentialsProvider.java index 16955af1b..73d46440f 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/azure/AzureDatabaseCredentialsProvider.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/azure/AzureDatabaseCredentialsProvider.java @@ -1,7 +1,8 @@ package gov.hhs.cdc.trustedintermediary.external.azure; -import gov.hhs.cdc.trustedintermediary.external.database.DatabaseCredentialsProvider; -import javax.inject.Inject; +import com.azure.core.credential.TokenRequestContext; +import com.azure.identity.DefaultAzureCredentialBuilder; +import gov.hhs.cdc.trustedintermediary.wrappers.database.DatabaseCredentialsProvider; /** * AzureDatabaseCredentialsProvider is a class responsible for providing credentials for a database @@ -12,8 +13,6 @@ public class AzureDatabaseCredentialsProvider implements DatabaseCredentialsProv private static final AzureDatabaseCredentialsProvider INSTANCE = new AzureDatabaseCredentialsProvider(); - @Inject AzureClient azureClient; - public static AzureDatabaseCredentialsProvider getInstance() { return INSTANCE; } @@ -22,6 +21,11 @@ private AzureDatabaseCredentialsProvider() {} @Override public String getPassword() { - return azureClient.getScopedToken("https://ossrdbms-aad.database.windows.net/.default"); + return new DefaultAzureCredentialBuilder() + .build() + .getTokenSync( + new TokenRequestContext() + .addScopes("https://ossrdbms-aad.database.windows.net/.default")) + .getToken(); } } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/localfile/EnvironmentDatabaseCredentialsProvider.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/localfile/EnvironmentDatabaseCredentialsProvider.java similarity index 93% rename from etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/localfile/EnvironmentDatabaseCredentialsProvider.java rename to shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/localfile/EnvironmentDatabaseCredentialsProvider.java index d73d1fe97..258db8f36 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/localfile/EnvironmentDatabaseCredentialsProvider.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/localfile/EnvironmentDatabaseCredentialsProvider.java @@ -1,7 +1,7 @@ package gov.hhs.cdc.trustedintermediary.external.localfile; import gov.hhs.cdc.trustedintermediary.context.ApplicationContext; -import gov.hhs.cdc.trustedintermediary.external.database.DatabaseCredentialsProvider; +import gov.hhs.cdc.trustedintermediary.wrappers.database.DatabaseCredentialsProvider; /** * The EnvironmentDatabaseCredentialsProvider class is an implementation of the diff --git a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/wrappers/SqlDriverManager.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/wrappers/SqlDriverManager.java deleted file mode 100644 index 1989a163b..000000000 --- a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/wrappers/SqlDriverManager.java +++ /dev/null @@ -1,11 +0,0 @@ -package gov.hhs.cdc.trustedintermediary.wrappers; - -import java.sql.Connection; -import java.sql.SQLException; -import java.util.Properties; - -/** Interface for SqlDriverManager, this allows for easier testing */ -public interface SqlDriverManager { - - Connection getConnection(String url, Properties props) throws SQLException; -} diff --git a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/wrappers/database/ConnectionPool.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/wrappers/database/ConnectionPool.java new file mode 100644 index 000000000..62c5e32b5 --- /dev/null +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/wrappers/database/ConnectionPool.java @@ -0,0 +1,10 @@ +package gov.hhs.cdc.trustedintermediary.wrappers.database; + +import java.sql.Connection; +import java.sql.SQLException; + +/** Wrapper interface for connection pool libraries. */ +public interface ConnectionPool { + + Connection getConnection() throws SQLException; +} diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/DatabaseCredentialsProvider.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/wrappers/database/DatabaseCredentialsProvider.java similarity index 72% rename from etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/DatabaseCredentialsProvider.java rename to shared/src/main/java/gov/hhs/cdc/trustedintermediary/wrappers/database/DatabaseCredentialsProvider.java index f751aeb65..b27d6255b 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/external/database/DatabaseCredentialsProvider.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/wrappers/database/DatabaseCredentialsProvider.java @@ -1,4 +1,4 @@ -package gov.hhs.cdc.trustedintermediary.external.database; +package gov.hhs.cdc.trustedintermediary.wrappers.database; /** This interface represents a provider for retrieving database credentials. */ public interface DatabaseCredentialsProvider { diff --git a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/HikariConnectionPoolTest.groovy b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/HikariConnectionPoolTest.groovy new file mode 100644 index 000000000..03f94ce9d --- /dev/null +++ b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/HikariConnectionPoolTest.groovy @@ -0,0 +1,34 @@ +package gov.hhs.cdc.trustedintermediary.external + +import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext +import gov.hhs.cdc.trustedintermediary.wrappers.database.DatabaseCredentialsProvider +import spock.lang.Specification + +class HikariConnectionPoolTest extends Specification { + def setup() { + TestApplicationContext.reset() + TestApplicationContext.init() + def credProviders = Mock(DatabaseCredentialsProvider) + TestApplicationContext.addEnvironmentVariable("DB_USER", "test_user") + TestApplicationContext.addEnvironmentVariable("DB_URL", "test_url") + TestApplicationContext.addEnvironmentVariable("DB_NAME", "test_name") + TestApplicationContext.addEnvironmentVariable("DB_PORT", "1234") + TestApplicationContext.addEnvironmentVariable("DB_PASS", "test_pass") + + credProviders.getPassword() >> "test_pass" + TestApplicationContext.register(DatabaseCredentialsProvider, credProviders) + TestApplicationContext.injectRegisteredImplementations() + } + + def "connection pool works" () { + when: + def result = HikariConnectionPool.constructHikariConfig() + + then: + result.getDataSourceProperties().get("user") == "test_user" + result.getDataSourceProperties().get("password") == "test_pass" + result.getDataSourceProperties().get("serverName") == "test_url" + result.getDataSourceProperties().get("databaseName") == "test_name" + result.getDataSourceProperties().get("portNumber") == "1234" + } +} diff --git a/shared/src/testFixtures/groovy/gov/hhs/cdc/trustedintermediary/context/TestApplicationContext.groovy b/shared/src/testFixtures/groovy/gov/hhs/cdc/trustedintermediary/context/TestApplicationContext.groovy index a5a8c3b75..24c37e2a8 100644 --- a/shared/src/testFixtures/groovy/gov/hhs/cdc/trustedintermediary/context/TestApplicationContext.groovy +++ b/shared/src/testFixtures/groovy/gov/hhs/cdc/trustedintermediary/context/TestApplicationContext.groovy @@ -16,9 +16,14 @@ class TestApplicationContext extends ApplicationContext { def static reset() { OBJECT_MAP.clear() IMPLEMENTATIONS.clear() + TEST_ENV_VARS.clear() } def static injectRegisteredImplementations() { injectRegisteredImplementations(true) } + + def static addEnvironmentVariable(String key, String value) { + TEST_ENV_VARS.put(key, value) + } }