From 19db29759f85a1167720c89f7c119a03d9bc339d Mon Sep 17 00:00:00 2001 From: Bobby Sharp Date: Tue, 7 Nov 2023 08:48:25 -0500 Subject: [PATCH] MODFQMMGR-76: Periodically refresh materialized views --- descriptors/ModuleDescriptor-template.json | 21 ++++++++- .../MaterializedViewRefreshRepository.java | 30 +++++++++++++ .../MaterializedViewRefreshController.java | 21 +++++++++ .../MaterializedViewRefreshService.java | 15 +++++++ .../v1.0.3/add_materialized_view_indexes.xml | 22 ++++++++++ .../changes/v1.0.3/changelog-v1.0.3.xml | 3 +- ...refactor_drv_item_callnumber_location.xml} | 0 .../swagger.api/mod-fqm-manager.yaml | 13 ++++++ ...MaterializedViewRefreshControllerTest.java | 43 +++++++++++++++++++ ...MaterializedViewRefreshRepositoryTest.java | 29 +++++++++++++ .../MaterializedViewRefreshServiceTest.java | 28 ++++++++++++ 11 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/folio/fqm/repository/MaterializedViewRefreshRepository.java create mode 100644 src/main/java/org/folio/fqm/resource/MaterializedViewRefreshController.java create mode 100644 src/main/java/org/folio/fqm/service/MaterializedViewRefreshService.java create mode 100644 src/main/resources/db/changelog/changes/v1.0.3/add_materialized_view_indexes.xml rename src/main/resources/db/changelog/changes/v1.0.3/{sql/refactor drv_item_callnumber_location.xml => refactor_drv_item_callnumber_location.xml} (100%) create mode 100644 src/test/java/org/folio/fqm/controller/MaterializedViewRefreshControllerTest.java create mode 100644 src/test/java/org/folio/fqm/repository/MaterializedViewRefreshRepositoryTest.java create mode 100644 src/test/java/org/folio/fqm/service/MaterializedViewRefreshServiceTest.java diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index eb27c3d2..f50c5528 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -1,6 +1,6 @@ { "id": "@artifactId@-@version@", - "name": "The module descriptor for mod-fqm-manager.", + "name": "FQM Manager Module", "provides": [ { "id": "_tenant", @@ -78,6 +78,11 @@ "methods": ["DELETE"], "pathPattern": "/query/{query-id}", "permissionsRequired": ["fqm.query.async.delete"] + }, + { + "methods": ["POST"], + "pathPattern": "/materialized-views/refresh", + "permissionsRequired": ["fqm.materializedViews.post"] } ] }, @@ -87,10 +92,16 @@ "interfaceType": "system", "handlers": [ { - "methods": [ "POST" ], + "methods": ["POST"], "pathPattern": "/query/purge", "unit": "hour", "delay": "1" + }, + { + "methods": ["POST"], + "pathPattern": "/materialized-views/refresh", + "unit": "hour", + "delay": "24" } ] } @@ -144,6 +155,12 @@ "description": "Run a query synchronously and get results", "visible": true }, + { + "permissionName": "fqm.materializedViews.post", + "displayName": "FQM - Refresh materialized views", + "description": "Refresh FQM materialized views", + "visible": true + }, { "permissionName": "fqm.query.all", "displayName": "FQM - All permissions", diff --git a/src/main/java/org/folio/fqm/repository/MaterializedViewRefreshRepository.java b/src/main/java/org/folio/fqm/repository/MaterializedViewRefreshRepository.java new file mode 100644 index 00000000..e45d9f83 --- /dev/null +++ b/src/main/java/org/folio/fqm/repository/MaterializedViewRefreshRepository.java @@ -0,0 +1,30 @@ +package org.folio.fqm.repository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.jooq.DSLContext; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +@Log4j2 +public class MaterializedViewRefreshRepository { + private static final String REFRESH_MATERIALIZED_VIEW_SQL = "REFRESH MATERIALIZED VIEW CONCURRENTLY "; + + private static final List materializedViewNames = List.of( + "drv_circulation_loan_status", + "drv_inventory_item_status" + ); + + private final DSLContext jooqContext; + + public void refreshMaterializedViews(String tenantId) { + for (String matViewName : materializedViewNames) { + String fullName = tenantId + "_mod_fqm_manager." + matViewName; + log.info("Refreshing materialized view {}", fullName); + jooqContext.execute(REFRESH_MATERIALIZED_VIEW_SQL + fullName); + } + } +} diff --git a/src/main/java/org/folio/fqm/resource/MaterializedViewRefreshController.java b/src/main/java/org/folio/fqm/resource/MaterializedViewRefreshController.java new file mode 100644 index 00000000..7da2c3cb --- /dev/null +++ b/src/main/java/org/folio/fqm/resource/MaterializedViewRefreshController.java @@ -0,0 +1,21 @@ +package org.folio.fqm.resource; + +import lombok.RequiredArgsConstructor; +import org.folio.fqm.service.MaterializedViewRefreshService; +import org.folio.spring.FolioExecutionContext; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class MaterializedViewRefreshController implements MaterializedViewsApi { + private final FolioExecutionContext executionContext; + private final MaterializedViewRefreshService materializedViewRefreshService; + + @Override + public ResponseEntity refreshMaterializedViews() { + materializedViewRefreshService.refreshMaterializedViews(executionContext.getTenantId()); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } +} diff --git a/src/main/java/org/folio/fqm/service/MaterializedViewRefreshService.java b/src/main/java/org/folio/fqm/service/MaterializedViewRefreshService.java new file mode 100644 index 00000000..a7d1e00f --- /dev/null +++ b/src/main/java/org/folio/fqm/service/MaterializedViewRefreshService.java @@ -0,0 +1,15 @@ +package org.folio.fqm.service; + +import lombok.RequiredArgsConstructor; +import org.folio.fqm.repository.MaterializedViewRefreshRepository; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class MaterializedViewRefreshService { + private final MaterializedViewRefreshRepository materializedViewRefreshRepository; + + public void refreshMaterializedViews(String tenantId) { + materializedViewRefreshRepository.refreshMaterializedViews(tenantId); + } +} diff --git a/src/main/resources/db/changelog/changes/v1.0.3/add_materialized_view_indexes.xml b/src/main/resources/db/changelog/changes/v1.0.3/add_materialized_view_indexes.xml new file mode 100644 index 00000000..d9b5eaec --- /dev/null +++ b/src/main/resources/db/changelog/changes/v1.0.3/add_materialized_view_indexes.xml @@ -0,0 +1,22 @@ + + + + + + SELECT COUNT(*) FROM pg_matviews WHERE schemaname = '${tenant_id}_mod_fqm_manager'AND matviewname = 'drv_inventory_item_status'; + + + SELECT COUNT(*) FROM pg_matviews WHERE schemaname = '${tenant_id}_mod_fqm_manager'AND matviewname = 'drv_circulation_loan_status'; + + + + + + + + + + diff --git a/src/main/resources/db/changelog/changes/v1.0.3/changelog-v1.0.3.xml b/src/main/resources/db/changelog/changes/v1.0.3/changelog-v1.0.3.xml index 9de85dd3..52ca9bd9 100644 --- a/src/main/resources/db/changelog/changes/v1.0.3/changelog-v1.0.3.xml +++ b/src/main/resources/db/changelog/changes/v1.0.3/changelog-v1.0.3.xml @@ -4,5 +4,6 @@ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd"> - + + diff --git a/src/main/resources/db/changelog/changes/v1.0.3/sql/refactor drv_item_callnumber_location.xml b/src/main/resources/db/changelog/changes/v1.0.3/refactor_drv_item_callnumber_location.xml similarity index 100% rename from src/main/resources/db/changelog/changes/v1.0.3/sql/refactor drv_item_callnumber_location.xml rename to src/main/resources/db/changelog/changes/v1.0.3/refactor_drv_item_callnumber_location.xml diff --git a/src/main/resources/swagger.api/mod-fqm-manager.yaml b/src/main/resources/swagger.api/mod-fqm-manager.yaml index 789f6019..51a3c965 100644 --- a/src/main/resources/swagger.api/mod-fqm-manager.yaml +++ b/src/main/resources/swagger.api/mod-fqm-manager.yaml @@ -44,6 +44,19 @@ paths: $ref: '#/components/responses/badRequestResponse' '500': $ref: '#/components/responses/internalServerErrorResponse' + /materialized-views/refresh: + post: + operationId: refreshMaterializedViews + tags: + - materializedViews + description: Refresh all materialized views for a tenant. + responses: + '204': + description: 'Views refreshed' + '400': + $ref: '#/components/responses/badRequestResponse' + '500': + $ref: '#/components/responses/internalServerErrorResponse' components: diff --git a/src/test/java/org/folio/fqm/controller/MaterializedViewRefreshControllerTest.java b/src/test/java/org/folio/fqm/controller/MaterializedViewRefreshControllerTest.java new file mode 100644 index 00000000..baff8f67 --- /dev/null +++ b/src/test/java/org/folio/fqm/controller/MaterializedViewRefreshControllerTest.java @@ -0,0 +1,43 @@ +package org.folio.fqm.controller; + +import org.folio.fqm.resource.MaterializedViewRefreshController; +import org.folio.fqm.service.MaterializedViewRefreshService; +import org.folio.spring.FolioExecutionContext; +import org.folio.spring.integration.XOkapiHeaders; +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.MockBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.RequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(MaterializedViewRefreshController.class) +class MaterializedViewRefreshControllerTest { + @Autowired + private MockMvc mockMvc; + @MockBean + private FolioExecutionContext executionContext; + @MockBean + private MaterializedViewRefreshService materializedViewRefreshService; + + @Test + void refreshMaterializedViewsTest() throws Exception { + String tenantId = "tenant_01"; + RequestBuilder requestBuilder = MockMvcRequestBuilders.post("/materialized-views/refresh") + .header(XOkapiHeaders.TENANT, tenantId) + .contentType(APPLICATION_JSON); + when(executionContext.getTenantId()).thenReturn(tenantId); + doNothing().when(materializedViewRefreshService).refreshMaterializedViews(tenantId); + mockMvc.perform(requestBuilder) + .andExpect(status().isNoContent()); + verify(materializedViewRefreshService, times(1)).refreshMaterializedViews(tenantId); + } +} diff --git a/src/test/java/org/folio/fqm/repository/MaterializedViewRefreshRepositoryTest.java b/src/test/java/org/folio/fqm/repository/MaterializedViewRefreshRepositoryTest.java new file mode 100644 index 00000000..565b963c --- /dev/null +++ b/src/test/java/org/folio/fqm/repository/MaterializedViewRefreshRepositoryTest.java @@ -0,0 +1,29 @@ +package org.folio.fqm.repository; + +import org.jooq.DSLContext; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class MaterializedViewRefreshRepositoryTest { + @InjectMocks + private MaterializedViewRefreshRepository materializedViewRefreshRepository; + @Mock + private DSLContext jooqContext; + + @Test + void refreshMaterializedViewsTest() { + String tenantId = "tenant_01"; + String expectedItemStatusSql = "REFRESH MATERIALIZED VIEW CONCURRENTLY tenant_01_mod_fqm_manager.drv_inventory_item_status"; + String expectedLoanStatusSql = "REFRESH MATERIALIZED VIEW CONCURRENTLY tenant_01_mod_fqm_manager.drv_circulation_loan_status"; + when(jooqContext.execute(anyString())).thenReturn(1); + materializedViewRefreshRepository.refreshMaterializedViews(tenantId); + verify(jooqContext, times(1)).execute(expectedItemStatusSql); + verify(jooqContext, times(1)).execute(expectedLoanStatusSql); + } +} diff --git a/src/test/java/org/folio/fqm/service/MaterializedViewRefreshServiceTest.java b/src/test/java/org/folio/fqm/service/MaterializedViewRefreshServiceTest.java new file mode 100644 index 00000000..70afa579 --- /dev/null +++ b/src/test/java/org/folio/fqm/service/MaterializedViewRefreshServiceTest.java @@ -0,0 +1,28 @@ +package org.folio.fqm.service; + +import org.folio.fqm.repository.MaterializedViewRefreshRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class MaterializedViewRefreshServiceTest { + @InjectMocks + private MaterializedViewRefreshService materializedViewRefreshService; + @Mock + private MaterializedViewRefreshRepository materializedViewRefreshRepository; + + @Test + void refreshMaterializedViewsTest() { + String tenantId = "tenant_01"; + doNothing().when(materializedViewRefreshRepository).refreshMaterializedViews(tenantId); + materializedViewRefreshService.refreshMaterializedViews(tenantId); + verify(materializedViewRefreshRepository, times(1)).refreshMaterializedViews(tenantId); + } +}