Flow<T>.distinctUntilChanged() is not working - kotlin

I'm trying to have a coroutine run while a SharedFlow is active (subscriptionCount is greater than zero) and be cancelled when the count drops. But somehow even something as simple as distinctUntilChanged() is not working as it should and I'm baffled.
For this I'm making a "onActive" extension like this:
fun <T : Any> MutableSharedFlow<T>.onActive(
block: suspend CoroutineScope.() -> Unit
): Flow<T> {
val original = this
val isActiveFlow: Flow<Boolean> = subscriptionCount
.map {
println("Class: Count is $it")
it > 0
}
.distinctUntilChanged()
return isActiveFlow.flatMapLatest { isActive ->
println("Class: isActive is $isActive")
// here would be the code that calls `block`
// but just this exactly as is, already triggers the error
original // still emits the original flow,
// that is needed or else subscriptionCount never changes
}
}
This initially this seems to work, but running a test on it that adds several subscribers, will print "isActive is true" several times in a row. Why is distinctUntilChanged() not working? This repeated call messes-up with the rest of the logic in the redacted area.
The test is like this:
#Test
fun `onActive is called only once with multiple subscribers`() = runBlocking {
val flow = MutableSharedFlow<Int>(
replay = 2,
onBufferOverflow = BufferOverflow.DROP_OLDEST
).apply {
repeat(5) { tryEmit(it) }
}.onActive {
}
val jobs = mutableListOf<Job>()
repeat(3) { count ->
jobs.add(flow.onEach {
println("Test: Listener $count received $it")
}.launchIn(this))
}
delay(100)
jobs.forEach { it.cancel() }
jobs.forEach { it.join() }
}
running this the output is:
Class: Count is 0
Class: isActive is false
Class: Count is 1
Class: Count is 1
Class: isActive is true
Class: Count is 2
Class: Count is 2
Class: isActive is true
Class: Count is 3
Test: Listener 0 received 3
Test: Listener 0 received 4
Test: Listener 1 received 3
Test: Listener 1 received 4
Test: Listener 2 received 3
Test: Listener 2 received 4
Class: Count is 2
Class: isActive is true
Class: Count is 3
Class: Count is 3
Class: Count is 3
Test: Listener 0 received 3
Test: Listener 0 received 4
So the question, why is distinctUntilChanged() not working and how can I fix it?

It seems the behaviour you're seeing is actually correct as far as distinctUntilChanged is concerned:
the first registered subscriber collects the original 2 replayed elements with the starting isActive=false value
then isActive becomes true because of that first susbcription, so that first subscriber recollects the original flow due to flatMapLatest, and thus gets again the replayed elements
the other 2 subscribers arrive when the subscriptionCount is already non-0 so isActive stays true for them until they are cancelled
If the coroutine you launch "while there are subscribers" is meant to produce elements in the SharedFlow, I would rather define the flow like a channelFlow/callbackFlow initially, and then use shareIn with SharingStarted.WhileSubscribed to have this "run when there are susbcribers" behaviour.
If it's "just on the side", you probably want an external scope and just launch a coroutine separately to listen to sharedFlow.subscribersCount and start/stop the "sidecar" coroutine.

Related

How to switch flow based on condition?

There is query and two filters. When set search filter and query is not empty, need to one flow. When set checked filter need to other flow.
According debug, onClickSearchFilter or onClickCheckedFilter calls with query or filter changed - return new flow. But in UI no changes, collector dont work second time.
How to switch flow based on condition?
When I debugging with breakpoints in flows, app crash every time A/libc: Fatal signal 11 (SIGSEGV), code 1, fault addr 0x14 in tid 23174 (JDWP), pid 23167 . Rebuild, clear cache, reload device - dont' help.
repeatOnStarted(viewModel.itemsFlow) {
// it doesn't work when flow is switched
pagingAdapter.submitData(it)
}
val itemsFlow = queryFlow
.debounce(1000)
.combine(filterFlow) { query, filter ->
when (filter) {
R.id.search_result_chip -> onClickSearchFilter(query)
R.id.checked_chip -> onClickCheckedFilter()
else -> throw Exception("")
}
}.flatMapMerge { // in order to switch from Flow<Flow<*>> to Flow<*>
it
}
private fun onClickSearchFilter(query: String): Flow<PagingData<ItemEntity>> {
return if (query.length < 2)
emptyFlow()
else Pager(BasePagingSource.getConfig()) {
SearchPagingSource(query, client)
}.flow.cachedIn(viewModelScope)
}
private fun onClickCheckedFilter(): Flow<PagingData<ItemEntity>> {
return Pager(
config = BasePagingSource.getConfig(),
remoteMediator = RemoteMediator(checkedIds, cacheDatabase, client)
) {
cacheDatabase.itemDao.getPagingSource(type, checkedIds)
}.flow
}

It should return true but it says false and I dont know why

