I compile a jar to provide coroutines for java, like 'kotlin-coroutines-java-support.jar'.
It works well until org.springframework.boot.context.properties.bind.Binder
used.
I use binder like this:
//custom properties
Map<String, Object> map = new LinkedHashMap<>();
map.put("demo.get", 1);
map.put("demo.list", "1,2,3");
map.put("demo.map.m1", true);
map.put("demo.map.m2", false);
map.put("demo.el", "el,${demo.get}");
StandardEnvironment env = new StandardEnvironment();
//remove default propertySources
//......
//add new
OriginTrackedMapPropertySource propertySource = new OriginTrackedMapPropertySource("newEnv", map, true);
env.getPropertySources().addFirst(propertySource);
//class to bind
@Data
public static class DemoBinderToUse {
private int get;
private String el;
private List<Integer> list;
private Map<String, Boolean> map;
}
//finally run in coroutines concurrently
Binder.get(env).bindOrCreate("demo", DemoBinderToUse.class);
List's value is wrong sometimes
Comment From: philwebb
I'm not familiar enough with Kotlin coroutines to be able to help from the description alone. Can you please provide a sample application that we can run and debug?
Comment From: Linyuzai
Kotlin code maybe enough?
I found Binder in multi-thread has the same problem with all parameters are new instance.
package com.test.demo
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.bind.Binder
import org.springframework.boot.env.OriginTrackedMapPropertySource
import org.springframework.boot.runApplication
import org.springframework.core.env.Environment
import org.springframework.core.env.StandardEnvironment
@SpringBootApplication
open class DemoKotlinApplication : ApplicationRunner {
override fun run(args: ApplicationArguments?) {
//runNormal()
runMultiThread()
//runCoroutines()
}
//Work well
private fun runNormal() {
for (index in 0..300) {
val binderDemo = Binder.get(getEnv()).bindOrCreate("demo", BinderDemo::class.java)
println(binderDemo)
}
}
//List's value is wrong sometimes
private fun runMultiThread() {
for (index in 0..300) {
Thread {
val binderDemo = Binder.get(getEnv()).bindOrCreate("demo", BinderDemo::class.java)
println(binderDemo)
}.start()
}
}
//List's value is wrong sometimes
private fun runCoroutines() {
runBlocking {
val deferredList = mutableListOf<Deferred<BinderDemo>>()
for (index in 0..300) {
val deferred = async(Dispatchers.IO) {
Binder.get(getEnv()).bindOrCreate("demo", BinderDemo::class.java)
}
deferredList.add(deferred)
}
deferredList.forEach {
println(it.await())
}
}
}
//New instance for all
private fun getEnv(): Environment = StandardEnvironment().apply {
val map = mapOf(
"demo.get" to 1,
"demo.list" to "1,2,3",
"demo.map.m1" to true,
"demo.map.m2" to false,
"demo.el" to "el,\${demo.get}"
)
val propertySource = OriginTrackedMapPropertySource(
"newEnv", map, true
)
propertySources.addFirst(propertySource)
}
}
data class BinderDemo(
var get: Int? = null,
var list: List<Int>? = null,
var map: Map<String, Boolean>? = null,
var el: String? = null
)
fun main(args: Array<String>) {
runApplication<DemoKotlinApplication>(*args)
}
Comment From: Linyuzai
I got an exception when change the 'map'.
val map = mapOf(
"demo.get" to 1,
"demo.list[0]" to "1",
"demo.list[1]" to "2",
"demo.list[2]" to "3",
"demo.map.m1" to true,
"demo.map.m2" to false,
"demo.el" to "el,\${demo.get}"
)
//coroutines
Binding to target [Bindable@4aac85fa type = java.util.List<java.lang.Integer>, value = 'provided', annotations = array<Annotation>[[empty]]] failed:
Property: demo.list[1]
Value: 2
Origin: "demo.list[1]" from property source "newEnv"
Reason: The elements [demo.list[1],demo.list[2]] were left unbound.
Property: demo.list[2]
Value: 3
Origin: "demo.list[2]" from property source "newEnv"
Reason: The elements [demo.list[1],demo.list[2]] were left unbound.
//multi-thread
Caused by: org.springframework.boot.context.properties.bind.UnboundConfigurationPropertiesException: The elements [demo.list[0],demo.list[1],demo.list[2]] were left unbound.
at org.springframework.boot.context.properties.bind.IndexedElementsBinder.assertNoUnboundChildren(IndexedElementsBinder.java:137)
at org.springframework.boot.context.properties.bind.IndexedElementsBinder.bindIndexed(IndexedElementsBinder.java:114)
at org.springframework.boot.context.properties.bind.IndexedElementsBinder.bindIndexed(IndexedElementsBinder.java:87)
at org.springframework.boot.context.properties.bind.IndexedElementsBinder.bindIndexed(IndexedElementsBinder.java:71)
at org.springframework.boot.context.properties.bind.CollectionBinder.bindAggregate(CollectionBinder.java:49)
at org.springframework.boot.context.properties.bind.AggregateBinder.bind(AggregateBinder.java:56)
at org.springframework.boot.context.properties.bind.Binder.lambda$bindAggregate$3(Binder.java:414)
at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:571)
at org.springframework.boot.context.properties.bind.Binder$Context.access$100(Binder.java:512)
at org.springframework.boot.context.properties.bind.Binder.bindAggregate(Binder.java:414)
at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:375)
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:319)
... 17 more
Comment From: philwebb
Thanks for the additional info. I've managed to replicate this in a pure Java application. I think the root cause is that our TypeConverterConversionService
is using the same SimpleTypeConverter
from multiple threads. The javadoc for that class states:
Due to its reliance on
PropertyEditors
SimpleTypeConverter
is not thread-safe. Use a separate instance for each thread.
We somehow missed that warning.
Comment From: philwebb
Fixed in 87dbda2339815f983e423b26154815d89a7fea2e