Search code examples
javaclasscastexceptionoptaplanner

ClassCastException in OptaPlanner due to a groupBy operation


I'm creating a school schedule generator and I'm getting an exception from one of my constraints which checks that all students have lunch breaks every day.

I'm using the constraint stream Java API. My constraint compiles (obviously) and looks like this:

public Constraint studentsShouldHaveLunchEveryDay(ConstraintFactory cf) {
    return cf.from(RecurringLecture.class)
            .filter(RecurringLecture::isScheduled)                     // [Lecture]...
            .groupBy(l -> l, l -> l.getCourseRound().getStudents())    // [Lecture, Set<Student>]...
            .flattenLast(students -> students)                         // [Lecture, Student]...
            .groupBy((l, s) -> s,                                      // [Student, Map<Day, Set<Lecture>>]...
                    ConstraintCollectors.toMap(
                            (l, s) -> l.getStartTimeslot().getDay(),
                            (l, s) -> l))
            //.flattenLast(Map::values)                                // [Student, Set<Lecture>]...
            //.filter((student, dailyLectures) -> !LunchBreakAnalyzer.hasLunchBreak(dailyLectures))
            .penalize(StudentLunchProblem.class.getName(), HardSoftScore.ONE_HARD);
}

I have temporarily commented out the flattenLast and filter for debugging reasons, and the problem still reproduces. If I comment out the last groupBy though, the issue does not seem to reproduce.

For some reason, the framework tries to cast a Student (ImmutableStudent to be precise) to an Object[].

The line in the framework that throws is in ArrayElementReader and looks like this:

public Object getValue(InternalWorkingMemory workingMemory,
                       Object object) {
    Object[] array = (Object[]) this.arrayReadAccessor.getValue( workingMemory,
                                                                 object );
    return array[this.index];
}

The full exception looks as follows:

java.util.concurrent.ExecutionException: java.lang.IllegalStateException: The move thread with moveThreadIndex (1) has thrown an exception. Relayed here in the parent thread.
    at java.base/java.util.concurrent.FutureTask.report(FutureTask.java:122)
    at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:191)
    at java.desktop/javax.swing.SwingWorker.get(SwingWorker.java:613)
    at vngschedules.ui.automation.AutomationPanel$1$1.done(AutomationPanel.java:44)
    at java.desktop/javax.swing.SwingWorker$5.run(SwingWorker.java:750)
    at java.desktop/javax.swing.SwingWorker$DoSubmitAccumulativeRunnable.run(SwingWorker.java:847)
    at java.desktop/sun.swing.AccumulativeRunnable.run(AccumulativeRunnable.java:112)
    at java.desktop/javax.swing.SwingWorker$DoSubmitAccumulativeRunnable.actionPerformed(SwingWorker.java:857)
    at java.desktop/javax.swing.Timer.fireActionPerformed(Timer.java:317)
    at java.desktop/javax.swing.Timer$DoPostEvent.run(Timer.java:249)
    at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:313)
    at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:770)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:721)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:715)
    at java.base/java.security.AccessController.doPrivileged(Native Method)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
    at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:740)
    at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
    at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)
Caused by: java.lang.IllegalStateException: The move thread with moveThreadIndex (1) has thrown an exception. Relayed here in the parent thread.
    at org.optaplanner.core.impl.heuristic.thread.OrderByMoveIndexBlockingQueue.take(OrderByMoveIndexBlockingQueue.java:147)
    at org.optaplanner.core.impl.localsearch.decider.MultiThreadedLocalSearchDecider.forageResult(MultiThreadedLocalSearchDecider.java:189)
    at org.optaplanner.core.impl.localsearch.decider.MultiThreadedLocalSearchDecider.decideNextStep(MultiThreadedLocalSearchDecider.java:160)
    at org.optaplanner.core.impl.localsearch.DefaultLocalSearchPhase.solve(DefaultLocalSearchPhase.java:95)
    at org.optaplanner.core.impl.solver.AbstractSolver.runPhases(AbstractSolver.java:99)
    at org.optaplanner.core.impl.solver.DefaultSolver.solve(DefaultSolver.java:192)
