Skip to content

External Storage Integration: NexusWorker - #3018

Open
cconstable wants to merge 10 commits into
mainfrom
extstore/nexus-worker
Open

External Storage Integration: NexusWorker#3018
cconstable wants to merge 10 commits into
mainfrom
extstore/nexus-worker

Conversation

@cconstable

@cconstable cconstable commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

What was changed

  • NexusWorker now stores and retrieves payloads.

Why?

  • Nexus workers should use external storage.

Checklist

@cconstable
cconstable changed the base branch from main to extstore/foundation August 19, 2026 17:51
@cconstable cconstable changed the title extstore/nexus worker External Storage Integration: NexusWorker Aug 19, 2026
@cconstable
cconstable marked this pull request as ready for review August 19, 2026 18:57
@cconstable
cconstable requested a review from a team as a code owner August 19, 2026 18:57
@cconstable
cconstable force-pushed the extstore/foundation branch from 460bfbf to b3804da Compare August 21, 2026 20:35
@cconstable
cconstable force-pushed the extstore/nexus-worker branch from 609161f to c553b3d Compare August 21, 2026 21:56
@cconstable
cconstable force-pushed the extstore/foundation branch from b3804da to ca09b50 Compare August 24, 2026 01:23
@cconstable
cconstable force-pushed the extstore/nexus-worker branch 3 times, most recently from e8155ee to 5ef0bd6 Compare August 27, 2026 18:34
@cconstable

Copy link
Copy Markdown
Contributor Author

Missing: NexusClient integration. Adding now.

@cconstable
cconstable force-pushed the extstore/nexus-worker branch from 41fa991 to 94eaaca Compare August 27, 2026 21:15
task.getCompletionCallback());
}

private void storeOutbound(Message.Builder builder) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this storeOutbound work differently then this one ? Should we just make one common helper ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some callers of storeOutbound have already built messages and some have builders. Some also need a target (ActivityWorker) and others also need a visitor (the WorkflowWorker one). We could create a MessageOrBuilder type for this (maybe there already is one) and do some refactoring. Personally, I'd like that to be a follow-up if that is the direction we want to head in.

@Quinn-With-Two-Ns

Copy link
Copy Markdown
Contributor

Is there any tests showing external storage working for a Nexus operation? or what PR would that be in?

@Quinn-With-Two-Ns

Copy link
Copy Markdown
Contributor

Codex flagged these issues as well

