Closed codefromthecrypt closed 4 years ago
Hi, as a user of both gRPC and OpenCensus, there is some level of convenience in having a single "Context" class/instance I can use to pass the (OpenCensus) trace and (gRPC) request context explicitly from method to method.
I certainly understand that being exposed to a class named io.grpc.Context
is confusing for applications that do not use gRPC, but if OpenTelemetry were to create its own Context-like class with a different name, for codebases already "bought in" to io.grpc.Context then having a second Context class to explicitly propagate from method-to-method and thread-to-thread would add friction as well. For "microservice"-like Java applications that use multiple threads to handle processing of a request (at least those that are exposing gRPC servers), I have found it unworkable to use implicit propagation with ThreadLocals, so a second OpenTelemetry-specific Context class would have to be passed explicitly as well (or would force developers to define their own value class holding multiple Context implementations to have a single variable to pass).
I can't help but notice that opentelemetry-go can easily refer to context.Context
from Go's standard library in its Tracer API, which I think speaks to the usefulness of having a commonly used Context implementation/interface across libraries and RPC frameworks. It'd be awesome if OpenTelemetry, gRPC, and other projects could find a way to share a common Context definition with a minimal API surface (so that it changes rarely), since it seems unlikely for anything to be added to the JDK.
Hi,
I work on MicroProfile Context Propagation (https://download.eclipse.org/microprofile/microprofile-context-propagation-1.0/microprofile-context-propagation.html) and noticed this issue which touches on a lot of topics we are concerned with, so I thought perhaps I can shed some light on what we do about context propagation.
First, unlike gRPC Context, MicroProfile Context Propagation (MP-CP for short) does not provide storage: it doesn't give you a context class where you can store your context info (type-safe or not). It doesn't do this because it's trivial to implement using a class with a ThreadLocal
(or equivalent), a few methods for get/set and lifecycle. And also because a lot of existing libraries have existing context classes that they want to keep, or their lifecycle is complicated, or they have custom requirements for serialising it.
What MP-CP does however, is propagate any number of pluggable contexts without caring how they're defined by the frameworks that use them. In short, if your framework has a context class, you only have to implement a ThreadContextProvider
interface and provide it with methods for:
ThreadLocal
that holds your context for example)ThreadLocal
that holds your context for example)ThreadLocal
that holds your context to null
for example)And once you do that, all applications that use context propagation will have your context propagated.
The propagation is really the hard part, because many applications support either async parallel methods that spawn a request (if your framework is request-based) into offshoots of the request processing that run on separate threads or executors, none of which would normally see the ThreadLocal
where frameworks store their contexts. Or in the case of reactive programming, there's going to be a lot of callbacks executed on whatever current thread calls them, or executors, which again in both cases will have no ThreadLocal
or worse: the wrong one.
What MP-CP does is that it allows applications that want contexts to be propagated to wrap their callbacks into ones that capture the current context on creation, and restore it for the duration of its execution. There's context propagation for common callbacks such as Runnable
, Function
, Supplier
but also CompletionStage
and CompletableFuture
.
We also have a plugin mechanism which allows us to hook automatic context propagation for reactive libraries such as RxJava or Mutiny, which means the user doesn't have to change their code at all to get context propagation automatically and intuitively.
As far as I'm aware, MP-CP is the only solution which provides pluggable context propagation without forcing you to handle the propagation yourself, or to change your context storage solution, or to change your underlying reactive API.
Given your requirements, I think you could just define your own storage class, which can have type-safe keys if you need that, and custom serialisation if you send this over the wire, and as long as you allow access to getting and setting it, you can (or someone else can) implement a plugin for MP-CP that will make it propagated for everyone using MP-CP for propagation.
ATM MP-CP only defines interfaces and depends only on MP-Config so it's a fairly small dependency, and you don't need to depend on an implementation (such as SmallRye Context Propagation). You could split your ThreadContextProvider
into a new module just containing that class if you want to avoid the dependency, and people who want propagation of your context would import it. Or you can put that class in your base module and everyone using context propagation would automatically get your context propagated when they import your module.
It's not clear to me whether you need an implementation of context propagation in your module, however, given that you currently use gRPC context which doesn't do propagation (in process), so I think this is not a requirement for you ATM.
Hope that helps, and let me know if you have any question. Cheers.
@FroMage This is very interesting for us in auto instrumentation for Java. Can you please take a look at this use-case? How does MP-CP handle such "run-away" contexts?
@FroMage Super interesting. I'm going to set aside from time to read the spec document. Thanks for linking this here!
@FroMage thanks for the link. It sounds like a perfect substitute for my hand-rolled api. I'll definitely take a look at it.
Can you please take a look at this use-case? How does MP-CP handle such "run-away" contexts?
You can either inject a ManagedExecutor
which clears all contexts for async actions if the context is meant to be closed before they terminate, or if the context is meant to be kept open and used until all such async actions are completed, then it's up to your application to keep track of when those async actions terminate before you can close your context.
@FroMage Is Microprofile Context Propagation locked to java 8+, or could it be used with java 7?
Assigning this to @malafeev as he will do an initial digging & comparison of MicroProfile and grpc's Context.
Java 7 is not supported by MP-CP API. It's based on Java 8 features. When I tried to compile
ManagedExecutor executor = ManagedExecutor.builder()
.maxAsync(5)
.propagated(ThreadContext.CDI, ThreadContext.APPLICATION)
.build();
ThreadContext threadContext = ThreadContext.builder()
.propagated(ThreadContext.SECURITY)
.build();
I got
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:compile (default-compile) on project test: Compilation failure: Compilation failure:
[ERROR] /Users/malafes/projects/test/src/main/java/Main.java:[8,47] static interface method invocations are not supported in -source 1.7
[ERROR] (use -source 8 or higher to enable static interface method invocations)
[ERROR] /Users/malafes/projects/test/src/main/java/Main.java:[13,48] static interface method invocations are not supported in -source 1.7
[ERROR] (use -source 8 or higher to enable static interface method invocations)
Yeah, it's JDK8+
I would summarize this as another key consumer/integration besides grpc-context. There is also https://github.com/alibaba/transmittable-thread-local which focuses on some of this even if we formerly mostly discussed RPC and reactive.
The main idea here was keeping the deps clear and knowing that while solving for gRPC is important, so is making other things possible, which share different dependency characteristics, etc.
from my MP CP review:
MP CP is not a solution which we can take and use without doing own implementation of interfaces.
Well, there's SmallRye-ContextPropagation which implements it that you can use. I don't see the point in re-implementing it.
MP CP is coupled to Executors. Threads should be managed by container...
That's only the case for ManagedExecutor
but not for ThreadContext
which is what you will want to use.
MP CP is java 8+. It’s based on java 8 concurrency api: completable future is a first citizen.
Yes, that's the case.
MP CP must to be compatible with Java EE Concurrency model/api, therefore it may have unnecessary functionality.
This is only true of ManagedExecutor
: a single class.
I was not able to create an example to propagate value like a string from one thread to another thread reading documentation. So it’s still a black box for me.
Do you want me to provide a complete example here?
Do you want me to provide a complete example here?
@FroMage it would be nice, thanks
So, there's your test class:
package whatever;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.eclipse.microprofile.context.ThreadContext;
import org.eclipse.microprofile.context.spi.ContextManager;
import org.eclipse.microprofile.context.spi.ContextManagerProvider;
import org.junit.Assert;
import org.junit.Test;
public class DemoTest {
@Test
public void test() throws InterruptedException, ExecutionException {
ContextManager contextManager = ContextManagerProvider.instance().getContextManager();
ThreadContext threadContext = contextManager.newThreadContextBuilder().build();
ExecutorService executorService = Executors.newFixedThreadPool(2);
MyContext.clear();
// start with no context
Assert.assertNull(MyContext.get());
// verify that new threads also get no context
executorService.submit(() -> {
Assert.assertNull(MyContext.get());
}).get();
// now set the context
MyContext ctx = new MyContext();
MyContext.set(ctx);
// verify that we have a context
Assert.assertEquals(ctx, MyContext.get());
// verify that new threads still get no context
executorService.submit(() -> {
Assert.assertNull(MyContext.get());
}).get();
// verify that new threads with context propagation do get context
executorService.submit(threadContext.contextualRunnable(() -> {
Assert.assertEquals(ctx, MyContext.get());
})).get();
}
}
And here's your context class:
package whatever;
public class MyContext {
private static ThreadLocal<MyContext> context = new ThreadLocal<MyContext>();
public static void init() {
context.set(new MyContext());
}
public static void clear() {
context.remove();
}
public static MyContext get() {
return context.get();
}
public static void set(MyContext newContext) {
context.set(newContext);
}
// put your context properties here
private String reqId;
public void set(String reqId) {
this.reqId = reqId;
}
public String getReqId() {
return reqId;
}
}
This is typically where you'd put whatever context values you need.
You'll need to have this src/main/resources/META-INF/services/org.eclipse.microprofile.context.spi.ThreadContextProvider
file too:
whatever.MyThreadContextProvider
You'll want to use SmallRye-CP:
<dependency>
<groupId>io.smallrye</groupId>
<artifactId>smallrye-context-propagation</artifactId>
<version>1.0.16</version>
</dependency>
That's all. Now you can both propagate any number of contexts, and make sure that your context will be propagated to any CP user.
Does that help?
In the 9/15[16] java meeting, it was agreed that @anuraaga will create a new module and start building our own context implementation. Thanks @anuraaga for spearheading this work!
I understand why opentelemetry has a bias towards google's gRPC library. This codebase primarily borrows from work that started internally, became google's instrumentation-java, later renamed to opencensus, and now mostly copy/pasted from that. The original primary customer was not just gRPC protocol, but specifically google's library. For example, gRPC is the most primary user of the prior work, and it is helpful to have a user to guide.
Time has passed, though, and what started as an end-to-end instrumentation library has split into a Api + SDK approach, as facilitated by the weaving in of OpenTracing. Yet, code opinons towards google's gRPC implementation still exist in the form of a strict dependency in the api package, and helper code.
While it is understandable that alliances exists, and having good integration with Google's gRPC implementation is important, even gRPC itself has options. For example, Armeria has gRPC protocol support without the need of Google's library. We have already encountered problems where other google projects drag in gRPC because of Census. This should stop now. OpenTelemetry as a body of people, sure that can advocate for Google. However, the instrumentation api must not pick winners and losers in RPC frameworks.
Please remove all traces of grpc-context from the api library and move the support functions to a context integration package.
This is a follow-up from here: https://github.com/open-telemetry/opentelemetry-java/pull/571#issuecomment-536193532