rh3h
rh3h

Reputation: 1

Spring Pass request scope beans to child threads

I’m facing an issue with request scope Java beans and async processing.

I’m developing a microservice-based application where the services communicate with each other via REST APIs. Authorization between these microservices is handled using the JWT token of the currently logged-in user.

I want to implement an asynchronous method to handle some long-running processes. Once these processes are complete, a notification should be sent to the user to inform them.

I've made this AsyncTaskService to process some tasks async.

// async service

@Service
@RequiredArgsConstructor
public class AsyncTaskService {
    private final AsyncTaskRepository
    private final ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
    
    Public UUID startAsyncTask (Supplier<?> task) {
        ClassLoader contentClassLoader = this.getClass().getClassLoader();
        UUID someId = UUID.randomUUID();
        
        CompletableFuture<?> future = CompletableFuture.supplyAsync(() -> {
            Thread.currentThread().setContextClassLoader(contentClassLoader);
            return task.get();
        } executorService);
        
        future.whenComplete((result, throwable) -> {
            if (throwable != null) {
                log.error("Task with id {} finished unsuccessfully. ", someId, throwable);
            } else {
                log.info("Task with id {} finished successfully"., someId)
            }
            return someId;
        })
    }
}

For example, this is the usage in some long lasting process.

// long-lasting processes service
@Service
@RequiredArgsConstructor
public class LongLastingProcessesService {
    private final AsyncTaskService asyncTaskService;
    private final NotificationApi notificationApi;
    
    
    public UUID handleLongLastingProcess() {
        
        RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
        
        return asyncTaskService.startTask(() -> {
            RequestContextHolder.setRequestAttributes(requestAttributes, true);
            
            var result = someService.someMethod(someData);
            
            notificationApi.createNotification(someDataToBuildNotification);
            
            requestContextHolder.resetRequestAttributes();
            return result;
        })
    }
}

When the user makes the corresponding request, controller handles the handleLongLastingProcess method

But everytime, I am getting an error.

Error creating bean with name ''scopedTarget.notificationServiceProxy'': Scope ''request'' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton
// notificationApi

@HttpExchange(url = "someUrlForNotificationMicroservice")
public interface NotificationApi {

    @PostExchange
    Notification createNotification(@RequestBody NotificationBody notificationBody);
}

And request scope beans

// proxy configuration

@Configuration
class ProxyConfiguration {
    
      @Bean
    @RequestScope
    RestClient notificationRestClient () {
        if (SecurityContextHolder.getContext().getAuthentication() != null) {
            return createRestClient("notification");
        }
        return RestClient.builder().baseUrl(retrieveInstanceUrl("notification")).defaultHeaders(httpHeaders -> {
            httpHeaders.setBearerAuth(( (JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication() ).getToken().getTokenValue());
        }).build();

    }

    @Bean
    @RequestScope
    NotificationApi notificationServiceProxy () {
        RestClientAdapter adapter = RestClientAdapter.create(notificationRestClient());
        HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();

        return factory.createClient(NotificationApi.class);
    }
    
    private RestClient createRestClient (String appName) {
        return RestClient.builder().baseUrl(retrieveInstanceUrl(appName)).defaultStatusHandler(
            HttpStatusCode::isError,
            (request, response) -> log.error(
                "Error occurred when communicating between services: {} {}",
                request, response
            )
        ).defaultHeaders(httpHeaders -> {
            httpHeaders.setBearerAuth(( (JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication() ).getToken().getTokenValue());
    }).build();

}

As you can see, methods to handle communication with notification microservice are RequestScope.

I've tried to pass request to thread with RequestContextHolder but it didn't worked. Any tips how Can I access those beans in child thread?

Upvotes: 0

Views: 40

Answers (1)

gui
gui

Reputation: 1

The reason why this occurs is because request-scoped beans such as yours notificationServiceProxy are tied to the http request lifecycle, so when you run async code, the original http request context is lost.

Turns out Spring has built-in support for context propagation, it's called TaskDecorator

First, you must define a TaskDecorator bean:

public class CustomTaskDecorator implements TaskDecorator {

    @Override
    public Runnable decorate(Runnable runnable) {
        RequestAttributes context = RequestContextHolder.getRequestAttributes();

        return () -> {
            try {
                RequestContextHolder.setRequestAttributes(context);
                runnable.run();
            } finally {
                RequestContextHolder.resetRequestAttributes();
            }
        };
    }
}

Then you modify your ThreadPoolExecutor to use this decorator and use the executor in your AsyncTaskService.

Upvotes: 0

Related Questions