Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: addition of a reactive pitfalls reference #1546

Merged
merged 1 commit into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions documentation/docs/reference/going-reactive-a-few-pitfalls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
tags:
- reference
- beginner
---

# Going reactive: a few pitfalls

Don't get us wrong, reactive programming is a fantastic way to write resource-efficient code!

That being said, reactive programming has a learning curve that should not be taken lightly, and in some cases it is safer to write imperative code that you fully comprehend over reactive code that you don't fully grok.

We have assembled a few considerations that we think new users should know before they embark into writing complex reactive business logic.

## Mutiny doesn't auto-magically make your code asynchronous

This is a common source of confusion for new reactive programmers.
Mutiny itself **does not perform any scheduling work**, except for the [`emitOn` and `runSubscriptionOn` operators](../guides/emit-on-vs-run-subscription-on.md).

Consider the following code where we _join_ results from multiple asynchronous operations, materialised by the `Uni<String>`-returning `fetch` method:

```java linenums="1"
{{ insert('java/guides/reference/PitfallsTest.java', 'noMagicJoin') }}
```

You might think that the `join` operator schedules the calls to `fetch` to be run concurrently, and then collects the results into a list.
This is not how it works!

The `join` operator does subscribe to each `Uni<String>` returned by each call to `fetch`.
When it receives a value, it puts it into a list, and when all values have been received, that list is emitted.
The threads involved here are the ones that emit values in `fetch`.
If `fetch` uses async I/O underneath then you should observe true concurrency, but if `fetch` just emits a value right when the subscription happens then you will merely observe a sequential execution of each call to `fetch`, in order.

## When to prefer `Uni<List<T>>` over `Multi<T>`

The reason why `Multi` exists is to model streams over back-pressured sources.
By conforming to the [Reactive Streams protocol](https://www.reactive-streams.org/), a `Multi` respects the control flow requests from its subscribers, avoiding classic problems such as a fast producer and a slow consumer that can yield to memory exhaustion problems.

That being said, not everything is a stream.
Take the example of relational databases: **databases don't stream!** (for the most parts)

When you do a query such as `SELECT * FROM ABC WHERE INDEX < 123`, you get result rows.
While you might wrap the results in a `Multi<Row>` as a convenience, the network protocol of the database still sends you all `Row` values and is very unlikely to support any notion of back-pressure on a SQL query result.

This is why `Uni<List<Row>>` is in this case a better representation of an asynchronous operation than `Multi<Row>`, because the underlying networked service protocol does not provide you with any back-pressured stream.

## Creating `Uni` and `Multi` from in-memory data might be suspicious

You will find lots of occurrences of creating `Uni` and `Multi` from in-memory data in this documentation, as in:

```java linenums="1"
{{ insert('java/guides/reference/PitfallsTest.java', 'inMemoryData') }}
```

This is convenient and expected when creating tests and examples, but this should be a strong warning in production.
Indeed, if we have a method such as the following:

```java linenums="1"
{{ insert('java/guides/reference/PitfallsTest.java', 'suspiciousPublisher') }}
```

then it is clear that there is nothing _"reactive"_ in this code _(sadly, you can find such idioms in some well-known "reactive" client libraries, but we digress)_.

As a rule of thumb, if your **initial** publisher does not make any I/O operation and it already has the data available in memory, then it is suspicious:

- if it is a `Uni`, then it does not really model an asynchronous I/O operation because the data is already here, and
- if it is a `Multi` then not only there is no asynchronous I/O operation involved, but there is no need for a back-pressure protocol either (see the previous section).

What is not suspicious however is to create, say, a `Multi` to perform a transformation operation:

```java linenums="1"
{{ insert('java/guides/reference/PitfallsTest.java', 'flatmap-ism') }}
```

1 change: 1 addition & 0 deletions documentation/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ nav:
- 'reference/what-is-reactive-programming.md'
- 'reference/what-makes-mutiny-different.md'
- 'reference/uni-and-multi.md'
- 'reference/going-reactive-a-few-pitfalls.md'
- 'reference/publications.md'
- 'Tags index': 'tags-index.md'
- 'API': 'https://javadoc.io/doc/io.smallrye.reactive/mutiny/latest/index.html'
Expand Down
64 changes: 64 additions & 0 deletions documentation/src/test/java/guides/reference/PitfallsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package guides.reference;

import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import org.junit.jupiter.api.Test;

import java.util.List;

public class PitfallsTest {

private Uni<String> fetch(String id) {
return Uni.createFrom().item("Yolo");
}

@Test
public void noMagicScheduling() {
// <noMagicJoin>
Uni<List<String>> data = Uni.join().all(
fetch("abc"),
fetch("def"),
fetch("123")).andFailFast();
// </noMagicJoin>
}

@Test
public void inMemoryData() {
// <inMemoryData>
Uni<Integer> uni = Uni.createFrom().item(123);
Multi<Integer> multi = Multi.createFrom().items(1, 2, 3, 4, 5);
// </inMemoryData>
}

private List<String> obtainValues(String key) {
return List.of();
}

// <suspiciousPublisher>
public Multi<String> fetchData(String key) {
List<String> strings = obtainValues(key);
return Multi.createFrom().iterable(strings);
}
// </suspiciousPublisher>

private Multi<String> streamData(String key) {
return Multi.createFrom().items("");
}

private Multi<String> extractData(String line) {
return Multi.createFrom().items("");
}

private Multi<String> flatmap() {
// <flatmap-ism>
Multi<String> stream = streamData("abc");
return stream.onItem().transformToMultiAndMerge(line -> {
if (line.trim().length() > 10) {
return extractData(line);
} else {
return Multi.createFrom().item("[N/A]");
}
});
// </flatmap-ism>
}
}