| Availability | Confluence 11.0 or later |
Experimental in Confluence 11.0
Asynchronous macro execution is off by default. Behavior, configuration, and APIs may change before this feature becomes supported. Validate this guide against each release until the feature is stable.
This page is aimed at app developers who maintain macro modules. It explains how asynchronous macro execution works, the contracts your macro accepts when it opts in, and how to implement concurrency safety, cancellation, and result caching.
Confluence normally executes every macro on a page sequentially on the Tomcat HTTP request thread. A slow macro can therefore delay the entire page.
With asynchronous macro execution, eligible macros are dispatched to a dedicated worker thread pool and can run concurrently. The feature is controlled by the atlassian.macros.async.execution dark feature, which is disabled by default.
The rendering pipeline has two phases:
Execution order and output order are different. The final HTML preserves document order, but eligible macros may execute concurrently in an arbitrary order. A macro must not depend on another macro having executed first.
Confluence applies these policies in priority order for each macro execution:
If the worker queue is saturated, the macro falls back to synchronous execution on the HTTP request thread.
A macro is dispatched asynchronously only when all of the following conditions are met:
true from Macro.isConcurrentExecutionSafe().The Macro interface exposes three opt-in methods. Each method defaults to false.
1 2 3 4default boolean isConcurrentExecutionSafe() { return false; } // since 11.0.0 default boolean isCancellable() { return false; } // since 11.0.0 default boolean isCacheable() { return false; } // since 11.0.0
No macro shipped with Confluence 11.0 overrides isConcurrentExecutionSafe(). Therefore, no product macro opts in by default.
An administrator can save an asynchronous setting for a macro that has not declared itself safe. Confluence stores the setting and logs a warning, but the setting has no effect until the macro declares support. The read-only supportedByMacro field in the REST response reflects the macro declaration.
Override isConcurrentExecutionSafe() to declare that your macro can run on a worker thread alongside other macros.
1 2 3 4 5@Override public boolean isConcurrentExecutionSafe() { return true; }
This declaration is a contract. You are asserting that the macro produces correct output when it runs off the request thread and concurrently with other macros sharing the same ConversionContext.
The declaration covers everything the macro renders, not only its own execute() method. If the macro renders nested content, the declaration also asserts that the nested content is safe.
A macro is a reasonable candidate when all of the following are true:
ConversionContext.Do not declare a macro safe if it, or anything it renders:
ThreadLocal, or relies on one set by a servlet filter.ThreadLocalCache or HttpServletRequest. Neither is available on a worker thread.ConversionContext property or mutates a non-thread-safe value stored in it.| Carried across dispatch | Not carried across dispatch |
|---|---|
| Authenticated user | ThreadLocalCache |
| Request cache snapshot; writes are discarded | HttpServletRequest |
| Logging context (MDC) | Application-owned ThreadLocal |
| Tracing span | |
| Scopes cache | |
| Output type | |
| Content include stack copy |
Apps cannot add additional state to this set. Test with the dark feature enabled on a non-production instance before shipping the declaration.
CancellationListener receives the event and calls the cancellation handler.Thread.interrupt() to the execution thread and sets a cancellation flag in ExecutionCancellationContext.isCancellable() and handles the signal, it stops promptly.Override isCancellable() to declare graceful cancellation support.
1 2 3 4 5@Override public boolean isCancellable() { return true; }
Cancellation is also a contract. If a macro returns true but ignores cancellation signals, administrators may believe the operation stopped while the macro continues consuming worker resources.
Your macro must handle both signals sent by the framework.
Thread interruption is used for blocking operations such as sleep and blocking I/O.
1 2 3 4 5 6 7 8try { Thread.sleep(5000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); rollbackPartialWork(); return "<p>Execution cancelled.</p>"; }
Never swallow InterruptedException. Always restore the interrupt status with Thread.currentThread().interrupt(). Swallowing the exception hides the signal from the rest of the call stack and can break cancellation handling.
Use the cancellation flag for CPU-bound loops and other non-blocking code.
1 2 3 4 5 6 7 8 9 10import com.atlassian.macro.async.execution.cancellation.ExecutionCancellationContextThreadLocal; for (Item item : itemsToProcess) { if (ExecutionCancellationContextThreadLocal.isCancelled()) { cleanupAndRollback(); return "<p>Execution cancelled.</p>"; } // Process the item. }
ExecutionCancellationContextThreadLocal.isCancelled() is safe to call from any execution thread. When called outside an executor thread, it returns false rather than throwing an exception.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19@Override public String execute(Map<String, String> params, String body, ConversionContext context) throws MacroExecutionException { for (Item item : getItems(params)) { if (ExecutionCancellationContextThreadLocal.isCancelled()) { return "<p>Execution cancelled.</p>"; } try { processItem(item); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return "<p>Execution cancelled.</p>"; } } return renderResult(); }
Override isCacheable() to opt into the macro result cache.
1 2 3 4 5@Override public boolean isCacheable() { return true; }
Results are keyed by macroParameters, a hash of the macro body, and userId.
The cache key does not include the page or space. Identical markup rendered in different contexts can therefore share a cache entry. If output varies by anything outside the parameters and body, do not enable caching.
| Check | Why it matters |
|---|---|
| Output depends only on parameters, body, and current user | External state such as database contents, current time, or permissions may make cached output incorrect. |
| Output can be up to 60 seconds stale | The default TTL is 60 seconds. Adjust atlassian.macros.result.cache.expire.after.write.millis if required. |
| The macro has no per-view side effects | Cache hits skip execution, so side effects do not run on every page view. |
| Output does not contain inappropriate sensitive data | The cache is per user, but verify that external user-specific data is safe to include. |
The cache is node-local. In a clustered deployment, users accessing different nodes have independent cache entries and may see different levels of staleness.
When an administrator configures a macro through the REST API, MacroExecutionControlsValidator applies these rules:
MacroManager. The hosting plugin must be active.rateLimit, when provided, must be a positive integer. Zero and negative values are rejected.timeLimit, when provided, must be a positive integer. Zero and negative values are rejected.blog-posts, include, and excerpt-include, cannot use deduplication: true.If the macro plugin is disabled, an administrator cannot configure it for asynchronous execution. The plugin must be active when configuration is saved.
The following limitations apply in Confluence 11.0. Solutions are planned for future releases.
XhtmlContent cannot force synchronous execution for an individual conversion. disableAsyncRenderSafe() is not consulted before dispatch. If a third-party macro declares itself safe and renders content containing your macros, or invokes your storage-to-view transformers, your code may run on a worker thread even if you have not opted in. Nothing your macro declares prevents this. A future ConversionContext property is intended to allow a transformer registered early in the chain to suppress asynchronous dispatch for conversions it does not own.ConversionContext created during macro execution reports the enclosing macro identity.execute(). Mutating the map after registration can change the key.macroName only and may leave macroDefinition and macroMetadata at their last written values.Rate this page: