diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java index 18dae2a2a0..e3e82ce675 100644 --- a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java +++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java @@ -1,9 +1,5 @@ package org.keycloak.authorization.policy.provider.js; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - import org.keycloak.Config; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.model.Policy; @@ -24,7 +20,7 @@ import org.keycloak.scripting.ScriptingProvider; public class JSPolicyProviderFactory implements PolicyProviderFactory { private final JSPolicyProvider provider = new JSPolicyProvider(this::getEvaluatableScript); - private final Map scripts = Collections.synchronizedMap(new HashMap<>()); + private ScriptCache scriptCache; @Override public String getName() { @@ -74,12 +70,14 @@ public class JSPolicyProviderFactory implements PolicyProviderFactory { + return scriptCache.computeIfAbsent(policy.getId(), id -> { final ScriptingProvider scripting = authz.getKeycloakSession().getProvider(ScriptingProvider.class); ScriptModel script = getScriptModel(policy, authz.getRealm(), scripting); return scripting.prepareEvaluatableScript(script); @@ -115,6 +113,7 @@ public class JSPolicyProviderFactory implements PolicyProviderFactoryPedro Igor + */ +public class ScriptCache { + + /** + * The load factor. + */ + private static final float DEFAULT_LOAD_FACTOR = 0.75f; + + private final Map cache; + + private final AtomicBoolean writing = new AtomicBoolean(false); + + private final long maxAge; + + /** + * Creates a new instance. + * + * @param maxEntries the maximum number of entries to keep in the cache + */ + public ScriptCache(int maxEntries) { + this(maxEntries, -1); + } + + /** + * Creates a new instance. + * + * @param maxEntries the maximum number of entries to keep in the cache + * @param maxAge the time in milliseconds that an entry can stay in the cache. If {@code -1}, entries never expire + */ + public ScriptCache(final int maxEntries, long maxAge) { + cache = new LinkedHashMap(16, DEFAULT_LOAD_FACTOR, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return cache.size() > maxEntries; + } + }; + this.maxAge = maxAge; + } + + public EvaluatableScriptAdapter computeIfAbsent(String id, Function function) { + try { + if (parkForWriteAndCheckInterrupt()) { + return null; + } + + CacheEntry entry = cache.computeIfAbsent(id, key -> new CacheEntry(key, function.apply(id), maxAge)); + + if (entry != null) { + return entry.value(); + } + + return null; + } finally { + writing.lazySet(false); + } + } + + public EvaluatableScriptAdapter get(String uri) { + if (parkForReadAndCheckInterrupt()) { + return null; + } + + CacheEntry cached = cache.get(uri); + + if (cached != null) { + return removeIfExpired(cached); + } + + return null; + } + + public void remove(String key) { + try { + if (parkForWriteAndCheckInterrupt()) { + return; + } + + cache.remove(key); + } finally { + writing.lazySet(false); + } + } + + private EvaluatableScriptAdapter removeIfExpired(CacheEntry cached) { + if (cached == null) { + return null; + } + + if (cached.isExpired()) { + remove(cached.key()); + return null; + } + + return cached.value(); + } + + private boolean parkForWriteAndCheckInterrupt() { + while (!writing.compareAndSet(false, true)) { + LockSupport.parkNanos(1L); + if (Thread.interrupted()) { + return true; + } + } + return false; + } + + private boolean parkForReadAndCheckInterrupt() { + while (writing.get()) { + LockSupport.parkNanos(1L); + if (Thread.interrupted()) { + return true; + } + } + return false; + } + + private static final class CacheEntry { + + final String key; + final EvaluatableScriptAdapter value; + final long expiration; + + CacheEntry(String key, EvaluatableScriptAdapter value, long maxAge) { + this.key = key; + this.value = value; + if(maxAge == -1) { + expiration = -1; + } else { + expiration = System.currentTimeMillis() + maxAge; + } + } + + String key() { + return key; + } + + EvaluatableScriptAdapter value() { + return value; + } + + boolean isExpired() { + return expiration != -1 ? System.currentTimeMillis() > expiration : false; + } + } +}