Spring reactor parallel flux is stuck - spring-boot

I am using reactor to create an infinite flux,
once I make it parallel, the stream gets stuck after the first passed value, can't figure out why
val source = source().parallel().runOn(Schedulers.parallel())
.map(this::toUpperCase)
.subscribe(sink())
private fun sink() = SimpleSink<SimpleDaoModel>()
private fun toUpperCase(simpleDaoModel: SimpleDaoModel) = simpleDaoModel.copy(stringValue = simpleDaoModel.stringValue.toUpperCase())
private fun source() = Flux.create { sink: FluxSink<SimpleDaoModel> ->
fun getNextAsync(): Job = GlobalScope.launch(Dispatchers.Default) {
val task = customSimpleModelRepository.getNextTask()
if (task != null) {
logger.info("emitting next task")
sink.next(task)
} else {
logger.info("No more tasks")
Timer("nextTaskBackoff", false).schedule(1000) {
getNextAsync()
}
}
}
sink.onRequest { getNextAsync() }
}
class SimpleSink<T> : BaseSubscriber<T>() {
public override fun hookOnSubscribe(subscription: Subscription) {
println("Subscribed")
request(1)
}
public override fun hookOnNext(value: T) {
println(value)
request(1)
}
}
If I remove the parallel operator, everything works like a charm.
Note: getNextTask is a suspended function

Related

Testable Kotlin objects that uses a dispatcher in init block?

I have a class that looks like this:
object SomeRepository {
private val logger = Logger(this)
private val flow: MutableStateFlow<List<SomeClass>> = MutableStateFlow(listOf())
init {
GlobalScope.launch(Dispatchers.IO) {
// some code
}
}
fun list() = flow.value
fun observe(): StateFlow<List<SomeClass>> = flow
}
It is recommended to inject dispatchers in tests.
Also, there is a risk to get weird problems such as AppNotIdleException if not following this practice.
One option would be to make these into variables and add setters, but not very nice. Also it creates a race condition (init-block vs setters):
object SomeRepository {
private val logger = Logger(this)
private val flow: MutableStateFlow<List<SomeClass>> = MutableStateFlow(listOf())
private var coroutineScope: CoroutineScope = GlobalScope
private var dispatcher: CoroutineDispatcher = Dispatchers.IO
init {
dispatcher.launch(coroutineScope) {
// some code
}
}
fun list() = flow.value
fun observe(): StateFlow<List<SomeClass>> = flow
#VisibleForTesting
fun setDispatcher(dispatcher: CoroutineDispatcher) {
this.dispatcher = dispatcher
}
#VisibleForTesting
fun setCoroutineScope(coroutineScope: CoroutineScope) {
this.coroutineScope = coroutineScope
}
}
Version that avoids the race condition (test needs to explicitly invoke init()):
object SomeRepository {
private val logger = Logger(this)
private val flow: MutableStateFlow<List<SomeClass>> = MutableStateFlow(listOf())
private var coroutineScope: CoroutineScope = GlobalScope
private var dispatcher: CoroutineDispatcher = Dispatchers.IO
init {
// var isInTesting = Build.FINGERPRINT == "robolectric"
if (!isInTesting) {
init()
}
}
#VisibleForTesting
fun init() {
dispatcher.launch(coroutineScope) {
// some code
}
}
fun list() = flow.value
fun observe(): StateFlow<List<SomeClass>> = flow
#VisibleForTesting
fun setDispatcher(dispatcher: CoroutineDispatcher) {
this.dispatcher = dispatcher
}
#VisibleForTesting
fun setCoroutineScope(coroutineScope: CoroutineScope) {
this.coroutineScope = coroutineScope
}
}
How can I inject a coroutine scope and dispatcher to my tests and avoid variables?

Test case is failing after upgrading the spring boot version from 2.1.x to 2.6.x

Test case is failing with following
Exception : after upgrading the Springboot version from 2.1.x to 2.6.x
org.awaitility.core.ConditionTimeoutException: Condition with lambda expression in com.timer.Relax that uses kotlin.jvm.functions.Function0 was not fulfilled within 20 seconds.
at org.awaitility.core.ConditionAwaiter.await(ConditionAwaiter.java:165)
at org.awaitility.core.CallableCondition.await(CallableCondition.java:78)
at org.awaitility.core.CallableCondition.await(CallableCondition.java:26)
at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:895)
at org.awaitility.core.ConditionFactory.until(ConditionFactory.java:864)
at com.timer.Relax.until(Relax.kt:14)
code snipet:
object Relax {
private const val INCLUSION_TIMEOUT = 20L
fun until(runnable: () -> Unit) {
await()
.pollInterval(1, TimeUnit.SECONDS)
.atMost(INCLUSION_TIMEOUT, TimeUnit.SECONDS)
.until(callable(runnable))
}
private fun callable(runnable: () -> Unit): Callable<Boolean> {
return Callable {
try {
runnable.invoke()
true
} catch (throwable: Throwable) {
false
}
}
}
}
You code works for me as expected:
class SO72046468 {
#Test
fun `verify awaitility good`() {
Relax.until { println("Hi Jimmy!") }
}
#Test
fun `verify awaitility bad`() {
Relax.until { throw RuntimeException("intentional") }
}
object Relax {
private const val INCLUSION_TIMEOUT = 3L
fun until(runnable: () -> Unit) {
await()
.pollInterval(1, TimeUnit.SECONDS)
.atMost(INCLUSION_TIMEOUT, TimeUnit.SECONDS)
.until(callable(runnable))
}
private fun callable(runnable: () -> Unit): Callable<Boolean> {
return Callable {
try {
runnable.invoke()
true
} catch (throwable: Throwable) {
false
}
}
}
}
}
Kotlin 1.6.10 and Awaitility 4.2.0. And yes: JUnit 5.8.2.

I have a problem with the code in ViewModel + Kotlin Coroutines + LiveData

I have a ViewModel
#HiltViewModel
class LoginViewModel #Inject constructor(
private val apiRepository: ApiRepository
) : ViewModel() {
private val account = MutableLiveData<String>("123")
private val password = MutableLiveData<String>("123")
val message: MutableLiveData<String> = MutableLiveData()
var loginResult: LiveData<Resource<UserInfo>> = MutableLiveData()
fun signIn() {
if (TextUtils.isEmpty(account.value)) {
message.postValue("Please enter your account")
return
}
if (TextUtils.isEmpty(password.value)) {
message.postValue("please enter your password")
return
}
// In this code, it doesn’t work. I think it’s because I didn’t observe it.
// Is there any better way to write it here?
loginResult = apiRepository.signIn(account.value!!, password.value!!)
}
fun inputAccount(accountValue: String) {
account.value = accountValue
}
fun inputPassword(passwordValue: String) {
password.value = passwordValue
}
}
This is my interface
#AndroidEntryPoint
class LoginActivity : BaseActivity<ActivityLoginBinding>() {
private val viewModel: LoginViewModel by viewModels()
......
override fun initEvent() {
binding.account.editText!!.addTextChangedListener { viewModel.inputAccount(it.toString()) }
binding.password.editText!!.addTextChangedListener { viewModel.inputPassword(it.toString()) }
binding.signIn.setOnClickListener {
viewModel.signIn()
}
}
override fun setupObservers() {
viewModel.message.observe(this) {
Snackbar.make(binding.root, it, Snackbar.LENGTH_SHORT).show()
}
/**
* There will be no callback here, I know it’s because I’m observing
* `var loginResult: LiveData<Resource<UserInfo>> = MutableLiveData()`
* instead of `apiRepository.signIn(account.value!!, password.value!!)`
* because it was reassigned
*/
viewModel.loginResult.observe(this) {
Log.d("TAG", "setupObservers: $it")
}
}
}
So I adjusted the code a bit
LoginViewModel.signIn
fun signIn(): LiveData<Resource<UserInfo>>? {
if (TextUtils.isEmpty(account.value)) {
message.postValue("Please enter your account")
return null
}
if (TextUtils.isEmpty(password.value)) {
message.postValue("please enter your password")
return null
}
return apiRepository.signIn(account.value!!, password.value!!)
}
LoginActivity.initEvent
override fun initEvent() {
binding.signIn.setOnClickListener {
viewModel.signIn()?.observe(this) {
Log.d("TAG", "setupObservers: $it")
}
}
}
I have checked the official documents of LiveData, and all call livedata{} during initialization. There has been no re-assignment, but if you log in, you cannot directly start the application and request the network.
coroutines doucument
Although I finally achieved my results, I think this is not the best practice, so I want to ask for help!
Supplementary code
ApiRepository
class ApiRepository #Inject constructor(
private val apiService: ApiService
) : BaseRemoteDataSource() {
fun signIn(account: String, password: String) =
getResult { apiService.signIn(account, password) }
}
BaseRemoteDataSource
abstract class BaseRemoteDataSource {
protected fun <T> getResult(call: suspend () -> Response<T>): LiveData<Resource<T>> =
liveData(Dispatchers.IO) {
try {
val response = call.invoke()
if (response.isSuccessful) {
val body = response.body()
if (body != null) emit(Resource.success(body))
} else {
emit(Resource.error<T>(" ${response.code()} ${response.message()}"))
}
} catch (e: Exception) {
emit(Resource.error<T>(e.message ?: e.toString()))
}
}
}
Or i write like this
fun signIn() {
if (TextUtils.isEmpty(account.value)) {
message.postValue("Please enter your account")
return
}
if (TextUtils.isEmpty(password.value)) {
message.postValue("please enter your password")
return
}
viewModelScope.launch {
repository.signIn(account.value, password.value).onEach {
loginResult.value = it
}
}
}
But I think this is not perfect

