In our previous article about Mutiny we have covered some basics, instead today we go deeper, you may already know that Mutiny revolves around two key players: Uni
and Multi
. Think of Uni
as your go-to guy for single, asynchronous operations, while Multi
is the life of the party, handling streams of events like a boss.
Uni<String> singleResult = Uni.createFrom().item("Hello, Mutiny!");
Multi<Integer> numberStream = Multi.createFrom().range(1, 10);
Event Composition: When Async Streams Collide
Now, let's get to the juicy part - combining multiple async streams. It's like being a DJ, but instead of mixing tracks, you're orchestrating data flows.
The Combine Technique
Imagine you're fetching user data and their latest order simultaneously. Here's how you'd mash that up with Mutiny:
Uni<User> userUni = fetchUser(userId);
Uni<Order> orderUni = fetchLatestOrder(userId);
Uni<String> combined = Uni.combine()
.all().unis(userUni, orderUni)
.asTuple()
.map(tuple -> tuple.getItem1().getName() + " ordered " + tuple.getItem2().getProductName());
Neat, right? We've just created a symphony of async operations.
The Zip Maneuver
But wait, there's more! When you need to pair up elements from two streams, zip
comes to the rescue:
Multi<Integer> numbers = Multi.createFrom().range(1, 5);
Multi<String> letters = Multi.createFrom().items("a", "b", "c", "d", "e");
Multi<String> zipped = numbers.zip(letters, (n, l) -> n + l);
// Result: 1a, 2b, 3c, 4d, 5e
Error Handling: When Async Goes South
Let's face it, in the world of async, errors are like uninvited guests at a party. But fear not! Mutiny's got your back with some nifty error-handling tricks.
The Recover and Retry Dance
Uni<String> flakeyService = callUnreliableService()
.onFailure().retry().atMost(3)
.onFailure().recoverWithItem("Backup Plan Activated!");
This snippet tries your flaky service three times before falling back to a backup plan. It's like having a safety net... for your safety net.
The Fallback Flip
Sometimes, you just need a plan B:
Uni<WeatherReport> weatherReport = getWeatherReport()
.onFailure().call(this::logError)
.onFailure().fallback(WeatherReport::getDefaultReport);
If getting the weather report fails, we log the error and return a default report. Because let's be honest, "sunny with a chance of exceptions" isn't a real forecast.
Flow Control: Taming the Data Torrent
When dealing with high-velocity data streams, it's crucial to avoid drowning in a sea of events. Mutiny offers several life rafts to keep you afloat.
The Throttle Valve
Multi<String> throttledStream = sourceStream
.throttle().iterations(10).per(Duration.ofSeconds(1))
.onOverflow().drop();
This code ensures we process no more than 10 items per second, dropping any excess. It's like having a bouncer for your data nightclub.
The Backpressure Backstop
When you need more control over backpressure:
Multi<Data> controlledStream = sourceStream
.onOverflow().buffer(100)
.onItem().transformToUniAndMerge(this::processSlowly);
Here, we buffer up to 100 items and process them one by one, ensuring our downstream operations aren't overwhelmed. It's the async equivalent of "slow and steady wins the race".
Multi Mastery: Streaming Like a Pro
Working with Multi
is where Mutiny really shines. It's like having a Swiss Army knife for data streams. (Dang it, I did it again with the Swiss Army knife. Let's say it's like having a fully-equipped workshop for data streams.)
The Filter-Map-Collect Trifecta
Multi.createFrom().range(1, 100)
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.collect().asList()
.subscribe().with(
list -> System.out.println("Squared evens: " + list),
error -> System.err.println("Oops: " + error)
);
This snippet filters even numbers, squares them, collects the results into a list, and then prints it. It's like a assembly line for your data, but way cooler.
Async Transactions: The Mutiny Way
Handling transactions in an async world can be tricky, but Mutiny makes it feel like a walk in the park.
Uni<TransactionResult> txResult = Uni.createFrom().item(() -> beginTransaction())
.chain(tx -> performOperation1(tx)
.chain(() -> performOperation2(tx))
.chain(() -> commitTransaction(tx))
.onFailure().call(() -> rollbackTransaction(tx))
);
This code starts a transaction, performs two operations, and either commits or rolls back based on the outcome. It's like having a safety harness while walking a tightrope.
Bridging Sync and Async: The Best of Both Worlds
Sometimes, you need to mix sync and async code. Mutiny's got you covered:
CompletableFuture<String> future = someAsyncOperation();
Uni<String> uni = Uni.createFrom().completionStage(future);
// Or the other way around
Uni<Integer> uni = Uni.createFrom().item(() -> 42);
CompletableFuture<Integer> future = uni.subscribeAsCompletionStage();
It's like being bilingual in the programming world - you can switch between sync and async dialects effortlessly.
Parallel Universe: Concurrency with Mutiny
When you need to kick things into high gear, parallelism is your friend:
Multi<String> results = Multi.createFrom().iterable(hugeListOfUrls)
.onItem().transformToUniAndMerge(url ->
Uni.createFrom().item(() -> fetchData(url))
.runSubscriptionOn(Infrastructure.getDefaultWorkerPool())
)
.collect().asList();
This code fetches data from multiple URLs in parallel, utilizing Quarkus' worker pool. It's like having a team of ninjas fetching your data - fast and efficient.
Reactive REST APIs: The Mutiny Edition
Building reactive REST APIs with Mutiny is a breeze. Here's a quick example:
@Path("/users")
public class UserResource {
@Inject
UserService userService;
@GET
@Path("/{id}")
public Uni<Response> getUser(@PathParam("id") Long id) {
return userService.findById(id)
.onItem().transform(user -> Response.ok(user).build())
.onFailure().recoverWithItem(Response.status(Status.NOT_FOUND).build());
}
}
This endpoint returns a user by ID, handling the not-found case gracefully. It's reactive, it's clean, and it's ready for high concurrency.
Wrapping Up: Mutiny Best Practices
As we wrap up our journey through the async wonderland of Mutiny, here are some parting thoughts:
- Embrace the reactive paradigm - think in terms of streams and events.
- Use
Uni
for single async operations andMulti
for streams. - Handle errors proactively - the
onFailure()
chain is your friend. - Control the flow - use backpressure mechanisms to prevent overwhelming your system.
- Combine sync and async code carefully - Mutiny provides tools, but use them judiciously.
- Test thoroughly - async code can be tricky, so comprehensive testing is crucial.
Remember, with great power comes great responsibility. Mutiny gives you the tools to build highly concurrent, efficient applications, but it's up to you to wield them wisely.
Now go forth and conquer the async world with Mutiny! And remember, in the land of reactive programming, the only constant is change. Happy coding!