Skip to content
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

Merged
merged 1 commit into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions descriptors/ModuleDescriptor-template.json
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",
Copy link
Collaborator

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 🙂

"provides": [
{
"id": "_tenant",
Expand Down Expand Up @@ -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"]
}
]
},
Expand All @@ -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"
}
]
}
Expand Down Expand Up @@ -144,6 +155,12 @@
"description": "Run a query synchronously and get results",
"visible": true
},
{
"permissionName": "fqm.materializedViews.post",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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",
Expand Down
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
Expand Up @@ -4,5 +4,6 @@
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd">

<include file="sql/refactor drv_item_callnumber_location.xml" relativeToChangelogFile="true"/>
<include file="refactor_drv_item_callnumber_location.xml" relativeToChangelogFile="true"/>
<include file="add_materialized_view_indexes.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
13 changes: 13 additions & 0 deletions src/main/resources/swagger.api/mod-fqm-manager.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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:
Expand Down
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 {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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);
}
}