diff --git a/common/source/java/ch/systemsx/cisd/common/jython/evaluator/Evaluator.java b/common/source/java/ch/systemsx/cisd/common/jython/evaluator/Evaluator.java index 6bc76ad18126db514bae65f14ce089ec18d8300a..bf7a8a6c8ccc874052c5b0d5f5e4460f36ac5050 100644 --- a/common/source/java/ch/systemsx/cisd/common/jython/evaluator/Evaluator.java +++ b/common/source/java/ch/systemsx/cisd/common/jython/evaluator/Evaluator.java @@ -19,6 +19,7 @@ package ch.systemsx.cisd.common.jython.evaluator; import java.math.BigInteger; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -516,7 +517,7 @@ public final class Evaluator { results.add(key.toString()); } - return results; + return Collections.unmodifiableCollection(results); } } diff --git a/common/source/java/ch/systemsx/cisd/common/jython/evaluator/EvaluatorCache.java b/common/source/java/ch/systemsx/cisd/common/jython/evaluator/EvaluatorCache.java deleted file mode 100644 index 36085987809f228967b91f1741457959aebab5af..0000000000000000000000000000000000000000 --- a/common/source/java/ch/systemsx/cisd/common/jython/evaluator/EvaluatorCache.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2012 ETH Zuerich, CISD - * - * 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 ch.systemsx.cisd.common.jython.evaluator; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -/** - * Cache for Evaluator instances. Enables considerable speedup of python expressions, as the python - * code needs to be loaded into an interpreter only once (per thread; every thread has a cache of - * its own). - * - * @author anttil - */ -public class EvaluatorCache -{ - - /** - * ThreadLocal cache of Evaluator instances and their initial states. The Key consists of the - * script and the expression of the Evaluator. - */ - private static ThreadLocal<Map<Key, EvaluatorState>> caches = - new ThreadLocal<Map<Key, EvaluatorState>>() - { - @Override - protected Map<Key, EvaluatorState> initialValue() - { - return new HashMap<Key, EvaluatorState>(); - } - }; - - /** - * Returns an Evaluator instance for given script and expression from cache. If the cache - * contains no such Evaluator, then it is created and added to the cache. All global variables - * that were set after the Evaluator instance was cached are cleared. - */ - public static Evaluator getEvaluator(String expression, Class<?> supportFunctionsOrNull, - String scriptOrNull) - { - Key key = new Key(scriptOrNull, expression); - EvaluatorState state = caches.get().get(key); - if (state == null) - { - Evaluator evaluator = new Evaluator(expression, supportFunctionsOrNull, scriptOrNull); - Collection<String> globals = evaluator.getGlobalVariables(); - state = new EvaluatorState(evaluator, globals); - caches.get().put(key, state); - } - return state.getCleanInstance(); - } - - /** - * A class containing an Evaluator instance and its initial state. - * - * @author anttil - */ - private static class EvaluatorState - { - private final Evaluator evaluator; - - private final Collection<String> globals; - - public EvaluatorState(Evaluator evaluator, Collection<String> globals) - { - this.evaluator = evaluator; - this.globals = globals; - } - - /** - * Returns an evaluator instance in its initial state. - */ - public Evaluator getCleanInstance() - { - for (String value : evaluator.getGlobalVariables()) - { - if (globals.contains(value) == false) - { - evaluator.delete(value); - } - } - return this.evaluator; - } - } - - /** - * Caching key. - * - * @author anttil - */ - private static class Key - { - private final String script; - - private final String expression; - - public Key(String script, String expression) - { - this.script = (script != null ? script : ""); - this.expression = (expression != null ? expression : ""); - } - - public String getScript() - { - return script; - } - - public String getExpression() - { - return expression; - } - - @Override - public boolean equals(Object o) - { - if (o instanceof Key) - { - Key key = (Key) o; - return key.getScript().equals(getScript()) - && key.getExpression().equals(getExpression()); - } else - { - return false; - } - } - - @Override - public int hashCode() - { - return script.hashCode() + 17 * expression.hashCode(); - } - } - -} diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/jython/evaluator/EvaluatorCacheTest.java b/common/sourceTest/java/ch/systemsx/cisd/common/jython/evaluator/EvaluatorCacheTest.java deleted file mode 100644 index 6950914d8092cd313d1c1ae067752024da628b8c..0000000000000000000000000000000000000000 --- a/common/sourceTest/java/ch/systemsx/cisd/common/jython/evaluator/EvaluatorCacheTest.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright 2012 ETH Zuerich, CISD - * - * 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 ch.systemsx.cisd.common.jython.evaluator; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.sameInstance; -import static org.hamcrest.MatcherAssert.assertThat; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.charset.Charset; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -import org.testng.annotations.Test; - -/** - * @author anttil - */ -public class EvaluatorCacheTest -{ - - private static String DUMMY_SCRIPT = "x=1\ny=2"; - - private static String DUMMY_SCRIPT_2 = "x=2\ny=1"; - - private ExecutorService executor = new ThreadPoolExecutor(1, 1, 1000, TimeUnit.HOURS, - new LinkedBlockingDeque<Runnable>()); - - @Test - public void sameThreadGetsAlwaysSameEvaluatorInstanceForSameScriptAndExpression() - throws Exception - { - Evaluator evaluator1 = EvaluatorCache.getEvaluator("", null, DUMMY_SCRIPT); - Evaluator evaluator2 = EvaluatorCache.getEvaluator("", null, DUMMY_SCRIPT); - - assertThat(evaluator1, is(sameInstance(evaluator2))); - } - - @Test - public void differenceInScriptsCausesDifferentEvaluatorInstancesToBeReturned() throws Exception - { - Evaluator evaluator1 = EvaluatorCache.getEvaluator("", null, DUMMY_SCRIPT); - Evaluator evaluator2 = EvaluatorCache.getEvaluator("", null, DUMMY_SCRIPT_2); - - assertThat(evaluator1, is(not(sameInstance(evaluator2)))); - } - - @Test - public void differenceInExpressionsCausesDifferentEvaluatorInstancesToBeReturned() - throws Exception - { - Evaluator evaluator1 = EvaluatorCache.getEvaluator("get_something()", null, DUMMY_SCRIPT); - Evaluator evaluator2 = - EvaluatorCache.getEvaluator("get_something_else()", null, DUMMY_SCRIPT); - - assertThat(evaluator1, is(not(sameInstance(evaluator2)))); - } - - @Test - public void differentThreadsGetDifferentEvaluatorsForSameScriptAndSameExpressions() - throws Exception - { - Evaluator evaluator1 = EvaluatorCache.getEvaluator("", null, DUMMY_SCRIPT); - Future<Evaluator> evaluator2 = executor.submit(new Callable<Evaluator>() - { - @Override - public Evaluator call() throws Exception - { - return EvaluatorCache.getEvaluator("", null, DUMMY_SCRIPT); - } - }); - - assertThat(evaluator1, is(not(sameInstance(evaluator2.get())))); - } - - @Test - public void globalVariablesSetInPreviousRunAreClearedBeforeHandingOutAnEvaluatorInstance() - throws Exception - { - Evaluator evaluator = - EvaluatorCache - .getEvaluator("return_x_if_defined_else_0()", null, readFile("script.py")); - evaluator.evalFunction("set_x", "3"); - - assertThat(evaluator.evalAsString(), is("3")); - evaluator = - EvaluatorCache - .getEvaluator("return_x_if_defined_else_0()", null, readFile("script.py")); - assertThat(evaluator.evalAsString(), is("0")); - } - - private static String readFile(String name) - { - String filePath = "sourceTest/java/ch/systemsx/cisd/common/jython/evaluator/" + name; - try - { - FileInputStream stream = new FileInputStream(new File(filePath)); - try - { - FileChannel fc = stream.getChannel(); - MappedByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size()); - return Charset.defaultCharset().decode(bb).toString(); - } finally - { - stream.close(); - } - } catch (IOException e) - { - throw new IllegalArgumentException("Could not read file " + filePath); - } - } -} diff --git a/openbis/resource/dependency-structure.ddf b/openbis/resource/dependency-structure.ddf index f9de9f7157cdc3b76505a8e9eaee9d03a7abbf7d..55a502a32d348be8571ecbddc3e8c637cb2d6aeb 100644 --- a/openbis/resource/dependency-structure.ddf +++ b/openbis/resource/dependency-structure.ddf @@ -10,7 +10,7 @@ [generic.client] = ${generic}.client.* [generic.server] = ${generic}.server.* -[generic.shared] = ${generic}.shared.* +[generic.shared] = ${generic}.shared.* excluding ch.systemsx.cisd.openbis.generic.shared.managed_property.ManagedPropertyEvaluatorFactory check sets [generic.client] [generic.server] [generic.shared] diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/JythonEvaluatorPool.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/JythonEvaluatorPool.java new file mode 100644 index 0000000000000000000000000000000000000000..f3da537e7c768d4f00047e9867a8b6d475bfc0e0 --- /dev/null +++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/JythonEvaluatorPool.java @@ -0,0 +1,210 @@ +/* + * Copyright 2012 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.openbis.generic.server; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.log4j.Logger; + +import ch.systemsx.cisd.common.jython.evaluator.Evaluator; +import ch.systemsx.cisd.common.jython.evaluator.EvaluatorException; +import ch.systemsx.cisd.common.logging.LogCategory; +import ch.systemsx.cisd.common.logging.LogFactory; +import ch.systemsx.cisd.openbis.generic.server.dataaccess.IDAOFactory; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ScriptType; +import ch.systemsx.cisd.openbis.generic.shared.dto.ScriptPE; +import ch.systemsx.cisd.openbis.generic.shared.managed_property.IAtomicEvaluation; +import ch.systemsx.cisd.openbis.generic.shared.managed_property.IEvaluationRunner; +import ch.systemsx.cisd.openbis.generic.shared.managed_property.ManagedPropertyFunctions; + +/** + * Pool of Jython Evaluators for managed properties. When the pool is created, it is filled with new + * Evaluator instances for every managed property script. Enables thread-safe execution of calls to + * python functions defined in these scripts. + * + * @author anttil + */ +public class JythonEvaluatorPool +{ + private static Logger log = LogFactory.getLogger(LogCategory.OPERATION, + JythonEvaluatorPool.class); + + public static int DEFAULT_POOL_SIZE = 100; + + public static JythonEvaluatorPool INSTANCE; + + private Map<String, EvaluatorState> cache; + + private Lock cacheLock; + + public JythonEvaluatorPool(IDAOFactory daoFactory, String poolSize) + { + this(daoFactory, createCache(poolSize)); + } + + public JythonEvaluatorPool(IDAOFactory daoFactory, Map<String, EvaluatorState> cache) + { + this.cache = cache; + this.cacheLock = new ReentrantLock(); + + for (ScriptPE script : daoFactory.getScriptDAO().listEntities(ScriptType.MANAGED_PROPERTY, + null)) + { + try + { + cache.put(script.getScript(), + new EvaluatorState(createEvaluatorFor(script.getScript()))); + } catch (EvaluatorException e) + { + log.warn("Could not create evaluator for script " + script.getName(), e); + } + } + log.info("Initialization successful with " + cache.size() + " evaluators"); + INSTANCE = this; + } + + /** + * Return runner that can be used to evaluate python functions using evaluator in the pool. + */ + public IEvaluationRunner getRunner(final String script) + { + return new IEvaluationRunner() + { + @Override + public <T> T evaluate(IAtomicEvaluation<T> evaluation) + { + return JythonEvaluatorPool.this.evaluate(script, evaluation); + } + }; + } + + /** + * Evaluate python functions from a script using an Evaluator instance in the pool. If the + * Evaluator instance does not exist, create it. Give access to the instance for only one thread + * at a time. + */ + private <T> T evaluate(String script, IAtomicEvaluation<T> evaluation) + { + EvaluatorState state = cache.get(script); + if (state == null) + { + cacheLock.lock(); + try + { + state = cache.get(script); + if (state == null) + { + state = new EvaluatorState(createEvaluatorFor(script)); + cache.put(script, state); + } + } finally + { + cacheLock.unlock(); + } + } + + Lock lock = state.getLock(); + lock.lock(); + try + { + return evaluation.evaluate(state.getCleanInstance()); + } finally + { + lock.unlock(); + } + } + + private Evaluator createEvaluatorFor(String script) + { + return new Evaluator("", ManagedPropertyFunctions.class, script); + } + + /** + * The pooled object. Contains the Evaluator and its initial state to which it is always + * returned to when a new evaluation is started. + */ + public static class EvaluatorState + { + private final Evaluator evaluator; + + private final Collection<String> globals; + + private final Lock lock; + + public EvaluatorState(Evaluator evaluator) + { + this.evaluator = evaluator; + this.globals = evaluator.getGlobalVariables(); + this.lock = new ReentrantLock(); + } + + /** + * Returns an evaluator instance in its initial state. + */ + public synchronized Evaluator getCleanInstance() + { + for (String value : evaluator.getGlobalVariables()) + { + if (globals.contains(value) == false) + { + evaluator.delete(value); + } + } + return this.evaluator; + } + + public Lock getLock() + { + return lock; + } + } + + public static Map<String, EvaluatorState> createCache(String poolSize) + { + int size = DEFAULT_POOL_SIZE; + try + { + int number = Integer.parseInt(poolSize); + if (number > 0) + { + size = number; + } + } catch (NumberFormatException e) + { + } + + return createCache(size); + } + + public static Map<String, EvaluatorState> createCache(final int poolSize) + { + return new LinkedHashMap<String, EvaluatorState>(16, 0.75f, true) + { + private static final long serialVersionUID = 1L; + + @Override + protected boolean removeEldestEntry(Map.Entry<String, EvaluatorState> eldest) + { + return size() > poolSize; + } + }; + } +} diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/dynamic_property/calculator/DynamicPropertyCalculator.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/dynamic_property/calculator/DynamicPropertyCalculator.java index 7aec427b39c73e302aa4c689532483dd213e7ed2..70d095c2f05347e1f6c0d71062b4d7507bae1e21 100644 --- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/dynamic_property/calculator/DynamicPropertyCalculator.java +++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/dynamic_property/calculator/DynamicPropertyCalculator.java @@ -17,7 +17,6 @@ package ch.systemsx.cisd.openbis.generic.server.dataaccess.dynamic_property.calculator; import ch.systemsx.cisd.common.jython.evaluator.Evaluator; -import ch.systemsx.cisd.common.jython.evaluator.EvaluatorCache; import ch.systemsx.cisd.openbis.generic.server.dataaccess.dynamic_property.calculator.api.IEntityAdaptor; import ch.systemsx.cisd.openbis.generic.shared.calculator.AbstractCalculator; @@ -55,8 +54,7 @@ public class DynamicPropertyCalculator extends AbstractCalculator initialScript += expression; calculatedExpression = INVOKE_CALCULATE_EXPR; } - return new DynamicPropertyCalculator(EvaluatorCache.getEvaluator(calculatedExpression, - Math.class, + return new DynamicPropertyCalculator(new Evaluator(calculatedExpression, Math.class, initialScript)); } diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/dynamic_property/calculator/EntityValidationCalculator.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/dynamic_property/calculator/EntityValidationCalculator.java index 9867d1902d9785dd2b2f224be6995eb054e995ad..d94f4957278d49cc1501b52d26ea5d46508f75ba 100644 --- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/dynamic_property/calculator/EntityValidationCalculator.java +++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/dataaccess/dynamic_property/calculator/EntityValidationCalculator.java @@ -17,7 +17,6 @@ package ch.systemsx.cisd.openbis.generic.server.dataaccess.dynamic_property.calculator; import ch.systemsx.cisd.common.jython.evaluator.Evaluator; -import ch.systemsx.cisd.common.jython.evaluator.EvaluatorCache; import ch.systemsx.cisd.openbis.generic.server.dataaccess.dynamic_property.calculator.api.IEntityAdaptor; import ch.systemsx.cisd.openbis.generic.shared.calculator.AbstractCalculator; @@ -58,8 +57,7 @@ public class EntityValidationCalculator extends AbstractCalculator initialScript += expression; String calculatedExpression = INVOKE_CALCULATE_EXPR; - return new EntityValidationCalculator(EvaluatorCache.getEvaluator(calculatedExpression, - Math.class, + return new EntityValidationCalculator(new Evaluator(calculatedExpression, Math.class, initialScript), validationRequestedDelegate); } diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/managed_property/IAtomicEvaluation.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/managed_property/IAtomicEvaluation.java new file mode 100644 index 0000000000000000000000000000000000000000..31599d06c19ab81fed9118c5171e74ef275a6e6b --- /dev/null +++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/managed_property/IAtomicEvaluation.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.openbis.generic.shared.managed_property; + +import ch.systemsx.cisd.common.jython.evaluator.Evaluator; + +/** + * Evaluation of python code loaded in the interpreter of an Evaluator. + * + * @author anttil + */ +public interface IAtomicEvaluation<T> +{ + public T evaluate(Evaluator evaluator); +} diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/managed_property/IEvaluationRunner.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/managed_property/IEvaluationRunner.java new file mode 100644 index 0000000000000000000000000000000000000000..0ff971f17b571ece19d243076313eb217d59b9a5 --- /dev/null +++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/managed_property/IEvaluationRunner.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.openbis.generic.shared.managed_property; + +/** + * Any object that is able to execute evaluations defined with IAtomicEvaluation. + * + * @author anttil + */ +public interface IEvaluationRunner +{ + public <T> T evaluate(IAtomicEvaluation<T> evaluation); +} diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/managed_property/ManagedPropertyEvaluator.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/managed_property/ManagedPropertyEvaluator.java index 070338b973becbb6012e87b19726abf57cfdbc85..c11ae92693b23575408f240918495d35fb3bf819 100644 --- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/managed_property/ManagedPropertyEvaluator.java +++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/managed_property/ManagedPropertyEvaluator.java @@ -25,7 +25,6 @@ import java.util.Set; import org.apache.log4j.Logger; import ch.systemsx.cisd.common.jython.evaluator.Evaluator; -import ch.systemsx.cisd.common.jython.evaluator.EvaluatorCache; import ch.systemsx.cisd.common.jython.evaluator.EvaluatorException; import ch.systemsx.cisd.common.logging.LogCategory; import ch.systemsx.cisd.common.logging.LogFactory; @@ -107,7 +106,7 @@ public class ManagedPropertyEvaluator private static final String PERSON_VARIABLE_NAME = "person"; - private final Evaluator evaluator; + private final IEvaluationRunner runner; private final List<String> columnNames; @@ -117,16 +116,68 @@ public class ManagedPropertyEvaluator private List<IManagedInputWidgetDescription> inputWidgetDescriptions; - public ManagedPropertyEvaluator(String scriptExpression) + public ManagedPropertyEvaluator(final String scriptExpression) { - evaluator = - EvaluatorCache.getEvaluator("", ManagedPropertyFunctions.class, scriptExpression); - updateFromBatchFunctionDefined = evaluator.hasFunction(UPDATE_FROM_BATCH_INPUT_FUNCTION); + this(new IEvaluationRunner() + { + private Evaluator evaluator; + { + this.evaluator = + new Evaluator("", ManagedPropertyFunctions.class, scriptExpression); + } + + @Override + public <T> T evaluate(IAtomicEvaluation<T> evaluation) + { + return evaluation.evaluate(evaluator); + } + + }); + } + + public ManagedPropertyEvaluator(IEvaluationRunner runner) + { + this.runner = runner; + updateFromBatchFunctionDefined = + runner.evaluate(new IAtomicEvaluation<Boolean>() + { + @Override + public Boolean evaluate(Evaluator evaluator) + { + return evaluator.hasFunction(UPDATE_FROM_BATCH_INPUT_FUNCTION); + } + }); + updateFromRegistrationFormFunctionDefined = - evaluator.hasFunction(UPDATE_FROM_REGISTRATION_FORM_FUNCTION); + runner.evaluate(new IAtomicEvaluation<Boolean>() + { + @Override + public Boolean evaluate(Evaluator evaluator) + { + return evaluator.hasFunction(UPDATE_FROM_REGISTRATION_FORM_FUNCTION); + } + }); + boolean batchColumnNamesFunctionDefined = - evaluator.hasFunction(BATCH_COLUMN_NAMES_FUNCTION); - boolean inputWidgetsFunctionDefined = evaluator.hasFunction(INPUT_WIDGETS_FUNCTION); + runner.evaluate(new IAtomicEvaluation<Boolean>() + { + @Override + public Boolean evaluate(Evaluator evaluator) + { + return evaluator.hasFunction(BATCH_COLUMN_NAMES_FUNCTION); + } + }); + + boolean inputWidgetsFunctionDefined = + runner.evaluate(new IAtomicEvaluation<Boolean>() + { + @Override + public Boolean evaluate(Evaluator evaluator) + { + return evaluator.hasFunction(INPUT_WIDGETS_FUNCTION); + } + }); + checkCombinationsOfDefinedFunctions(batchColumnNamesFunctionDefined, inputWidgetsFunctionDefined); columnNames = new ArrayList<String>(); @@ -209,9 +260,17 @@ public class ManagedPropertyEvaluator } } - private List<?> evalFunction(String functionName) + private List<?> evalFunction(final String functionName) { - Object result = evaluator.evalFunction(functionName); + Object result = runner.evaluate(new IAtomicEvaluation<Object>() + { + @Override + public Object evaluate(Evaluator evaluator) + { + return evaluator.evalFunction(functionName); + } + }); + if (result instanceof List == false) { throw new EvaluatorException("Function '" + functionName @@ -221,7 +280,8 @@ public class ManagedPropertyEvaluator return (List<?>) result; } - public void configureUI(IManagedProperty managedProperty, EntityPropertyPE entityPropertyPE) + public void configureUI(final IManagedProperty managedProperty, + final EntityPropertyPE entityPropertyPE) { if (operationLog.isDebugEnabled()) { @@ -229,13 +289,21 @@ public class ManagedPropertyEvaluator managedProperty)); } - evaluator.set(PROPERTY_VARIABLE_NAME, managedProperty); - evaluator.set(PROPERTY_PE_VARIABLE_NAME, entityPropertyPE); - evaluator.evalFunction(CONFIGURE_UI_FUNCTION); + runner.evaluate(new IAtomicEvaluation<Void>() + { + @Override + public Void evaluate(Evaluator evaluator) + { + evaluator.set(PROPERTY_VARIABLE_NAME, managedProperty); + evaluator.set(PROPERTY_PE_VARIABLE_NAME, entityPropertyPE); + evaluator.evalFunction(CONFIGURE_UI_FUNCTION); + return null; + } + }); } - public void updateFromUI(IManagedProperty managedProperty, IPerson person, - IManagedUiAction action) + public void updateFromUI(final IManagedProperty managedProperty, final IPerson person, + final IManagedUiAction action) { if (operationLog.isDebugEnabled()) { @@ -243,9 +311,17 @@ public class ManagedPropertyEvaluator managedProperty)); } - evaluator.set(PROPERTY_VARIABLE_NAME, managedProperty); - evaluator.set(PERSON_VARIABLE_NAME, person); - evaluator.evalFunction(UPDATE_FROM_UI_FUNCTION, action); + runner.evaluate(new IAtomicEvaluation<Void>() + { + @Override + public Void evaluate(Evaluator evaluator) + { + evaluator.set(PROPERTY_VARIABLE_NAME, managedProperty); + evaluator.set(PERSON_VARIABLE_NAME, person); + evaluator.evalFunction(UPDATE_FROM_UI_FUNCTION, action); + return null; + } + }); } public List<String> getBatchColumnNames() @@ -258,8 +334,8 @@ public class ManagedPropertyEvaluator return inputWidgetDescriptions; } - public void updateFromBatchInput(IManagedProperty managedProperty, IPerson person, - Map<String, String> bindings) + public void updateFromBatchInput(final IManagedProperty managedProperty, final IPerson person, + final Map<String, String> bindings) { if (updateFromBatchFunctionDefined == false) { @@ -269,20 +345,38 @@ public class ManagedPropertyEvaluator } } else { - evaluator.set(PROPERTY_VARIABLE_NAME, managedProperty); - evaluator.set(PERSON_VARIABLE_NAME, person); - evaluator.evalFunction(UPDATE_FROM_BATCH_INPUT_FUNCTION, bindings); + runner.evaluate(new IAtomicEvaluation<Void>() + { + @Override + public Void evaluate(Evaluator evaluator) + { + evaluator.set(PROPERTY_VARIABLE_NAME, managedProperty); + evaluator.set(PERSON_VARIABLE_NAME, person); + evaluator.evalFunction(UPDATE_FROM_BATCH_INPUT_FUNCTION, bindings); + return null; + } + }); } } - public void updateFromRegistrationForm(IManagedProperty managedProperty, IPerson person, - List<Map<String, String>> bindings) + public void updateFromRegistrationForm(final IManagedProperty managedProperty, + final IPerson person, + final List<Map<String, String>> bindings) { if (updateFromRegistrationFormFunctionDefined) { - evaluator.set(PROPERTY_VARIABLE_NAME, managedProperty); - evaluator.set(PERSON_VARIABLE_NAME, person); - evaluator.evalFunction(UPDATE_FROM_REGISTRATION_FORM_FUNCTION, bindings); + runner.evaluate(new IAtomicEvaluation<Void>() + { + @Override + public Void evaluate(Evaluator evaluator) + { + evaluator.set(PROPERTY_VARIABLE_NAME, managedProperty); + evaluator.set(PERSON_VARIABLE_NAME, person); + evaluator.evalFunction(UPDATE_FROM_REGISTRATION_FORM_FUNCTION, + bindings); + return null; + } + }); } } } diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/managed_property/ManagedPropertyEvaluatorFactory.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/managed_property/ManagedPropertyEvaluatorFactory.java index 666a73ba0dd0b162c0c21babc9fc4dfed10cb635..566ffab4e8a5d84578d79bc225f4ee4144c20bd3 100644 --- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/managed_property/ManagedPropertyEvaluatorFactory.java +++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/managed_property/ManagedPropertyEvaluatorFactory.java @@ -16,6 +16,7 @@ package ch.systemsx.cisd.openbis.generic.shared.managed_property; +import ch.systemsx.cisd.openbis.generic.server.JythonEvaluatorPool; /** * Factory for creating managed property evaluators. (Could do some caching or other cleverness.) @@ -24,8 +25,16 @@ package ch.systemsx.cisd.openbis.generic.shared.managed_property; */ public class ManagedPropertyEvaluatorFactory { + public static ManagedPropertyEvaluator createManagedPropertyEvaluator(String scriptExpression) { - return new ManagedPropertyEvaluator(scriptExpression); + if (JythonEvaluatorPool.INSTANCE != null) + { + return new ManagedPropertyEvaluator(JythonEvaluatorPool.INSTANCE + .getRunner(scriptExpression)); + } else + { + return new ManagedPropertyEvaluator(scriptExpression); + } } } diff --git a/openbis/source/java/genericApplicationContext.xml b/openbis/source/java/genericApplicationContext.xml index bf4e1fb663c24fe92c8b41a7787708dbea4f1b0c..1ca5fc4f561c5cb3b3613d3a75311b8565440b1f 100644 --- a/openbis/source/java/genericApplicationContext.xml +++ b/openbis/source/java/genericApplicationContext.xml @@ -213,6 +213,11 @@ <bean id="entity-operation-checker" class="ch.systemsx.cisd.openbis.generic.server.business.EntityOperationChecker"/> + <bean id="jython-evaluator-pool" + class="ch.systemsx.cisd.openbis.generic.server.JythonEvaluatorPool"> + <constructor-arg ref="dao-factory" /> + <constructor-arg value="${jython-evaluator-pool-size}" /> + </bean> <!-- // Transaction --> diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/JythonEvaluatorPoolTest.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/JythonEvaluatorPoolTest.java new file mode 100644 index 0000000000000000000000000000000000000000..d3fa266895a8be4fc8c22091b00c10508afacbb3 --- /dev/null +++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/JythonEvaluatorPoolTest.java @@ -0,0 +1,314 @@ +/* + * Copyright 2012 ETH Zuerich, CISD + * + * 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 ch.systemsx.cisd.openbis.generic.server; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import ch.systemsx.cisd.common.jython.evaluator.Evaluator; +import ch.systemsx.cisd.openbis.generic.server.JythonEvaluatorPool.EvaluatorState; +import ch.systemsx.cisd.openbis.generic.server.dataaccess.IDAOFactory; +import ch.systemsx.cisd.openbis.generic.server.dataaccess.IScriptDAO; +import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ScriptType; +import ch.systemsx.cisd.openbis.generic.shared.dto.ScriptPE; +import ch.systemsx.cisd.openbis.generic.shared.managed_property.IAtomicEvaluation; + +/** + * @author anttil + */ +public class JythonEvaluatorPoolTest +{ + private JythonEvaluatorPool pool; + + private Map<String, EvaluatorState> cache; + + private static int POOL_SIZE = 10; + + @Test + public void sameEvaluatorIsAlwaysUsedForSameScript() throws Exception + { + long start = System.currentTimeMillis(); + while (secondsSpentSince(start) < 2) + { + List<Evaluator> resultList = executeParallel(getEvaluatorInUse()); + assertThat(resultList, isFilledWithSameInstanceOf(Evaluator.class)); + } + } + + @Test + public void globalVariablesSetInPreviousRunsAreClearedBeforeNewEvaluation() throws Exception + { + long start = System.currentTimeMillis(); + while (secondsSpentSince(start) < 2) + { + List<Integer> results = + executeParallel(returnZeroIfAVariableIsNotSetThenSetItToSomeValue()); + + Set<Integer> set = new HashSet<Integer>(results); + assertThat(set.size(), is(1)); + assertThat(set.contains(new Integer(0)), is(true)); + } + } + + @Test + public void poolSizeLimitIsNotExceeded() throws Exception + { + List<String> scripts = new ArrayList<String>(); + for (int i = 0; i < POOL_SIZE * 2; i++) + { + scripts.add(createRandomScript()); + } + + for (String script : scripts) + { + pool.getRunner(script).evaluate(dummyEvaluation()); + } + + assertThat(cache.size(), is(POOL_SIZE)); + } + + @Test + public void whenCacheSizeLimitIsExceededLeastAccessedElementIsEvicted() throws Exception + { + List<String> scripts = new ArrayList<String>(); + for (int i = 0; i < POOL_SIZE + 1; i++) + { + scripts.add(createRandomScript()); + } + + for (String script : scripts) + { + pool.getRunner(script).evaluate(dummyEvaluation()); + pool.getRunner(scripts.get(0)).evaluate(dummyEvaluation()); + } + + assertThat(cache.containsKey(scripts.get(0)), is(true)); + assertThat(cache.containsKey(scripts.get(1)), is(false)); + } + + private String createRandomScript() + { + return "x = '" + UUID.randomUUID().toString() + "'"; + } + + private long secondsSpentSince(long time) + { + return (System.currentTimeMillis() - time) / 1000; + } + + private ExecutorService executor = new ThreadPoolExecutor(10, 50, 1000, TimeUnit.HOURS, + new LinkedBlockingDeque<Runnable>()); + + private <T> List<T> executeParallel(Callable<T> task) throws Exception + { + List<Callable<T>> tasks = new ArrayList<Callable<T>>(); + for (int i = 0; i < 100; i++) + { + tasks.add(task); + } + List<Future<T>> futures = executor.invokeAll(tasks); + return getFutureValues(futures); + } + + private <T> List<T> getFutureValues(List<Future<T>> futures) throws InterruptedException, + ExecutionException + { + List<T> results = new ArrayList<T>(); + for (Future<T> future : futures) + { + results.add(future.get()); + } + return results; + } + + private Callable<Evaluator> getEvaluatorInUse() + { + final String script = + "def something():\n\treturn '" + UUID.randomUUID().toString() + "'"; + + return new Callable<Evaluator>() + { + @Override + public Evaluator call() throws Exception + { + return pool.getRunner(script).evaluate( + new IAtomicEvaluation<Evaluator>() + { + @Override + public Evaluator evaluate(Evaluator evaluator) + { + return evaluator; + } + } + ); + } + }; + } + + private Callable<Integer> returnZeroIfAVariableIsNotSetThenSetItToSomeValue() + { + final String script = readFile("script.py"); + + return new Callable<Integer>() + { + @Override + public Integer call() throws Exception + { + return pool.getRunner(script).evaluate( + new IAtomicEvaluation<Integer>() + { + @Override + public Integer evaluate(Evaluator evaluator) + { + Integer x = + (Integer) evaluator + .evalFunction("return_x_if_defined_else_0"); + evaluator.evalFunction("set_x", 3); + return x; + } + } + ); + } + }; + } + + private IAtomicEvaluation<Void> dummyEvaluation() + { + return new IAtomicEvaluation<Void>() + { + @Override + public Void evaluate(Evaluator evaluator) + { + return null; + } + }; + } + + private String readFile(String name) + { + String filePath = "sourceTest/java/ch/systemsx/cisd/openbis/generic/server/" + name; + try + { + FileInputStream stream = new FileInputStream(new File(filePath)); + try + { + FileChannel fc = stream.getChannel(); + MappedByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size()); + return Charset.defaultCharset().decode(bb).toString(); + } finally + { + stream.close(); + } + } catch (IOException e) + { + throw new IllegalArgumentException("Could not read file " + filePath); + } + } + + private <T> Matcher<Collection<T>> isFilledWithSameInstanceOf(Class<T> clazz) + { + return new TypeSafeMatcher<Collection<T>>() + { + @Override + public void describeTo(Description description) + { + description.appendText("A collection with only one instance in it"); + } + + @Override + public boolean matchesSafely(Collection<T> collection) + { + if (collection.isEmpty()) + { + return false; + } + if (collection.size() == 1) + { + return true; + } + + T t = null; + for (T element : collection) + { + if (t == null) + { + t = element; + } else if (t == element) + { + continue; + } else + { + return false; + } + } + return true; + } + }; + } + + @BeforeMethod + public void fixture() + { + Mockery context = new Mockery(); + final IDAOFactory daoFactory = context.mock(IDAOFactory.class); + final IScriptDAO scriptDao = context.mock(IScriptDAO.class); + context.checking(new Expectations() + { + { + List<ScriptPE> scripts = new ArrayList<ScriptPE>(); + + allowing(daoFactory).getScriptDAO(); + will(returnValue(scriptDao)); + + allowing(scriptDao).listEntities(ScriptType.MANAGED_PROPERTY, null); + will(returnValue(scripts)); + } + }); + + cache = JythonEvaluatorPool.createCache(Integer.toString(POOL_SIZE)); + pool = new JythonEvaluatorPool(daoFactory, cache); + } +} diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/jython/evaluator/script.py b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/script.py similarity index 100% rename from common/sourceTest/java/ch/systemsx/cisd/common/jython/evaluator/script.py rename to openbis/sourceTest/java/ch/systemsx/cisd/openbis/generic/server/script.py