Search code examples
kotlintestingquarkusoptaplannertimefold

ClassCastException during QuarkusTest with OptaPlanner and Kotlin


I'm using OptaPlanner 9.37.0.Final with quarkus 3.1.3.Final and Kotlin 1.8.21 to solve a VRP.

When I run quarkusDev, my current setup runs just fine, but when I run a @QuarkusTest, I get the following ClassCastException, hinting that something with class loading might be broken.

java.lang.ClassCastException: class com.cargonexx.vehiclerouting.domain.LoadJob$OptaPlanner$MemberAccessor$Field$arrivalTime cannot be cast to class org.optaplanner.core.impl.domain.common.accessor.MemberAccessor (com.cargonexx.vehiclerouting.domain.LoadJob$OptaPlanner$MemberAccessor$Field$arrivalTime is in unnamed module of loader 'OptaPlanner Gizmo SolutionCloner ClassLoader' @611d16c9; org.optaplanner.core.impl.domain.common.accessor.MemberAccessor is in unnamed module of loader io.quarkus.bootstrap.classloading.QuarkusClassLoader @319988b0)
    at org.optaplanner.core.impl.domain.common.accessor.gizmo.GizmoMemberAccessorImplementor.createInstance(GizmoMemberAccessorImplementor.java:118)
    at org.optaplanner.core.impl.domain.common.accessor.gizmo.GizmoMemberAccessorImplementor.createAccessorFor(GizmoMemberAccessorImplementor.java:112)
    at org.optaplanner.core.impl.domain.common.accessor.gizmo.GizmoMemberAccessorFactory.buildGizmoMemberAccessor(GizmoMemberAccessorFactory.java:40)
    at org.optaplanner.core.impl.domain.common.accessor.MemberAccessorFactory.buildMemberAccessor(MemberAccessorFactory.java:38)
    at org.optaplanner.core.impl.domain.common.accessor.MemberAccessorFactory.lambda$buildAndCacheMemberAccessor$0(MemberAccessorFactory.java:125)
    at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1708)
    at org.optaplanner.core.impl.domain.common.accessor.MemberAccessorFactory.buildAndCacheMemberAccessor(MemberAccessorFactory.java:124)
    at org.optaplanner.core.impl.domain.entity.descriptor.EntityDescriptor.processPlanningVariableAnnotation(EntityDescriptor.java:239)
    at org.optaplanner.core.impl.domain.entity.descriptor.EntityDescriptor.processAnnotations(EntityDescriptor.java:154)
    at org.optaplanner.core.impl.domain.solution.descriptor.SolutionDescriptor.buildSolutionDescriptor(SolutionDescriptor.java:106)
    at org.optaplanner.core.impl.solver.DefaultSolverFactory.buildSolutionDescriptor(DefaultSolverFactory.java:147)
    at org.optaplanner.core.impl.solver.DefaultSolverFactory.<init>(DefaultSolverFactory.java:69)
    at org.optaplanner.core.api.solver.SolverFactory.create(SolverFactory.java:106)
    at org.optaplanner.quarkus.bean.DefaultOptaPlannerBeanProvider.solverFactory(DefaultOptaPlannerBeanProvider.java:46)
    at org.optaplanner.quarkus.bean.DefaultOptaPlannerBeanProvider_ProducerMethod_solverFactory_3b1fa4ff0a3de7781ba3e1239701086bba97ef14_Bean.doCreate(Unknown Source)
    at org.optaplanner.quarkus.bean.DefaultOptaPlannerBeanProvider_ProducerMethod_solverFactory_3b1fa4ff0a3de7781ba3e1239701086bba97ef14_Bean.create(Unknown Source)
    at org.optaplanner.quarkus.bean.DefaultOptaPlannerBeanProvider_ProducerMethod_solverFactory_3b1fa4ff0a3de7781ba3e1239701086bba97ef14_Bean.get(Unknown Source)
    at org.optaplanner.quarkus.bean.DefaultOptaPlannerBeanProvider_ProducerMethod_solverFactory_3b1fa4ff0a3de7781ba3e1239701086bba97ef14_Bean.get(Unknown Source)
    at org.optaplanner.quarkus.bean.DefaultOptaPlannerBeanProvider_ProducerMethod_solverManager_d6636211e93ca3985f0495d972987bdadf803f37_Bean.doCreate(Unknown Source)
    at org.optaplanner.quarkus.bean.DefaultOptaPlannerBeanProvider_ProducerMethod_solverManager_d6636211e93ca3985f0495d972987bdadf803f37_Bean.create(Unknown Source)
    at org.optaplanner.quarkus.bean.DefaultOptaPlannerBeanProvider_ProducerMethod_solverManager_d6636211e93ca3985f0495d972987bdadf803f37_Bean.get(Unknown Source)
    at org.optaplanner.quarkus.bean.DefaultOptaPlannerBeanProvider_ProducerMethod_solverManager_d6636211e93ca3985f0495d972987bdadf803f37_Bean.get(Unknown Source)
    at com.cargonexx.vehiclerouting.rest.SolverResource_Bean.doCreate(Unknown Source)
    at com.cargonexx.vehiclerouting.rest.SolverResource_Bean.create(Unknown Source)
    at com.cargonexx.vehiclerouting.rest.SolverResource_Bean.create(Unknown Source)
    at io.quarkus.arc.impl.AbstractSharedContext.createInstanceHandle(AbstractSharedContext.java:113)
    at io.quarkus.arc.impl.AbstractSharedContext$1.get(AbstractSharedContext.java:37)
    at io.quarkus.arc.impl.AbstractSharedContext$1.get(AbstractSharedContext.java:34)
    at io.quarkus.arc.impl.LazyValue.get(LazyValue.java:26)
    at io.quarkus.arc.impl.ComputingCache.computeIfAbsent(ComputingCache.java:69)
    at io.quarkus.arc.impl.AbstractSharedContext.get(AbstractSharedContext.java:34)
    at com.cargonexx.vehiclerouting.rest.SolverResource_Bean.get(Unknown Source)
    at com.cargonexx.vehiclerouting.rest.SolverResource_Bean.get(Unknown Source)
    at io.quarkus.arc.impl.ArcContainerImpl.beanInstanceHandle(ArcContainerImpl.java:499)
    at io.quarkus.arc.impl.ArcContainerImpl.beanInstanceHandle(ArcContainerImpl.java:479)
    at io.quarkus.arc.impl.ArcContainerImpl.beanInstanceHandle(ArcContainerImpl.java:512)
    at io.quarkus.arc.impl.ArcContainerImpl$2.get(ArcContainerImpl.java:287)
    at io.quarkus.arc.impl.ArcContainerImpl$2.get(ArcContainerImpl.java:284)
    at io.quarkus.arc.runtime.BeanContainerImpl$1.create(BeanContainerImpl.java:46)
    at io.quarkus.resteasy.reactive.common.runtime.ArcBeanFactory.createInstance(ArcBeanFactory.java:27)
    at org.jboss.resteasy.reactive.server.handlers.InstanceHandler.handle(InstanceHandler.java:26)
    at io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.invokeHandler(QuarkusResteasyReactiveRequestContext.java:139)
    at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:145)
    at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:576)
    at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538)
    at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)
    at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.base/java.lang.Thread.run(Thread.java:833)

