keycloak-aplcache

Merge pull request #4244 from pedroigor/master [KEYCLOAK-5702]

6/21/2017 4:19:36 PM

Details

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 18dae2a..e3e82ce 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<JSPolicyRepresentation> {
 
     private final JSPolicyProvider provider = new JSPolicyProvider(this::getEvaluatableScript);
-    private final Map<String, EvaluatableScriptAdapter> scripts = Collections.synchronizedMap(new HashMap<>());
+    private ScriptCache scriptCache;
 
     @Override
     public String getName() {
@@ -74,12 +70,14 @@ public class JSPolicyProviderFactory implements PolicyProviderFactory<JSPolicyRe
 
     @Override
     public void onRemove(final Policy policy, final AuthorizationProvider authorization) {
-        scripts.remove(policy.getId());
+        scriptCache.remove(policy.getId());
     }
 
     @Override
     public void init(Config.Scope config) {
-
+        int maxEntries = Integer.parseInt(config.get("cache-max-entries", "100"));
+        int maxAge = Integer.parseInt(config.get("cache-entry-max-age", "-1"));
+        scriptCache = new ScriptCache(maxEntries, maxAge);
     }
 
     @Override
@@ -98,7 +96,7 @@ public class JSPolicyProviderFactory implements PolicyProviderFactory<JSPolicyRe
     }
 
     private EvaluatableScriptAdapter getEvaluatableScript(final AuthorizationProvider authz, final Policy policy) {
-        return scripts.computeIfAbsent(policy.getId(), id -> {
+        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 PolicyProviderFactory<JSPolicyRe
     }
 
     private void updatePolicy(Policy policy, String code) {
+        scriptCache.remove(policy.getId());
         policy.putConfig("code", code);
     }
 }
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/ScriptCache.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/ScriptCache.java
new file mode 100644
index 0000000..4180db6
--- /dev/null
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/js/ScriptCache.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.policy.provider.js;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.LockSupport;
+import java.util.function.Function;
+
+import org.keycloak.scripting.EvaluatableScriptAdapter;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ScriptCache {
+
+    /**
+     * The load factor.
+     */
+    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
+
+    private final Map<String, CacheEntry> 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<String, CacheEntry>(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<String, EvaluatableScriptAdapter> 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;
+        }
+    }
+}