Skip to content

Commit aea260c

Browse files
Boereckjamesagnew
andauthored
Fixing concurrency issue in WorkerContextValidationSupportAdapter (#6554) (#6648)
* Fixing concurrency issue in VersionSpecificWorkerContextWrapper (#6554) When validating if a code is valid against a ValueSet, a potentially cached (and therefore shared) result object was mutated. This mutation is now done on a copy of the result object. * Added test case for concurrent validateCode with issue in VS and CS The added test case checks if concurrent validation of a code, that is neither in the ValueSet nor in the CodeSystem creates a validation result with the CS issue added to the validation result correctly. * Credit for #6648 --------- Co-authored-by: James Agnew <[email protected]>
1 parent 15662b8 commit aea260c

File tree

3 files changed

+84
-0
lines changed

3 files changed

+84
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
type: fix
3+
issue: 6648
4+
title: "When performing validation with high concurrency, code validations could
5+
fail with a ConcurrentModificationException. Thanks to Max Bureck for the contribution!"

hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/WorkerContextValidationSupportAdapter.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import ca.uhn.fhir.context.support.ConceptValidationOptions;
66
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
77
import ca.uhn.fhir.context.support.IValidationSupport;
8+
import ca.uhn.fhir.context.support.IValidationSupport.BaseConceptProperty;
9+
import ca.uhn.fhir.context.support.IValidationSupport.CodeValidationIssue;
810
import ca.uhn.fhir.context.support.ValidationSupportContext;
911
import ca.uhn.fhir.i18n.Msg;
1012
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
@@ -898,6 +900,7 @@ private IValidationSupport.CodeValidationResult validateCodeInValueSet(
898900
final boolean valueSetResultContainsInvalidDisplay = result.getIssues().stream()
899901
.anyMatch(WorkerContextValidationSupportAdapter::hasInvalidDisplayDetailCode);
900902
if (codeSystemResult != null) {
903+
result = copyCodeValidationResult(result);
901904
for (IValidationSupport.CodeValidationIssue codeValidationIssue : codeSystemResult.getIssues()) {
902905
/* Value set validation should already have checked the display name. If we get INVALID_DISPLAY
903906
issues from code system validation, they will only repeat what was already caught.
@@ -911,6 +914,33 @@ private IValidationSupport.CodeValidationResult validateCodeInValueSet(
911914
return result;
912915
}
913916

917+
private IValidationSupport.CodeValidationResult copyCodeValidationResult(
918+
IValidationSupport.CodeValidationResult toCopy) {
919+
IValidationSupport.CodeValidationResult result = new IValidationSupport.CodeValidationResult();
920+
921+
List<CodeValidationIssue> issues = toCopy.getIssues();
922+
List<BaseConceptProperty> properties = toCopy.getProperties();
923+
924+
result.setCode(toCopy.getCode())
925+
.setCodeSystemName(toCopy.getCodeSystemName())
926+
.setCodeSystemVersion(toCopy.getCodeSystemVersion())
927+
.setDisplay(toCopy.getDisplay())
928+
.setIssues(copyList(issues))
929+
.setMessage(toCopy.getMessage())
930+
.setSeverity(toCopy.getSeverity())
931+
.setSourceDetails(toCopy.getSourceDetails())
932+
.setProperties(copyList(properties));
933+
return result;
934+
}
935+
936+
private <T> List<T> copyList(List<T> issues) {
937+
if (issues == null) {
938+
return null;
939+
} else {
940+
return new ArrayList<>(issues);
941+
}
942+
}
943+
914944
@Nonnull
915945
private ValidationSupportContext newValidationSupportContext() {
916946
return new ValidationSupportContext(myValidationSupport);

hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/validator/WorkerContextValidationSupportAdapterTest.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,26 @@
33
import ca.uhn.fhir.context.FhirContext;
44
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
55
import ca.uhn.fhir.context.support.IValidationSupport;
6+
import ca.uhn.fhir.context.support.IValidationSupport.CodeValidationIssue;
7+
import ca.uhn.fhir.context.support.IValidationSupport.CodeValidationIssueCode;
8+
import ca.uhn.fhir.context.support.IValidationSupport.CodeValidationIssueCoding;
9+
import ca.uhn.fhir.context.support.IValidationSupport.CodeValidationResult;
10+
import ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity;
611
import ca.uhn.fhir.context.support.ValidationSupportContext;
712
import ca.uhn.fhir.fhirpath.BaseValidationTestWithInlineMocks;
813
import org.hl7.fhir.r4.model.StructureDefinition;
14+
import org.hl7.fhir.r5.model.Coding;
915
import org.hl7.fhir.r5.model.PackageInformation;
1016
import org.hl7.fhir.r5.model.Resource;
1117
import org.hl7.fhir.r5.model.ValueSet;
18+
import org.hl7.fhir.r5.terminologies.utilities.ValidationResult;
1219
import org.hl7.fhir.utilities.validation.ValidationOptions;
1320
import org.junit.jupiter.api.Test;
1421

22+
import java.util.ArrayList;
1523
import java.util.List;
24+
import java.util.concurrent.ForkJoinPool;
25+
import java.util.concurrent.ForkJoinTask;
1626

1727
import static org.assertj.core.api.Assertions.assertThat;
1828
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -109,6 +119,45 @@ public void validateCode_codeNotInValueSet_doesNotResolveSystem() {
109119
verify(myValidationSupport, never()).validateCode(any(), any(), any(), any(), any(), any());
110120
}
111121

122+
@Test
123+
public void validateCode_codeNotInValueSet_codeNotInSystem_concurrent() {
124+
// setup
125+
setupValidation();
126+
127+
String system = "http://codesystems.com/system";
128+
String badCode = "code3";
129+
130+
CodeValidationResult codeInValuesetValResult = new CodeValidationResult().setCode(badCode).setCodeSystemName(system).setMessage("Bad code in VS");
131+
when(myValidationSupport.validateCodeInValueSet(any(), any(), eq(system), eq(badCode), any(), any())).thenReturn(codeInValuesetValResult);
132+
133+
// We expect this code validation result as an additional result on the validation, if code is in value set
134+
String issueMessage = "Code not in here!";
135+
CodeValidationIssue codeValidationIssue = new CodeValidationIssue(issueMessage, IssueSeverity.ERROR, CodeValidationIssueCode.NOT_FOUND, CodeValidationIssueCoding.NOT_FOUND);
136+
CodeValidationResult codeValResult = new CodeValidationResult().setMessage("Bad code in CS").setCode(badCode).setCodeSystemName(system).setSeverity(IssueSeverity.ERROR).addIssue(codeValidationIssue);
137+
when(myValidationSupport.validateCode(any(), any(), eq("http://codesystems.com/system"), eq(badCode) , any(), eq(null))).thenReturn(codeValResult);
138+
139+
ForkJoinPool pool = ForkJoinPool.commonPool();
140+
List<ForkJoinTask<?>> futures = new ArrayList<>();
141+
for(int i=0; i<1000; i++) {
142+
ForkJoinTask<?> f = pool.submit(() -> {
143+
org.hl7.fhir.r5.model.ValueSet valueSet = new ValueSet();
144+
valueSet.getCompose().addInclude().setSystem(system).addConcept().setCode("code0");
145+
valueSet.getCompose().addInclude().setSystem("http://codesystems.com/system2").addConcept().setCode("code2");
146+
147+
ValidationResult result = myWorkerContextWrapper.validateCode(new ValidationOptions(), new Coding(system, badCode, ""), valueSet);
148+
149+
// here we check if the additional code validation result was added
150+
assertThat(result.getIssues()).hasSize(1);
151+
assertThat(result.getIssues().get(0).getDiagnostics()).isEqualTo(issueMessage);
152+
});
153+
futures.add(f);
154+
}
155+
// test if there was no assertion exception for any invocation
156+
for(ForkJoinTask<?> f : futures) {
157+
f.join();
158+
}
159+
}
160+
112161
@Test
113162
public void isPrimitive_primitive() {
114163
// setup

0 commit comments

Comments
 (0)