Caused by: java.lang.IllegalStateException: The move thread with moveThreadIndex (1) has thrown an exception. Relayed here in the parent thread.

    at vngschedules.ui.automation.SolverSwingWorker.doInBackground(SolverSwingWorker.java:29)
    at vngschedules.ui.automation.SolverSwingWorker.doInBackground(SolverSwingWorker.java:10)
    at java.desktop/javax.swing.SwingWorker$1.call(SwingWorker.java:304)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at java.desktop/javax.swing.SwingWorker.run(SwingWorker.java:343)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.ClassCastException: class vngschedules.schedule.ImmutableStudent cannot be cast to class [Ljava.lang.Object; (vngschedules.schedule.ImmutableStudent is in unnamed module of loader 'app'; [Ljava.lang.Object; is in module java.base of loader 'bootstrap')
    at org.drools.core.base.extractors.ArrayElementReader.getValue(ArrayElementReader.java:162)
    at org.drools.core.base.extractors.ArrayElementReader.getValue(ArrayElementReader.java:279)
    at org.drools.modelcompiler.KiePackagesBuilder.lambda$getBindingFunction$66de2761$1(KiePackagesBuilder.java:794)
    at org.drools.modelcompiler.constraints.LambdaReadAccessor.getValue(LambdaReadAccessor.java:43)
    at org.drools.core.rule.Declaration.getValue(Declaration.java:257)
    at org.optaplanner.core.impl.score.stream.drools.common.AbstractAccumulator.extractValue(AbstractAccumulator.java:42)
Caused by: java.lang.ClassCastException: class vngschedules.schedule.ImmutableStudent cannot be cast to class [Ljava.lang.Object; (vngschedules.schedule.ImmutableStudent is in unnamed module of loader 'app'; [Ljava.lang.Object; is in module java.base of loader 'bootstrap')

    at org.optaplanner.core.impl.score.stream.drools.common.BiAccumulator.accumulate(BiAccumulator.java:55)
    at org.drools.core.rule.SingleAccumulate.accumulate(SingleAccumulate.java:96)
    at org.drools.modelcompiler.constraints.LambdaGroupByAccumulate.accumulate(LambdaGroupByAccumulate.java:121)
    at org.drools.modelcompiler.constraints.LambdaGroupByAccumulate.accumulate(LambdaGroupByAccumulate.java:114)
    at org.drools.core.phreak.PhreakAccumulateNode.addMatch(PhreakAccumulateNode.java:736)
    at org.drools.core.phreak.PhreakAccumulateNode.doRightInserts(PhreakAccumulateNode.java:253)
    at org.drools.core.phreak.PhreakAccumulateNode.doNode(PhreakAccumulateNode.java:99)
    at org.drools.core.phreak.RuleNetworkEvaluator.switchOnDoBetaNode(RuleNetworkEvaluator.java:586)
    at org.drools.core.phreak.RuleNetworkEvaluator.evalBetaNode(RuleNetworkEvaluator.java:555)
    at org.drools.core.phreak.RuleNetworkEvaluator.evalNode(RuleNetworkEvaluator.java:382)
    at org.drools.core.phreak.RuleNetworkEvaluator.innerEval(RuleNetworkEvaluator.java:342)
    at org.drools.core.phreak.RuleNetworkEvaluator.evalStackEntry(RuleNetworkEvaluator.java:240)
    at org.drools.core.phreak.RuleNetworkEvaluator.outerEval(RuleNetworkEvaluator.java:183)
    at org.drools.core.phreak.RuleNetworkEvaluator.evaluateNetwork(RuleNetworkEvaluator.java:136)
    at org.drools.core.phreak.RuleExecutor.reEvaluateNetwork(RuleExecutor.java:235)
    at org.drools.core.phreak.RuleExecutor.evaluateNetworkAndFire(RuleExecutor.java:91)
    at org.drools.core.concurrent.AbstractRuleEvaluator.internalEvaluateAndFire(AbstractRuleEvaluator.java:33)
    at org.drools.core.concurrent.SequentialRuleEvaluator.evaluateAndFire(SequentialRuleEvaluator.java:43)
    at org.drools.core.common.DefaultAgenda.fireLoop(DefaultAgenda.java:869)
    at org.drools.core.common.DefaultAgenda.internalFireAllRules(DefaultAgenda.java:816)
    at org.drools.core.common.DefaultAgenda.fireAllRules(DefaultAgenda.java:808)
    at org.drools.core.impl.StatefulKnowledgeSessionImpl.internalFireAllRules(StatefulKnowledgeSessionImpl.java:1343)
    at org.drools.core.impl.StatefulKnowledgeSessionImpl.fireAllRules(StatefulKnowledgeSessionImpl.java:1334)
    at org.drools.core.impl.StatefulKnowledgeSessionImpl.fireAllRules(StatefulKnowledgeSessionImpl.java:1326)
    at org.optaplanner.core.impl.score.director.stream.DroolsConstraintStreamScoreDirector.calculateScore(DroolsConstraintStreamScoreDirector.java:90)
    at org.optaplanner.core.impl.score.director.AbstractScoreDirector.doAndProcessMove(AbstractScoreDirector.java:220)
    at org.optaplanner.core.impl.heuristic.thread.MoveThreadRunner.run(MoveThreadRunner.java:147)
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    ... 3 more

I have commented out all other constraint streams in my program.

Questions

  1. Does this look like it's a fault in my code, or is it an issue with OptaPlanner?
  2. If it's in my code, are there any tricks to debug this? I suspect that my constraint is compiled into Drools-something. Is there a way to skip this compilation, so that the exception can be traced back to my code? (As of now, no lines in the exception trace back to my code.)

Happy to provide more types if needed, but RecurringLecture is quite simple really, and Student is an Immutables interface compiled into an ImmutableStudent.

I'm using OptaPlanner 8.12.0.Final and Java 11.

Update

I tried rewriting the constraint stream as follows:

public Constraint studentsShouldHaveLunchEveryDay(ConstraintFactory cf) {
    return cf.from(RecurringLecture.class)
            .filter(RecurringLecture::isScheduled)                     // [Lecture]...
            .groupBy(l -> l, l -> l.getCourseRound().getStudents())    // [Lecture, Set<Student>]...
            .flattenLast(students -> students)                         // [Lecture, Student]...
            .groupBy((l, s) -> ImmutableStudentAndDay.of(s, l.getStartTimeslot().getDay()),
                    ConstraintCollectors.toSet((l, s) -> l))
            .filter((student, dailyLectures) -> !LunchBreakAnalyzer.hasLunchBreak(dailyLectures))
            .penalize(StudentLunchProblem.class.getName(), HardSoftScore.ONE_HARD);
}

This avoids the toMap collector. I still run into the exact same issue however.


Solution

  • Generally speaking, if your constraint compiles and the code still throws ClassCastException at runtime, you should expect the bug to be on the OptaPlanner side. Unless you are willing to look at the Drools executable model, there is nothing for you to debug there.

    I recommend refactoring the constraint to look like this:

    public Constraint studentsShouldHaveLunchEveryDay(ConstraintFactory cf) {
        return cf.from(RecurringLecture.class)
            .filter(RecurringLecture::isScheduled)
            .join(Student.class,
                  Joiners.filtering((l, s) -> l.getCourseRound().getStudents().contains(s)))
            .groupBy((l, s) -> ImmutableStudentAndDay.of(s, l.getStartTimeslot().getDay()),
                    ConstraintCollectors.toSet((l, s) -> l))
            .filter((student, dailyLectures) -> !LunchBreakAnalyzer.hasLunchBreak(dailyLectures))
            .penalize(StudentLunchProblem.class.getName(), HardSoftScore.ONE_HARD);
    }
    

    I expect this to perform much better, possibly also getting rid of the exception. Regardless, the exception is still a problem and I will investigate it further if you provide your simplified executable reproducer.