Optaplanner 多线程异常:"The externalObject ... has no known workingObject"

Optaplanner multithreading exception: "The externalObject ... has no known workingObject"

TLDR: 在Optaplanner中启用多线程应该是一行,但会抛出异常

我正在尝试使用电子游戏中的可配置负载来优化伤害计算。就上下文而言,玩家可以为他们拥有的每件物品配置“重铸”,这会增加力量或暴击伤害的统计数据。最终的伤害计算必须最大化为强度和暴击伤害的组合。出于这个原因,我正在使用 Optaplanner 来为物品分配重铸。

但是,在 XML 求解器配置中通过 <moveThreadCount>AUTO</moveThreadCount> 启用多线程会引发异常(在单线程执行中不会发生):

Caused by: java.lang.IllegalStateException: The externalObject (ReforgeProblemFact(id=897f4bab-80e0-4eb9-a1d7-974f7cddfd9e, name=Fierce, rarity=COMMON, strength=4, critDamage=0)) with planningId ((class net.javaman.optaplanner_reproducible.ReforgeProblemFact,897f4bab-80e0-4eb9-a1d7-974f7cddfd9e)) has no known workingObject (null).
Maybe the workingObject was never added because the planning solution doesn't have a @ProblemFactCollectionProperty annotation on a member with instances of the externalObject's class (class net.javaman.optaplanner_reproducible.ReforgeProblemFact).

This SO question 类似,但其答案并未修复此示例中的异常。

我在下面的代码中删除了包和导入。 Full GitHub repository link

项目结构:

src/main/
    kotlin/
        net/javaman/optaplanner_reproducible/
            Rarity.kt
            ReforgeProblemFact.kt        
            ItemPlanningEntity.kt
            ReforgePlanningSolution.kt
            MaximizeDamageConstraintProvider.kt
            Main.kt
    resources/
        reforgeSolverConfig.xml

Rarity.kt:

enum class Rarity {
    COMMON,
    RARE,
    LEGENDARY
}

ReforgeProblemFact.kt:

data class ReforgeProblemFact(
    @PlanningId
    val id: UUID,
    val name: String,
    val rarity: Rarity,
    val strength: Int,
    val critDamage: Int
)

ItemPlanningEntity.kt:

@PlanningEntity
data class ItemPlanningEntity @JvmOverloads constructor(
    @PlanningId
    val id: UUID? = null,
    val rarity: Rarity? = null,
    @PlanningVariable(valueRangeProviderRefs = ["reforgeRange"])
    var reforge: ReforgeProblemFact? = null,
    @ValueRangeProvider(id = "reforgeRange")
    @ProblemFactCollectionProperty
    val availableReforges: List<ReforgeProblemFact>? = null
)

ReforgePlanningSolution.kt:

@PlanningSolution
class ReforgePlanningSolution @JvmOverloads constructor(
    @PlanningEntityCollectionProperty
    val availableItems: List<ItemPlanningEntity>? = null,
    @PlanningScore
    val score: HardSoftScore? = null,
)

MaximizeDamageConstraintProvider.kt:

class MaximizeDamageConstraintProvider : ConstraintProvider {
    override fun defineConstraints(factory: ConstraintFactory): Array<Constraint> = arrayOf(maximizeDamage(factory))

    // This approach does not take full advantage of incremental solving,
    // but it is necessary to compute strength and critDamage together in the same equation
    private fun maximizeDamage(factory: ConstraintFactory) = factory.from(ItemPlanningEntity::class.java)
        .map(ItemPlanningEntity::reforge) // Get each item's reforge
        .groupBy({ 0 }, toList { reforge: ReforgeProblemFact? -> reforge }) // Compile into one List<ReforgeProblemFact>
        .reward("damage", HardSoftScore.ONE_SOFT) { _, reforges: List<ReforgeProblemFact?> ->
            val strengthSum = reforges.stream().collect(Collectors.summingInt { reforge -> reforge?.strength ?: 0 })
            val critDamageSum = reforges.stream().collect(Collectors.summingInt { reforge -> reforge?.critDamage ?: 0 })
            (100 + strengthSum) * (100 + critDamageSum)
        }
}

Main.kt:

class Main {
    companion object {
        private val allReforges = listOf(
            ReforgeProblemFact(UUID.randomUUID(), "Clean", Rarity.COMMON, 0, 3),
            ReforgeProblemFact(UUID.randomUUID(), "Fierce", Rarity.COMMON, 4, 0),
            ReforgeProblemFact(UUID.randomUUID(), "Shiny", Rarity.COMMON, 2, 1),
            ReforgeProblemFact(UUID.randomUUID(), "Clean", Rarity.RARE, 1, 3),
            ReforgeProblemFact(UUID.randomUUID(), "Fierce", Rarity.RARE, 5, 0),
            ReforgeProblemFact(UUID.randomUUID(), "Shiny", Rarity.RARE, 3, 2),
            ReforgeProblemFact(UUID.randomUUID(), "Clean", Rarity.LEGENDARY, 1, 4),
            ReforgeProblemFact(UUID.randomUUID(), "Fierce", Rarity.LEGENDARY, 6, 0),
            ReforgeProblemFact(UUID.randomUUID(), "Shiny", Rarity.LEGENDARY, 4, 2),
        )
        private val solverManager: SolverManager<ReforgePlanningSolution, UUID> = SolverManager.create(
            SolverConfig.createFromXmlResource("reforgeSolverConfig.xml")
        )

        @JvmStatic
        fun main(args: Array<String>) {
            val availableItems = generateAvailableItems(
                mapOf(
                    Rarity.COMMON to 4,
                    Rarity.RARE to 3,
                    Rarity.LEGENDARY to 1
                )
            )
            val solverJob = solverManager.solve(UUID.randomUUID(), ReforgePlanningSolution(availableItems))
            val solution = solverJob.finalBestSolution
            solution.availableItems!!
                .map { it.reforge!! }
                .forEach { println(it.rarity.name + " " + it.name) }
        }

        private fun generateAvailableItems(itemCounts: Map<Rarity, Int>): MutableList<ItemPlanningEntity> {
            val availableItems = mutableListOf<ItemPlanningEntity>()
            for (itemCount in itemCounts) {
                for (count in 0 until itemCount.value) {
                    val rarity = itemCount.key
                    availableItems.add(
                        ItemPlanningEntity(
                            UUID.randomUUID(),
                            rarity,
                            null,
                            allReforges.filter { it.rarity == rarity }
                        )
                    )
                }
            }
            return availableItems
        }
    }
}

The externalObject (ReforgeProblemFact(id=897f...)) with planningId ((class ReforgeProblemFact,897f...)) has no known workingObject (null).

planningId ((class ReforgeProblemFact 没有意义,因为 planningId class 是模型中的 UUID。 查看PlanningIdLookUpStrategy第71行的代码,报错信息正确。在该行上放置一个断点,然后查看 planningId 变量的类型 class。应该是UUID。

我重新访问了 the similar SO question。在尝试了几个不同版本的答案后,它终于奏效了。这里有一个比另一个更详细的解释post:

每个 PlanningEntityProblemFactCollectionProperty 必须是 PlanningSolution 中主要 ProblemFactCollectionProperty 的一部分。这意味着实体和解决方案都应该定义它们的问题事实。以下是为我修复的问题:

保持 ItemPlanningEntity.kt 不变。

在 ReforgePlanningSolution.kt 中包含全局 ProblemFactCollectionProperty:

@PlanningSolution
class ReforgePlanningSolution @JvmOverloads constructor(
    @PlanningEntityCollectionProperty
    val availableItems: List<ItemPlanningEntity>? = null,
    @ProblemFactCollectionProperty
    val allReforges: List<ReforgeProblemFact>? = null,
    @PlanningScore
    val score: HardSoftScore? = null
)

实例化Main.kt中的解决方案时定义全局集合:

val solverJob = solverManager.solve(UUID.randomUUID(), ReforgePlanningSolution(availableItems, allReforges))

Upgrade OptaPlanner,例如最近发布的 8.12.0.Final,以获得像这样的有用错误消息:

Caused by: java.lang.IllegalStateException: The externalObject (2018-10-01T10:15-12:15) with planningId ((class org.optaplanner.examples.conferencescheduling.domain.Timeslot, 0)) has no known workingObject (null).
Maybe the workingObject was never added because the planning solution doesn't have a @ProblemFactCollectionProperty annotation on a member with instances of the externalObject's class (class org.optaplanner.examples.conferencescheduling.domain.Timeslot).
    at org.optaplanner.core.impl.domain.lookup.PlanningIdLookUpStrategy.lookUpWorkingObject(PlanningIdLookUpStrategy.java:76)

有一条错误消息,其中有一条“可能”行直接指向另一个答案中显示的解决方案。