Controller returning reactive paged result from total item count as Mono, items as Flux - kotlin

I've got 2 endpoints returning the same data in two different JSON-formats.
The first endpoint returns a JSON-array, and starts the response right away.
#Get("/snapshots-all")
fun allSnapshots(
#Format("yyyy-MM-dd") cutoffDate: LocalDate
): Flux<PersonVm> = snapshotDao.getAllSnapshots(cutoffDate)
The next endpoint that returns a paged result, is more sluggish. It starts the response when both streams are completed. It also requires a whole lot more of memory than the previous endpoint, even though the previous endpoint returns all records from BigQuery.
#Get("/snapshots")
fun snapshots(
#Format("yyyy-MM-dd") cutoffDate: LocalDate,
pageable: Pageable
): Mono<Page<PersonVm>> = Mono.zip(
snapshotDao.getSnapshotCount(cutoffDate),
snapshotDao.getSnapshots(
cutoffDate,
pageable.size,
pageable.offset
).collectList()
).map {
CustomPage(
items = it.t2,
totalNumberOfItems = it.t1,
pageable = pageable
)
}
(Question update) BigQuery is at the bottom of this endpoint. The strength of BigQuery compared to e.g. Postgres, is querying huge tables. The weakness is relatively high latency for simple queries. Hence I'm running the queries in parallel in order to keep latency for the endpoint at a minimum. Running the queries in sequence, will add at least a second to the total processing time.
Question is: Is there a possible rewrite of the chain that will speed up the /snapshots endpoint?
Solution requirements (question update after suggested approaches)
The consumer of this endpoint is external to the project, and every endpoint in this project is documented at a detailed level. Hence, pagination may only occur one time in the returned JSON. Else feel free to suggest new types for returning pagination along with the PersonVm collection.
If it turns out that another solution is impossible, that's an answer as well.
SnapshotDao#getSnapshotCount returns a Mono<Long>
SnapshotDao#getSnapshots returns a Flux<PersonVm>
PersonVm is defined like this:
#Introspected
data class PersonVm(
val volatilePersonId : UUID,
val cases: List<PublicCaseSnapshot>
)
CustomPage is defined like this:
#Introspected
data class CustomPage<T>(
private val items: List<T> = listOf(),
private val totalNumberOfItems: Long,
private val pageable: Pageable
) : Page<T> {
override fun getContent(): MutableList<T> = items.toMutableList()
override fun getTotalSize(): Long = totalNumberOfItems
override fun getPageable(): Pageable = pageable
}
PublicCaseSnapshot is a complex structure, and left out for brevity. It should not be required for solving this issue.
Code used during test of suggested approach from #Denis
In this approach, chain starts with SnapshotDao#getSnapshotCount, and is mapped into an HttpResponse instance with response body containing the Flux<PersonVm>, and total item count in header.
Queries will now run in sequence, and numerous comparison tests between below code and existing code, showed that the original code performs better (by approx. 1 second). Different page sizes were used during the tests, and BigQuery was warmed up by running same query multiple times. Best results were recorded.
Please note that in cases where time spent on total item count query is negligible (or total item count is cacheable) and pagination is not required to be part of the JSON, this should be considered as a viable approach.
#Get("/snapshots-with-total-count-in-header")
fun snapshotsWithTotalCountInHeader(
#Format("yyyy-MM-dd") cutoffDate: LocalDate,
pageable: Pageable
): Mono<HttpResponse<Flux<PersonVm>>> = snapshotDao.getSnapshotCount(cutoffDate)
.map { totalItemCount ->
HttpResponse.ok(
snapshotDao.getSnapshots(
cutoffDate,
pageable.size,
pageable.offset
)
).apply {
headers.add("total-item-count", totalItemCount.toString())
}
}

You need to rewrite the method to return a publisher of the items. I can see a few options here:
Return the pagination information in the header. Your method will have return type Mono<HttpResponse<Flux<PersonVm>>>.
Return the pagination information on every item: Flux<Tuple<PageInfo, PersonVm>>

Related

Is it possible to generate scenario outline definition dynamically [duplicate]