I found two actionable issues. Your question about integration tests was
  warranted.

  1. High — Storage failures never produce a retryable Nexus task failure

     ExternalStorageTaskFailure says it is reported as a retryable handler error

     (https://github.com/temporalio/sdk-java/blob/94eaaca69c34a780b00803f1ddac129112ad2107/temporal-sdk/src/main/java/io/temporal/internal/worker/NexusWorker.java#L287-L295),
     but the class is unused. Both inbound retrieval and outbound storage errors

     (https://github.com/temporalio/sdk-java/blob/94eaaca69c34a780b00803f1ddac129112ad2107/temporal-sdk/src/main/java/io/temporal/internal/worker/NexusWorker.java#L553-L574)
     escape the worker. The generic poll executor only forwards them to the
     uncaught-exception handler; it does not call RespondNexusTaskFailed.

     Recovery therefore waits for task timeout/redelivery. For an outbound
     failure, the user handler has already executed, so redelivery can repeat
     its side effects. Catch storage errors and immediately send a small,
     inline, retryable INTERNAL handler failure—without passing that failure
     through external storage again. Add transient retrieve/store failure
     integration tests.

  2. Medium — External retrieval is excluded from the used-slot lifecycle

     markSlotUsed now happens after retrieveInboundPayloads

     (https://github.com/temporalio/sdk-java/blob/94eaaca69c34a780b00803f1ddac129112ad2107/temporal-sdk/src/main/java/io/temporal/internal/worker/NexusWorker.java#L325-L350).
     During a slow retrieval, used-slot metrics and custom supplier state
     under-report active work. If retrieval fails, the completion callback
     releases the permit as taskComplete, but TrackingSlotSupplier has no slot
     info because it was never marked used

     (https://github.com/temporalio/sdk-java/blob/94eaaca69c34a780b00803f1ddac129112ad2107/temporal-sdk/src/main/java/io/temporal/internal/worker/TrackingSlotSupplier.java#L70-L91).

     Mark the slot used before retrieval, or release failed pre-handler
     retrievals through an explicit never-used/error path.

@cconstable
cconstable force-pushed the extstore/foundation branch from 2d2d90e to 504467c Compare August 28, 2026 16:10
Base automatically changed from extstore/foundation to main August 28, 2026 17:27
@cconstable
cconstable force-pushed the extstore/nexus-worker branch from 94eaaca to 78778d3 Compare August 28, 2026 21:27
@cconstable
cconstable force-pushed the extstore/nexus-worker branch from 78778d3 to d877ec7 Compare August 31, 2026 15:42
@cconstable

Copy link
Copy Markdown
Contributor Author

The codex finds are interesting. I think it's correct, it doesn't look like the we are catching failures. Adding some logic to send a nexus task failed so the server can retry instead of waiting for timeout.

@cconstable

Copy link
Copy Markdown
Contributor Author

Addressed the feedback and issues codex found. One thing to note @Quinn-With-Two-Ns is that when we fail the nexus task (because of an external storage issue which happens AFTER the task is handled + "complete") and respond with a RespondNexusTaskFailed this means that the server can retry which means there will be issues if the handler is not doing idempotent work. Is that ok?

Another interesting idea @jmaeagle99 is that we could have a fallback for scenarios like this where we would skip external storage since the nexus task already completed for scenarios where making an idempotent task is more of a detriment than just skipping external storage... so external storage would be configured with a "do your best to offload these payloads but if you cant (because the provider is down, etc) its ok to send them through as regular payloads".

@cconstable

Copy link
Copy Markdown
Contributor Author

Also added some tests @Quinn-With-Two-Ns to address your feedback.

@cconstable

Copy link
Copy Markdown
Contributor Author

The other PRs #3017 and #3020 also needed to handle the extstore store failures in the same way we were handling here. There is some duplication but I can follow up with a PR to consolidate all that once these PRs are merged.

@jmaeagle99

Copy link
Copy Markdown
Contributor

Addressed the feedback and issues codex found. One thing to note @Quinn-With-Two-Ns is that when we fail the nexus task (because of an external storage issue which happens AFTER the task is handled + "complete") and respond with a RespondNexusTaskFailed this means that the server can retry which means there will be issues if the handler is not doing idempotent work. Is that ok?

Another interesting idea @jmaeagle99 is that we could have a fallback for scenarios like this where we would skip external storage since the nexus task already completed for scenarios where making an idempotent task is more of a detriment than just skipping external storage... so external storage would be configured with a "do your best to offload these payloads but if you cant (because the provider is down, etc) its ok to send them through as regular payloads".

I don't think this is a new error mode for Nexus Operation handlers though. They already have at-least-once attempt semantics. The operation's work needs to be idempotent regardless of external storage.

On skipping external storage, if the result is too large, then the task should be failed by the server (have to double check that) in a terminal way. Even if it doesn't, it will likely poison downstream consumers.

.inc(1);
HandlerException handlerException =
new HandlerException(HandlerException.ErrorType.INTERNAL, "External storage failed", e);
sendReply(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs to handle ExternalStorageTaskFailure in case the failure cannot be externally stored and then submit a minimal NexusTaskHandler.Result from a small exception instance.

task = retrieveInboundPayloads(task);
} catch (Throwable e) {
taskFailed = true;
sendStorageFailure(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to log "External storage failed for a nexus task" but external storage is not configured, so the log is misleading.

ExternalStorage externalStorageConfig = options.getExternalStorage();
if (externalStorageConfig != null) {
options =
NexusClientOptions.newBuilder(options)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this setExternalStorage(null)?

}

@Nullable
public ExternalStorage getExternalStorage() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs javadoc

return this;
}

public NexusClientOptions.Builder setExternalStorage(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs javadoc

public void interruptingShutdownCancelsInFlightStorage() throws Exception {
NexusWorker worker = worker();

worker.shutdown(new ShutdownManager(), true).get();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be closed, probably in a finally, in every test.

slotSupplier.markSlotUsed(
new NexusSlotInfo(
service, operation, taskQueue, options.getIdentity(), options.getBuildId()),
task.getPermit());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code and above used to be outside of the try block. Not sure it needs to be inside. It might incidentally fix some existing problems. But its not the focus of this PR. If it doesn't need to be in the try to enable external storage, then let's move it back. If it is fixing something separately, then open a different PR for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants