-
Notifications
You must be signed in to change notification settings - Fork 238
External Storage Integration: Lazy resolving references, general refactoring #3016
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| package io.temporal.internal.payload.storage; | ||
|
|
||
| import com.google.common.base.Throwables; | ||
| import com.google.protobuf.Message; | ||
| import io.temporal.api.common.v1.Payload; | ||
| import io.temporal.api.sdk.v1.ExternalStorageReference; | ||
| import io.temporal.common.CancellationToken; | ||
| import io.temporal.internal.payload.visitor.MessageVisitor; | ||
| import io.temporal.internal.payload.visitor.PayloadVisitorOptions; | ||
| import io.temporal.internal.payload.visitor.PayloadVisitors; | ||
| import io.temporal.payload.storage.ExternalStorageOptions; | ||
| import io.temporal.payload.storage.StorageDriver; | ||
| import io.temporal.payload.storage.StorageDriverTargetInfo; | ||
| import java.util.List; | ||
| import java.util.concurrent.CancellationException; | ||
| import java.util.concurrent.CompletableFuture; | ||
| import java.util.concurrent.CompletionException; | ||
| import java.util.concurrent.ExecutionException; | ||
| import javax.annotation.Nullable; | ||
|
|
||
| /** | ||
| * External storage offloads large payloads via {@link StorageDriver}s. It walks messages using | ||
| * {@link PayloadVisitors} transforming payloads to and from {@link ExternalStorageReference} using | ||
| * {@link ExternalStoragePayloadTransformer}. Use {@link ExternalStorageOptions} via {@link#create} | ||
| * to configure external storage. | ||
| */ | ||
| public final class ExternalStorage { | ||
| private final ExternalStoragePayloadTransformer payloadTransformer; | ||
| private final int payloadVisitConcurrency; | ||
|
|
||
| public static ExternalStorage create(ExternalStorageOptions options) { | ||
| return new ExternalStorage( | ||
| ExternalStoragePayloadTransformer.fromOptions(options), | ||
| options.getMaxConcurrentPayloadVisits()); | ||
| } | ||
|
|
||
| ExternalStorage( | ||
| ExternalStoragePayloadTransformer payloadTransformer, int payloadVisitConcurrency) { | ||
| this.payloadTransformer = payloadTransformer; | ||
| this.payloadVisitConcurrency = payloadVisitConcurrency; | ||
| } | ||
|
|
||
| public <T extends Message> T storeBlocking(T message, @Nullable StorageDriverTargetInfo target) { | ||
| return storeBlocking(message, target, CancellationToken.none()); | ||
| } | ||
|
|
||
| public <T extends Message> T storeBlocking( | ||
| T message, | ||
| @Nullable StorageDriverTargetInfo target, | ||
| CancellationToken<CancellationException> cancellationToken) { | ||
| return getOrThrowIfCancelled(store(message, target, cancellationToken), cancellationToken); | ||
| } | ||
|
|
||
| public <T extends Message> T storeBlocking( | ||
| T message, | ||
| @Nullable StorageDriverTargetInfo target, | ||
| @Nullable MessageVisitor<StorageDriverTargetInfo> targetVisitor) { | ||
| CancellationToken<CancellationException> cancellationToken = CancellationToken.none(); | ||
| return getOrThrowIfCancelled( | ||
| PayloadVisitors.visit(message, storeOptions(target, targetVisitor, cancellationToken)), | ||
| cancellationToken); | ||
| } | ||
|
|
||
| public <T extends Message> T retrieveBlocking(T message) { | ||
| CancellationToken<CancellationException> cancellationToken = CancellationToken.none(); | ||
| return getOrThrowIfCancelled(retrieve(message, cancellationToken), cancellationToken); | ||
| } | ||
|
|
||
| public <T extends Message> CompletableFuture<T> retrieveAsync(T message) { | ||
| return retrieve(message, CancellationToken.none()); | ||
| } | ||
|
|
||
| /** | ||
| * Throws {@link ExternalStorageNotConfiguredException} if {@code message} contains any reference | ||
| * payload. Used at inbound task boundaries when external storage is not configured. | ||
| */ | ||
| public static void throwIfContainsReference(Message message) { | ||
| PayloadVisitorOptions<Void> options = | ||
| PayloadVisitorOptions.<Void>newBuilder( | ||
| (context, payloads) -> { | ||
| for (Payload payload : payloads) { | ||
| if (ExternalStorageReferences.isReference(payload)) { | ||
| CompletableFuture<List<Payload>> found = new CompletableFuture<>(); | ||
| found.completeExceptionally(new ExternalStorageNotConfiguredException()); | ||
| return found; | ||
| } | ||
| } | ||
| return CompletableFuture.completedFuture(payloads); | ||
| }) | ||
| .setSkipSearchAttributes(true) | ||
| .build(); | ||
| try { | ||
| PayloadVisitors.visit(message.toBuilder(), options).join(); | ||
| } catch (CompletionException e) { | ||
| Throwable cause = e.getCause() != null ? e.getCause() : e; | ||
| Throwables.throwIfUnchecked(cause); | ||
| throw e; | ||
| } | ||
| } | ||
|
|
||
| private static <T> T getOrThrowIfCancelled( | ||
| CompletableFuture<T> future, CancellationToken<CancellationException> cancellationToken) { | ||
| try { | ||
| CompletableFuture.anyOf(future, cancellationToken.getCancellationFuture()).get(); | ||
| return future.get(); | ||
| } catch (InterruptedException e) { | ||
| Thread.currentThread().interrupt(); | ||
| throw new CancellationException("External storage operation interrupted"); | ||
| } catch (ExecutionException e) { | ||
| Throwable cause = e.getCause() != null ? e.getCause() : e; | ||
| Throwables.throwIfUnchecked(cause); | ||
| throw new CompletionException(cause); | ||
| } | ||
| } | ||
|
|
||
| <T extends Message> CompletableFuture<T> store( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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. |
||
| T message, | ||
| @Nullable StorageDriverTargetInfo target, | ||
| CancellationToken<CancellationException> cancellationToken) { | ||
| return PayloadVisitors.visit(message, storeOptions(target, null, cancellationToken)); | ||
| } | ||
|
|
||
| CompletableFuture<Void> store( | ||
| Message.Builder builder, | ||
| @Nullable StorageDriverTargetInfo target, | ||
| CancellationToken<CancellationException> cancellationToken) { | ||
| return PayloadVisitors.visit(builder, storeOptions(target, null, cancellationToken)); | ||
| } | ||
|
|
||
| <T extends Message> CompletableFuture<T> retrieve( | ||
| T message, CancellationToken<CancellationException> cancellationToken) { | ||
| return PayloadVisitors.visit(message, retrieveOptions(cancellationToken)); | ||
| } | ||
|
|
||
| CompletableFuture<Void> retrieve( | ||
| Message.Builder builder, CancellationToken<CancellationException> cancellationToken) { | ||
| return PayloadVisitors.visit(builder, retrieveOptions(cancellationToken)); | ||
| } | ||
|
|
||
| private PayloadVisitorOptions<StorageDriverTargetInfo> storeOptions( | ||
| @Nullable StorageDriverTargetInfo target, | ||
| @Nullable MessageVisitor<StorageDriverTargetInfo> targetVisitor, | ||
| CancellationToken<CancellationException> cancellationToken) { | ||
| return PayloadVisitorOptions.<StorageDriverTargetInfo>newBuilder( | ||
| (visitedTarget, payloads) -> | ||
| payloadTransformer.store(payloads, visitedTarget, cancellationToken)) | ||
| .setInitialContext(target) | ||
| .setMessageVisitor(targetVisitor) | ||
| .setConcurrency(payloadVisitConcurrency) | ||
| .setSkipSearchAttributes(true) | ||
| .build(); | ||
| } | ||
|
|
||
| private PayloadVisitorOptions<Void> retrieveOptions( | ||
| CancellationToken<CancellationException> cancellationToken) { | ||
| return PayloadVisitorOptions.<Void>newBuilder( | ||
| (context, payloads) -> payloadTransformer.retrieve(payloads, cancellationToken)) | ||
| .setConcurrency(payloadVisitConcurrency) | ||
| .setSkipSearchAttributes(true) | ||
| .build(); | ||
| } | ||
| } | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package io.temporal.internal.payload.storage; | ||
|
|
||
| import io.temporal.common.converter.DataConverterException; | ||
|
|
||
| /** | ||
| * Signals that an external storage reference reached a data converter without storage configured. | ||
| * Logs a TMPRL1105 error. | ||
| */ | ||
| public final class ExternalStorageNotConfiguredException extends DataConverterException { | ||
| public ExternalStorageNotConfiguredException() { | ||
| super( | ||
| "[TMPRL1105] Encountered an external-storage reference payload but external storage is not " | ||
| + "configured. Configure WorkflowClientOptions.Builder.setExternalStorage(...) with a " | ||
| + "driver able to retrieve it."); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.