From 41f7e85a2db1e1e45f8ad906ed53b141b75b4c70 Mon Sep 17 00:00:00 2001 From: John Hendrikx Date: Thu, 4 Jun 2026 17:38:31 +0200 Subject: [PATCH] Avoid costly stack walk on every task start in AxonTimeLimitedTask Thread.getStackTrace() was called on every start() to capture the caller class name for log trimming, even though that information is only used when a timeout fires. Callers already know their own class, so the name is now passed as a Class constructor parameter and stored once. The stack frame comparison is also tightened from a substring search on toString() to an exact match on getClassName(). --- .../core/timeout/AxonTimeLimitedTask.java | 70 +++++++++++++++++-- .../TimeoutWrappedMessageHandlingMember.java | 3 +- .../UnitOfWorkTimeoutInterceptorBuilder.java | 3 +- 3 files changed, 67 insertions(+), 9 deletions(-) diff --git a/messaging/src/main/java/org/axonframework/messaging/core/timeout/AxonTimeLimitedTask.java b/messaging/src/main/java/org/axonframework/messaging/core/timeout/AxonTimeLimitedTask.java index cd4b54d676d..641a00a8386 100644 --- a/messaging/src/main/java/org/axonframework/messaging/core/timeout/AxonTimeLimitedTask.java +++ b/messaging/src/main/java/org/axonframework/messaging/core/timeout/AxonTimeLimitedTask.java @@ -15,6 +15,7 @@ */ package org.axonframework.messaging.core.timeout; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import java.util.concurrent.Future; @@ -46,12 +47,13 @@ class AxonTimeLimitedTask { private final String taskName; private final ScheduledExecutorService scheduledExecutorService; private final Logger logger; + @Nullable + private final String callerClassName; // stored as name to avoid getName() on every stack frame check private boolean completed = false; private boolean interrupted = false; private boolean interruptedExternally = false; private long startTimeMs = -1; private Future currentScheduledFuture = null; - private String startStackTrace; /** @@ -72,12 +74,37 @@ public AxonTimeLimitedTask(String taskName, int timeout, int warningThreshold, int warningInterval) { + this(taskName, timeout, warningThreshold, warningInterval, AxonTaskJanitor.INSTANCE, AxonTaskJanitor.LOGGER, null); + } + + /** + * Creates a new {@link AxonTimeLimitedTask} for the given {@code task} with the given {@code timeout}, + * {@code warningThreshold} and {@code warningInterval}. Runs the provided task on the current thread after + * scheduling a timeout and warnings on another thread. + *

+ * The {@code callerClass} is used to trim the stack trace in timeout/warning logs, cutting off framework internals + * below the caller. If you do not need trimming, use + * {@link #AxonTimeLimitedTask(String, int, int, int)}. + * + * @param taskName The task's name to be included in the logging + * @param timeout The timeout in milliseconds + * @param warningThreshold The threshold in milliseconds after which a warning is logged. Setting this to a value + * equal or higher than {@code timeout} will disable warnings. + * @param warningInterval The interval in milliseconds between warnings. + * @param callerClass the class of the direct caller, used to trim the stack trace in timeout/warning logs + */ + public AxonTimeLimitedTask(String taskName, + int timeout, + int warningThreshold, + int warningInterval, + Class callerClass) { this(taskName, timeout, warningThreshold, warningInterval, AxonTaskJanitor.INSTANCE, - AxonTaskJanitor.LOGGER + AxonTaskJanitor.LOGGER, + callerClass ); } @@ -97,8 +124,8 @@ public AxonTimeLimitedTask(String taskName, * @param warningThreshold The threshold in milliseconds after which a warning is logged. Setting this to a * value equal or higher than {@code timeout} will disable warnings. * @param warningInterval The interval in milliseconds between warnings. - * @param scheduledExecutorService The executor service to schedule the timeout and warnings - * @param logger The logger to log the warnings and errors + * @param scheduledExecutorService the executor service to schedule the timeout and warnings + * @param logger the logger to log the warnings and errors */ public AxonTimeLimitedTask(String taskName, int timeout, @@ -106,6 +133,36 @@ public AxonTimeLimitedTask(String taskName, int warningInterval, ScheduledExecutorService scheduledExecutorService, Logger logger) { + this(taskName, timeout, warningThreshold, warningInterval, scheduledExecutorService, logger, null); + } + + /** + * Creates a new {@link AxonTimeLimitedTask} for the given {@code task} with the given {@code timeout}, + * {@code warningThreshold} and {@code warningInterval}. For scheduling, the provided + * {@code scheduledExecutorService} will be used. To log warnings and errors, the provided {@code logger} will be + * used. + *

+ * The {@code callerClass} is used to trim the stack trace in timeout/warning logs, cutting off framework internals + * below the caller. If you do not need trimming, use + * {@link #AxonTimeLimitedTask(String, int, int, int, ScheduledExecutorService, Logger)}. + * + * @param taskName The task's name to be included in the logging + * @param timeout The timeout in milliseconds + * @param warningThreshold The threshold in milliseconds after which a warning is logged. Setting this to a + * value equal or higher than {@code timeout} will disable warnings. + * @param warningInterval The interval in milliseconds between warnings. + * @param scheduledExecutorService the executor service to schedule the timeout and warnings + * @param logger the logger to log the warnings and errors + * @param callerClass the class of the direct caller, used to trim the stack trace in timeout/warning + * logs + */ + public AxonTimeLimitedTask(String taskName, + int timeout, + int warningThreshold, + int warningInterval, + ScheduledExecutorService scheduledExecutorService, + Logger logger, + Class callerClass) { if (taskName == null || taskName.isEmpty()) { throw new IllegalArgumentException("Task name cannot be null or empty"); } @@ -115,6 +172,7 @@ public AxonTimeLimitedTask(String taskName, this.warningInterval = warningInterval; this.scheduledExecutorService = scheduledExecutorService; this.logger = logger; + this.callerClassName = callerClass != null ? callerClass.getName() : null; this.thread = Thread.currentThread(); } @@ -130,7 +188,6 @@ public void start() { throw new IllegalStateException("Task can only be run once"); } startTimeMs = System.currentTimeMillis(); - startStackTrace = thread.getStackTrace()[2].getClassName(); if (warningThreshold < 0 || warningThreshold >= timeout) { scheduleImmediateInterrupt(); @@ -311,8 +368,7 @@ private String getCurrentStackTrace() { StringBuilder sb = new StringBuilder(); for (StackTraceElement element : stackTrace) { sb.append(element).append("\n"); - // This is the start of the stack trace of the framework internals calling the method - if (element.toString().contains(startStackTrace)) { + if (callerClassName != null && element.getClassName().equals(callerClassName)) { break; } } diff --git a/messaging/src/main/java/org/axonframework/messaging/core/timeout/TimeoutWrappedMessageHandlingMember.java b/messaging/src/main/java/org/axonframework/messaging/core/timeout/TimeoutWrappedMessageHandlingMember.java index 8315cf8fd40..4dc7e05229f 100644 --- a/messaging/src/main/java/org/axonframework/messaging/core/timeout/TimeoutWrappedMessageHandlingMember.java +++ b/messaging/src/main/java/org/axonframework/messaging/core/timeout/TimeoutWrappedMessageHandlingMember.java @@ -70,7 +70,8 @@ public Object handleSync(Message message, ProcessingContext context, @Nullable T taskName, timeout, warningThreshold, - warningInterval + warningInterval, + getClass() ); task.start(); try { diff --git a/messaging/src/main/java/org/axonframework/messaging/core/timeout/UnitOfWorkTimeoutInterceptorBuilder.java b/messaging/src/main/java/org/axonframework/messaging/core/timeout/UnitOfWorkTimeoutInterceptorBuilder.java index 078691635d0..2e4750d3914 100644 --- a/messaging/src/main/java/org/axonframework/messaging/core/timeout/UnitOfWorkTimeoutInterceptorBuilder.java +++ b/messaging/src/main/java/org/axonframework/messaging/core/timeout/UnitOfWorkTimeoutInterceptorBuilder.java @@ -165,7 +165,8 @@ void initializeTimeoutIfNotInitialized(ProcessingContext context) { warningThreshold, warningInterval, executorService, - logger + logger, + UnitOfWorkTimeoutInterceptorBuilder.class ); context.putResource(TRANSACTION_TIME_LIMIT_CONTEXT_RESOURCE_KEY, taskTimeout); taskTimeout.start();