flowable/flowable-engine · error · IllegalArgumentException

there must be at least one job entity manager

Error message

there must be at least one job entity manager

What it means

ResetExpiredJobsRunnable is constructed with a varargs array of job entity managers it must scan to reset expired (locked-but-stale) jobs. The constructor enforces that at least one manager is supplied, throwing IllegalArgumentException otherwise. Without a manager the runnable would have nothing to reset, which is a programming error, not a runtime condition.

Source

Thrown at modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/asyncexecutor/ResetExpiredJobsRunnable.java:56

 */
public class ResetExpiredJobsRunnable implements Runnable {

    private static final Logger LOGGER = LoggerFactory.getLogger(ResetExpiredJobsRunnable.class);

    protected final String name;
    protected final AsyncExecutor asyncExecutor;
    protected final Collection<JobInfoEntityManager<? extends JobInfoEntity>> jobInfoEntityManagers;

    protected volatile boolean isInterrupted;
    protected final Object MONITOR = new Object();
    protected final AtomicBoolean isWaiting = new AtomicBoolean(false);

    public ResetExpiredJobsRunnable(String name, AsyncExecutor asyncExecutor,
            JobInfoEntityManager<? extends JobInfoEntity>... jobEntityManagers) {
        this.name = name;
        this.asyncExecutor = asyncExecutor;
        if (jobEntityManagers.length < 1) {
            throw new IllegalArgumentException("there must be at least one job entity manager");
        }
        this.jobInfoEntityManagers = Arrays.asList(jobEntityManagers);
    }

    @Override
    public synchronized void run() {
        LOGGER.info("starting to reset expired jobs for engine {}", getEngineName());
        Thread.currentThread().setName(name);

        while (!isInterrupted) {

            resetJobs();

            // Sleep
            try {

                synchronized (MONITOR) {
                    if (!isInterrupted) {

View on GitHub (pinned to d6d39ce1c6)

Solutions

  1. Pass at least one JobInfoEntityManager (e.g. jobEntityManager, timerJobEntityManager) when constructing the runnable.
  2. If using the standard DefaultAsyncJobExecutor, let it wire the runnable instead of constructing it manually.
  3. Guard custom bootstrap code with a length check / config source so managers are always provided.

Example fix

// before
resetExpiredJobsRunnable = new ResetExpiredJobsRunnable("reset", asyncExecutor);

// after
resetExpiredJobsRunnable = new ResetExpiredJobsRunnable("reset", asyncExecutor,
    jobServiceConfiguration.getJobEntityManager(),
    jobServiceConfiguration.getTimerJobEntityManager());
Defensive patterns

Strategy: validation

Validate before calling

if (jobEntityManagers == null || jobEntityManagers.length == 0) {
    throw new IllegalArgumentException("ResetExpiredJobsRunnable requires at least one job entity manager");
}

Prevention

When it happens

Trigger: new ResetExpiredJobsRunnable(name, asyncExecutor) with an empty varargs array — typically in custom async executor bootstrap code that builds the runnable manually.

Common situations: Custom AsyncExecutor implementations that construct ResetExpiredJobsRunnable directly without passing the timer/message job entity managers; test scaffolding forgetting the managers.

Related errors


AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11). Data as JSON: /api/errors/f25de5357cbd0ccf. Report an issue: GitHub.