I am programming something and I was testing something with assertEquals and I am pretty sure that it should return true but the test says it is false. The value im testing is "hasEnded"
I will show you the relevant code to my case:
Note: Everything I deleted was tested already and kept the only important code to my case "hopefully"
class Schwimmen(
var hasEnded: Boolean = false
)
fun nextTurn() {
val game = rootService.currentGame
game.hasEnded= checkForEnd()
private fun checkForEnd(): Boolean {
val game = rootService.currentGame
return (game.cards.size < 3 && game.playerPassCounter >= game.players.size) ||
(game.activePlayer == game.endGamePlayer)
}
fun testNextTurn(){
testRootService.schwimmenService.startSchwimmen(players)
testRootService.currentGame?.cards?.clear()
testRootService.currentGame?.playerPassCounter = 5
assertEquals(true, testRootService.currentGame?.hasEnded)
}
The Test failed and I got: AssertionFailedError: expected: "true" but was: "false"
I found the answer:
I forgot to call "nextTurn()" to run checkForEnd() in test
#Test
fun testNextTurn(){
testRootService.schwimmenService.startSchwimmen(players)
testRootService.currentGame?.cards?.clear()
testRootService.currentGame?.playerPassCounter = 5
**testRootService.schwimmenService.nextTurn()**
assertEquals(true, testRootService.currentGame?.hasEnded)
}

Efficient way to update progress of long task while processing a task with Mono&Flux

I tried to make a method for long task which sends events of progress.
The method implemented by using Flux and Mono.
I figured out two ways to do it.
But I can't decide which approach is more efficient and elegant, or the other one.
Way 1.
var progressEvents = Sinks.Many<Progress> = Sinks.many().multicast()
fun doLongTask(taskFile : Mono<File>) : Mono<Another> {
return taskFile.map{ file ->
progressEvents.tryEmit(Progress("1"))
doSomething(file)
}.map{ something ->
progressEvents.tryEmit(Progress("2"))
doAnother(something)
}
}
Way2.
var progressEvents = Sinks.Many<Progress> = Sinks.many().multicast()
fun doLongTask(taskFile : Mono<File>) : Mono<Another> {
var doSomeMap = taskFile.map{ file ->
doSomething(file)
}.share()
doSomeMap.map{ something -> Progress("1")}.subscribe{progressEvents::tryEmit}
var doAnotherMap = doSomeMap.map{ something ->
doAnother(something)
}.share()
doAnotherMap.map{ another -> Progress("2")}.subscribe{progressEvents::tryEmit}
return doAnotherMap
}

How to execute predefined number of jobs at one time in Nestjs bull

I have 65 same named jobs to execute in one queue with 3 instances of consumers (A, B, C). I want to do at one time each consumer execute 10 jobs. After the 10 execution completed, if there are available jobs greater than 10 in the queue that consumer again execute 10 jobs. If not execute available jobs.
jobs 1 to 65
Consumer A execute 1 to 10
Consumer B execute 11 to 20
Consumer C execute 21 to 30
Lets take B, A, C finished the execution in order.
then
B - 31,32,33,.40
A - 41,42,43,.50
C - 51,52,53,.60
if C finish the execution first, C execute the remaining 5 jobs.
Please can I know are there any ways to achieve this.
producer
#Injectable()
export class SampleQueueProducerService {
constructor(#InjectQueue('sample-queue') private sampleQueue: Queue) {}
async sendDataToJob(message: string) {
await this.sampleQueue.add('job', { message });
}
}
consumer
#Processor('sample-queue')
export class SampleQueueConsumerService {
#Process({ name: 'job' })
async sampleJob(job: Job<any>) {
console.log(job.data);
}
}
all 3 consumers are same as above.
You can get x jobs using this functionality (bull reference)
However you don't have this exactly supported with NestJS
What you can do is use #OnGlobalQueueWaiting() or #OnQueueWaiting() to get the waiting jobs, storing them in an array, once it reaches 10, you can send them to be processed.
Something like:
#OnGlobalQueueWaiting()
async onGlobalWaiting(jobId: number, result: any) {
const job = await this.myQueue.getJob(jobId);
this.jobArray.push(job)
if (this.jobArray.length >= 10) {
await this.processJobs(this.jobArray);
this.jobArray = [];
}
}
async processJobs(jobs: Job[]){'
jobs.forEach(job => do something)
}

Timeout test does not return TimeoutException - rxjava

The test fails as there is no exception thrown. It simply completes instead of timing out.
#Test
fun timeout() {
val testScheduler = TestScheduler()
val sub = Observable.just(true)
.filter{ it -> !it }
.timeout(10, TimeUnit.SECONDS, testScheduler)
val testSubscriber = sub.subscribeOn(testScheduler).test()
testScheduler.advanceTimeBy(20, TimeUnit.SECONDS)
testSubscriber.assertError(TimeoutException::class.java)
}
I've been at this block for over an hour and I just don't see why it's failing. It's probably something super obvious, but I feel I need another set of eyes to point it out for me.
Here's a test that gets the expected result:
#Test
fun timeout() {
val testScheduler = TestScheduler()
val sub = Observable.just(true) // 1
.delaySubscription(Observable.never<Boolean>()) // 2
.timeout(10, TimeUnit.SECONDS, testScheduler) // 3
val testSubscriber = sub.subscribeOn(testScheduler).test()
testScheduler.advanceTimeBy(20, TimeUnit.SECONDS)
testSubscriber.assertError(TimeoutException::class.java)
Here's an explanation of what's going on:
We just need a Observable to start with, the filter you had before was just making this be an empty one
We want to delay the subscription with something that won't finish, so one alternative is an Observable than never emits
From here on, it's the same you had