Allow partition date for non-partitionable resources (#2407)

* Allow partition date for searchparam resources

* Cleanup

* Test fix

* Documentation update

* Docs tweak
This commit is contained in:
James Agnew 2021-02-22 20:49:09 -05:00 committed by GitHub
parent 0e314a9382
commit b108e43600
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 126 additions and 49 deletions

View File

@ -37,31 +37,7 @@
</signature>
</configuration>
</execution>
<!--
<execution>
<id>check-android-api</id>
<phase>test</phase>
<inherited>true</inherited>
<goals>
<goal>check</goal>
</goals>
<configuration>
<signature>
<groupId>net.sf.androidscents.signature</groupId>
<artifactId>android-api-level-21</artifactId>
<version>5.0.1_r2</version>
</signature>
</configuration>
</execution>
-->
</executions>
<dependencies>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-all</artifactId>
<version>5.0.4</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.basepom.maven</groupId>

View File

@ -226,6 +226,11 @@ public class RequestPartitionId {
return fromPartitionIds(Collections.singletonList(null));
}
@Nonnull
public static RequestPartitionId defaultPartition(@Nullable LocalDate thePartitionDate) {
return fromPartitionIds(Collections.singletonList(null), thePartitionDate);
}
@Nonnull
public static RequestPartitionId fromPartitionId(@Nullable Integer thePartitionId) {
return fromPartitionIds(Collections.singletonList(thePartitionId));
@ -238,7 +243,12 @@ public class RequestPartitionId {
@Nonnull
public static RequestPartitionId fromPartitionIds(@Nonnull Collection<Integer> thePartitionIds) {
return new RequestPartitionId(null, toListOrNull(thePartitionIds), null);
return fromPartitionIds(thePartitionIds, null);
}
@Nonnull
public static RequestPartitionId fromPartitionIds(@Nonnull Collection<Integer> thePartitionIds, @Nullable LocalDate thePartitionDate) {
return new RequestPartitionId(null, toListOrNull(thePartitionIds), thePartitionDate);
}
@Nonnull

View File

@ -164,7 +164,7 @@ ca.uhn.fhir.jpa.patch.JsonPatchUtils.failedToApplyPatch=Failed to apply JSON pat
ca.uhn.fhir.jpa.graphql.JpaStorageServices.invalidGraphqlArgument=Unknown GraphQL argument "{0}". Value GraphQL argument for this type are: {1}
ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc.blacklistedResourceTypeForPartitioning=Resource type {0} can not be partitioned
ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc.nonDefaultPartitionSelectedForNonPartitionable=Resource type {0} can not be partitioned
ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc.unknownPartitionId=Unknown partition ID: {0}
ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc.unknownPartitionName=Unknown partition name: {0}

View File

@ -0,0 +1,6 @@
---
type: add
issue: 2407
title: "When using the JPA server in partitioned mode with a partition interceptor, the interceptor is now called even for
resource types that can not be placed in a non-default partition (e.g. SearchParameter, CodeSystem, etc.). The interceptor
may return null or default in this case, but can include a non-null partition date if needed."

View File

@ -82,6 +82,25 @@ A hook against the [`Pointcut.STORAGE_PARTITION_IDENTIFY_READ`](/hapi-fhir/apido
As of HAPI FHIR 5.3.0, the *Identify Partition for Read* hook method may return multiple partition names or IDs. If more than one partition is identified, the server will search in all identified partitions.
## Non-Partitionable Resources
Some resource types can not be placed in any partition other than the DEFAULT partition. When a resource of one of these types is being created, the *STORAGE_PARTITION_IDENTIFY_CREATE* pointcut is invoked, but the hook method must return [defaultPartition()](https://hapifhir.io/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/interceptor/model/RequestPartitionId.html#defaultPartition()). A partition date may optionally be included.
The following resource types may not be placed in any partition except the default partition:
* CapabilityStatement
* CodeSystem
* CompartmentDefinition
* ConceptMap
* NamingSystem
* OperationDefinition
* Questionnaire
* SearchParameter
* StructureDefinition
* StructureMap
* Subscription
* ValueSet
## Examples
See [Partition Interceptor Examples](./partition_interceptor_examples.html) for various samples of how partitioning interceptors can be set up.

View File

@ -50,7 +50,7 @@ import static ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster.hasHooks;
public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
private final HashSet<Object> myPartitioningBlacklist;
private final HashSet<Object> myNonPartitionableResourceNames;
@Autowired
private IInterceptorBroadcaster myInterceptorBroadcaster;
@ -62,25 +62,25 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
private PartitionSettings myPartitionSettings;
public RequestPartitionHelperSvc() {
myPartitioningBlacklist = new HashSet<>();
myNonPartitionableResourceNames = new HashSet<>();
// Infrastructure
myPartitioningBlacklist.add("Subscription");
myPartitioningBlacklist.add("SearchParameter");
myNonPartitionableResourceNames.add("Subscription");
myNonPartitionableResourceNames.add("SearchParameter");
// Validation and Conformance
myPartitioningBlacklist.add("StructureDefinition");
myPartitioningBlacklist.add("Questionnaire");
myPartitioningBlacklist.add("CapabilityStatement");
myPartitioningBlacklist.add("CompartmentDefinition");
myPartitioningBlacklist.add("OperationDefinition");
myNonPartitionableResourceNames.add("StructureDefinition");
myNonPartitionableResourceNames.add("Questionnaire");
myNonPartitionableResourceNames.add("CapabilityStatement");
myNonPartitionableResourceNames.add("CompartmentDefinition");
myNonPartitionableResourceNames.add("OperationDefinition");
// Terminology
myPartitioningBlacklist.add("ConceptMap");
myPartitioningBlacklist.add("CodeSystem");
myPartitioningBlacklist.add("ValueSet");
myPartitioningBlacklist.add("NamingSystem");
myPartitioningBlacklist.add("StructureMap");
myNonPartitionableResourceNames.add("ConceptMap");
myNonPartitionableResourceNames.add("CodeSystem");
myNonPartitionableResourceNames.add("ValueSet");
myNonPartitionableResourceNames.add("NamingSystem");
myNonPartitionableResourceNames.add("StructureMap");
}
@ -97,7 +97,7 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
if (myPartitionSettings.isPartitioningEnabled()) {
// Handle system requests
if ((theRequest == null && myPartitioningBlacklist.contains(theResourceType))) {
if ((theRequest == null && myNonPartitionableResourceNames.contains(theResourceType))) {
return RequestPartitionId.defaultPartition();
}
@ -128,10 +128,6 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
RequestPartitionId requestPartitionId;
if (myPartitionSettings.isPartitioningEnabled()) {
// Handle system requests
if ((theRequest == null && myPartitioningBlacklist.contains(theResourceType))) {
return RequestPartitionId.defaultPartition();
}
// Interceptor call: STORAGE_PARTITION_IDENTIFY_CREATE
HookParams params = new HookParams()
@ -140,6 +136,12 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
.addIfMatchesType(ServletRequestDetails.class, theRequest);
requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE, params);
// Handle system requests
boolean nonPartitionableResource = myNonPartitionableResourceNames.contains(theResourceType);
if (nonPartitionableResource && requestPartitionId == null) {
requestPartitionId = RequestPartitionId.defaultPartition();
}
String resourceName = myFhirContext.getResourceType(theResource);
validateSinglePartitionForCreate(requestPartitionId, resourceName, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE);
@ -271,8 +273,8 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc {
if ((theRequestPartitionId.hasPartitionIds() && !theRequestPartitionId.getPartitionIds().contains(null)) ||
(theRequestPartitionId.hasPartitionNames() && !theRequestPartitionId.getPartitionNames().contains(JpaConstants.DEFAULT_PARTITION_NAME))) {
if (myPartitioningBlacklist.contains(theResourceName)) {
String msg = myFhirContext.getLocalizer().getMessageSanitized(RequestPartitionHelperSvc.class, "blacklistedResourceTypeForPartitioning", theResourceName);
if (myNonPartitionableResourceNames.contains(theResourceName)) {
String msg = myFhirContext.getLocalizer().getMessageSanitized(RequestPartitionHelperSvc.class, "nonDefaultPartitionSelectedForNonPartitionable", theResourceName);
throw new UnprocessableEntityException(msg);
}

View File

@ -10,10 +10,13 @@ import ca.uhn.fhir.jpa.entity.PartitionEntity;
import ca.uhn.fhir.jpa.interceptor.ex.PartitionInterceptorReadAllPartitions;
import ca.uhn.fhir.jpa.interceptor.ex.PartitionInterceptorReadPartitionsBasedOnScopes;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.StringUtils;
@ -21,6 +24,7 @@ import org.apache.commons.lang3.Validate;
import org.hamcrest.Matchers;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.StructureDefinition;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -82,6 +86,51 @@ public class PartitioningInterceptorR4Test extends BaseJpaR4SystemTest {
}
@Test
public void testCreateNonPartionableResourceWithPartitionDate() {
myPartitionInterceptor.addCreatePartition(RequestPartitionId.defaultPartition(LocalDate.of(2021, 2, 22)));
StructureDefinition sd = new StructureDefinition();
sd.setUrl("http://foo");
myStructureDefinitionDao.create(sd);
runInTransaction(()->{
List<ResourceTable> resources = myResourceTableDao.findAll();
assertEquals(1, resources.size());
assertEquals(null, resources.get(0).getPartitionId().getPartitionId());
assertEquals(22, resources.get(0).getPartitionId().getPartitionDate().getDayOfMonth());
});
}
@Test
public void testCreateNonPartionableResourceWithNullPartitionReturned() {
myPartitionInterceptor.addCreatePartition(null);
StructureDefinition sd = new StructureDefinition();
sd.setUrl("http://foo");
myStructureDefinitionDao.create(sd);
runInTransaction(()->{
List<ResourceTable> resources = myResourceTableDao.findAll();
assertEquals(1, resources.size());
assertEquals(null, resources.get(0).getPartitionId());
});
}
@Test
public void testCreateNonPartionableResourceWithDisallowedPartitionReturned() {
myPartitionInterceptor.addCreatePartition(RequestPartitionId.fromPartitionName("FOO"));
StructureDefinition sd = new StructureDefinition();
sd.setUrl("http://foo");
try {
myStructureDefinitionDao.create(sd);
fail();
} catch (UnprocessableEntityException e) {
assertEquals("Resource type StructureDefinition can not be partitioned", e.getMessage());
}
}
/**
* Should fail if no interceptor is registered for the READ pointcut
*/

View File

@ -118,6 +118,21 @@ public class MultitenantServerR4Test extends BaseMultitenantResourceProviderR4Te
}
@Test
public void testCreateAndRead_NonPartitionableResource_DefaultTenant() {
// Create patients
IIdType idA = createResource("NamingSystem", withTenant(JpaConstants.DEFAULT_PARTITION_NAME), withStatus("draft"));
runInTransaction(() -> {
ResourceTable resourceTable = myResourceTableDao.findById(idA.getIdPartAsLong()).orElseThrow(() -> new IllegalStateException());
assertNull(resourceTable.getPartitionId());
});
}
@Test
public void testCreate_InvalidTenant() {

View File

@ -144,7 +144,7 @@ public interface ITestDataBuilder {
return createResource("Organization", theModifiers);
}
default IIdType createResource(String theResourceType, Consumer<IBaseResource>[] theModifiers) {
default IIdType createResource(String theResourceType, Consumer<IBaseResource>... theModifiers) {
IBaseResource resource = buildResource(theResourceType, theModifiers);
if (isNotBlank(resource.getIdElement().getValue())) {

View File

@ -1958,7 +1958,7 @@
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>animal-sniffer-maven-plugin</artifactId>
<version>1.19</version>
<version>1.20</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>