I have a PlanningEntity along the lines of:

@PlanningEntity
class LoadJob {
    @PlanningId lateinit var id: String

    @InverseRelationShadowVariable(sourceVariableName = "tour") //
    var vehicle: Vehicle? = null

    @PreviousElementShadowVariable(sourceVariableName = "tour") //
    var previousLoadJob: LoadJob? = null

    @NextElementShadowVariable(sourceVariableName = "tour") //
    var nextLoadJob: LoadJob? = null

    @JvmField
    @ShadowVariable(
        variableListenerClass = ArrivalTimeUpdateListener::class,
        sourceVariableName = "vehicle"
    )
    @ShadowVariable(
        variableListenerClass = ArrivalTimeUpdateListener::class,
        sourceVariableName = "previousLoadJob"
    )
    var arrivalTime: LocalDateTime? = null
}

which is used in the PlanningListVariable in the Vehicle class:

@PlanningEntity
class Vehicle {
    @PlanningId lateinit var planningId: String

    @PlanningListVariable lateinit var tour: MutableList<LoadJob>
}

Both are part of the PlanningSolution

@PlanningSolution
class TourPlan {
    lateinit var id: String

    @PlanningEntityCollectionProperty //
    lateinit var vehicles: List<Vehicle>

    @ValueRangeProvider
    @ProblemFactCollectionProperty //
    lateinit var loadJobs: List<LoadJob>

    @PlanningScore //
    var score: HardMediumSoftLongScore? = null

    @ConstraintConfigurationProvider
    var constraintConfiguration: TourPlanConstraintConfiguration = TourPlanConstraintConfiguration()

    constructor() // required for optaplanner
}

Has anybody else run into an issue like that? Are there any known workarounds or fixes for this issue?

I have been working for almost a month with this setup now, but in the long run it might not be feasible to just don't run any QuarkusTests as they are broken with the current setup.

Notes

  • When using REFLECTION instead of GIZMO for optaplanner domain access, I don't have this issue. However, since reflection is slower, as the optaplanner documentation also mentions, I'd like to be able to use GIZMO for the performance gain and not have to use diverging configurations for tests and regular builds
  • I'm aware of the fact that quarkus loads classes differently during tests. Unfortunately, I haven't had the time yet to look into quarkus class loading in depth (or at least further than the documentation goes)
  • I have stumbled upon https://github.com/quarkusio/quarkus/issues/34099 and https://github.com/quarkusio/quarkus/pull/34681 which may be related to the root cause of this exception, but that's more a guess than anything I could say for sure.
  • I haven't tried if the problem also exists with timefold instead of optaplanner yet, but I'd be willing to invest the time and try to extract a reproducer with timefold in case that's of interest to the timefold team

Solution

  • I believe that this issue was fixed in Timefold 1.0.0. (The issue description helpfully also contains a workaround, which you already found yourself.) Please check that version and, if your case is still not fixed, kindly file an issue, ideally with a reproducer.

    Whether OptaPlanner will fix this issue and when, I do not know.