Method addObserver must be called on the main thread Exception, While inserting data to room database

I am trying to insert data into the room database using the kotlin coroutine. But I always get an exception java.lang.IllegalStateException: Method addObserver must be called on the main thread
But I don't have an observer in this code, the insert call is called from launch with Dispatchers IO
DocumentDao.kt
#Dao
interface DocumentDao {
#Insert
suspend fun insertDocument(document: Document): Long
}
Repository.kt
class Repository#Inject constructor(val db: MyDB) {
suspend fun demoInsert(
uri: String,
albumId: Long
): Long {
val newDoc = Document(0, albumId, rawUri = uri)
return db.documentDao().insertDocument(newDoc)
}
}
MyViewModel.kt
#HiltViewModel
class MyViewModel#Inject constructor(val repo: Repository) : ViewModel() {
suspend fun demoInsert(
uri: String,
albumId: Long
): Long {
return repo.demoInsert(uri, albumId)
}
}
MyFrag.kt
#AndroidEntryPoint
class MyFrag: Fragment() {
val viewModel: MyViewModel by viewModels()
....
....
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.insert.setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) {
val res = viewModel.demoInsert("test", Random.nextLong(500))
Log.d(TAG, "onViewCreated: $res")
}
}
........
.......
}
}
what is wrong with this code? please help me
I'm not sure about this but you can launch coroutine inside listener with Main Dispatcher and later use withContext inside DB function, to change context.
I was facing the same issue and I solved yhis way :
private fun insertAllItemsInDb(data : List<PostResponse>){
val listPost = data.map { it.toUI() }
val scope = CoroutineScope(Job() + Dispatchers.Main)
scope.launch {
localViewModel.insertAllPosts(listPost)
}
}
ViewModel:
fun insertAllPosts(posts: List<PostItem>) {
viewModelScope.launch {
dbRepository.insertAllPosts(posts)
}
}
Creating view model with:
val viewModel: MyViewModel by viewModels()
Will result in lazy creating. Creation of real object will be performed when you access your object for first time. This happens inside:
lifecycleScope.launch(Dispatchers.IO) {
val res = viewModel.demoInsert("test", Random.nextLong(500))
Log.d(TAG, "onViewCreated: $res")
}
And since implementation of method viewModels<>() looks like this:
#MainThread
public inline fun <reified VM : ViewModel> Fragment.viewModels(
noinline ownerProducer: () -> ViewModelStoreOwner = { this },
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> = createViewModelLazy(VM::class, { ownerProducer().viewModelStore }, factoryProducer)
You are getting
Method addObserver must be called on the main thread
You should be able to fix this with something like this.
lifecycleScope.launch(Dispatchers.IO) {
val res = withContext(Dispatchers.Main + lifecycleScope.coroutineContext){}.demoInsert("test", Random.nextLong(500))
Log.d(TAG, "onViewCreated: $res")
}
MyViewModel.kt
#HiltViewModel
class MyViewModel#Inject constructor(val repo: Repository) : ViewModel() {
suspend fun demoInsert(
uri: String,
albumId: Long
): Long {
viewModelScope.launch {
repo.demoInsert(uri, albumId)
}
}
}
MyFrag.kt
#AndroidEntryPoint
class MyFrag: Fragment() {
val viewModel: MyViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.insert.setOnClickListener {
lifecycleScope.launch(Dispatchers.Main) {
viewModel.demoInsert("test", Random.nextLong(500))
}
}
}
}

