question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Enforce Future or void return declaration for each asynchronously executed method (e.g. with class-level @Async)

See original GitHub issue

https://github.com/spring-projects/spring-framework/blob/79d3f5c64c94a356831916ec78be4296fba92b18/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java#L113-L127

I have found an odd behaviour working with @Async-annotated classes in Spring. Please note that there is a fundamental error in my code. Unfortunately, this post has to be long and detailed.

Let’s say I have already made a synchronous REST API generated by Swagger generator. Following code omits all documentation-level annotations


public interface TaxonomiesApi {
   
    ResponseEntity<GenericTaxonomyItem> disableItem(Integer idTaxonomyType, String idTaxonomy, String appSource);

}

This API is easily implemented via RestTemplate, but I won’t discuss the inner details.

Now, suppose I want to provide an async version to developers consuming the API. What I have done is to create another interface with some search&replace-fu 🥋🥋

@Async
public interface TaxonomiesApiAsync extends TaxonomyApi {
   
    default CompletableFuture<ResponseEntity<GenericTaxonomyItem>> disableItemAsync(Integer idTaxonomyType, String idTaxonomy, String appSource) {
        try {
            return completedFuture(this.disableItem(idTaxonomyType, idTaxonomy, appSource));
        } catch (Exception ex) {
            return failedFuture(ex);
        }
    }
}

With the search&replace, I basically created an async-ish version of every method that should be backed by Spring’s @Async annotation. My original idea was that synchronous methods can be invoked as they are, but if you instantiate TaxonomiesApiAsync you also have access to the async version.

I have discovered I made a fundamental mistake by applying the @Async annotation at interface level when the class contains both sync and async methods. I found that synchronous disableItem was performed in the same @Async context. Accoding to design (correctly), Spring found the @Async annotation at interface level so every method, including inherited ones, was invoked asynchronously.

But the method always returned null. By debugging and looking at the code, I found that Spring tries to resolve the return value of the invoked method only if it’s a Future. What if the returned value is a Present object?

That means that if the returned value is not a Future<ResponseEntity<GenericTaxonomyItem>> but rather just a ResponseEntity<GenericTaxonomyItem> Spring neither throws an exception nor returns that value directly.

Example of working calling code (invoking a different method)

    protected CompletableFuture<Set<TaxonomyLegalEntityDTO>> importTaxonomyLegalEntities(int userId) {
        TaxonomySearchParameters searchParameters = new TaxonomySearchParameters();
        searchParameters.setIdTaxonomyType(amlcProperties.getTaxonomies().getTaxonomyLegalEntitiesId());
        searchParameters.setLogicalState(1);
        return taxonomiesApiAsync.getAllTaxonomyItemsAsync(searchParameters)
                .thenApply(ResponseEntity::getBody)
                .thenApply(taxonomyLegalEntityMasterDbMapping::toLegalEntity) // Costruisco i DTO che voglio utilizzare
                .whenComplete(traceLoggerConsumer("Legal entity"))
                .thenApply(dtos -> taxonomyLegalEntityManager.mergeFromMasterDb(dtos, userId))
                .whenComplete((ignored, ex) -> {
                    if (ex != null)
                        log.error("Error importing legal entities: " + ex.getMessage(), ex);
                })
                .thenApply(TaxonomyMasterDbMergeDTO::getSnapshot);
    }

Example of non-working code; the result of the CompletableFuture is always null. In this code, I decided not to use the executor embedded in the API service, but rather the executor injected in the consuming service. So I ran a sync method in an executor, expecting the same result.

    protected CompletableFuture<Set<TaxonomyLegalEntityDTO>> importTaxonomyLegalEntities(int userId) {
        TaxonomySearchParameters searchParameters = new TaxonomySearchParameters();
        searchParameters.setIdTaxonomyType(amlcProperties.getTaxonomies().getTaxonomyLegalEntitiesId());
        searchParameters.setLogicalState(1);
        return CompletableFuture.supplyAsync(() -> taxonomiesApi.getAllTaxonomyItems(searchParameters), taxonomyBatchImportServiceExecutor)
                .thenApply(ResponseEntity::getBody)
                .thenApply(taxonomyLegalEntityMasterDbMapping::toLegalEntity)
                .whenComplete(traceLoggerConsumer("Legal entity"))
                .thenApplyAsync(dtos -> taxonomyLegalEntityManager.mergeFromMasterDb(dtos, userId))
                .whenComplete((ignored, ex) -> {
                    if (ex != null)
                        log.error("Error importing legal entities: " + ex.getMessage(), ex);
                })
                .thenApply(TaxonomyMasterDbMergeDTO::getSnapshot);
    }

Since I spent one hour debugging that problem, I decided to spend more of my after-work time to document the issue here.

Proposed fix

In the code I linked, if the instanceof check fails the returned value is simply null. I don’t yet understand the implications, but what about not unwrapping the value from Future if that’s not a future? I mean return result

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:5 (4 by maintainers)

github_iconTop GitHub Comments

2reactions
jhoellercommented, Nov 29, 2021

I’m inclined to explicitly throw an exception for non-Future/void return type declarations whenever we attempt to execute a method asynchronously. While this may not be much of an issue with an explicit annotated method, a class-level @Async declaration is certainly harder to track when some specific method mismatches then.

1reaction
djecheloncommented, Jan 4, 2022

@LifeIsStrange as you can see in the code, what is done is a == check on the returnType, which is already available (already reflected). So, it’s not going to add any overhead.

As for your question about the instanceof performance, I found an interesting reading and the tl;dr says

In Java 1.8 instanceof is the fastest approach, although getClass() is very close.

Nevertheless, it doesn’t apply to this fix.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Async return types (C#) - Microsoft Learn
Learn about the return types that async methods can have in C# with code examples for each type.
Read more >
36.1 Asynchronous Method Invocation - Java Platform ...
Asynchronous methods that return void can't declare application exceptions, but if they return Future<V> , they may declare application exceptions. For example:
Read more >
Asynchronous programming: futures, async, await - Dart
Learn about and practice writing asynchronous code in DartPad! ... Because it doesn't return a usable value, fetchUserOrder() has the type Future<void> ....
Read more >
Spring v6.0.0-M1 released! - Backend News - Devtalk
... Enforce Future or void return declaration for each asynchronously executed method (e.g. with class-level @Async ) #27734; Avoid unescape when CONNECT ...
Read more >
AsyncTask - Android Developers
An asynchronous task is defined by a computation that runs on a ... Invoking this method will cause subsequent calls to isCancelled() to...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found