-
Notifications
You must be signed in to change notification settings - Fork 129
feat(ContextPropagator): Replace imperative set/unset context propagation with lexically-scoped runWithContext #1074
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 3 commits
f3b269a
bd55f4a
90df484
a1bb4fd
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,65 @@ | ||
| /* | ||
| * Modifications Copyright (c) 2017-2020 Uber Technologies Inc. | ||
| * Portions of the Software are attributed to Copyright (c) 2020 Temporal Technologies Inc. | ||
| * Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"). You may not | ||
| * use this file except in compliance with the License. A copy of the License is | ||
| * located at | ||
| * | ||
| * http://aws.amazon.com/apache2.0 | ||
| * | ||
| * or in the "license" file accompanying this file. This file is distributed on | ||
| * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either | ||
| * express or implied. See the License for the specific language governing | ||
| * permissions and limitations under the License. | ||
| */ | ||
|
|
||
| package com.uber.cadence.internal.context; | ||
|
|
||
| import com.uber.cadence.context.ContextPropagator; | ||
| import java.util.List; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| /** | ||
| * Thrown when a {@link ContextPropagator#runWithContext} implementation violates its contract: | ||
| * either by catching and suppressing an exception thrown by the wrapped task instead of letting it | ||
| * propagate, or by invoking the task a number of times other than exactly once (e.g. retrying it | ||
| * after a failure, or never invoking it at all). This indicates a bug in the propagator | ||
| * implementation, not a workflow or activity failure, so it is modeled as an {@link Error} rather | ||
| * than a checked/unchecked exception. | ||
| */ | ||
| public final class ContextPropagatorContractViolationError extends Error { | ||
|
|
||
| private ContextPropagatorContractViolationError(String message, Throwable cause) { | ||
| super(message, cause); | ||
| } | ||
|
|
||
| static ContextPropagatorContractViolationError swallowedException( | ||
| List<ContextPropagator> appliedPropagators, Throwable swallowed) { | ||
| return new ContextPropagatorContractViolationError( | ||
| "A ContextPropagator swallowed an exception thrown by the task it wraps instead of " | ||
| + "propagating it. ContextPropagator#runWithContext must not catch and suppress " | ||
| + "exceptions from the task it is given -- only wrap the call in try/finally, never " | ||
| + "try/catch. One of these configured propagators must be fixed: " | ||
| + propagatorNames(appliedPropagators), | ||
| swallowed); | ||
| } | ||
|
|
||
| static ContextPropagatorContractViolationError unexpectedInvocationCount( | ||
| List<ContextPropagator> appliedPropagators, int invocationCount) { | ||
| return new ContextPropagatorContractViolationError( | ||
| "A ContextPropagator invoked the task it wraps " | ||
| + invocationCount | ||
| + " time(s) instead of exactly once. ContextPropagator#runWithContext must call " | ||
| + "task.run() exactly once -- it must not skip the call, and it must not retry the " | ||
| + "task after catching an exception from it. One of these configured propagators " | ||
| + "must be fixed: " | ||
| + propagatorNames(appliedPropagators), | ||
| null); | ||
| } | ||
|
|
||
| private static String propagatorNames(List<ContextPropagator> propagators) { | ||
| return propagators.stream().map(p -> p.getClass().getName()).collect(Collectors.joining(", ")); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,11 +18,14 @@ | |
| package com.uber.cadence.internal.context; | ||
|
|
||
| import com.uber.cadence.context.ContextPropagator; | ||
| import com.uber.cadence.context.ContextPropagator.ContextRunnable; | ||
| import com.uber.cadence.workflow.WorkflowThreadLocal; | ||
| import java.util.ArrayList; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.concurrent.atomic.AtomicInteger; | ||
| import java.util.concurrent.atomic.AtomicReference; | ||
| import java.util.function.Supplier; | ||
|
|
||
| /** This class holds the current set of context propagators */ | ||
|
|
@@ -57,20 +60,68 @@ public static Map<String, Object> getCurrentContextForPropagation() { | |
| return contextData; | ||
| } | ||
|
|
||
| public static void propagateContextToCurrentThread(Map<String, Object> contextData) { | ||
| if (contextData == null || contextData.isEmpty()) { | ||
| public static void runWithContext(Map<String, Object> contextData, ContextRunnable task) | ||
|
Member
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. From AI: custom code can now swallow exceptions that WorkflowThread throws. Do you want to add some guard on this? For example we can compare the original result, throwable and compare them with the new output.
Contributor
Author
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. good point. discussed offline, the fix will be to detect such bad |
||
| throws Exception { | ||
| runWithContext(contextPropagators.get(), contextData, task); | ||
| } | ||
|
|
||
| /** | ||
| * Executes {@code task} inside every applicable propagator context. | ||
| * | ||
| * <p>Propagators are composed in configuration order, so the first propagator is the outermost | ||
| * context and cleanup occurs in reverse order. Legacy propagators retain their existing set/unset | ||
| * behavior through {@link ContextPropagator#runWithContext(Object, ContextRunnable)}. | ||
| * | ||
| * <p>Every {@link ContextPropagator#runWithContext(Object, ContextRunnable)} implementation in | ||
| * the chain is required to call {@code task.run()} exactly once and propagate any exception it | ||
| * throws, rather than skipping the call, retrying it, or catching and suppressing the exception. | ||
| * This method verifies that contract: if {@code task} is invoked a number of times other than | ||
| * exactly one, or if it throws but no exception escapes the composed propagator chain, a {@link | ||
| * ContextPropagatorContractViolationError} is thrown instead of silently continuing as if {@code | ||
| * task} had succeeded normally. | ||
| */ | ||
| public static void runWithContext( | ||
| List<ContextPropagator> propagators, Map<String, Object> contextData, ContextRunnable task) | ||
| throws Exception { | ||
| if (propagators == null | ||
| || propagators.isEmpty() | ||
| || contextData == null | ||
| || contextData.isEmpty()) { | ||
| task.run(); | ||
| return; | ||
| } | ||
| for (ContextPropagator propagator : contextPropagators.get()) { | ||
|
|
||
| List<ContextPropagator> applied = new ArrayList<>(); | ||
| AtomicInteger invocationCount = new AtomicInteger(); | ||
| AtomicReference<Throwable> thrown = new AtomicReference<>(); | ||
| ContextRunnable invocation = | ||
| () -> { | ||
| invocationCount.incrementAndGet(); | ||
| try { | ||
| task.run(); | ||
| } catch (Throwable t) { | ||
| thrown.set(t); | ||
| throw t; | ||
| } | ||
| }; | ||
| for (int i = propagators.size() - 1; i >= 0; i--) { | ||
| ContextPropagator propagator = propagators.get(i); | ||
| if (contextData.containsKey(propagator.getName())) { | ||
| propagator.setCurrentContext(contextData.get(propagator.getName())); | ||
| applied.add(0, propagator); | ||
|
gitar-bot[bot] marked this conversation as resolved.
|
||
| Object context = contextData.get(propagator.getName()); | ||
| ContextRunnable next = invocation; | ||
| invocation = () -> propagator.runWithContext(context, next); | ||
| } | ||
| } | ||
| } | ||
| invocation.run(); | ||
|
|
||
| public static void unsetCurrentContext() { | ||
| for (ContextPropagator propagator : contextPropagators.get()) { | ||
| propagator.unsetCurrentContext(); | ||
| if (invocationCount.get() != 1) { | ||
| throw ContextPropagatorContractViolationError.unexpectedInvocationCount( | ||
| applied, invocationCount.get()); | ||
| } | ||
| Throwable swallowed = thrown.get(); | ||
| if (swallowed != null) { | ||
| throw ContextPropagatorContractViolationError.swallowedException(applied, swallowed); | ||
| } | ||
|
gitar-bot[bot] marked this conversation as resolved.
|
||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.