Spring Integration Reactor configuration

I'm running an application that process tasks using spring integration.
I'd like to make it process multiple tasks concurrently but any attempt failed so far.
My configuration is:
ReactorConfiguration.java
#Configuration
#EnableAutoConfiguration
public class ReactorConfiguration {
#Bean
Environment reactorEnv() {
return new Environment();
}
#Bean
Reactor createReactor(Environment env) {
return Reactors.reactor()
.env(env)
.dispatcher(Environment.THREAD_POOL)
.get();
}
}
TaskProcessor.java
#MessagingGateway(reactorEnvironment = "reactorEnv")
public interface TaskProcessor {
#Gateway(requestChannel = "routeTaskByType", replyChannel = "")
Promise<Result> processTask(Task task);
}
IntegrationConfiguration.java (simplified)
#Bean
public IntegrationFlow routeFlow() {
return IntegrationFlows.from(MessageChannels.executor("routeTaskByType", Executors.newFixedThreadPool(10)))
.handle(Task.class, (payload, headers) -> {
logger.info("Task submitted!" + payload);
payload.setRunning(true);
//Try-catch
Thread.sleep(999999);
return payload;
})
.route(/*...*/)
.get();
}
My testing code can be simplified like this:
Task task1 = new Task();
Task task2 = new Task();
Promise<Result> resultPromise1 = taskProcessor.processTask(task1).flush();
Promise<Result> resultPromise2 = taskProcessor.processTask(task2).flush();
while( !task1.isRunning() || !task2.isRunning() ){
logger.info("Task2: {}, Task2: {}", task1, task2);
Thread.sleep(1000);
}
logger.info("Yes! your tasks are running in parallel!");
But unfortunately, the last log line, will never get executed!
Any ideas?
Thanks a lot
Well, I've reproduced it just with simple Reactor test-case:
#Test
public void testParallelPromises() throws InterruptedException {
Environment environment = new Environment();
final AtomicBoolean first = new AtomicBoolean(true);
for (int i = 0; i < 10; i++) {
final Promise<String> promise = Promises.task(environment, () -> {
if (!first.getAndSet(false)) {
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
return "foo";
}
);
String result = promise.await(10, TimeUnit.SECONDS);
System.out.println(result);
assertNotNull(result);
}
}
(It is with Reactor-2.0.6).
The problem is because of:
public static <T> Promise<T> task(Environment env, Supplier<T> supplier) {
return task(env, env.getDefaultDispatcher(), supplier);
}
where DefaultDispatcher is RingBufferDispatcher extends SingleThreadDispatcher.
Since the #MessagingGateway is based on the request/reply scenario, we are waiting for reply within that RingBufferDispatcher's Thread. Since you don't return reply there (Thread.sleep(999999);), we aren't able to accept the next event within RingBuffer.
Your dispatcher(Environment.THREAD_POOL) doesn't help here because it doesn't affect the Environment. You should consider to use reactor.dispatchers.default = threadPoolExecutor property. Something like this file: https://github.com/reactor/reactor/blob/2.0.x/reactor-net/src/test/resources/META-INF/reactor/reactor-environment.properties#L46.
And yes: upgrade, please, to the latest Reactor.

Resources