I currently use junit5, wiremock and restassured for my integration tests. Karate looks very promising, yet I am struggling with the setup of data-driven tests a bit as I need to prepare a nested data structures which, in the current setup, looks like the following:
abstract class StationRequests(val stations: Collection<String>): ArgumentsProvider {
override fun provideArguments(context: ExtensionContext): java.util.stream.Stream<out Arguments>{
val now = LocalDateTime.now()
val samples = mutableListOf<Arguments>()
stations.forEach { station ->
Subscription.values().forEach { subscription ->
listOf(
*Device.values(),
null
).forEach { device ->
Stream.Protocol.values().forEach { protocol ->
listOf(
null,
now.minusMinutes(5),
now.minusHours(2),
now.minusDays(1)
).forEach { startTime ->
samples.add(
Arguments.of(
subscription, device, station, protocol, startTime
)
)
}
}
}
}
}
return java.util.stream.Stream.of(*samples.toTypedArray())
}
}
Is there any preferred way how to setup such nested data structures with karate? I initially thought about defining 5 different arrays with sample values for subscription, device, station, protocol and startTime and to combine and merge them into a single array which would be used in the Examples: section.
I did not succeed so far though and I am wondering if there is a better way to prepare such nested data driven tests?
I don't recommend nesting unless absolutely necessary. You may be able to "flatten" your permutations into a single table, something like this: https://github.com/intuit/karate/issues/661#issue-402624580
That said, look out for the alternate option to Examples: which just might work for your case: https://github.com/intuit/karate#data-driven-features
EDIT: In version 1.3.0, a new #setup life cycle was introduced that changes the example below a bit.
Here's a simple example:
Feature:
Scenario:
* def data = [{ rows: [{a: 1},{a: 2}] }, { rows: [{a: 3},{a: 4}] }]
* call read('called.feature#one') data
and this is: called.feature:
#ignore
Feature:
#one
Scenario:
* print 'one:', __loop
* call read('called.feature#two') rows
#two
Scenario:
* print 'two:', __loop
* print 'value of a:', a
This is how it looks like in the new HTML report (which is in 0.9.6.RC2 and may need more fine tuning) and it shows off how Karate can support "nesting" even in the report, which Cucumber cannot do. Maybe you can provide feedback and let us know if it is ready for release :)

what is the best practice to return total of obects with webflux?

I am developing api with web flux.
#GetMapping(value = "/shipments/containernumbers")
#Operation(summary = "Returns a list of related container numbers", description = "Return related container numbers based on input")
public Flux<ContainerNumber> containerNumbers(final String searchTerm, final Pageable pageable) {
return shipmentService.containerNumbers(searchTerm, pageable)
.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND, "these is not container number with your input ")));
}
This will return list of objects with paging. But this will not tell front end how many object it has. The front end need know the total number of object to prepare pages.I checked some samples but did not find right way to do it.
What it the best way to realize this requirement means return list of object with paging function as well as total number of object with webflux?
thanks in advance.
The only way I can think of would be to create a second endpoint like:
#GetMapping(value = "/shipments/containernumbers/count")
#Operation(summary = "Returns a list of related container numbers", description = "Return related container numbers based on input")
public Mono<Long> containerNumbers(final String searchTerm) {
return shipmentService.containerNumbersCount(searchTerm)
.collectList()
.map(list -> list.size())
.switchIfEmpty(Mono.just(0L));
}
There is no other way if you want server side pagination. You need one endpoint to query the data. This endpoint will employ a Pageable parameter just as you did in your question above.
But to display the number of pages in the frontend, you need a full count of the objects. No big deal, just a few additional lines of code. I suggest a second endpoint for that as shown here.
The database query can be made even simpler if you use spring data. Then you create a countByXyz() query and don't need the collectList() and list.size() part of my code example.

What is the difference between FlatMapMerge, FlatMapConcat and FlatMapLatest?

I'm having trouble differentiating the exact difference between these three operators.
The documentation for:
FlatMapMerge
FlatMapConcat
FlatMapLatest
These documentation links indicate that two Flows are flatmapped into a single Flow object. I am fine with that, but I have a hard time understanding how the emissions would change between these three operators.
I went looking through the source code and found a sensible guide that I didn't find via googling. https://kotlinlang.org/docs/reference/coroutines/flow.html#flattening-flows
The guide explains the differences but it was still a bit unclear to me, so I rewrote it in my own words here.
The basic difference between the three are determined by the way in which the inner and outer flow react to new emissions from either flow. So for the given code:
val outerFlow: Flow<>
val flatMappedFlow = outerflow
.flatMapXXXXX { innerFlow(it) }
.collect { processFlatMapResult(it) }
FlatMapConcat
This operator is sequential and paired. Once the outerFlow emits once, the innerFlow must emit once before the final result is collected. Once either flow emits a Nth time, the other flow must emit a Nth time before the Nth flatMapResult is collected.
FlatMapMerge
This operator has the least restrictions on emissions, but can result in too many emissions. Every time the outerFlow emits a value, each of the innerFlow emissions are flatMapped from that value into the final flatMapResult to be collected. The final emission count is a multiplication of innerFlow and outerFlow emissions.
FlatMapLatest
This operator cares only about the latest emitted results and does not process old emissions. Every time the outerFlow emits a value, it is flatMapped with the latest innerFlow value. Every time the innerFlow emits a value, it is flatMapped with the latest outerFlow value. Thus the final emission count is a value between zero and innerFlow emissions times outerFlow emissions.
flatmapConcate It is used to transform a flow of elements into a flow of other elements. It applies a transformation function to each element of the flow and returns a new flow that emits the transformed elements in the order in which they were produced.
Let's say you're building an app that fetches data from two different APIs, one for user information and another for user posts. You want to display a list of all the user posts along with the user's name and profile picture.
To do this, you can use Kotlin coroutines and flatMapConcat to combine the data from the two APIs into a single flow of Post objects. Here's how you could do it:
suspend fun getPosts(): Flow<Post> = flow {
val users = userApi.getUsers() // Get list of users
for (user in users) {
val posts = postApi.getPostsForUser(user.id) // Get posts for each user
for (post in posts) {
val profilePicture = userApi.getProfilePicture(user.id) // Get user's profile picture
val postWithUser = PostWithUser(post, user.name, profilePicture)
emit(postWithUser) // Emit combined post and user data
}
}
}.flatMapConcat { postWithUser ->
val imageUrl = postWithUser.userProfilePictureUrl
val imageBitmap = downloadImage(imageUrl) // Download user's profile picture
val postWithImage = PostWithImage(postWithUser.post, postWithUser.userName, imageBitmap)
emit(postWithImage) // Emit post data with profile picture
}
flatMapMerge : flatMapMerge is used to transform a flow of elements into a flow of other elements, just like flatMapConcat. However, instead of emitting the output elements in the order in which they were produced, flatMapMerge emits them as soon as they are available.
Let's say you're building an app that displays a list of photos downloaded from an API. Each photo has a unique ID, and you want to display the title of the album that the photo belongs to. You can use the photo ID to fetch the album data from a separate API.
Here's how you could use flatMapMerge to combine the photo data with the album data:
suspend fun getPhotosWithAlbum(): Flow<PhotoWithAlbum> = photoApi.getPhotos()
.flatMapMerge { photo ->
albumApi.getAlbum(photo.albumId)
.map { album ->
PhotoWithAlbum(photo, album.title)
}
}

