diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/MaxSizeSessionCacheDecorator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/MaxSizeSessionCacheDecorator.java new file mode 100644 index 0000000000..73d16e9f08 --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/MaxSizeSessionCacheDecorator.java @@ -0,0 +1,83 @@ +package org.hl7.fhir.validation.cli.services; + +import org.hl7.fhir.validation.ValidationEngine; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; + +public class MaxSizeSessionCacheDecorator implements SessionCache { + + public final int maxSize; + public final SessionCache sessionCache; + + private final List sessionIds; + + public MaxSizeSessionCacheDecorator(SessionCache sessionCache, int maxSize) { + this.sessionCache = sessionCache; + this.maxSize = maxSize; + this.sessionIds = new ArrayList<>(sessionCache.getSessionIds()); + if (this.sessionIds.size() > maxSize) { + throw new IllegalArgumentException("Session cache size exceeds the maximum size"); + } + } + + @Override + public String cacheSession(ValidationEngine validationEngine) { + checkSizeAndMaintainMax(null); + String key = sessionCache.cacheSession(validationEngine); + sessionIds.add(key); + return key; + } + + @Override + public String cacheSession(Supplier validationEngineSupplier) { + checkSizeAndMaintainMax(null); + ValidationEngine validationEngine = validationEngineSupplier.get(); + return sessionCache.cacheSession(validationEngine); + } + + private void checkSizeAndMaintainMax(String keyToAdd) { + if (keyToAdd != null || sessionCache.sessionExists(keyToAdd)) { + return; + } + Set sessionIds = sessionCache.getSessionIds(); + //Sync our tracked keys, in case the underlying cache has changed + this.sessionIds.removeIf(key -> !sessionIds.contains(key)); + + if (this.sessionIds.size() >= maxSize) { + final String key = this.sessionIds.remove(0); + sessionCache.removeSession(key); + } + } + + @Override + public String cacheSession(String sessionId, ValidationEngine validationEngine) { + checkSizeAndMaintainMax(sessionId); + cacheSession(sessionId, validationEngine); + return sessionCache.cacheSession( + sessionId, validationEngine); + } + + @Override + public boolean sessionExists(String sessionId) { + return sessionCache.sessionExists(sessionId); + } + + @Override + public ValidationEngine removeSession(String sessionId) { + sessionIds.remove(sessionId); + return sessionCache.removeSession(sessionId); + } + + @Override + public ValidationEngine fetchSessionValidatorEngine(String sessionId) { + return sessionCache.fetchSessionValidatorEngine(sessionId); + } + + @Override + public Set getSessionIds() { + return sessionCache.getSessionIds(); + } +} diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/PassiveExpiringSessionCache.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/PassiveExpiringSessionCache.java index b3a9a18b6e..d68a7ae3a2 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/PassiveExpiringSessionCache.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/PassiveExpiringSessionCache.java @@ -3,6 +3,7 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import org.apache.commons.collections4.map.PassiveExpiringMap; import org.hl7.fhir.validation.ValidationEngine; @@ -44,6 +45,12 @@ public String cacheSession(ValidationEngine validationEngine) { return generatedId; } + @Override + public String cacheSession(Supplier validationEngineSupplier) { + ValidationEngine engine = validationEngineSupplier.get(); + return this.cacheSession(engine); + } + /** * Stores the initialized {@link ValidationEngine} in the cache with the passed in id as the key. If a null key is * passed in, a new key is generated and returned. @@ -96,6 +103,11 @@ public boolean sessionExists(String sessionId) { return cachedSessions.containsKey(sessionId); } + @Override + public ValidationEngine removeSession(String sessionId) { + return cachedSessions.remove(sessionId); + } + /** * Returns the stored {@link ValidationEngine} associated with the passed in session id, if one such instance exists. * @param sessionId The {@link String} session id. diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/SessionCache.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/SessionCache.java index 11b5afd0dd..73be98c49e 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/SessionCache.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/SessionCache.java @@ -1,6 +1,7 @@ package org.hl7.fhir.validation.cli.services; import java.util.Set; +import java.util.function.Supplier; import org.hl7.fhir.validation.ValidationEngine; @@ -14,6 +15,14 @@ public interface SessionCache { */ String cacheSession(ValidationEngine validationEngine); + /** + * Uses the passed {@link Supplier} to generate a {@link ValidationEngine} and add it to the cache. Returns the + * session id that will be associated with the generated instance. + * @param validationEngineSupplier {@link Supplier} of {@link ValidationEngine} + * @return The {@link String} id associated with the stored instance. + */ + String cacheSession(Supplier validationEngineSupplier); + /** * Stores the initialized {@link ValidationEngine} in the cache with the passed in id as the key. If a null key is * passed in, a new key is generated and returned. @@ -23,9 +32,6 @@ public interface SessionCache { */ String cacheSession(String sessionId, ValidationEngine validationEngine); - - - /** * Checks if the passed in {@link String} id exists in the set of stored session id. * @param sessionId The {@link String} id to search for. @@ -33,6 +39,13 @@ public interface SessionCache { */ boolean sessionExists(String sessionId); + /** + * Removes the {@link ValidationEngine} associated with the passed in session id. + * @param sessionId The {@link String} session id. + * @return The {@link ValidationEngine} instance that was removed. + */ + ValidationEngine removeSession(String sessionId); + /** * Returns the stored {@link ValidationEngine} associated with the passed in session id, if one such instance exists. * @param sessionId The {@link String} session id. diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java index cab82c48c1..f85a01122b 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/validation/cli/services/ValidationService.java @@ -489,9 +489,20 @@ public String initializeValidator(CliContext cliContext, String definitions, Tim if (sessionId != null) { System.out.println("No such cached session exists for session id " + sessionId + ", re-instantiating validator."); } - ValidationEngine validationEngine = getValidationEngineFromCliContext(cliContext, definitions, tt); - sessionId = sessionCache.cacheSession(validationEngine); - System.out.println("Cached new session. Cache size = " + sessionCache.getSessionIds().size()); + + // Send a supplier instead of instantiating. This will permit the sessionCache to manage existing sessions (drop + // or expire old sessions as needed) + sessionId = sessionCache.cacheSession(() -> { + + try { + return buildValidationEngine(cliContext, definitions, tt); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + + }); } else { System.out.println("Cached session exists for session id " + sessionId + ", returning stored validator session id. Cache size = " + sessionCache.getSessionIds().size()); } diff --git a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/cli/services/MaxSizeSessionCacheDecoratorTest.java b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/cli/services/MaxSizeSessionCacheDecoratorTest.java new file mode 100644 index 0000000000..0f04d4d69b --- /dev/null +++ b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/cli/services/MaxSizeSessionCacheDecoratorTest.java @@ -0,0 +1,54 @@ +package org.hl7.fhir.validation.cli.services; + +import org.hl7.fhir.validation.ValidationEngine; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.ArrayList; + + +import static org.mockito.Mockito.mock; + +public class MaxSizeSessionCacheDecoratorTest { + + private List getMockedEngines(int count) { + List engines = new ArrayList<>(); + for (int i = 0; i < count; i++) { + engines.add(mock(ValidationEngine.class)); + } + return engines; + } + + private LinkedHashMap addMockedEngines(SessionCache cache, int count) { + LinkedHashMap engineMap = new LinkedHashMap<>(); + List engines = getMockedEngines(count); + for (ValidationEngine engine : engines) { + String key = cache.cacheSession(engine); + engineMap.put(key, engine); + } + return engineMap; + } + + @Test + public void trivialCase() { + + MaxSizeSessionCacheDecorator maxSizeSessionCacheDecorator = new MaxSizeSessionCacheDecorator(new PassiveExpiringSessionCache(), 4); + + LinkedHashMap initialEngines = addMockedEngines(maxSizeSessionCacheDecorator, 3); + + Assertions.assertEquals(3, maxSizeSessionCacheDecorator.getSessionIds().size()); + + List newEngines = getMockedEngines(2); + + for (ValidationEngine engine : newEngines) { + maxSizeSessionCacheDecorator.cacheSession(engine); + } + + Assertions.assertEquals(4, maxSizeSessionCacheDecorator.getSessionIds().size()); + + Assertions.assertTrue(maxSizeSessionCacheDecorator.getSessionIds().contains() + } + +}