Skip to content

Uni.combine().all().unis() v.s. Multi..onItem().transformToMultiAndConcatenate().collect()

In my Quarkus service, I need to fetch a list of results from external service, and I figured out there are two methods to achieve the same goal:

1st approach based on Uni.combine().all():

List<Uni<Result>> results = new ArrayList();
for (Parameter p : parameters) {
  // callService returns Uni<Result>
  results.add(callService(p));
}
// collect all the results
Uni<List<Result>> combined = Uni.combine().all().unis(results)...

2nd approach based on Multi..onItem().transformToMultiAndConcatenate().collect()

Multi.createFrom().iterable(parameters)
.onItem()
.transformToMultiAndConcatenate(p -> callService(p))
.collect().asList()

In the beginning I don’t think there exists any true difference between the two approaches, as Uni are evaluated lazily and either Uni.combine or Multi.collect are like syntax sugar to me. But I still would like to ask if there exists any difference? especially performance-wise difference.

With the 1st approach, I am debugging an error that when the size of parameters exceeds 25 it starts to give error but under 25 it is fine. Therefore I suspect it 1st approach shall incur a very high QPS that floods the external service. However, I doubt the 2nd approach will help with throttling either.

Answer

The concurrency model between Uni combination (Uni.combine().all().uni(…)), transformToMultiAndConcatenate and transformToMultiAndMerge are different.

Uni combination runs all the passed Unis concurrently. So if you pass, let’s say 100 unis, all 100 unis will be executed concurrently. In your case, it means sending 100 requests. So, yes, better be sure that the remote service handles concurrent requests gracefully. The combination function receives the list of results in the same order as the combined unis. So combining UniA and UniB will produce a list containing ResponseForA, ResponseForB.

transformToMultiAndConcatenate and transformToUniAndConcatenate have a level of concurrency of 1. Basically, it takes each item from the upstream individually, invokes your service, and when done, switch to the next item. So, not much concurrency here. However, it guarantees that the responses are in the same order as the items from the upstream. So, if you have [A, B, C] as upstream items, the resulting multi will be [response for A, response for B, response for C].

transformToMultiAndMerge and transformToUniAndMerge are going to execute a few request concurrently. The default concurrency is 128. So, it will consume 128 items from the upstream and invoke your remote service. Responses may not be ordered. As soon as one response is received, another item is consumed. So [A, B, C] may produce [response for B, response for C, response for A].

When using a merge, the concurrency can be configured using: multi.onItem().transformToMulti(mapper).merge(concurrency)

For references: