Why Dubbo Async Calls Return False for boolean? A Deep Dive and Fix
An in‑depth investigation reveals why Dubbo’s asynchronous calls return false when the service method returns a primitive boolean, reproduces the issue with a demo, traces the problem to the asyncCall implementation, and proposes a code fix to prevent this subtle bug.
Problem Reproduction
A colleague reported a Dubbo async‑call issue where a method returning boolean gave true on the server side but false on the client, while changing the return type to Boolean worked correctly.
今天发现一个问题 有一个dubbo接口返回类型是boolean, 把接口从同步改成异步 server 端返回true 消费端却返回false,把boolean改成Boolean就能正常返回结果 有碰到过这个问题吗
Interface return type is boolean Switching from sync to async changes the returned value
Changing to the wrapper type Boolean fixes the problem
The Java Development Manual recommends using wrapper types for RPC interfaces, but the observed behavior suggests a bug in Dubbo’s async handling.
Demo Code
public interface DemoService {
boolean isUser();
Boolean isFood();
} @Service
public class DemoServiceImpl implements DemoService {
@Override
public boolean isUser() {
System.out.println("server is user : true");
return true;
}
@Override
public Boolean isFood() {
System.out.println("server is food : true");
return true;
}
} @RestController
public class DemoCallerService {
@Reference(injvm = false, check = false)
private DemoService demoService;
@GetMapping(path = "/isUser")
public String isUser() throws Exception {
BlockingQueue<Boolean> q = new ArrayBlockingQueue<>(1);
RpcContext.getContext().asyncCall(() -> demoService.isUser())
.handle((isUser, throwable) -> {
System.out.println("client is user = " + isUser);
q.add(isUser);
return isUser;
});
q.take();
return "ok";
}
@GetMapping(path = "/isFood")
public String isFood() throws Exception {
BlockingQueue<Boolean> q = new ArrayBlockingQueue<>(1);
RpcContext.getContext().asyncCall(() -> demoService.isFood())
.handle((isFood, throwable) -> {
System.out.println("client is food = " + isFood);
q.add(isFood);
return isFood;
});
q.take();
return "ok";
}
}Test Results
Calling isUser (returns primitive boolean) prints:
// client ...
client is user = false
// server ...
server is user : trueCalling isFood (returns wrapper Boolean) prints:
// client ...
client is food = true
// server ...
server is food : trueInvestigation
The first guess was a server‑side conversion error, but the client actually receives the wrong value. The call stack leads to
com.alibaba.dubbo.remoting.exchange.support.DefaultFuture#doReceived. In async mode Dubbo wraps the call in a Callable. If the callable returns a non‑null value that is not a CompletableFuture, Dubbo immediately completes the future with CompletableFuture.completedFuture(o).
Because the proxy’s invoke method returns the primitive boolean result directly, the value false (the default for a primitive) is returned before the remote response arrives, and the future is completed with false.
Using Arthas to inspect the generated proxy class ( *.proxy0) confirms that the this.handler.invoke call returns false for the primitive case, while the wrapper type correctly yields a CompletableFuture.
The problematic code in asyncCall contains a comment “local invoke will return directly”. For non‑injvm (remote) calls this shortcut should be skipped, otherwise primitive returns are forced to false.
Fix
The proposed fix adds a condition that only completes the future immediately when the call is an injvm (local) invocation. For remote async calls the method should fall through to the normal future handling.
public <T> CompletableFuture<T> asyncCall(Callable<T> callable) {
try {
setAttachment(ASYNC_KEY, Boolean.TRUE.toString());
T o = callable.call();
if (o != null) {
if (o instanceof CompletableFuture) {
return (CompletableFuture<T>) o;
}
if (injvm()) { // keep current behavior for local calls
return CompletableFuture.completedFuture(o);
}
} else {
// normal sync method, get future from RpcContext
}
} catch (Exception e) {
throw new RpcException(e);
} finally {
removeAttachment(ASYNC_KEY);
}
return (CompletableFuture<T>) getContext().getFuture();
}Conclusion
The bug exists in Dubbo 2.7.4 and later; submitting a PR with the above change is encouraged. Following the guideline of using wrapper types for RPC return values avoids this subtle issue, and thorough debugging tools like Arthas help uncover such hidden problems.
Xiao Lou's Tech Notes
Backend technology sharing, architecture design, performance optimization, source code reading, troubleshooting, and pitfall practices
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
