Skip to content

Commit 7a58f0e

Browse files
LEGLINK-969: Validation: fetchValueSet requests full ValueSet expansions with _summary=false, transferring hundreds of thousands of concepts per bundle (#1834)
Fetch value sets in summary mode Co-authored-by: John Britton <johnbritton@users.noreply.github.com>
1 parent b3db811 commit 7a58f0e

2 files changed

Lines changed: 103 additions & 2 deletions

File tree

Java/validation/src/main/java/com/lantanagroup/link/validation/providers/RemoteTermServiceValidation.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,9 +261,26 @@ private IValidationSupport.ConceptDesignation createConceptDesignation(org.hl7.f
261261
return conceptDesignation;
262262
}
263263

264+
/**
265+
* Summary mode used when HAPI asks us to resolve a bound ValueSet.
266+
* <p>
267+
* Deliberately {@link SummaryEnum#TRUE}: {@code _summary=false} is the condition that makes the Link
268+
* terminology service enumerate every code of the value set into {@code ValueSet.expansion.contains}
269+
* (see {@code Terminology/Services/FhirService.cs}), which for large value sets is the client-side
270+
* trigger for terminology-service memory exhaustion. The expansion is not read by this class -- when
271+
* the resolved ValueSet carries a canonical URL, {@link #validateCodeInValueSet} uses only that URL and
272+
* routes validation through {@code $validate-code}. The Link terminology service's summary handling is
273+
* bespoke rather than element-filtering: at {@code _summary=true} it still returns the stored ValueSet
274+
* with its {@code compose} intact, so downstream chain members (e.g. HAPI's in-memory terminology
275+
* support) can still expand it in-process.
276+
* <p>
277+
* Change this only with before/after comparison of validation results -- see
278+
* {@code RemoteTermServiceValidationTest#fetchValueSet_requestsSummaryModeTrue}.
279+
*/
280+
static final SummaryEnum FETCH_VALUE_SET_SUMMARY_MODE = SummaryEnum.TRUE;
281+
264282
public IBaseResource fetchValueSet(String theValueSetUrl) {
265-
SummaryEnum summaryParam = SummaryEnum.FALSE;
266-
return this.fetchValueSet(theValueSetUrl, summaryParam);
283+
return this.fetchValueSet(theValueSetUrl, FETCH_VALUE_SET_SUMMARY_MODE);
267284
}
268285

269286
@Nullable

Java/validation/src/test/java/com/lantanagroup/link/validation/providers/RemoteTermServiceValidationTest.java

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@
33
import ca.uhn.fhir.context.FhirContext;
44
import ca.uhn.fhir.context.support.IValidationSupport.CodeValidationResult;
55
import ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity;
6+
import ca.uhn.fhir.rest.api.SummaryEnum;
67
import ca.uhn.fhir.rest.client.api.IGenericClient;
8+
import ca.uhn.fhir.rest.gclient.ICriterion;
79
import ca.uhn.fhir.rest.gclient.IOperation;
810
import ca.uhn.fhir.rest.gclient.IOperationUnnamed;
911
import ca.uhn.fhir.rest.gclient.IOperationUntyped;
1012
import ca.uhn.fhir.rest.gclient.IOperationUntypedWithInputAndPartialOutput;
13+
import ca.uhn.fhir.rest.gclient.IQuery;
14+
import ca.uhn.fhir.rest.gclient.IUntypedQuery;
1115
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
1216
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
1317
import com.lantanagroup.link.shared.utils.LogUtils;
18+
import org.hl7.fhir.instance.model.api.IBaseBundle;
19+
import org.hl7.fhir.instance.model.api.IBaseResource;
1420
import org.hl7.fhir.r4.model.*;
1521
import org.junit.jupiter.api.Test;
1622
import org.mockito.ArgumentCaptor;
@@ -66,6 +72,34 @@ private IOperation stubClientChain(RemoteTermServiceValidation subject, Object e
6672
return operation;
6773
}
6874

75+
/**
76+
* Stubs the fluent {@code client.search().forResource(..).where(..).summaryMode(..).returnBundle(..).execute()}
77+
* chain so that {@code execute()} returns the supplied bundle. Returns the query mock so callers can capture
78+
* the summary mode requested.
79+
*/
80+
@SuppressWarnings({"rawtypes", "unchecked"})
81+
private IQuery stubSearchChain(RemoteTermServiceValidation subject, IBaseBundle executeResult) {
82+
IGenericClient client = mock(IGenericClient.class);
83+
IUntypedQuery untypedQuery = mock(IUntypedQuery.class);
84+
IQuery query = mock(IQuery.class);
85+
86+
doReturn(client).when(subject).provideClient();
87+
when(client.search()).thenReturn(untypedQuery);
88+
when(untypedQuery.forResource(anyString())).thenReturn(query);
89+
when(query.where(any(ICriterion.class))).thenReturn(query);
90+
when(query.summaryMode(any(SummaryEnum.class))).thenReturn(query);
91+
when(query.returnBundle(any(Class.class))).thenReturn(query);
92+
when(query.execute()).thenReturn(executeResult);
93+
return query;
94+
}
95+
96+
private Bundle searchsetContaining(Resource resource) {
97+
Bundle bundle = new Bundle();
98+
bundle.setType(Bundle.BundleType.SEARCHSET);
99+
bundle.addEntry().setResource(resource);
100+
return bundle;
101+
}
102+
69103
private Parameters validateCodeResponse(boolean result, String paramName, String paramValue) {
70104
Parameters params = new Parameters();
71105
params.addParameter().setName("result").setValue(new BooleanType(result));
@@ -298,6 +332,56 @@ void validateCodeInValueSet_withoutCanonicalUrl_sendsValueSetInlineAndDetectsIna
298332
verify(operation).onType("ValueSet");
299333
}
300334

335+
// ---------- fetchValueSet summary mode ----------
336+
// fetchValueSet(String) is the public IValidationSupport hook HAPI calls to resolve a bound ValueSet.
337+
// Requesting _summary=false makes the Link terminology service enumerate every code of the value set
338+
// into ValueSet.expansion.contains, which is the client-side trigger for terminology-service memory
339+
// exhaustion -- and the expansion is never read (validateCodeInValueSet uses only the canonical URL).
340+
// These tests pin the summary mode so it cannot be silently changed back.
341+
342+
@Test
343+
@SuppressWarnings("rawtypes")
344+
void fetchValueSet_requestsSummaryModeTrue() {
345+
RemoteTermServiceValidation subject = newSpy();
346+
ValueSet valueSet = new ValueSet();
347+
valueSet.setUrl(VALUE_SET_URL);
348+
IQuery query = stubSearchChain(subject, searchsetContaining(valueSet));
349+
350+
IBaseResource result = subject.fetchValueSet(VALUE_SET_URL);
351+
352+
ArgumentCaptor<SummaryEnum> summaryMode = ArgumentCaptor.forClass(SummaryEnum.class);
353+
verify(query).summaryMode(summaryMode.capture());
354+
assertEquals(SummaryEnum.TRUE, summaryMode.getValue(),
355+
"fetchValueSet must not request _summary=false: that makes the terminology service "
356+
+ "enumerate the full expansion for a resource whose expansion is never read.");
357+
assertSame(valueSet, result);
358+
}
359+
360+
@Test
361+
@SuppressWarnings("rawtypes")
362+
void fetchValueSet_noMatch_returnsNull() {
363+
RemoteTermServiceValidation subject = newSpy();
364+
Bundle empty = new Bundle();
365+
empty.setType(Bundle.BundleType.SEARCHSET);
366+
IQuery query = stubSearchChain(subject, empty);
367+
368+
assertNull(subject.fetchValueSet(VALUE_SET_URL));
369+
verify(query).summaryMode(SummaryEnum.TRUE);
370+
}
371+
372+
@Test
373+
@SuppressWarnings("rawtypes")
374+
void invokeIsValueSetSupported_requestsSummaryModeTrue() {
375+
// The existence check has always used _summary=true; fetchValueSet(String) now matches it.
376+
RemoteTermServiceValidation subject = newSpy();
377+
ValueSet valueSet = new ValueSet();
378+
valueSet.setUrl(VALUE_SET_URL);
379+
IQuery query = stubSearchChain(subject, searchsetContaining(valueSet));
380+
381+
assertTrue(subject.invokeIsValueSetSupported(VALUE_SET_URL));
382+
verify(query).summaryMode(SummaryEnum.TRUE);
383+
}
384+
301385
// ---------- isCodeSystemSupported / isValueSetSupported delegation to cache ----------
302386
// These methods are hit per-system per-chain-traversal by HAPI's ValidationSupportChain, so the
303387
// remote lookup has to go through ValidationCacheService. The tests below verify the delegation

0 commit comments

Comments
 (0)