From cc6ab290fa180a61fbeb7aaf15804c39f1cb74ca Mon Sep 17 00:00:00 2001 From: bbimber Date: Wed, 21 Jan 2026 17:13:51 -0800 Subject: [PATCH 1/5] Allow trigger scripts to customize their own timeout --- api/src/org/labkey/api/action/ApiResponseWriter.java | 3 ++- .../org/labkey/api/data/triggers/ScriptTrigger.java | 3 ++- api/src/org/labkey/api/query/QueryService.java | 2 ++ core/src/org/labkey/core/script/RhinoService.java | 8 +++++++- .../org/labkey/query/controllers/QueryController.java | 10 +++++----- 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/api/src/org/labkey/api/action/ApiResponseWriter.java b/api/src/org/labkey/api/action/ApiResponseWriter.java index fcb9d70691d..1656cb0c636 100644 --- a/api/src/org/labkey/api/action/ApiResponseWriter.java +++ b/api/src/org/labkey/api/action/ApiResponseWriter.java @@ -23,6 +23,7 @@ import org.json.JSONObject; import org.labkey.api.query.BatchValidationException; import org.labkey.api.query.PropertyValidationError; +import org.labkey.api.query.QueryService; import org.labkey.api.query.SimpleValidationError; import org.labkey.api.query.ValidationError; import org.labkey.api.query.ValidationException; @@ -445,7 +446,7 @@ public JSONObject toJSON(Throwable e) json.put("errors", arr); json.put("errorCount", arr.length()); json.put("exception", message); - json.put("extraContext", bve.getExtraContext()); + json.put(QueryService.EXTRA_CONTEXT, bve.getExtraContext()); return json; } diff --git a/api/src/org/labkey/api/data/triggers/ScriptTrigger.java b/api/src/org/labkey/api/data/triggers/ScriptTrigger.java index 99c18ca8f31..d34306e5045 100644 --- a/api/src/org/labkey/api/data/triggers/ScriptTrigger.java +++ b/api/src/org/labkey/api/data/triggers/ScriptTrigger.java @@ -23,6 +23,7 @@ import org.labkey.api.data.PHI; import org.labkey.api.data.TableInfo; import org.labkey.api.query.BatchValidationException; +import org.labkey.api.query.QueryService; import org.labkey.api.query.ValidationException; import org.labkey.api.script.ScriptReference; import org.labkey.api.security.User; @@ -258,7 +259,7 @@ private T _try(Container c, User user, Map extraContext, Scr Map bindings = new HashMap<>(); if (extraContext == null) extraContext = new HashMap<>(); - bindings.put("extraContext", extraContext); + bindings.put(QueryService.EXTRA_CONTEXT, extraContext); bindings.put("schemaName", _table.getPublicSchemaName()); bindings.put("tableName", _table.getPublicName()); diff --git a/api/src/org/labkey/api/query/QueryService.java b/api/src/org/labkey/api/query/QueryService.java index bb6d87b7614..82b56a71f08 100644 --- a/api/src/org/labkey/api/query/QueryService.java +++ b/api/src/org/labkey/api/query/QueryService.java @@ -69,6 +69,8 @@ public interface QueryService String SCHEMA_TEMPLATE_EXTENSION = ".template.xml"; + String EXTRA_CONTEXT = "extraContext"; + static QueryService get() { return ServiceRegistry.get().getService(QueryService.class); diff --git a/core/src/org/labkey/core/script/RhinoService.java b/core/src/org/labkey/core/script/RhinoService.java index 2def34d20ba..af3e147c524 100644 --- a/core/src/org/labkey/core/script/RhinoService.java +++ b/core/src/org/labkey/core/script/RhinoService.java @@ -16,11 +16,12 @@ package org.labkey.core.script; import com.sun.phobos.script.javascript.RhinoScriptEngineFactory; +import org.apache.commons.lang3.math.NumberUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; -import org.json.JSONObject; import org.json.JSONArray; +import org.json.JSONObject; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; @@ -35,6 +36,7 @@ import org.labkey.api.module.ModuleResourceCaches; import org.labkey.api.module.ResourceRootProvider; import org.labkey.api.query.BatchValidationException; +import org.labkey.api.query.QueryService; import org.labkey.api.query.ValidationException; import org.labkey.api.reader.Readers; import org.labkey.api.resource.Resource; @@ -58,6 +60,7 @@ import org.mozilla.javascript.NativeArray; import org.mozilla.javascript.NativeJavaObject; import org.mozilla.javascript.RhinoException; +import org.mozilla.javascript.ScriptRuntime; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.WrapFactory; @@ -1043,6 +1046,9 @@ public boolean visibleToScripts(String fullClassName) private static class SandboxContext extends Context { + public static final String SCRIPT_TIMEOUT_PROPERTY = "scriptTimeout"; + private static final int MAX_ALLOWABLE_TIMEOUT = 300; + private final long startTime; private SandboxContext(SandboxContextFactory factory) diff --git a/query/src/org/labkey/query/controllers/QueryController.java b/query/src/org/labkey/query/controllers/QueryController.java index 35ba9a5d411..e67c7b9e6fd 100644 --- a/query/src/org/labkey/query/controllers/QueryController.java +++ b/query/src/org/labkey/query/controllers/QueryController.java @@ -4643,7 +4643,7 @@ protected JSONObject executeJson(JSONObject json, CommandType commandType, boole } } - Map extraContext = json.has("extraContext") ? json.getJSONObject("extraContext").toMap() : new CaseInsensitiveHashMap<>(); + Map extraContext = json.has(QueryService.EXTRA_CONTEXT) ? json.getJSONObject(QueryService.EXTRA_CONTEXT).toMap() : new CaseInsensitiveHashMap<>(); Map auditDetails = json.has("auditDetails") ? json.getJSONObject("auditDetails").toMap() : new CaseInsensitiveHashMap<>(); @@ -5138,7 +5138,7 @@ else if (scope != tableInfo.getSchema().getScope()) } JSONArray resultArray = new JSONArray(); - JSONObject extraContext = json.optJSONObject("extraContext"); + JSONObject extraContext = json.optJSONObject(QueryService.EXTRA_CONTEXT); JSONObject auditDetails = json.optJSONObject("auditDetails"); int startingErrorIndex = 0; @@ -5162,11 +5162,11 @@ else if (scope != tableInfo.getSchema().getScope()) Map commandExtraContext = new HashMap<>(); if (extraContext != null) commandExtraContext.putAll(extraContext.toMap()); - if (commandObject.has("extraContext")) + if (commandObject.has(QueryService.EXTRA_CONTEXT)) { - commandExtraContext.putAll(commandObject.getJSONObject("extraContext").toMap()); + commandExtraContext.putAll(commandObject.getJSONObject(QueryService.EXTRA_CONTEXT).toMap()); } - commandObject.put("extraContext", commandExtraContext); + commandObject.put(QueryService.EXTRA_CONTEXT, commandExtraContext); Map commandAuditDetails = new HashMap<>(); if (auditDetails != null) commandAuditDetails.putAll(auditDetails.toMap()); From cdb75f5121b9f18726bcc6702614a71dc3c2bf9f Mon Sep 17 00:00:00 2001 From: bbimber Date: Wed, 21 Jan 2026 17:43:33 -0800 Subject: [PATCH 2/5] Missed with prior commit --- .../org/labkey/core/script/RhinoService.java | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/core/src/org/labkey/core/script/RhinoService.java b/core/src/org/labkey/core/script/RhinoService.java index af3e147c524..8c09fe05c06 100644 --- a/core/src/org/labkey/core/script/RhinoService.java +++ b/core/src/org/labkey/core/script/RhinoService.java @@ -1009,7 +1009,7 @@ protected void observeInstructionCount(Context cx, int instructionCount) { SandboxContext ctx = (SandboxContext)cx; long currentTime = HeartBeat.currentTimeMillis(); - final int timeout = 60; + final long timeout = ctx.getTimeout(); if (currentTime - ctx.startTime > timeout*1000) Context.reportError("Script execution exceeded " + timeout + " seconds."); } @@ -1050,6 +1050,7 @@ private static class SandboxContext extends Context private static final int MAX_ALLOWABLE_TIMEOUT = 300; private final long startTime; + private long timeout = 60; private SandboxContext(SandboxContextFactory factory) { @@ -1057,6 +1058,47 @@ private SandboxContext(SandboxContextFactory factory) setLanguageVersion(Context.VERSION_1_8); startTime = HeartBeat.currentTimeMillis(); } + + public long getTimeout() + { + if (ScriptRuntime.hasTopCall(this)) + { + Scriptable script = ScriptRuntime.getTopCallScope(this); + Object o = ScriptRuntime.getObjectProp(script, QueryService.EXTRA_CONTEXT, this); + if (o instanceof Map mp) + { + if (mp.containsKey(QueryService.EXTRA_CONTEXT) && mp.get(QueryService.EXTRA_CONTEXT) != null) + { + Object rawTimeout = mp.get(SCRIPT_TIMEOUT_PROPERTY); + if (NumberUtils.isCreatable(String.valueOf(rawTimeout))) + { + try + { + int scriptTimeout = Integer.parseInt(String.valueOf(rawTimeout)); + if (scriptTimeout > MAX_ALLOWABLE_TIMEOUT) + { + scriptTimeout = MAX_ALLOWABLE_TIMEOUT; + LOG.error("Script timeout is greater than max allowable, using: {}", MAX_ALLOWABLE_TIMEOUT); + } + + return scriptTimeout; + } + catch (Exception e) + { + LOG.error("Non-integer value provided to extractContext.scriptTimeout for script: {}", rawTimeout); + } + } + } + } + } + + return timeout; + } + + public void setTimeout(long timeout) + { + this.timeout = timeout; + } } private static class SandboxWrapFactory extends WrapFactory From a92e9c2f5a01cb1dbfab68ff7171e2cc965caf47 Mon Sep 17 00:00:00 2001 From: bbimber Date: Wed, 21 Jan 2026 19:06:56 -0800 Subject: [PATCH 3/5] Improve implementation --- .../org/labkey/core/script/RhinoService.java | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/core/src/org/labkey/core/script/RhinoService.java b/core/src/org/labkey/core/script/RhinoService.java index 8c09fe05c06..7d2208acf9a 100644 --- a/core/src/org/labkey/core/script/RhinoService.java +++ b/core/src/org/labkey/core/script/RhinoService.java @@ -1046,11 +1046,11 @@ public boolean visibleToScripts(String fullClassName) private static class SandboxContext extends Context { - public static final String SCRIPT_TIMEOUT_PROPERTY = "scriptTimeout"; + private static final String SCRIPT_TIMEOUT_PROPERTY = "scriptTimeout"; private static final int MAX_ALLOWABLE_TIMEOUT = 300; + private final long DEFAULT_TIMEOUT = 60; private final long startTime; - private long timeout = 60; private SandboxContext(SandboxContextFactory factory) { @@ -1065,11 +1065,11 @@ public long getTimeout() { Scriptable script = ScriptRuntime.getTopCallScope(this); Object o = ScriptRuntime.getObjectProp(script, QueryService.EXTRA_CONTEXT, this); - if (o instanceof Map mp) + if (o instanceof ScriptableMap mp) { - if (mp.containsKey(QueryService.EXTRA_CONTEXT) && mp.get(QueryService.EXTRA_CONTEXT) != null) + if (mp.getMap().get(SCRIPT_TIMEOUT_PROPERTY) != null) { - Object rawTimeout = mp.get(SCRIPT_TIMEOUT_PROPERTY); + Object rawTimeout = mp.getMap().get(SCRIPT_TIMEOUT_PROPERTY); if (NumberUtils.isCreatable(String.valueOf(rawTimeout))) { try @@ -1092,12 +1092,7 @@ public long getTimeout() } } - return timeout; - } - - public void setTimeout(long timeout) - { - this.timeout = timeout; + return DEFAULT_TIMEOUT; } } From c7710aca7d12498efbff8fb64f6bf39e0d6f343b Mon Sep 17 00:00:00 2001 From: bbimber Date: Wed, 21 Jan 2026 19:10:09 -0800 Subject: [PATCH 4/5] Fix test --- core/src/org/labkey/core/script/RhinoService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/org/labkey/core/script/RhinoService.java b/core/src/org/labkey/core/script/RhinoService.java index 7d2208acf9a..697bda0cd3d 100644 --- a/core/src/org/labkey/core/script/RhinoService.java +++ b/core/src/org/labkey/core/script/RhinoService.java @@ -240,8 +240,8 @@ public void testModuleResourceCache() if (null != simpleTest) { - assertEquals("Scripts from the simpletest module", 15, ScriptReferenceImpl.SCRIPT_CACHE.getResourceMap(simpleTest).size()); - assertEquals("Top-level script timestamps from the simpletest module", 15, LabKeyModuleSourceProvider.TOP_LEVEL_SCRIPT_CACHE.getResourceMap(simpleTest).size()); + assertEquals("Scripts from the simpletest module", 16, ScriptReferenceImpl.SCRIPT_CACHE.getResourceMap(simpleTest).size()); + assertEquals("Top-level script timestamps from the simpletest module", 16, LabKeyModuleSourceProvider.TOP_LEVEL_SCRIPT_CACHE.getResourceMap(simpleTest).size()); } } } From b94b92299f1a0c59cf58751186526b2f8fbae7d4 Mon Sep 17 00:00:00 2001 From: bbimber Date: Wed, 21 Jan 2026 19:16:35 -0800 Subject: [PATCH 5/5] Remove redundancy --- .../org/labkey/core/script/RhinoService.java | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/core/src/org/labkey/core/script/RhinoService.java b/core/src/org/labkey/core/script/RhinoService.java index 697bda0cd3d..258a902ac73 100644 --- a/core/src/org/labkey/core/script/RhinoService.java +++ b/core/src/org/labkey/core/script/RhinoService.java @@ -1070,23 +1070,20 @@ public long getTimeout() if (mp.getMap().get(SCRIPT_TIMEOUT_PROPERTY) != null) { Object rawTimeout = mp.getMap().get(SCRIPT_TIMEOUT_PROPERTY); - if (NumberUtils.isCreatable(String.valueOf(rawTimeout))) + try { - try + int scriptTimeout = Integer.parseInt(String.valueOf(rawTimeout)); + if (scriptTimeout > MAX_ALLOWABLE_TIMEOUT) { - int scriptTimeout = Integer.parseInt(String.valueOf(rawTimeout)); - if (scriptTimeout > MAX_ALLOWABLE_TIMEOUT) - { - scriptTimeout = MAX_ALLOWABLE_TIMEOUT; - LOG.error("Script timeout is greater than max allowable, using: {}", MAX_ALLOWABLE_TIMEOUT); - } - - return scriptTimeout; - } - catch (Exception e) - { - LOG.error("Non-integer value provided to extractContext.scriptTimeout for script: {}", rawTimeout); + scriptTimeout = MAX_ALLOWABLE_TIMEOUT; + LOG.error("Script timeout is greater than max allowable, using: {}", MAX_ALLOWABLE_TIMEOUT); } + + return scriptTimeout; + } + catch (Exception e) + { + LOG.error("Non-integer value provided to extractContext.scriptTimeout for script: {}", rawTimeout); } } }