Variable passing of Reactor asynchronous thread

Posted by gamesmad on Sat, 22 Feb 2020 08:24:42 +0100

order

This paper mainly studies the variable transfer of reactor asynchronous thread

The problem of threadlocal

In the traditional request / response synchronization mode, it is very convenient to use threadlocal to pass context variables. It can save adding common variables to each method parameter, such as the current login user. However, business methods may use async or execute asynchronously in other thread pools. At this time, threadlocal fails.

The solution at this time is to adopt the propagation mode, that is, to propagate this variable at the joint of synchronous thread and asynchronous thread.

TaskDecorator

For example, spring provides the TaskDecorator. By implementing this interface, you can control and propagate those variables by yourself. For example:

class MdcTaskDecorator implements TaskDecorator {
 
  @Override
  public Runnable decorate(Runnable runnable) {
    // Right now: Web thread context !
    // (Grab the current thread MDC data)
    Map<String, String> contextMap = MDC.getCopyOfContextMap();
    return () -> {
      try {
        // Right now: @Async thread context !
        // (Restore the Web thread context's MDC data)
        MDC.setContextMap(contextMap);
        runnable.run();
      } finally {
        MDC.clear();
      }
    };
  }
}

Pay attention to clear in finally

Configure this taskDecorator

@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {
 
  @Override
  public Executor getAsyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setTaskDecorator(new MdcTaskDecorator());
    executor.initialize();
    return executor;
  }

}

For complete examples, please refer to Spring 4.3: Using a TaskDecorator to copy MDC data to @Async threads

Reactor Context

Spring 5 introduces webplus, and its underlying layer is based on reactor. How does reactor propagate context variables? Context objects are officially provided to replace threadlocal.

Its characteristics are as follows:

  • kv operations like map, such as put(Object key, Object value),putAll(Context), hasKey(Object key)
  • immutable, i.e. the same key, will not be overwritten by put
  • Provide getOrDefault, getOrEmpty method
  • Context is bound to each Subscriber on the action chain
  • Accessed through subscriberContext(Context)
  • The function of Context is from bottom to top

Example

Setting and reading

    @Test
    public void testSubscriberContext(){
        String key = "message";
        Mono<String> r = Mono.just("Hello")
                .flatMap( s -> Mono.subscriberContext()
                        .map( ctx -> s + " " + ctx.get(key)))
                .subscriberContext(ctx -> ctx.put(key, "World"));

        StepVerifier.create(r)
                .expectNext("Hello World")
                .verifyComplete();
    }

Here, set the message value to World from the bottom subscriberContext, and then access it through subscriberContext in flatMap.

Bottom-up

    @Test
    public void testContextSequence(){
        String key = "message";
        Mono<String> r = Mono.just("Hello")
                //NOTE this subscriberContext is set too high
                .subscriberContext(ctx -> ctx.put(key, "World"))
                .flatMap( s -> Mono.subscriberContext()
                        .map( ctx -> s + " " + ctx.getOrDefault(key, "Stranger")));

        StepVerifier.create(r)
                .expectNext("Hello Stranger")
                .verifyComplete();
    }

Because the subscriberContext of this example is set too high, it cannot act on Mono.subscriberContext() in flatMap

Immutable

    @Test
    public void testContextImmutable(){
        String key = "message";

        Mono<String> r = Mono.subscriberContext()
                .map( ctx -> ctx.put(key, "Hello"))
                //A new one is returned here, so the above settings are invalid
                .flatMap( ctx -> Mono.subscriberContext())
                .map( ctx -> ctx.getOrDefault(key,"Default"));

        StepVerifier.create(r)
                .expectNext("Default")
                .verifyComplete();
    }

subscriberContext always returns a new

Multiple consecutive subscriberContext

    @Test
    public void testReadOrder(){
        String key = "message";
        Mono<String> r = Mono.just("Hello")
                .flatMap( s -> Mono.subscriberContext()
                        .map( ctx -> s + " " + ctx.get(key)))
                .subscriberContext(ctx -> ctx.put(key, "Reactor"))
                .subscriberContext(ctx -> ctx.put(key, "World"));

        StepVerifier.create(r)
                .expectNext("Hello Reactor")
                .verifyComplete();
    }

The operator will only read the nearest context

subscriberContext between flatmaps

    @Test
    public void testContextBetweenFlatMap(){
        String key = "message";
        Mono<String> r = Mono.just("Hello")
                .flatMap( s -> Mono.subscriberContext()
                        .map( ctx -> s + " " + ctx.get(key)))
                .subscriberContext(ctx -> ctx.put(key, "Reactor"))
                .flatMap( s -> Mono.subscriberContext()
                        .map( ctx -> s + " " + ctx.get(key)))
                .subscriberContext(ctx -> ctx.put(key, "World"));

        StepVerifier.create(r)
                .expectNext("Hello Reactor World")
                .verifyComplete();
    }

flatMap reads the nearest context

subscriberContext in flatMap

    @Test
    public void testContextInFlatMap(){
        String key = "message";
        Mono<String> r =
                Mono.just("Hello")
                        .flatMap( s -> Mono.subscriberContext()
                                .map( ctx -> s + " " + ctx.get(key))
                        )
                        .flatMap( s -> Mono.subscriberContext()
                                .map( ctx -> s + " " + ctx.get(key))
                                .subscriberContext(ctx -> ctx.put(key, "Reactor"))
                        )
                        .subscriberContext(ctx -> ctx.put(key, "World"));

        StepVerifier.create(r)
                .expectNext("Hello World Reactor")
                .verifyComplete();
    }

Here, the first flatMap cannot read the context inside the second flatMap

Summary

reactor realizes the function similar to thread local by providing Context, which is very powerful and worth pondering.

doc

Topics: Programming Spring Java