From c69fa6c86fb4d39c12d00b8a3bfe45aeaf6864a0 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Thu, 7 May 2026 17:57:47 -0700 Subject: [PATCH 1/3] Improve cache clearing/GC/heap dump behavior --- .../org/labkey/api/security/GroupManager.java | 4 +- core/src/org/labkey/core/CoreController.java | 14 +- .../labkey/core/admin/AdminController.java | 383 +++++++++--------- core/src/org/labkey/core/admin/memTracker.jsp | 12 +- core/webapp/admin/caches.js | 40 ++ .../security/SecurityController.java | 16 +- 6 files changed, 264 insertions(+), 205 deletions(-) create mode 100644 core/webapp/admin/caches.js diff --git a/api/src/org/labkey/api/security/GroupManager.java b/api/src/org/labkey/api/security/GroupManager.java index a9d49fb0567..fff7ec7623a 100644 --- a/api/src/org/labkey/api/security/GroupManager.java +++ b/api/src/org/labkey/api/security/GroupManager.java @@ -205,7 +205,9 @@ private static void appendDotAttribute(StringBuilder sb, boolean prependComma, S if (prependComma) sb.append(", "); - sb.append(name).append("=\"").append(value).append("\""); + // Escape backslashes first, then quotes, to produce a valid DOT quoted string + String escaped = value.replace("\\", "\\\\").replace("\"", "\\\""); + sb.append(name).append("=\"").append(escaped).append("\""); } public static void exportGroupMembers(Group group, List memberGroups, List memberUsers, GroupType xmlGroupType) diff --git a/core/src/org/labkey/core/CoreController.java b/core/src/org/labkey/core/CoreController.java index d80b090fa49..acaf441e3aa 100644 --- a/core/src/org/labkey/core/CoreController.java +++ b/core/src/org/labkey/core/CoreController.java @@ -487,7 +487,7 @@ else if (form.getSchemaName() != null && form.getQueryName() != null && form.get { throw new NotFoundException("The file '" + file.getName() + "' attached to the object '" + identifiable.getName() + "' cannot be found. It may have been deleted."); } - throw new NotFoundException("File " + file.getPath() + " does not exist on the server file system. It may have been deleted."); + throw new NotFoundException("File " + file.getName() + " does not exist on the server file system. It may have been deleted."); } if (file.isDirectory()) @@ -654,6 +654,7 @@ private static byte[] compressCSS(String s) catch (StackOverflowError e) { // replaceAll() can blow up + _log.error("StackOverflowError compressing CSS"); } return Compress.compressGzip(c.trim()); } @@ -935,6 +936,11 @@ public void validateForm(SimpleApiJsonForm form, Errors errors) errors.reject(ERROR_MSG, "The container '" + parentIdentifier + "' is not a valid parent folder."); return; } + + if (!target.hasPermission(getUser(), AdminPermission.class)) + { + throw new UnauthorizedException("You must be an administrator for the target container"); + } } @Override @@ -2390,6 +2396,10 @@ public Object execute(Object o, BindException errors) } } + /** + * This action doesn't require any permissions, as the call to WarningService.getWarnings() + * only returns warnings appropriate for the user/guest + */ @RequiresNoPermission @AllowedDuringUpgrade public static class DisplayWarningsAction extends MutatingApiAction @@ -2721,7 +2731,7 @@ public void setToFormat(String toFormat) } @SuppressWarnings("unused") // Called from JavaScript: discuss.js, wikiEdit.js - @RequiresNoPermission + @RequiresPermission(ReadPermission.class) public static class TransformWikiAction extends MutatingApiAction { @Override diff --git a/core/src/org/labkey/core/admin/AdminController.java b/core/src/org/labkey/core/admin/AdminController.java index 61764f3c6b1..da74ae70ea1 100644 --- a/core/src/org/labkey/core/admin/AdminController.java +++ b/core/src/org/labkey/core/admin/AdminController.java @@ -302,7 +302,6 @@ import org.labkey.api.view.NavTree; import org.labkey.api.view.NotFoundException; import org.labkey.api.view.Portal; -import org.labkey.api.view.RedirectException; import org.labkey.api.view.ShortURLRecord; import org.labkey.api.view.ShortURLService; import org.labkey.api.view.TabStripView; @@ -314,6 +313,7 @@ import org.labkey.api.view.ViewServlet; import org.labkey.api.view.WebPartView; import org.labkey.api.view.template.EmptyView; +import org.labkey.api.view.template.ClientDependency; import org.labkey.api.view.template.PageConfig; import org.labkey.api.view.template.PageConfig.Template; import org.labkey.api.wiki.WikiRendererType; @@ -415,9 +415,11 @@ import static org.labkey.api.util.DOM.Attribute.title; import static org.labkey.api.util.DOM.Attribute.type; import static org.labkey.api.util.DOM.Attribute.value; +import static org.labkey.api.util.DOM.B; import static org.labkey.api.util.DOM.BR; import static org.labkey.api.util.DOM.DIV; import static org.labkey.api.util.DOM.LI; +import static org.labkey.api.util.DOM.P; import static org.labkey.api.util.DOM.SPAN; import static org.labkey.api.util.DOM.STYLE; import static org.labkey.api.util.DOM.TABLE; @@ -1657,7 +1659,7 @@ public static class SiteValidationForm { private List _providers; private boolean _includeSubfolders = false; - private transient Consumer _logger = s -> { + private transient Consumer _logger = _ -> { }; // No-op by default public List getProviders() @@ -2690,23 +2692,41 @@ public PostgresTableSizesAction() } @AdminConsoleAction - public class DumpHeapAction extends SimpleViewAction + public class DumpHeapAction extends ConfirmAction { + private File _destination; + @Override - public ModelAndView getView(Object o, BindException errors) throws Exception + public ModelAndView getConfirmView(Object o, BindException errors) { - File destination = DebugInfoDumper.dumpHeap(); - return new HtmlView(HtmlString.of("Heap dumped to " + destination.getAbsolutePath())); + setTitle("Dump Heap"); + return HtmlView.of("Are you sure you want to dump the JVM heap to disk? This may temporarily slow the server and will consume significant disk space."); } @Override - public void addNavTrail(NavTree root) + public boolean handlePost(Object o, BindException errors) throws Exception { - setHelpTopic("dumpHeap"); - addAdminNavTrail(root, "Heap dump", getClass()); + _destination = DebugInfoDumper.dumpHeap(); + return true; } - } + @Override + public ModelAndView getSuccessView(Object o) + { + return new HtmlView(HtmlString.of("Heap dumped to " + _destination.getAbsolutePath())); + } + + @Override + public void validateCommand(Object o, Errors errors) + { + } + + @Override + public @NotNull URLHelper getSuccessURL(Object o) + { + return getShowAdminURL(); + } + } public static class ThreadsBean { @@ -3189,32 +3209,10 @@ public class CachesAction extends SimpleViewAction @Override public ModelAndView getView(MemForm form, BindException errors) { - if (form.isClearCaches()) - { - LOG.info("Clearing Introspector caches"); - Introspector.flushCaches(); - LOG.info("Purging all caches"); - CacheManager.clearAllKnownCaches(); - ActionURL redirect = getViewContext().cloneActionURL().deleteParameter("clearCaches"); - throw new RedirectException(redirect); - } + getPageConfig().addClientDependency(ClientDependency.fromPath("admin/caches.js")); List> caches = CacheManager.getKnownCaches(); - if (form.getDebugName() != null) - { - for (TrackingCache cache : caches) - { - if (form.getDebugName().equals(cache.getDebugName())) - { - LOG.info("Purging cache: " + cache.getDebugName()); - cache.clear(); - } - } - ActionURL redirect = getViewContext().cloneActionURL().deleteParameter("debugName"); - throw new RedirectException(redirect); - } - List cacheStats = new ArrayList<>(); List transactionStats = new ArrayList<>(); @@ -3226,13 +3224,13 @@ public ModelAndView getView(MemForm form, BindException errors) HtmlStringBuilder html = HtmlStringBuilder.of(); - html.append(LinkBuilder.labkeyLink("Clear Caches and Refresh", getCachesURL(true, false))); + html.append(createHtmlFragment(A(cl("labkey-text-link").at(href, "#").id("clearAllCaches"), "Clear Caches and Refresh"))); html.append(LinkBuilder.labkeyLink("Refresh", getCachesURL(false, false))); - html.unsafeAppend("

\n"); + html.append(createHtmlFragment(BR(), BR())); appendStats(html, "Caches", cacheStats, false); - html.unsafeAppend("

\n"); + html.append(createHtmlFragment(BR(), BR())); appendStats(html, "Transaction Caches", transactionStats, true); return new HtmlView(html); @@ -3242,79 +3240,65 @@ private void appendStats(HtmlStringBuilder html, String title, List { List stats = skipUnusedCaches ? allStats.stream() - .filter(stat->stat.getMaxSize() > 0) + .filter(stat -> stat.getMaxSize() > 0) .collect(Collectors.toCollection((Supplier>) ArrayList::new)) : allStats; Collections.sort(stats); - html.unsafeAppend("

"); - html.append(title); - html.append(" (").append(stats.size()).unsafeAppend(")

\n"); - - html.unsafeAppend("\n"); - html.unsafeAppend(""); - html.unsafeAppend(""); - html.unsafeAppend(""); - html.unsafeAppend(""); - html.unsafeAppend(""); - html.unsafeAppend(""); - html.unsafeAppend(""); - html.unsafeAppend(""); - html.unsafeAppend(""); - html.unsafeAppend(""); - html.unsafeAppend(""); - html.unsafeAppend(""); - html.unsafeAppend(""); - - long size = 0; - long gets = 0; - long misses = 0; - long puts = 0; - long expirations = 0; - long evictions = 0; - long removes = 0; - long clears = 0; - int rowCount = 0; - - for (CacheStats stat : stats) - { - size += stat.getSize(); - gets += stat.getGets(); - misses += stat.getMisses(); - puts += stat.getPuts(); - expirations += stat.getExpirations(); - evictions += stat.getEvictions(); - removes += stat.getRemoves(); - clears += stat.getClears(); - - html.unsafeAppend(""); - - appendDescription(html, stat.getDescription(), stat.getCreationStackTrace()); - - Long limit = stat.getLimit(); - long maxSize = stat.getMaxSize(); - - appendLongs(html, limit, maxSize, stat.getSize(), stat.getGets(), stat.getMisses(), stat.getPuts(), stat.getExpirations(), stat.getEvictions(), stat.getRemoves(), stat.getClears()); - appendDoubles(html, stat.getMissRatio()); - - html.unsafeAppend("\n"); - - if (null != limit && maxSize >= limit) - html.unsafeAppend(""); - - html.unsafeAppend("\n"); - rowCount++; - } - - double ratio = 0 != gets ? misses / (double)gets : 0; - html.unsafeAppend(""); - - appendLongs(html, null, null, size, gets, misses, puts, expirations, evictions, removes, clears); - appendDoubles(html, ratio); - - html.unsafeAppend("\n"); - html.unsafeAppend("
Debug NameLimitMax SizeCurrent SizeGetsMissesPutsExpirationsEvictionsRemovesClearsMiss PercentageClear
").append(LinkBuilder.labkeyLink("Clear", getCacheURL(stat.getDescription()))).unsafeAppend("This cache has been limited
Total
\n"); + long size = stats.stream().mapToLong(CacheStats::getSize).sum(); + long gets = stats.stream().mapToLong(CacheStats::getGets).sum(); + long misses = stats.stream().mapToLong(CacheStats::getMisses).sum(); + long puts = stats.stream().mapToLong(CacheStats::getPuts).sum(); + long expirations = stats.stream().mapToLong(CacheStats::getExpirations).sum(); + long evictions = stats.stream().mapToLong(CacheStats::getEvictions).sum(); + long removes = stats.stream().mapToLong(CacheStats::getRemoves).sum(); + long clears = stats.stream().mapToLong(CacheStats::getClears).sum(); + double ratio = gets != 0 ? misses / (double) gets : 0; + + AtomicInteger rowCount = new AtomicInteger(); + html.append(createHtmlFragment( + P(B(title + " (" + stats.size() + ")")), + TABLE(cl("labkey-data-region-legacy", "labkey-show-borders", "labkey-data-region-header-lock"), + TR( + TD(cl("labkey-column-header"), "Debug Name"), + TD(cl("labkey-column-header"), "Limit"), + TD(cl("labkey-column-header"), "Max Size"), + TD(cl("labkey-column-header"), "Current Size"), + TD(cl("labkey-column-header"), "Gets"), + TD(cl("labkey-column-header"), "Misses"), + TD(cl("labkey-column-header"), "Puts"), + TD(cl("labkey-column-header"), "Expirations"), + TD(cl("labkey-column-header"), "Evictions"), + TD(cl("labkey-column-header"), "Removes"), + TD(cl("labkey-column-header"), "Clears"), + TD(cl("labkey-column-header"), "Miss Percentage"), + TD(cl("labkey-column-header"), "Clear") + ), + stats.stream().map(stat -> { + Long limit = stat.getLimit(); + long maxSize = stat.getMaxSize(); + + String clearId = "clear_" + UniqueID.getServerSessionScopedUID(); + HttpView.currentPageConfig().addHandler(clearId, "click", + "LABKEY.Admin.Caches.clearSingle(" + PageFlowUtil.jsString(stat.getDescription()) + "); return false;"); + + return TR(cl(rowCount.getAndIncrement() % 2 == 0 ? "labkey-alternate-row" : "labkey-row"), + descriptionCell(stat.getDescription(), stat.getCreationStackTrace()), + longCells(limit, maxSize, stat.getSize(), stat.getGets(), stat.getMisses(), stat.getPuts(), stat.getExpirations(), stat.getEvictions(), stat.getRemoves(), stat.getClears()), + doubleCell(stat.getMissRatio()), + TD(A(cl("labkey-text-link").at(href, "#").id(clearId), "Clear")), + null != limit && maxSize >= limit ? TD(SPAN(cl("labkey-error"), "This cache has been limited")) : null + ); + }), + TR(cl("labkey-row"), + TD(B("Total")), + longCells(null, null, size, gets, misses, puts, expirations, evictions, removes, clears), + doubleCell(ratio) + ) + ) + ) + ); } private static final List PREFIXES_TO_SKIP = List.of( @@ -3325,7 +3309,8 @@ private void appendStats(HtmlStringBuilder html, String title, List "org.labkey.api.module.ModuleResourceCache" ); - private void appendDescription(HtmlStringBuilder html, String description, @Nullable StackTraceElement[] creationStackTrace) + @Nullable + private Renderable descriptionCell(String description, @Nullable StackTraceElement[] creationStackTrace) { StringBuilder sb = new StringBuilder(); @@ -3337,7 +3322,7 @@ private void appendDescription(HtmlStringBuilder html, String description, @Null // Skip the first few uninteresting stack trace elements to highlight the caller we care about if (trimming) { - if (PREFIXES_TO_SKIP.stream().anyMatch(prefix->element.toString().startsWith(prefix))) + if (PREFIXES_TO_SKIP.stream().anyMatch(prefix -> element.toString().startsWith(prefix))) continue; trimming = false; @@ -3347,30 +3332,31 @@ private void appendDescription(HtmlStringBuilder html, String description, @Null } } - if (!sb.isEmpty()) - { - String message = PageFlowUtil.jsString(sb); - String id = "id" + UniqueID.getServerSessionScopedUID(); - html.append(DOM.createHtmlFragment(TD(A(at(href, "#").id(id), description)))); - HttpView.currentPageConfig().addHandler(id, "click", "alert(" + message + ");return false;"); - } + if (sb.isEmpty()) + return TD(description); + + String message = PageFlowUtil.jsString(sb); + String id = "id" + UniqueID.getServerSessionScopedUID(); + HttpView.currentPageConfig().addHandler(id, "click", "alert(" + message + ");return false;"); + return TD(A(at(href, "#").id(id), description)); } - private void appendLongs(HtmlStringBuilder html, Long... stats) + private List longCells(Long... stats) { + List cells = new ArrayList<>(); for (Long stat : stats) { if (null == stat) - html.unsafeAppend(" "); + cells.add(TD(NBSP)); else - html.unsafeAppend("").append(commaf0.format(stat)).unsafeAppend(""); + cells.add(TD(at(style, "text-align:right"), commaf0.format(stat))); } + return cells; } - private void appendDoubles(HtmlStringBuilder html, double... stats) + private Renderable doubleCell(double stat) { - for (double stat : stats) - html.unsafeAppend("").append(percent.format(stat)).unsafeAppend(""); + return TD(at(style, "text-align:right"), percent.format(stat)); } @Override @@ -3381,6 +3367,60 @@ public void addNavTrail(NavTree root) } } + @AdminConsoleAction + public static class ClearCachesAction extends MutatingApiAction + { + @Override + public ApiResponse execute(MemForm form, BindException errors) + { + if (form.getDebugName() != null) + { + for (TrackingCache cache : CacheManager.getKnownCaches()) + { + if (form.getDebugName().equals(cache.getDebugName())) + { + LOG.info("Purging cache: " + cache.getDebugName()); + cache.clear(); + } + } + } + else if (form.isClearCaches() && form.isGc()) + { + long before = doGc(); + doClearCaches(); + long cacheMemoryUsed = before - doGc(); + String cacheMemUsed = cacheMemoryUsed > 0 ? FileUtils.byteCountToDisplaySize(cacheMemoryUsed) : "Unknown"; + LOG.info("Estimate of cache memory used: " + cacheMemUsed); + lastCacheMemUsed = cacheMemUsed; + } + else if (form.isClearCaches()) + { + doClearCaches(); + } + else if (form.isGc()) + { + doGc(); + } + return new ApiSimpleResponse("success", true); + } + + private long doGc() + { + LOG.info("Garbage collecting"); + System.gc(); + return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + } + + private void doClearCaches() + { + LOG.info("Clearing Introspector caches"); + Introspector.flushCaches(); + LOG.info("Purging all caches"); + CacheManager.clearAllKnownCaches(); + SearchService.get().purgeQueues(); + } + } + @RequiresSiteAdmin public class EnvironmentVariablesAction extends SimpleViewAction { @@ -3725,15 +3765,32 @@ public static ActionURL getCachesURL(boolean clearCaches, boolean gc) return url; } - public static ActionURL getCacheURL(String debugName) + public static ActionURL getClearCachesURL(boolean clearCaches, boolean gc) { - ActionURL url = new ActionURL(CachesAction.class, ContainerManager.getRoot()); + ActionURL url = new ActionURL(ClearCachesAction.class, ContainerManager.getRoot()); + + if (clearCaches) + url.addParameter(MemForm.Params.clearCaches, "1"); - url.addParameter(MemForm.Params.debugName, debugName); + if (gc) + url.addParameter(MemForm.Params.gc, "1"); return url; } + public static ActionURL getClearCacheURL(String debugName) + { + return new ActionURL(ClearCachesAction.class, ContainerManager.getRoot()) + .addParameter(MemForm.Params.debugName, debugName); + } + + /** @deprecated Use {@link #getClearCacheURL(String)} — individual cache clearing now requires a POST confirmation */ + @Deprecated + public static ActionURL getCacheURL(String debugName) + { + return getClearCacheURL(debugName); + } + private static volatile String lastCacheMemUsed = null; @AdminConsoleAction @@ -3743,60 +3800,9 @@ public class MemTrackerAction extends SimpleViewAction public ModelAndView getView(MemForm form, BindException errors) { Set objectsToIgnore = MemTracker.getInstance().beforeReport(); - - boolean gc = form.isGc(); - boolean cc = form.isClearCaches(); - - if (getUser().hasRootAdminPermission() && (gc || cc)) - { - // If both are requested then try to determine and record cache memory usage - if (gc && cc) - { - // gc once to get an accurate free memory read - long before = gc(); - clearCaches(); - // gc again now that we cleared caches - long cacheMemoryUsed = before - gc(); - - // Difference could be < 0 if JVM or other threads have performed gc, in which case we can't guesstimate cache memory usage - String cacheMemUsed = cacheMemoryUsed > 0 ? FileUtils.byteCountToDisplaySize(cacheMemoryUsed) : "Unknown"; - LOG.info("Estimate of cache memory used: " + cacheMemUsed); - lastCacheMemUsed = cacheMemUsed; - } - else if (cc) - { - clearCaches(); - } - else - { - gc(); - } - - LOG.info("Cache clearing and garbage collecting complete"); - } - return new JspView<>("/org/labkey/core/admin/memTracker.jsp", new MemBean(getViewContext().getRequest(), objectsToIgnore)); } - /** @return estimated current memory usage, post-garbage collection */ - private long gc() - { - LOG.info("Garbage collecting"); - System.gc(); - // This is more reliable than relying on just free memory size, as the VM can grow/shrink the heap at will - return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); - } - - private void clearCaches() - { - LOG.info("Clearing Introspector caches"); - Introspector.flushCaches(); - LOG.info("Purging all caches"); - CacheManager.clearAllKnownCaches(); - LOG.info("Purging SearchService queues"); - SearchService.get().purgeQueues(); - } - @Override public void addNavTrail(NavTree root) { @@ -3861,12 +3867,12 @@ private MemBean(HttpServletRequest request, Set objectsToIgnore) { MemTracker memTracker = MemTracker.getInstance(); List all = memTracker.getReferences(); - long threadId = Thread.currentThread().getId(); + long threadId = Thread.currentThread().threadId(); // Attempt to detect other threads running labkey code -- mem tracker page will warn if any are found for (Thread thread : new ThreadsBean().threads) { - if (thread.getId() == threadId) + if (thread.threadId() == threadId) continue; Thread.State state = thread.getState(); @@ -4963,7 +4969,7 @@ public void setOverrideDefault(String overrideDefault) public static class RConfigurationAction extends FolderManagementViewPostAction { @Override - protected HttpView getTabView(RConfigForm form, boolean reshow, BindException errors) + protected HttpView getTabView(RConfigForm form, boolean reshow, BindException errors) { return new JspView<>("/org/labkey/core/admin/rConfiguration.jsp", form, errors); } @@ -5396,7 +5402,7 @@ public class ImportFolderAction extends FolderManagementViewPostAction getTabView(ImportFolderForm form, boolean reshow, BindException errors) { // default the createSharedDatasets and validateQueries to true if this is not a form error reshow if (!errors.hasErrors()) @@ -5427,21 +5433,12 @@ public boolean handlePost(ImportFolderForm form, BindException errors) throws Ex Container container = getContainer(); PipeRoot pipelineRoot; FileLike pipelineUnzipDir; // Should be local & writable - PipelineUrls pipelineUrlProvider; if (form.getOrigin() == null) { form.setOrigin("Folder"); } - // make sure we have a pipeline url provider to use for the success URL redirect - pipelineUrlProvider = urlProvider(PipelineUrls.class); - if (pipelineUrlProvider == null) - { - errors.reject("folderImport", "Pipeline url provider does not exist."); - return false; - } - // make sure that the pipeline root is valid for this container pipelineRoot = PipelineService.get().findPipelineRoot(container); if (!PipelineService.get().hasValidPipelineRoot(container) || pipelineRoot == null) @@ -5490,7 +5487,7 @@ public boolean handlePost(ImportFolderForm form, BindException errors) throws Ex options.setActivity(ComplianceService.get().getCurrentActivity(getViewContext())); // finally, create the study or folder import pipeline job - _successURL = pipelineUrlProvider.urlBegin(container); + _successURL = urlProvider(PipelineUrls.class).urlBegin(container); PipelineService.get().runFolderImportJob(container, user, url, archiveXml, fiConfig.originalFileName, pipelineRoot, options); return !errors.hasErrors(); @@ -10269,6 +10266,10 @@ public ApiResponse execute(DeletedFoldersForm form, BindException errors) if (isBlank(form.getContainerPath())) throw new NotFoundException(); Container container = ContainerManager.getForPath(form.getContainerPath()); + if (!container.hasPermission(getUser(), AdminPermission.class)) + { + throw new UnauthorizedException(); + } for (String tabName : form.getResurrectFolders()) { ContainerManager.clearContainerTabDeleted(container, tabName, form.getNewFolderType()); @@ -11640,15 +11641,15 @@ private static boolean saveProjectSettings(Container c, User user, ProjectSettin } }); - boolean noErrors = !saveFolderSettings(c, user, props, form, errors); + boolean success = saveFolderSettings(c, user, props, form, errors); - if (noErrors) + if (success) { // Bump the look & feel revision so browsers retrieve the new theme stylesheet WriteableAppProps.incrementLookAndFeelRevisionAndSave(); } - return noErrors; + return success; } private static void setProperty(boolean inherited, Runnable clear, Runnable set) @@ -11889,7 +11890,7 @@ public static class LookAndFeelBean public final HtmlString customColumnRestrictionHelpLink = new HelpTopic("chartTrouble").getSimpleLinkHtml("more info..."); } - @RequiresPermission(AdminPermission.class) + @RequiresPermission(SiteAdminPermission.class) public static class AdjustSystemTimestampsAction extends FormViewAction { @Override diff --git a/core/src/org/labkey/core/admin/memTracker.jsp b/core/src/org/labkey/core/admin/memTracker.jsp index 9d70d5abf14..70f32873aae 100644 --- a/core/src/org/labkey/core/admin/memTracker.jsp +++ b/core/src/org/labkey/core/admin/memTracker.jsp @@ -26,8 +26,16 @@ <%@ page import="org.labkey.api.view.JspView" %> <%@ page import="org.labkey.core.admin.AdminController" %> <%@ page import="org.labkey.core.admin.AdminController.MemBean" %> +<%@ page import="org.labkey.api.view.template.ClientDependencies" %> <%@ page import="java.text.DecimalFormat" %> <%@ page extends="org.labkey.api.jsp.JspBase" %> +<%! + @Override + public void addClientDependencies(ClientDependencies dependencies) + { + dependencies.add("admin/caches.js"); + } +%> <% JspView me = HttpView.currentView(); MemBean bean = me.getModelBean(); @@ -45,8 +53,8 @@ %> <% if (hasAdminPerm) { %>

- <%=link("Clear Caches, GC and Refresh", AdminController.getMemTrackerURL(true, true))%> - <%=link("GC and Refresh", AdminController.getMemTrackerURL(false, true))%> + Clear Caches, GC and Refresh + GC and Refresh <%=link("Refresh", AdminController.getMemTrackerURL(false, false))%> <% if (getUser().hasSiteAdminPermission()) { %> <%=link("Memory Stress Test", new ActionURL(AdminController.MemoryStressTestAction.class, ContainerManager.getRoot()))%> <% } %>

diff --git a/core/webapp/admin/caches.js b/core/webapp/admin/caches.js new file mode 100644 index 00000000000..d4b34243592 --- /dev/null +++ b/core/webapp/admin/caches.js @@ -0,0 +1,40 @@ +LABKEY.Admin = LABKEY.Admin || {}; + +LABKEY.Admin.Caches = new function() { + var API_URL = LABKEY.ActionURL.buildURL('admin', 'clearCaches'); + + function doPost(params) { + LABKEY.Ajax.request({ + url: API_URL, + method: 'POST', + params: params, + success: reloadPage, + failure: LABKEY.Utils.getCallbackWrapper(null, null, true) + }); + } + + function reloadPage() { window.location.reload(); } + + // clearSingle is called from inline onclick handlers (addHandler), so must be defined immediately. + this.clearSingle = function(debugName) { + doPost({ debugName: debugName }); + }; + + // Bind button handlers once the DOM is ready — elements don't exist yet when this script runs. + window.addEventListener('DOMContentLoaded', function() { + function bindIfPresent(id, params) { + var el = document.getElementById(id); + if (el) { + el.addEventListener('click', function(e) { + e.preventDefault(); + el.disabled = true; + doPost(params); + }); + } + } + + bindIfPresent('clearAllCaches', { clearCaches: true }); + bindIfPresent('clearCachesGc', { clearCaches: true, gc: true }); + bindIfPresent('gcOnly', { gc: true }); + }); +}; diff --git a/study/src/org/labkey/study/controllers/security/SecurityController.java b/study/src/org/labkey/study/controllers/security/SecurityController.java index 0785f7377f2..643d2ae92ed 100644 --- a/study/src/org/labkey/study/controllers/security/SecurityController.java +++ b/study/src/org/labkey/study/controllers/security/SecurityController.java @@ -469,7 +469,7 @@ public List getTabList() } @Override - public HttpView getTabView(String tabId) + public HttpView getTabView(String tabId) { if (TAB_STUDY.equals(tabId)) { @@ -559,9 +559,7 @@ public void addNavTrail(NavTree root) if (getContainer().hasPermission(getUser(), AdminPermission.class)) root.addChild("Manage Views", urlProvider(ReportUrls.class).urlManageViews(getContainer()).getLocalURIString()); } - catch (Exception e) - { - } + catch (Exception _) {} root.addChild("Report and View Permissions"); } } @@ -701,7 +699,7 @@ public void setRemove(Integer remove) public int getAdd() { - return add == null ? 0 : remove.intValue(); + return add == null ? 0 : add.intValue(); } public void setAdd(Integer add) @@ -730,9 +728,9 @@ public String getTabId() } } - static class Overview extends WebPartView + static class Overview extends WebPartView { - private final HttpView _vbox; + private final HttpView _vbox; Overview(StudyImpl study) { @@ -773,7 +771,7 @@ static class Overview extends WebPartView @Override - protected void renderView(Object model, HtmlWriter out) throws Exception + protected void renderView(StudyImpl model, HtmlWriter out) throws Exception { include(_vbox, out.unwrap()); } @@ -783,7 +781,7 @@ protected void renderView(Object model, HtmlWriter out) throws Exception public static class StudySecurityViewFactory implements SecurityManager.ViewFactory { @Override - public HttpView createView(ViewContext context) + public StudySecurityPermissionsView createView(ViewContext context) { if (null != BaseStudyController.getStudy(context.getContainer())) return new StudySecurityPermissionsView(); From c97fddc533e2bca66d0274a2daf383d195f18058 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Fri, 8 May 2026 16:03:31 -0700 Subject: [PATCH 2/3] Include the benefits --- core/src/org/labkey/core/admin/AdminController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/org/labkey/core/admin/AdminController.java b/core/src/org/labkey/core/admin/AdminController.java index da74ae70ea1..31164daa1d7 100644 --- a/core/src/org/labkey/core/admin/AdminController.java +++ b/core/src/org/labkey/core/admin/AdminController.java @@ -2700,7 +2700,7 @@ public class DumpHeapAction extends ConfirmAction public ModelAndView getConfirmView(Object o, BindException errors) { setTitle("Dump Heap"); - return HtmlView.of("Are you sure you want to dump the JVM heap to disk? This may temporarily slow the server and will consume significant disk space."); + return HtmlView.of("Are you sure you want to dump the JVM heap to disk? Heap dumps are useful for troubleshooting memory leaks or OutOfMemoryErrors. This may temporarily slow the server and will consume significant disk space."); } @Override From 94c0c948a41d08646d3c4f48ddec46d762b3c16f Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Fri, 8 May 2026 16:06:59 -0700 Subject: [PATCH 3/3] Cleanup --- .../labkey/core/admin/AdminController.java | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/core/src/org/labkey/core/admin/AdminController.java b/core/src/org/labkey/core/admin/AdminController.java index 31164daa1d7..a34fe384cb3 100644 --- a/core/src/org/labkey/core/admin/AdminController.java +++ b/core/src/org/labkey/core/admin/AdminController.java @@ -3309,7 +3309,6 @@ private void appendStats(HtmlStringBuilder html, String title, List "org.labkey.api.module.ModuleResourceCache" ); - @Nullable private Renderable descriptionCell(String description, @Nullable StackTraceElement[] creationStackTrace) { StringBuilder sb = new StringBuilder(); @@ -3765,32 +3764,12 @@ public static ActionURL getCachesURL(boolean clearCaches, boolean gc) return url; } - public static ActionURL getClearCachesURL(boolean clearCaches, boolean gc) - { - ActionURL url = new ActionURL(ClearCachesAction.class, ContainerManager.getRoot()); - - if (clearCaches) - url.addParameter(MemForm.Params.clearCaches, "1"); - - if (gc) - url.addParameter(MemForm.Params.gc, "1"); - - return url; - } - public static ActionURL getClearCacheURL(String debugName) { return new ActionURL(ClearCachesAction.class, ContainerManager.getRoot()) .addParameter(MemForm.Params.debugName, debugName); } - /** @deprecated Use {@link #getClearCacheURL(String)} — individual cache clearing now requires a POST confirmation */ - @Deprecated - public static ActionURL getCacheURL(String debugName) - { - return getClearCacheURL(debugName); - } - private static volatile String lastCacheMemUsed = null; @AdminConsoleAction