External Storage Integration: Lazy resolving references, general refactoring - #3016
External Storage Integration: Lazy resolving references, general refactoring#3016cconstable wants to merge 6 commits into
Conversation
3fe47fb to
831c284
Compare
| private boolean allowActivityHeartbeatDuringShutdown; | ||
| private String workerControlTaskQueue; | ||
| private PreferredVersionProvider preferredVersionProvider; | ||
| private @Nullable ExternalStorage externalStorage; |
There was a problem hiding this comment.
This doesn't have any callers in this PR but three of the PRs that build off this will use it. Included it here since those PRs are being up together.
831c284 to
460bfbf
Compare
| } | ||
|
|
||
| if (ExternalStorageReferences.isReference(payload)) { | ||
| throw new ExternalStorageNotConfiguredException(); |
There was a problem hiding this comment.
IIUC, this looks to be a DataConverter, which means it runs within the workflow code context. This means that this exception is handleable by user code. I think we need to move this to somewhere before the workflow code executes so we can fail the workflow task without allowing the user code to compensate.
There was a problem hiding this comment.
The context here is that if we missed integrating external storage into some piece of the SDK or a new feature was added that forgot to integrate external storage, this would catch it and throw a not configured error. It's purely a defensive fallback to make sure that SDK bugs don't appear as "your payloads can't be decoded/transformed" errors.
I do think ExternalStorageNotConfiguredException is probably the wrong error to be throwing here because it's likely that it was configured right and the sdk just isn't using external storage. Should this be a "some feature you are using has not integrated external storage. please file a bug report" type thing?
There was a problem hiding this comment.
I added a new ExternalStorageUnhandledReferenceException for when a reference payload makes it to fromPayload without being retrieved. This is a guard against SDK features that fail or fail to correctly integrate external storage. Normal misconfiguration errors should be caught at a level above fromPayload and are done so in the PRs that build off this.
| /** | ||
| * A {@link DataConverter} that resolves external storage reference payloads before deserialization. | ||
| */ | ||
| public final class ExternalStorageResolvingDataConverter implements DataConverter { |
There was a problem hiding this comment.
There was a problem hiding this comment.
Removed this entirely in favor of just doing the lazy retrieving in the few spots we needed explicitly. After chatting this felt like too much abstraction.
| @@ -84,10 +98,21 @@ public Builder setPayloadSizeThreshold(int payloadSizeThreshold) { | |||
| return this; | |||
| } | |||
|
|
|||
| /** | |||
| * Maximum number of payload lists visited concurrently while offloading or restoring the | |||
| * payloads of a single message. Must be at least 1. Defaults to 3. | |||
There was a problem hiding this comment.
Not sure a user is going to understand what is meant by "a single message". And "a single message" isn't quite the right scope from a implementation perspective. Maybe should describe this in terms of client operations and worker tasks.
There was a problem hiding this comment.
I think this will get cleaned up in the concurrency pass. I believe (technically) a single message is correct but I agree it's not a helpful way to describe it.
| } | ||
| } | ||
|
|
||
| <T extends Message> CompletableFuture<T> store( |
There was a problem hiding this comment.
Are there legitimate places where we need to visit on the fully constructed message instead of the build (the next overload)? I presume that the caller already created a builder, constructed the message, then this would effective recreate another builder, and reconstruct the message again. Might be perf issues. I would check to see if we can drop the message overloads and only use the builder overloads to force callers into the better performing algorithm.
| } | ||
| } | ||
|
|
||
| <T extends Message> CompletableFuture<T> store( |
There was a problem hiding this comment.
This will not work for workflow task completions because the nested commands need to change the the context when they are encountered. Having an outer visitor doing that determination and then calling this method is probably okay.
There was a problem hiding this comment.
Correct but workflow task completions are routed through a different store overload that includes the MessageVisitor so that the context can be changed (it's the one earlier in the file).
public <T extends Message> T storeBlocking(
T message,
@Nullable StorageDriverTargetInfo target,
@Nullable MessageVisitor<StorageDriverTargetInfo> targetVisitor) { ... }Regardless, it is confusing that there are so many overloads. I'll take a pass at consolidating them. I think we could just have one or two public ones. Some of the overloads above organically grew during the refactors and can be removed.
| /** | ||
| * A {@link DataConverter} that resolves external storage reference payloads before deserialization. | ||
| */ | ||
| public final class ExternalStorageResolvingDataConverter implements DataConverter { |
There was a problem hiding this comment.
Maybe add a comment that this is used in non-workflow contexts.
…former to ExternalStorage, create a lazy extstore resolving data converter.
460bfbf to
b3804da
Compare
|
Pushed a few updates:
|
…e to match other sdks.
b3804da to
ca09b50
Compare
…h to workers. we've got the dataconverter already so we can just derive it where its needed.
…for configuring external storage.
…ce payload makes it to fromPayload without being retrieved. This is a guard against SDK features that fail or fail to correctly integrate external storage. Normal misconfiguration errors should be caught at a level above fromPayload.
| */ | ||
| @Experimental | ||
| @Nullable | ||
| default ExternalStorage getExternalStorage() { |
There was a problem hiding this comment.
External storage should not be on DataConverter but instead on WorkflowClientOptions. The IDataConverter instance is accessible from within the workflow context, where we do not want external storage be accessible, let alone executable.
| return (T) new RawValue(payload); | ||
| } | ||
|
|
||
| if (ExternalStorageReferences.isReference(payload)) { |
There was a problem hiding this comment.
I'm not really convinced this is necessary. If we forget to plumb through retrieval somewhere, then it should likely fail at deserialization time explaining that the ExternalStorageReference message couldn't be deserialized into the target type. I also don't think we should give customers something that they can handle and attempt to compensate for in their executions.
| public ExternalStorageUnhandledReferenceException() { | ||
| super( | ||
| "[BUG] An external storage reference reached payload conversion without being handled. This" | ||
| + "is likely an SDK bug. Please file a bug report."); |
There was a problem hiding this comment.
nit: Concatenation bug, missing a space
| cancellationToken); | ||
| } | ||
|
|
||
| public <T extends Message> T retrieve(T message) { |
There was a problem hiding this comment.
This and retrieveAsync should passthrough a cancellation token from the caller.
| } | ||
|
|
||
| @Test | ||
| public void rawValueBypassesTheGuard() { |
There was a problem hiding this comment.
I don't understand what this test is trying to demonstrate or prove.
| .setSkipSearchAttributes(true) | ||
| .build(); | ||
| try { | ||
| PayloadVisitors.visit(message.toBuilder(), options).join(); |
There was a problem hiding this comment.
nit: Not for this PR, but I wonder if we can generate a non-mutating visitor and use that here.
| (context, payloads) -> { | ||
| for (Payload payload : payloads) { | ||
| if (ExternalStorageReferences.isReference(payload)) { | ||
| CompletableFuture<List<Payload>> found = new CompletableFuture<>(); |
There was a problem hiding this comment.
Could this just raise an exception instead of returning an exceptionally completed future so that the walk it terminated? As it is now, everything in the graph is walked even if we saw the first set of payloads are storage references; it's a waste of compute when we already know that it's going to fail.
What was changed
ExternalStorageMessageTransformertoExternalStorage.ExternalStorageResolvingDataConverter, that does the external storage work and delegates to the existing data converter for everything else.fromPayload). We don't eagerly load them.Why?
These changes support the following PRs:
Checklist