blob: 5b4bac3dccfc40bc3fb86ff38f8a6dd9a1574cf3 [file] [view]
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
## Asynchronous programming
### Quick overview
Async driver methods return Java 8's [CompletionStage].
* don't call synchronous methods from asynchronous callbacks (the driver detects that and throws).
* callbacks execute on I/O threads: consider providing your own executor for expensive computations.
* be careful not to accidentally ignore errors thrown from callbacks.
-----
The driver exposes an asynchronous API that allows you to write fully non-blocking programs.
Asynchronous methods return instances of the JDK's [CompletionStage], that can be conveniently
chained and composed.
Here is a short example that opens a session and runs a query asynchronously:
```java
CompletionStage<CqlSession> sessionStage = CqlSession.builder().buildAsync();
// Chain one async operation after another:
CompletionStage<AsyncResultSet> responseStage =
sessionStage.thenCompose(
session -> session.executeAsync("SELECT release_version FROM system.local"));
// Apply a synchronous computation:
CompletionStage<String> resultStage =
responseStage.thenApply(resultSet -> resultSet.one().getString("release_version"));
// Perform an action once a stage is complete:
resultStage.whenComplete(
(version, error) -> {
if (error != null) {
System.out.printf("Failed to retrieve the version: %s%n", error.getMessage());
} else {
System.out.printf("Server version: %s%n", version);
}
sessionStage.thenAccept(CqlSession::closeAsync);
});
```
### Threading model
The driver uses two internal thread pools: one for request I/O and one for administrative tasks
(such as metadata refreshes, schema agreement or processing server events). Note that you can
control the size of these pools with the `advanced.netty` options in the
[configuration](../configuration).
When you register a callback on a completion stage, it will execute on a thread in the corresponding
pool:
```java
CompletionStage<CqlSession> sessionStage = CqlSession.builder().buildAsync();
sessionStage.thenAccept(session -> System.out.println(Thread.currentThread().getName()));
// prints s0-admin-n (admin pool thread)
CompletionStage<AsyncResultSet> resultStage =
session.executeAsync("SELECT release_version FROM system.local");
resultStage.thenAccept(resultSet -> System.out.println(Thread.currentThread().getName()));
// prints s0-io-n (I/O pool thread)
```
As long as you use the asynchronous API, the driver will behave in a non-blocking manner: its
internal threads will almost never block. There are a few exceptions to the rule though: see the
manual page on [non-blocking programming](../non_blocking) for details.
Because the asynchronous API is non-blocking, you can safely call a driver method from inside a
callback, even when the callback's execution is triggered by a future returned by the driver:
```java
// Get the department id for a given user:
CompletionStage<AsyncResultSet> idStage =
session.executeAsync("SELECT department_id FROM user WHERE id = 1");
// Once we have the id, query the details of that department:
CompletionStage<AsyncResultSet> dataStage =
idStage.thenCompose(
resultSet -> {
UUID departmentId = resultSet.one().getUuid(0);
return session.executeAsync(
SimpleStatement.newInstance(
"SELECT * FROM department WHERE id = ?", departmentId));
});
```
However, you can't call a synchronous method from a callback. This would be very unsafe, because the
driver blocks until the response is received; if the request happened to be assigned to the same
I/O thread that is currently running the callback, it would deadlock. In fact, the driver detects
this situation, and fails fast with a runtime exception to eliminate any chance of a hard-to-debug
deadlock:
```java
CompletionStage<ResultSet> dataStage =
idStage.thenApply(
resultSet -> {
UUID departmentId = resultSet.one().getUuid(0);
// WRONG: calling a synchronous method from an asynchronous callback. DON'T DO THIS!
return session.execute(
SimpleStatement.newInstance(
"SELECT * FROM department WHERE id = ?", departmentId));
});
// This is just to show the exception:
dataStage.whenComplete(
(resultSet, error) -> {
if (error != null) {
error.printStackTrace();
}
});
// java.util.concurrent.CompletionException:
// java.lang.IllegalStateException: Detected a synchronous API call on a driver thread,
// failing because this can cause deadlocks.
```
You should also be careful about expensive computations: if your callbacks hold I/O threads for too
long, they will negatively impact the driver's throughput. Consider providing your own executor:
```java
// Create this as a global resource in your application
Executor computeExecutor = ...
CompletionStage<Integer> resultStage =
responseStage.thenApplyAsync( // note: thenApplyAsync instead of thenApply
resultSet -> someVeryExpensiveComputation(resultSet),
computeExecutor);
```
Note that an alternate executor can also be used to allow synchronous driver API calls in callbacks,
but the recommended approach is to fully commit to the asynchronous model described above.
### Error propagation
One thing to pay attention to when programming asynchronously is error handling (this is not
specific to the driver). When all your callback does is a side effect, it's easy to accidentally
swallow an exception:
```java
CompletionStage<AsyncResultSet> responseStage =
sessionStage.thenCompose(
session -> session.executeAsync("SELECT release_version FROM system.local"));
responseStage.thenAccept(
resultSet -> {
String version = resultSet.one().getString(0);
System.out.printf("Server version: %s%n", version);
});
```
If the request fails, `responseStage` is failed, and `thenAccept` doesn't run the callback at all
(it just returns another failed stage). The error won't be surfaced anywhere, just silently ignored.
One way to address this is with `whenComplete`, which explicitly handles the error:
```java
responseStage.whenComplete(
(resultSet, error) -> {
if (error != null) {
System.out.printf("Failed to retrieve the version: %s%n", error.getMessage());
} else {
String version = resultSet.one().getString(0);
System.out.printf("Server version: %s%n", version);
}
});
```
Or you can chain more operations on the result of `printStage`, and handle the error further down
the chain:
```java
CompletionStage<Void> printStage =
responseStage.thenAccept(
resultSet -> {
String version = resultSet.one().getString(0);
System.out.printf("Server version: %s%n", version);
});
// Here trivially handled right away for the sake of example, but could be after more operations:
printStage.exceptionally(error -> {
System.out.printf("Failed to retrieve the version: %s%n", error.getMessage());
return null;
});
```
One more subtle source for errors is if the callback itself throws:
```java
responseStage.whenComplete(
(resultSet, error) -> {
if (error != null) {
System.out.printf("Request failed: %s%n", error.getMessage());
} else {
int v = resultSet.one().getInt(0);
System.out.printf("The result is %f%n", 1.0 / v);
}
});
```
There is a potential division by zero on the last line; the resulting `ArithmeticException` wouldn't
be handled anywhere. Either add a `try/catch` block in the callback, or don't ignore the result of
`whenComplete`.
### Asynchronous paging
Unlike previous versions of the driver, the asynchronous API never triggers synchronous behavior,
even when iterating through the results of a request. `session.executeAsync` returns a dedicated
[AsyncResultSet] that only iterates the current page, the next pages must be fetched explicitly.
This greatly simplifies asynchronous paging; see the [paging](../paging/#asynchronous-paging)
documentation for more details and an example.
[CompletionStage]: https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletionStage.html
[AsyncResultSet]: https://docs.datastax.com/en/drivers/java/4.17/com/datastax/oss/driver/api/core/cql/AsyncResultSet.html