-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
MODFQMMGR-76: Periodically refresh materialized views #72
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: this permission (along with fqm.query.purge) is not part of fqm.query.all |
||
"displayName": "FQM - Refresh materialized views", | ||
"description": "Refresh FQM materialized views", | ||
"visible": true | ||
}, | ||
{ | ||
"permissionName": "fqm.query.all", | ||
"displayName": "FQM - All permissions", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String> 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); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Void> refreshMaterializedViews() { | ||
materializedViewRefreshService.refreshMaterializedViews(executionContext.getTenantId()); | ||
return new ResponseEntity<>(HttpStatus.NO_CONTENT); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog" | ||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog | ||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd"> | ||
<changeSet id="add-materialized-view-indexes" author="[email protected]"> | ||
<!-- ensure other schemas/tables exist (primarily to prevent invalid references in integration tests) --> | ||
<preConditions onFail="CONTINUE"> | ||
<sqlCheck expectedResult="1"> | ||
SELECT COUNT(*) FROM pg_matviews WHERE schemaname = '${tenant_id}_mod_fqm_manager'AND matviewname = 'drv_inventory_item_status'; | ||
</sqlCheck> | ||
<sqlCheck expectedResult="1"> | ||
SELECT COUNT(*) FROM pg_matviews WHERE schemaname = '${tenant_id}_mod_fqm_manager'AND matviewname = 'drv_circulation_loan_status'; | ||
</sqlCheck> | ||
</preConditions> | ||
<createIndex indexName="fqm_item_status_idx" tableName="drv_inventory_item_status" unique="true"> | ||
<column name="item_status"/> | ||
</createIndex> | ||
<createIndex indexName="fqm_loan_status_idx" tableName="drv_circulation_loan_status" unique="true"> | ||
<column name="loan_status"/> | ||
</createIndex> | ||
</changeSet> | ||
</databaseChangeLog> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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' | ||
Comment on lines
+47
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for putting this here :) I meant to say something about it definitely not going in folio-query-tool-metadata (since that would put it in the edge module, too), and totally forgot |
||
|
||
|
||
components: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I figured that since the logic for this is so simple, we really only need to ensure that the SQL that gets executed is what we expect, and thus could just use Mockito for this (rather than the docker test-db stuff). |
||
@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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️
This drives me nuts every time I look at the list of installed modules in any environment 🙂