diff --git a/api/src/org/labkey/api/settings/AppProps.java b/api/src/org/labkey/api/settings/AppProps.java index 3ae76372530..27480cc7b45 100644 --- a/api/src/org/labkey/api/settings/AppProps.java +++ b/api/src/org/labkey/api/settings/AppProps.java @@ -173,6 +173,11 @@ static WriteableAppProps getWriteableInstance() /** Timeout in seconds for read-only HTTP requests, after which resources like DB connections and spawned processes will be killed. Set to 0 to disable. */ int getReadOnlyHttpRequestTimeout(); + int DEFAULT_SCRIPT_EXECUTION_TIMEOUT = 60; + + /** Timeout in seconds for server-side JavaScript (e.g. trigger scripts), measured in wall-clock time including database and other Java operations invoked by the script. Set to 0 to disable. */ + int getScriptExecutionTimeout(); + int getMaxBLOBSize(); ExceptionReportingLevel getExceptionReportingLevel(); diff --git a/api/src/org/labkey/api/settings/AppPropsImpl.java b/api/src/org/labkey/api/settings/AppPropsImpl.java index 60b22b957fe..ac90bf67310 100644 --- a/api/src/org/labkey/api/settings/AppPropsImpl.java +++ b/api/src/org/labkey/api/settings/AppPropsImpl.java @@ -321,6 +321,12 @@ public int getReadOnlyHttpRequestTimeout() return lookupIntValue(readOnlyHttpRequestTimeout, 0); } + @Override + public int getScriptExecutionTimeout() + { + return lookupIntValue(scriptExecutionTimeout, DEFAULT_SCRIPT_EXECUTION_TIMEOUT); + } + @Override public int getMaxBLOBSize() { diff --git a/api/src/org/labkey/api/settings/SiteSettingsProperties.java b/api/src/org/labkey/api/settings/SiteSettingsProperties.java index 961ecb54868..8cbc673f4aa 100644 --- a/api/src/org/labkey/api/settings/SiteSettingsProperties.java +++ b/api/src/org/labkey/api/settings/SiteSettingsProperties.java @@ -90,6 +90,14 @@ public void setValue(WriteableAppProps writeable, String value) writeable.setReadOnlyHttpRequestTimeout(Integer.parseInt(value)); } }, + scriptExecutionTimeout("Timeout in seconds for server-side JavaScript such as trigger scripts. Measured in wall-clock time, including database and other Java operations invoked by the script. Set to 0 to disable.") + { + @Override + public void setValue(WriteableAppProps writeable, String value) + { + writeable.setScriptExecutionTimeout(Integer.parseInt(value)); + } + }, maxBLOBSize("Maximum file size, in bytes, to allow in database BLOBs") { @Override diff --git a/api/src/org/labkey/api/settings/WriteableAppProps.java b/api/src/org/labkey/api/settings/WriteableAppProps.java index e3691f745b0..92df26b757b 100644 --- a/api/src/org/labkey/api/settings/WriteableAppProps.java +++ b/api/src/org/labkey/api/settings/WriteableAppProps.java @@ -94,6 +94,13 @@ public void setReadOnlyHttpRequestTimeout(int timeout) storeIntValue(readOnlyHttpRequestTimeout, timeout); } + public void setScriptExecutionTimeout(int timeout) + { + if (timeout < 0) + throw new IllegalArgumentException("scriptExecutionTimeout must be >= 0"); + storeIntValue(scriptExecutionTimeout, timeout); + } + public void setMaxBLOBSize(int maxSize) { if (maxSize < 0) diff --git a/core/src/org/labkey/core/admin/AdminController.java b/core/src/org/labkey/core/admin/AdminController.java index 64d0cdcc26e..97175dc35ba 100644 --- a/core/src/org/labkey/core/admin/AdminController.java +++ b/core/src/org/labkey/core/admin/AdminController.java @@ -1381,6 +1381,14 @@ public void validateCommand(SiteSettingsForm form, Errors errors) { errors.reject(ERROR_MSG, "Memory logging frequency must be non-negative"); } + if (form.getScriptExecutionTimeout() == null) + { + errors.reject(ERROR_MSG, "Script execution timeout is required; set to 0 to disable the timeout"); + } + else if (form.getScriptExecutionTimeout() < 0) + { + errors.reject(ERROR_MSG, "Script execution timeout must be non-negative"); + } } @Override @@ -1415,6 +1423,7 @@ public boolean handlePost(SiteSettingsForm form, BindException errors) throws Ex props.setSSLPort(form.getSslPort()); props.setMemoryUsageDumpInterval(form.getMemoryUsageDumpInterval()); props.setReadOnlyHttpRequestTimeout(form.getReadOnlyHttpRequestTimeout()); + props.setScriptExecutionTimeout(form.getScriptExecutionTimeout()); props.setMaxBLOBSize(form.getMaxBLOBSize()); props.setSelfReportExceptions(form.isSelfReportExceptions()); @@ -2384,6 +2393,7 @@ public static class SiteSettingsForm private int _sslPort; private int _memoryUsageDumpInterval; private int _readOnlyHttpRequestTimeout; + private Integer _scriptExecutionTimeout; private int _maxBLOBSize; private String _exceptionReportingLevel; private String _usageReportingLevel; @@ -2524,6 +2534,18 @@ public int getReadOnlyHttpRequestTimeout() return _readOnlyHttpRequestTimeout; } + /** Null when the request omits or blanks the parameter; rejected in validateCommand so an omitted value can never bind to 0 and silently disable the timeout. */ + @Nullable + public Integer getScriptExecutionTimeout() + { + return _scriptExecutionTimeout; + } + + public void setScriptExecutionTimeout(@Nullable Integer timeout) + { + _scriptExecutionTimeout = timeout; + } + public void setReadOnlyHttpRequestTimeout(int timeout) { _readOnlyHttpRequestTimeout = timeout; diff --git a/core/src/org/labkey/core/admin/customizeSite.jsp b/core/src/org/labkey/core/admin/customizeSite.jsp index 8bc659eb5ac..1de822de1e6 100644 --- a/core/src/org/labkey/core/admin/customizeSite.jsp +++ b/core/src/org/labkey/core/admin/customizeSite.jsp @@ -312,6 +312,11 @@ Click the Save button at any time to accept the current settings and continue. "After the timeout, resources like database connections and spawned processes will be killed to abort processing the request. Set to 0 to disable the timeout.")%>