What is the best way to get specific entity from room database

I was doing a project a while back where the user could choose a route/course and see its information on a separate page but got a problem regarding the extraction of the information of a specific entity stored in my Room database. I didn't end up finding the solution for the problem so I just changed my approach altogether and passed the necessary information through the intent that starts the activity instead of just getting the information by searching the database.
I've already tried to ask about the problem here How to get a specific Entity in Room but no one seems to see where the problem is so I'm changing the question.
What I want to know now is what is the best way to get a specific entity from Room database in Kotlin.
I looked at your question and i think the issue is you trying to get the value of live data which usually returns null. I had a similar issue and i solved it but returning the actual entity from the DAO and reading it in the IO thread in the viewmodel
DAO:
#Query("select * from groups WHERE groupid =:groupId ")
fun getGroup(groupId:String): Group
Repository:
override suspend fun getGroup(groupId: String): Group {
return runBlocking {
groupDao.getGroup(groupId)
}
}
ViewModel:
fun getGroup(groupID:String):Group{
return runBlocking {
async(Dispatchers.IO) {
groupsRepository.getGroup(groupID)
}.await()
}
}

Sorted table with a map in Apache Ignite

I initially want to accomplish something simple with Ignite. I have a type like this (simplified):
case class Product(version: Long, attributes: Map[String, String])
I have a key for each one to store it by (it's one of the attributes).
I'd like to store them such that I can retrieve a subset of them between two version numbers or, at the very least, WHERE version > n. The problem is that the cache API only seems to support either retrieval by key or table scan. On the other hand, SQL99 doesn't seem to have any kind of map type.
I was thinking I'd need to use a binary marshaler, but the docs say:
There is a set of 'platform' types that includes primitive types, String, UUID, Date, Timestamp, BigDecimal, Collections, Maps and arrays of thereof that will never be represented as a BinaryObject.
So... maps are supported?
Here's my test code. It fails with java.lang.IllegalArgumentException: Cache is not configured: ignite-sys-cache, though. Any help getting a simple test working would really aid my understanding of how this is supposed to work.
Oh, and also, do I need to configure the schema in the Ignite config file? Or are the field attributes a sufficient alternative to that?
case class Product(
#(QuerySqlField #field)(index = true) version: Long,
attributes: java.util.Map[String, String]
)
object Main {
val TestProduct = Product(2L, Map("pid" -> "123", "foo" -> "bar", "baz" -> "quux").asJava)
def main(args: Array[String]): Unit = {
Ignition.setClientMode(true)
val ignite = Ignition.start()
val group = ignite.cluster.forServers
val cacheConfig = new CacheConfiguration[String, Product]
cacheConfig.setName("inventory1")
cacheConfig.setIndexedTypes(classOf[String], classOf[Product])
val cache = ignite.getOrCreateCache(cacheConfig)
cache.put("P123", TestProduct)
val query = new SqlQuery(classOf[Product], "select * from Product where version > 1")
val resultSet = cache.query(query)
println(resultSet)
}
}
Ignite supports querying by indexed fields. Since version is a regular indexed field it should be feasible to do the described queries.
I've checked your code and it works on my side.
Please check that the Ignite version is consistent across all the nodes.
If you provide the full logs I could take a look at it.