Skip to content

Build1 publisher3 min readPublished

Cronflower trades Quartz's lock table for a scheduler cluster you run yourself

The posts show a two-process design: your app declares tasks with @Task, and a separate stateful scheduler cluster owns the schedule and enforces retry, timeout and misfire policy from outside your code.

The Engineer · Build desk

Illustration accompanying Cronflower trades Quartz's lock table for a scheduler cluster you run yourself

What happened

  • Cronflower is an open-source distributed, stateful scheduler for Spring Boot with a web console, and its posts say it forms its own cluster and needs no external database, broker or coordinator.
  • It splits into two process kinds: a scheduler that keeps task state in a store and decides when each task is due, and an executor, which is your own application declaring task methods.
  • The scheduler, not the application, enforces the production attributes on @Task, so every executor behaves the same way on retries, timeouts, misfires and bounded runs.
  • A second module, cronflow, declares workflows as Spring beans, where @DagNode methods are the steps, a to list gives the edges, and nodes pass data through named channels read from DagState.
  • A DAG can be started from the console with a JSON seed, from a scheduled @Task in the same bean whose return value becomes the input, or from a console task set to Trigger DAG.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • cost Dropping the Quartz lock table and the self-written dashboard does not take stateful infrastructure out of the deployment. The state moves into scheduler processes your team now patches, backs up and restores.
  • constraint A task's whole input surface is one String, so structured arguments have to be encoded into that parameter or fetched inside the method body.
  • decision Adoption turns on the durability of the scheduler cluster's own store, which is what a team has to test before a revenue job depends on it firing.
  • capability Work that previously needed a lock around a shared counter can be expressed as a declared merge, so the correctness argument moves out of your code and into the channel definition.

A method annotated with @Task is discovered on startup and registered with the scheduler, which then owns its schedule [4]. The annotation sits in your repository; the decision about when it fires happens in another process [2]. Your business code runs in the executor, and the scheduler is separate infrastructure you run alongside it [3].

The executor contract is narrow. A task method takes nothing, or a single String, which is the task's initialParameter, and that parameter can be a SpEL template evaluated on the executor at run time, so a value like today's date is recomputed on every fire [5]. The parser options are the distinctive part. Setting parser = "ycron" reads a year-based expression, and the month-based grammar has no way to write 0 0 12 ? ? 200, which in ycron means noon on the 200th day of the year [6]. And iso = "PT1H30M" covers every 90 minutes, an interval plain cron cannot span [7].

The no-database claim rests on the scheduler being stateful itself. It keeps task state in a store, decides when each task is due, and calls out to run it; run several and they form a cluster with a leader [2]. The posts call that persistence a store and go no further, naming no engine and saying nothing about how it replicates between nodes [25]. For a 02:00 rollup to survive the loss of the node holding its state, that store has to be replicated across the cluster, and I would want that documented before a billing job depended on it.

The reliability attributes are checkable against the console screenshots. With maxRetryCount = 2 and retryInterval = 1000, one fire becomes three rows in the execution history: attempt #0 fails, #1 fails, #2 succeeds [8]. The number therefore caps retries and not total runs, at 1 + maxRetryCount [12]. The same view names the scheduler node that dispatched the run and the executor that ran it [9].

timeout = 2000 on a method that sleeps 5000 ms marks the run as timed-out instead of letting it hang [10]. The run is 3000 ms past its budget at that point [11]. The post's verb is "marks" [10]. Whether the executor thread is interrupted would have to be read out of the source, and a 2 s timeout on a 5 s job that keeps running gives you an accurate dashboard while the worker stays busy.

Misfire policy answers the failure the first post opens with, where the box reboots at 02:00 and the nightly rollup quietly does not happen [14]. On recovery the scheduler can SKIP the missed fire, FIRE_ONCE_NOW, or FIRE_ALL of them, and the sample nightly rollup is set to SKIP [13].

The other half of the product is cronflow, the DAG orchestrator, sitting beside cronsmith, the scheduler [16]. It targets a different habit: one cron charges orders at 02:00, a second ships them at 02:15, and the fifteen-minute gap exists because that is usually long enough for the first to finish [17]. Declared as a graph, the ship step waits on an edge. I would copy the fan-in: three scorer nodes write the score channel in parallel, the channel's SUM_INT reducer merges them to 90, MAX gives maxWeight 40, and JOIN_CSV gives factors as credit,income,collateral [19].

What to watch

  • Whether the project documents the scheduler's storage engine and how task state replicates between nodes in the cluster.
  • Whether timeout gains documented cancellation semantics, so an overrunning executor thread is ended and not only recorded.
  • Whether the executor contract widens past one String parameter, which currently forces structured input to be encoded or fetched in-method.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories