'Couldn't retrieve job because a required class was not found, even though previous triggers fired successfully

I have a CRON trigger defined with Quartz, which successfully fires several times and ends up in error state after some cycles, with the following message (class names and package names have been redacted):

org.quartz.JobPersistenceException: Couldn't retrieve job because a required class was not found: xxx.xxx.xxx.MyQuartzJob
    at org.quartz.impl.jdbcjobstore.JobStoreSupport.retrieveJob(JobStoreSupport.java:1393) [quartz-2.3.2.jar!/:na]
    at org.quartz.impl.jdbcjobstore.JobStoreSupport.acquireNextTrigger(JobStoreSupport.java:2864) [quartz-2.3.2.jar!/:na]
    at org.quartz.impl.jdbcjobstore.JobStoreSupport$41.execute(JobStoreSupport.java:2805) [quartz-2.3.2.jar!/:na]
    at org.quartz.impl.jdbcjobstore.JobStoreSupport$41.execute(JobStoreSupport.java:2803) [quartz-2.3.2.jar!/:na]
    at org.quartz.impl.jdbcjobstore.JobStoreSupport.executeInNonManagedTXLock(JobStoreSupport.java:3864) [quartz-2.3.2.jar!/:na]
    at org.quartz.impl.jdbcjobstore.JobStoreSupport.acquireNextTriggers(JobStoreSupport.java:2802) [quartz-2.3.2.jar!/:na]
    at org.quartz.core.QuartzSchedulerThread.run(QuartzSchedulerThread.java:287) [quartz-2.3.2.jar!/:na]
Caused by: java.lang.ClassNotFoundException: xxx.xxx.xxx.MyQuartzJob
    at java.net.URLClassLoader.findClass(URLClassLoader.java:382) ~[na:1.8.0_302]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:418) ~[na:1.8.0_302]
    at org.springframework.boot.loader.LaunchedURLClassLoader.loadClass(LaunchedURLClassLoader.java:151) ~[app.jar:2.4.0-SNAPSHOT]
    at java.lang.ClassLoader.loadClass(ClassLoader.java:351) ~[na:1.8.0_302]
    at java.lang.Class.forName0(Native Method) ~[na:1.8.0_302]
    at java.lang.Class.forName(Class.java:348) ~[na:1.8.0_302]
    at org.springframework.util.ClassUtils.forName(ClassUtils.java:284) ~[spring-core-5.2.7.RELEASE.jar!/:5.2.7.RELEASE]
    at org.springframework.scheduling.quartz.ResourceLoaderClassLoadHelper.loadClass(ResourceLoaderClassLoadHelper.java:81) ~[spring-context-support-5.2.7.RELEASE.jar!/:5.2.7.RELEASE]
    at org.springframework.scheduling.quartz.ResourceLoaderClassLoadHelper.loadClass(ResourceLoaderClassLoadHelper.java:87) ~[spring-context-support-5.2.7.RELEASE.jar!/:5.2.7.RELEASE]
    at org.quartz.impl.jdbcjobstore.StdJDBCDelegate.selectJobDetail(StdJDBCDelegate.java:852) ~[quartz-2.3.2.jar!/:na]
    at org.quartz.impl.jdbcjobstore.JobStoreSupport.retrieveJob(JobStoreSupport.java:1390) [quartz-2.3.2.jar!/:na]

Once this error happens, the trigger updates itself to ERROR state and won't fire anymore. The strange thing here is that the trigger already fired successfully a few times (sometimes up to 4 times) and suddenly, at its next iteration, fails to load the class. If I manually update its state to WAITING again it triggers once right after the update, and resumes its schedule: it works for a few cycles, and at some point fails to launch again with the error I copied above, and updates itself to ERROR state.

I have no clue as to why, or how to fix this. There is no concurrency of access on the Quartz database as we are running a single server instance, so I don't understand why the class would be successfully found and loaded several times and then not, on the same version of the deployed server.

Fully qualified classname in the database is correct (package name + class name).

Any advice on this would be greatly appreciated. Feel free to ask more details if needed.



Solution 1:[1]

This is a very common problem encountered in Spring Boot applications where the Spring Quartz scheduler factory creates a Quartz scheduler instance that by default uses org.springframework.scheduling.quartz.ResourceLoaderClassLoadHelper as the class load helper. This class load helper can be seen in your stack trace. Quartz uses the class load helper to load job implementation classes.

To fix the issue, please add the following property to your quartz.properties:

org.quartz.scheduler.classLoadHelper.class = org.quartz.simpl.CascadingClassLoadHelper

Solution 2:[2]

I was trying to understand why I was still seeing errors mentioning the old ClassLoadHelper class after changing it in the config, it turns out we had a second "hidden" instance of our app running on our Cloud provider, with an older version of the code from a few months ago.

It repetitively tried and failed to fire the trigger because the Quartz Job class did not exist at that point, thus setting the trigger to error state in the same database used by the real up-to-date instance. To fix this situation, we just had to get rid of the extraneous instance.

Sorry for the misleading info, I was not aware of this other instance when posting this question.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Jan Moravec
Solution 2 Cécile Fecherolle