Script Mapper Performance Issues

Closes #11005
This commit is contained in:
mposolda 2022-03-30 08:30:30 +02:00 committed by Pedro Igor
parent 834a276767
commit fb81242658
3 changed files with 56 additions and 12 deletions

View file

@ -24,6 +24,7 @@ import javax.script.ScriptEngineManager;
import javax.script.ScriptException; import javax.script.ScriptException;
import org.keycloak.models.ScriptModel; import org.keycloak.models.ScriptModel;
import org.keycloak.services.ServicesLogger;
/** /**
* A {@link ScriptingProvider} that uses a {@link ScriptEngineManager} to evaluate scripts with a {@link ScriptEngine}. * A {@link ScriptingProvider} that uses a {@link ScriptEngineManager} to evaluate scripts with a {@link ScriptEngine}.
@ -32,14 +33,10 @@ import org.keycloak.models.ScriptModel;
*/ */
public class DefaultScriptingProvider implements ScriptingProvider { public class DefaultScriptingProvider implements ScriptingProvider {
private final ScriptEngineManager scriptEngineManager; private final DefaultScriptingProviderFactory factory;
DefaultScriptingProvider(ScriptEngineManager scriptEngineManager) { DefaultScriptingProvider(DefaultScriptingProviderFactory factory) {
if (scriptEngineManager == null) { this.factory = factory;
throw new IllegalStateException("scriptEngineManager must not be null!");
}
this.scriptEngineManager = scriptEngineManager;
} }
/** /**
@ -69,7 +66,7 @@ public class DefaultScriptingProvider implements ScriptingProvider {
throw new IllegalArgumentException("script must not be null or empty"); throw new IllegalArgumentException("script must not be null or empty");
} }
ScriptEngine engine = createPreparedScriptEngine(scriptModel); ScriptEngine engine = getPreparedScriptEngine(scriptModel);
if (engine instanceof Compilable) { if (engine instanceof Compilable) {
return new CompiledEvaluatableScriptAdapter(scriptModel, tryCompile(scriptModel, (Compilable) engine)); return new CompiledEvaluatableScriptAdapter(scriptModel, tryCompile(scriptModel, (Compilable) engine));
@ -99,13 +96,26 @@ public class DefaultScriptingProvider implements ScriptingProvider {
/** /**
* Looks-up a {@link ScriptEngine} with prepared {@link Bindings} for the given {@link ScriptModel Script}. * Looks-up a {@link ScriptEngine} with prepared {@link Bindings} for the given {@link ScriptModel Script}.
*/ */
private ScriptEngine createPreparedScriptEngine(ScriptModel script) { private ScriptEngine getPreparedScriptEngine(ScriptModel script) {
// Try to lookup shared engine in the cache first
if (factory.isEnableScriptEngineCache()) {
ScriptEngine scriptEngine = factory.getScriptEngineCache().get(script.getMimeType());
if (scriptEngine != null) return scriptEngine;
}
ScriptEngine scriptEngine = lookupScriptEngineFor(script); ScriptEngine scriptEngine = lookupScriptEngineFor(script);
if (scriptEngine == null) { if (scriptEngine == null) {
throw new IllegalStateException("Could not find ScriptEngine for script: " + script); throw new IllegalStateException("Could not find ScriptEngine for script: " + script);
} }
ServicesLogger.LOGGER.scriptEngineCreated(scriptEngine.getFactory().getEngineName(), scriptEngine.getFactory().getEngineVersion(), script.getMimeType());
// Nashorn scriptEngine is ok to cache and share across multiple threads
if (factory.isEnableScriptEngineCache()) {
factory.getScriptEngineCache().put(script.getMimeType(), scriptEngine);
}
return scriptEngine; return scriptEngine;
} }
@ -116,7 +126,7 @@ public class DefaultScriptingProvider implements ScriptingProvider {
ClassLoader cl = Thread.currentThread().getContextClassLoader(); ClassLoader cl = Thread.currentThread().getContextClassLoader();
try { try {
Thread.currentThread().setContextClassLoader(DefaultScriptingProvider.class.getClassLoader()); Thread.currentThread().setContextClassLoader(DefaultScriptingProvider.class.getClassLoader());
return scriptEngineManager.getEngineByMimeType(script.getMimeType()); return factory.getScriptEngineManager().getEngineByMimeType(script.getMimeType());
} }
finally { finally {
Thread.currentThread().setContextClassLoader(cl); Thread.currentThread().setContextClassLoader(cl);

View file

@ -16,10 +16,16 @@
*/ */
package org.keycloak.scripting; package org.keycloak.scripting;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.jboss.logging.Logger;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.protocol.oidc.OIDCWellKnownProviderFactory;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager; import javax.script.ScriptEngineManager;
/** /**
@ -27,20 +33,40 @@ import javax.script.ScriptEngineManager;
*/ */
public class DefaultScriptingProviderFactory implements ScriptingProviderFactory { public class DefaultScriptingProviderFactory implements ScriptingProviderFactory {
private static final Logger logger = Logger.getLogger(DefaultScriptingProviderFactory.class);
static final String ID = "script-based-auth"; static final String ID = "script-based-auth";
private ScriptEngineManager scriptEngineManager; private ScriptEngineManager scriptEngineManager;
private boolean enableScriptEngineCache;
// Key is mime-type. Value is engine for the particular mime-type. Cache can be used when the scriptEngine can be shared across multiple threads / requests (which is the case for nashorn)
private Map<String, ScriptEngine> scriptEngineCache;
@Override @Override
public ScriptingProvider create(KeycloakSession session) { public ScriptingProvider create(KeycloakSession session) {
lazyInit(); lazyInit();
return new DefaultScriptingProvider(scriptEngineManager); return new DefaultScriptingProvider(this);
} }
@Override @Override
public void init(Config.Scope config) { public void init(Config.Scope config) {
//NOOP this.enableScriptEngineCache = config.getBoolean("enable-script-engine-cache", true);
logger.debugf("Enable script engine cache: %b", this.enableScriptEngineCache);
}
ScriptEngineManager getScriptEngineManager() {
return scriptEngineManager;
}
boolean isEnableScriptEngineCache() {
return enableScriptEngineCache;
}
Map<String, ScriptEngine> getScriptEngineCache() {
return scriptEngineCache;
} }
@Override @Override
@ -63,6 +89,9 @@ public class DefaultScriptingProviderFactory implements ScriptingProviderFactory
synchronized (this) { synchronized (this) {
if (scriptEngineManager == null) { if (scriptEngineManager == null) {
scriptEngineManager = new ScriptEngineManager(); scriptEngineManager = new ScriptEngineManager();
if (enableScriptEngineCache) {
scriptEngineCache = new ConcurrentHashMap<>();
}
} }
} }
} }

View file

@ -462,4 +462,9 @@ public interface ServicesLogger extends BasicLogger {
@LogMessage(level = ERROR) @LogMessage(level = ERROR)
@Message(id=105, value="Response_mode 'query.jwt' is allowed only when the authorization response token is encrypted") @Message(id=105, value="Response_mode 'query.jwt' is allowed only when the authorization response token is encrypted")
void responseModeQueryJwtNotAllowed(); void responseModeQueryJwtNotAllowed();
@LogMessage(level = INFO)
@Message(id=106, value="Created script engine '%s', version '%s' for the mime type '%s'")
@Once
void scriptEngineCreated(String engineName, String engineVersion, String mimeType);
} }