feat(extstore): implement core extstore logic that uses payload visitor to batch and store/retrieve payloads. - #2976
feat(extstore): implement core extstore logic that uses payload visitor to batch and store/retrieve payloads.#2976cconstable wants to merge 5 commits into
Conversation
…or to batch and store/retrieve payloads.
|
|
||
| private CompletableFuture<List<IndexedValue<Payload>>> runStoreDrivers( | ||
| Map<String, Batch<Payload>> batches, @Nullable StorageDriverTargetInfo target) { | ||
| return TaskScope.withScope( |
There was a problem hiding this comment.
The previous logic here needed to manually account for external cancellation and handle sibling cancellation. The TaskScope handles that automatically. The scope.awaitAll() at the end has fail-fast semantics so it will immediately cancel other siblings on first error.
There was a problem hiding this comment.
Pull request overview
This PR introduces the core “extstore” plumbing in the Java SDK by adding internal converters that traverse proto messages, batch payloads by driver, and offload/retrieve large payloads via external storage drivers, with structured cancellation support.
Changes:
- Added internal payload converters and reference encoding/decoding to offload payloads to external storage and restore them on retrieval.
- Exposed payload-visitor utilities to support traversing nested proto payload fields and applying conversions with bounded concurrency.
- Extended storage driver contexts with cancellation tokens and added tests covering batching, thresholds, ordering, and cancellation behavior.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| temporal-sdk/src/main/java/io/temporal/payload/storage/StorageDriverStoreContext.java | Adds cancellation token to store context for aborting in-flight driver calls. |
| temporal-sdk/src/main/java/io/temporal/payload/storage/StorageDriverRetrieveContext.java | Adds cancellation token to retrieve context for aborting in-flight driver calls. |
| temporal-sdk/src/main/java/io/temporal/payload/storage/StorageDriver.java | Documents cancellation token usage for driver implementers. |
| temporal-sdk/src/main/java/io/temporal/internal/payload/visitor/PayloadVisitors.java | Makes payload visitor entrypoints public for cross-package internal reuse. |
| temporal-sdk/src/main/java/io/temporal/internal/payload/visitor/PayloadVisitorOptions.java | Makes visitor options public to configure traversal/concurrency/skips. |
| temporal-sdk/src/main/java/io/temporal/internal/payload/visitor/PayloadVisitor.java | Makes the payload visitor functional interface public. |
| temporal-sdk/src/main/java/io/temporal/internal/payload/storage/StorageDriverStoreContextImpl.java | Implements store context with target info + cancellation token wiring. |
| temporal-sdk/src/main/java/io/temporal/internal/payload/storage/ExternalStorageReferences.java | Encodes/decodes external storage reference payloads. |
| temporal-sdk/src/main/java/io/temporal/internal/payload/storage/ExternalStoragePayloadConverter.java | Core batching logic to store/retrieve payload lists with TaskScope cancellation. |
| temporal-sdk/src/main/java/io/temporal/internal/payload/storage/ExternalStorageMessageConverter.java | Bridges payload-list conversion over arbitrary proto messages via PayloadVisitors. |
| temporal-sdk/src/test/java/io/temporal/payload/storage/ExternalStorageOptionsTest.java | Updates tests for new cancellation token requirement in store context. |
| temporal-sdk/src/test/java/io/temporal/internal/payload/storage/ExternalStorageReferencesTest.java | Adds tests for reference round-tripping and validation behavior. |
| temporal-sdk/src/test/java/io/temporal/internal/payload/storage/ExternalStoragePayloadConverterTest.java | Adds tests for batching, thresholds, multi-driver routing, and cancellation on failure. |
| temporal-sdk/src/test/java/io/temporal/internal/payload/storage/ExternalStorageMessageConverterTest.java | Adds tests for message traversal, nested payload conversion, and skipping search attributes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| static boolean isReference(@Nonnull Payload payload) { | ||
| return payload.getExternalPayloadsCount() > 0; | ||
| } |
There was a problem hiding this comment.
I think the correct thing to do it replace isReference() + fromReferencePayload() with a single tryParseReference(Payload) that checks both external_payloads > 0 and messageType == temporal.api.sdk.v1.ExternalStorageReference and returns null instead of throwing. Will update this soon.
| static boolean isReference(@Nonnull Payload payload) { | ||
| return payload.getExternalPayloadsCount() > 0; | ||
| } |
There was a problem hiding this comment.
Going to replace this. See thread in conversation tab.
| static ParsedReference fromReferencePayload(@Nonnull Payload payload) { | ||
| ByteString messageType = payload.getMetadataMap().get(EncodingKeys.METADATA_MESSAGE_TYPE_KEY); | ||
| if (messageType == null || !REFERENCE_MESSAGE_TYPE.equals(messageType.toStringUtf8())) { | ||
| throw new IllegalArgumentException( | ||
| "Payload is not an external storage reference; expected messageType '" | ||
| + REFERENCE_MESSAGE_TYPE | ||
| + "' but was '" | ||
| + (messageType == null ? "" : messageType.toStringUtf8()) | ||
| + "'"); | ||
| } |
There was a problem hiding this comment.
Going to replace this. See thread in conversations tab.
| * Converts one payload list between inline payloads and external-storage references by routing | ||
| * entries to storage drivers. | ||
| */ | ||
| final class ExternalStoragePayloadConverter { |
There was a problem hiding this comment.
I might call this something like ExternalStoragePayloadTransformer or something to that effect. Avoid PayloadConverter since that is an actual concept already that is tangentially related.
| * storage reference this SDK understands. | ||
| */ | ||
| static @Nullable ParsedReference tryParseReference(@Nonnull Payload payload) { | ||
| if (payload.getExternalPayloadsCount() == 0) { |
There was a problem hiding this comment.
Remove this check. This is not a requirement for exchanging a reference for the externally stored payload.
There was a problem hiding this comment.
swapping back to the original implementation
| import javax.annotation.Nonnull; | ||
| import javax.annotation.Nullable; | ||
|
|
||
| final class StorageDriverStoreContextImpl implements StorageDriverStoreContext { |
There was a problem hiding this comment.
Any particular reason this one is in its own separate file but the StorageDriverRetrieveContextImpl is a nested static class of ExternalStoragePayloadConverter?
There was a problem hiding this comment.
No, but I'll make them consistent
| * Stores {@code payloads} and returns one {@link StorageDriverClaim} per payload, in the same | ||
| * order. The returned list must be the same length as {@code payloads}. | ||
| * | ||
| * <p>Drivers should use {@link StorageDriverStoreContext#getCancellationToken()} to abort |
There was a problem hiding this comment.
This comment reads as "I should call getCancellationToken to abort in-flight requests". I think it should be more like "call getCancellationToken() to get a token to observe when cancellation is requested". Could use better wording.
| */ | ||
| @Experimental | ||
| public interface StorageDriverRetrieveContext {} | ||
| public interface StorageDriverRetrieveContext { |
There was a problem hiding this comment.
I looked over the other SDKs and this is the first one where a user would have to implement an interface (that isn't structural) in their testing. And I totally acknowledge that I suggested it be as such. However, that means if we ever needed to add something to the interface after GA, it would technically be a breaking change. Wondering if we should document it as such that we are free to add more to it over time or if we should convert it to a class that allows users to instantiate for testing. Maybe not too much of an issue for Java since it has default interface implementations. Just something to think through for maintenance purposes.
| CompletableFuture<List<Payload>> store( | ||
| @Nullable StorageDriverTargetInfo target, List<Payload> payloads) { | ||
| StorageDriverStoreContext context = | ||
| new StorageDriverStoreContextImpl(target, CancellationToken.none()); |
There was a problem hiding this comment.
I think cancellation token should be plumbed through the store method and used here. Or are we not doing that because there are no callers that can offer cancellation? Might still do that so the API has the correct shape and then have the caller pass CancellationToken.none().
There was a problem hiding this comment.
I would imagine that at least worker shutdown has something to plumb into, if not some kind of cancellation per workflow or task.
| this.payloadVisitConcurrency = payloadVisitConcurrency; | ||
| } | ||
|
|
||
| <T extends Message> CompletableFuture<T> store( |
There was a problem hiding this comment.
Any possible efficiency in allowing support of Message.Builder as well? Might avoid a bunch of copying.
| if (payload.getExternalPayloadsCount() == 0) { | ||
| return null; | ||
| } | ||
| ByteString messageType = payload.getMetadataMap().get(EncodingKeys.METADATA_MESSAGE_TYPE_KEY); |
There was a problem hiding this comment.
Should we also be checking encoding == ENCODING_PROTOBUF_JSON?
What was changed
internal/payload/storage/ExternalStorageMessageConverter.javato transform payloads in messages.internal/payload/storage/ExternalStoragePayloadConverter.javato handle the actual transformation.TaskScopefor handling parent-child and sibling task cancellation.Why?
Checklist