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

SpringBoot Binder conversion fails intermittently when called from multiple threads

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