有没有办法在超时后不取消未来的情况下尝试等待一段CompletableFuture
时间才能返回不同的结果?
我有一个服务(让我们称之为expensiveService
)运行它自己的事情。它返回一个结果:
enum Result {
COMPLETED,
PROCESSING,
FAILED
}
我愿意 [阻止并] 等待它一小段时间(比如说 2 秒)。如果它没有完成,我想返回一个不同的结果,但我希望服务继续做自己的事情。然后询问服务是否完成(例如通过 websockets 或其他)将是客户的工作。
即我们有以下情况:
expensiveService.processAndGet()
需要 1 秒并完成它的未来。它返回COMPLETED
。expensiveService.processAndGet()
1 秒后失败。它返回FAILED
。expensiveService.processAndGet()
需要 5 秒并完成它的未来。它返回PROCESSING
。如果我们向另一个服务询问结果,我们会得到COMPLETED
.expensiveService.processAndGet()
5 秒后失败。它返回PROCESSING
。如果我们向另一个服务询问结果,我们会得到FAILED
.
在这种特定情况下,我们实际上需要在超时时获取当前结果对象,从而导致以下额外的边缘情况。这会导致以下建议的解决方案出现一些问题:
expensiveService.processAndGet()
需要 2.01 s 并完成它的未来。它返回PROCESSING
或COMPLETED
。
我也在使用 Vavr,并愿意接受使用 Vavr 的建议Future
。
我们创建了三种可能的解决方案,它们都有各自的优点和缺点:
#1 等待另一个未来
CompletableFuture<Result> f = expensiveService.processAndGet();
return f.applyToEither(Future.of(() -> {
Thread.sleep(2000);
return null;
}).map(v -> resultService.get(processId)).toCompletableFuture(),
Function.identity());
问题
- 第二个
resultService
总是被调用。 - 我们占用整个线程 2 秒。
#1a 等待另一个 Future 检查第一个 Future
CompletableFuture<Result> f = expensiveService.processAndGet();
return f.applyToEither(Future.of(() -> {
int attempts = 0;
int timeout = 20;
while (!f.isDone() && attempts * timeout < 2000) {
Thread.sleep(timeout);
attempts++;
}
return null;
}).map(v -> resultService.get(processId)).toCompletableFuture(),
Function.identity());
问题
- 第二个
resultService
仍然总是被调用。 - 我们需要将第一个 Future 传递给第二个,这不是很干净。
#2Object.notify
Object monitor = new Object();
CompletableFuture<Upload> process = expensiveService.processAndGet();
synchronized (monitor) {
process.whenComplete((r, e) -> {
synchronized (monitor) {
monitor.notifyAll();
}
});
try {
int attempts = 0;
int timeout = 20;
while (!process.isDone() && attempts * timeout < 2000) {
monitor.wait(timeout);
attempts++;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
if (process.isDone()) {
return process.toCompletableFuture();
} else {
return CompletableFuture.completedFuture(resultService.get(processId));
}
问题
- 复杂的代码(潜在的错误,不那么可读)。
#3 瓦夫Future.await
return Future.of(() -> expensiveService.processAndGet()
.await(2, TimeUnit.SECONDS)
.recoverWith(e -> {
if (e instanceof TimeoutException) {
return Future.successful(resultService.get(processId));
} else {
return Future.failed(e);
}
})
.toCompletableFuture();
问题
- 需要一个未来中的未来以避免
await
取消内部未来。 - 将第一个 Future 移到第二个会破坏依赖
ThreadLocal
s 的 [legacy] 代码。 recoverWith
并捕捉TimeoutException
不是那么优雅。
#4CompletableFuture.orTimeout
return expensiveService.processAndGet()
.orTimeout(2, TimeUnit.SECONDS)
.<CompletableFuture<Upload>>handle((u, e) -> {
if (u != null) {
return CompletableFuture.completedFuture(u);
} else if (e instanceof TimeoutException) {
return CompletableFuture.completedFuture(resultService.get(processId));
} else {
return CompletableFuture.failedFuture(e);
}
})
.thenCompose(Function.identity());
问题
- 虽然在我的情况下,
processAndGet
未来没有被取消,但根据文档,它应该是。 - 异常处理不好。
#5CompletableFuture.completeOnTimeout
return expensiveService.processAndGet()
.completeOnTimeout(null, 2, TimeUnit.SECONDS)
.thenApply(u -> {
if (u == null) {
return resultService.get(processId);
} else {
return u;
}
});
问题
- 虽然在我的情况下,
processAndGet
未来还没有完成,但根据文档,它应该是。 - 如果
processAndGet
想以null
不同的状态返回怎么办?
所有这些解决方案都有缺点并且需要额外的代码,但这感觉像是应该由CompletableFuture
VavrFuture
开箱即用的东西来支持。有一个更好的方法吗?