micrometer-metrics / context-propagation

Context Propagation API
Apache License 2.0
81 stars 23 forks source link

Why ContextExecutorService#wrap is deprecated? #295

Open michaldo opened 1 day ago

michaldo commented 1 day ago

I would like to parallelize IO task and I would like pass current observation to tasks. I found solution here: https://stackoverflow.com/a/78765658/2365727

There are 2 signatures of method ContextExecutorService.wrap(). One simple, intuitive and works for me and second is more complex but allows deeper customization.

Up to these point everything is perfect. Library allows simplified usage with default settings and powerful usage with full customization.

But default usage is deprecated. Why?

chemicL commented 1 day ago

There are a few configurable items there and your application needs to make informed decisions. Specifically, the deprecated method used the global ContextRegistry. It should be explicitly specified instead. Also an informed decision needs to be made regarding existing ThreadLocal values prior to setting them to the captured ones -> if a snapshot does not contain some values, should the ones for which ThreadLocalAccessors exist be cleaned or not? There's no obvious answer, hence the library doesn't make it easy to shoot oneself in the foot so to speak. Hope this helps.

michaldo commented 1 day ago

the library doesn't make it easy to shoot oneself in the foot, but encourage clients to play with loaded gun.

Actually, when client wants simply pass context to task from executor service, the library encounters him to implement ContextSnapshot. Simple need requires complex work to accomplish.

Then developer digs into library code, scans SO and finds ContextSnapshotFactory.builder().build().captureAll()

There's no obvious answer. No. Obvious answer is ContextSnapshotFactory.builder().build().captureAll(). Let's analyze defaults one by one:

  1. When I do not pass context registry, take global one Makes sense? Absolutely. Is there any well known context registry other than global one to consider? No.
  2. When I do not specify rule to capture key, take all Makes sense? Absolutely.
  3. When I do not mention about clear missing, do not clear. Makes sense? Honestly, I do not know. Someone decided it will be default. I'm fine with it. Especially, I think in real life it does not matter.

Considering all above, my proposal is modify ContextExecutorService#wrap(ExecutorService) to use default snapshot builder and clean up deprecation

chemicL commented 13 hours ago

IIRC the deprecation came from @rstoyanchev's suggestion in https://github.com/micrometer-metrics/context-propagation/pull/105#discussion_r1214676801 and I agree.

This API is not for simply wrapping tasks but for wrapping an entire ExecutorService. In this instance it should be an object shared between different tasks, long living and configured once for the lifetime of the app in most cases. You'd probably want to also combine that instantiation with the configuration of the ContextSnapshotFactory which then you can use readily as the Supplier of ContextSnapshot.

Honestly, I do not know. Someone decided it will be default. I'm fine with it. Especially, I think in real life it does not matter.

It actually does matter quite a lot, that's why the configuration is there. A real life fundamental problem in context propagation in reactive applications has dictated the API to evolve in this direction. This should also be of interest as it seems your application is going to run Java's Stream API on a shared executor. One task might have a value and another might not. This can lead to leakages and for instance show unexpected identifiers in unrelated work units. Ignorance of this aspect is something that we specifically want to prevent. As to the loaded gun analogy - correct, you decided to work with a complicated scenario, better understand the considerations. The context-propagation library is a low-level detail mostly meant for use in library code, not necessarily by end users.

Regarding the other enumerated points, I agree. Not on the clearing missing values though.

@michaldo please refrain from opening PRs until the discussion is settled.

Unless @rstoyanchev changed his opinion, I'd leave the API as is.

michaldo commented 13 hours ago

Let's start with this: The context-propagation library is a low-level detail mostly meant for use in library code

I never want use low-level API in business code. Please answer here or here https://stackoverflow.com/questions/78746378/spring-boot-3-micrometer-tracing-in-mdc/78765658#78765658 https://stackoverflow.com/questions/78889603/traceid-propagation-to-virtual-thread how solve it with high-level API and I will be totally satisfied and I promise never look again into context-propagation library.

About "clearing missing". I don't know 100% what should be default: true or false. Clearing missing may be important in real life reactive applications. From my perspective, reactive is outdated by virtual threads. For virtual threads clearing missing issue does not exists. It means considering clearing missing is dead end and I'm happy with any choice about default value. It means also that context propagation is something more than reactive and reactive should nor dictate API evolution.

Finally, I think that one PR is worth more than thousand words I never hesistate clarify discussion with PR

rstoyanchev commented 10 hours ago

Thanks for raising this. I can see how it's not very obvious what to do, but as @chemicL pointed out, there are important choices to understand. The global ContextRegistry propagates every ThreadLocal it knows about, through registrations made by the app or by 3rd party libraries. That brings overhead, and potential side effects. Many times it's not easy to customize this in code deep on the callstack, but ExecutorService is a shared component that should be easy to configure for the specific needs.

That said, I think there is room for improvement. We could add an alternative wrap method that is a more friendly place to start:

public static ExecutorService wrap(ExecutorService service, ContextSnapshotFactory snapshotFactory) {
    return new ContextExecutorService<>(service, snapshotFactory::captureAll);
}

This should make it more obvious what you need to provide, and the choices you need to consider. We can also update the Javadoc of the other wrap method to explain better what's involved in creating a snapshot, and maybe have some example code snippets.

Also a good time to use this opportunity to review the Javadoc on ContextSnapshotFactory and its Builder to ensure there is guidance around what each choice means. For example, the method for ContextRegistry can mention the choice of using the global instance vs a custom one.

michaldo commented 9 hours ago

Friendly alternative would be for me something like

public static ExecutorService wrapObserve(ExecutorService service) {
    return new ContextExecutorService<>(service, Observation.SNAPSHOT_FACTORY::captureAll);
}

Because what I really want is pass high level Observation abstraction from caller to task.