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

Time:10-29

TLDR: Enabling multithreading in Optaplanner is supposed to be a one-liner, but it throws an exception

I'm trying to optimize a damage calculation using configurable loadouts in a videogame. For context, a player may configure each item they own with a "reforge," which adds stats for strength or crit damage. The final damage calculation must be maximized as a combination of strength and crit damage. For this reason, I am using Optaplanner to allocate reforges to items.

However, enabling multithreading through <moveThreadCount>AUTO</moveThreadCount> in the XML solver config throws an exception (that does not occur in single-threaded execution):

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 is similar, but its answer doesn't fix the exception in this example.

I removed packages and imports in the code below. Full GitHub repository link

Project structure:

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
        }
    }
}

CodePudding user response:

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

That planningId ((class ReforgeProblemFact makes no sense, as the planningId class is UUID in your model. Looking at the code of PlanningIdLookUpStrategy line 71, the error message is correct. Put a breakpoint on that line and look at what kind of class the planningId variable is. It should be a UUID.

CodePudding user response:

I revisited the similar SO question. After trying a few different versions of the answer, it finally worked. Here's a more detailed explanation than the other post:

Each PlanningEntity's ProblemFactCollectionProperty must be part of the main ProblemFactCollectionProperty in the PlanningSolution. This means that both the entity and solution should have their problem facts defined. Here's what fixed it for me:

Keep ItemPlanningEntity.kt the same.

Include a global ProblemFactCollectionProperty in ReforgePlanningSolution.kt:

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

Define the global collection when instantiating the solution in Main.kt:

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