Skip to content

[improve][io] Replace Spring Framework with cron-utils in CronTriggerer - #26269

Merged
merlimat merged 1 commit into
apache:masterfrom
lhotari:lh-improve-remove-spring-dep
Aug 4, 2026
Merged

merlimat merged 1 commit into
apache:masterfrom
lhotari:lh-improve-remove-spring-dep

Conversation

@lhotari

@lhotari lhotari commented Aug 4, 2026

Copy link
Copy Markdown
Member

Motivation

CronTriggerer was the only production use of the Spring Framework in the Pulsar code base. It pulled spring-context and spring-core (3.3 MB) into the batch-source connector NARs just to parse a cron expression and run a timer.

Removing the dependency also removes Pulsar's exposure to Spring Framework CVEs. The pinned version, 6.2.12, falls within the affected range (6.2.0 -> 6.2.18.1) of four CVEs published on 2026-06-09:

CVE Issue
CVE-2026-41848 Denial of service via ReDoS in AntPathMatcher (CWE-1333)
CVE-2026-41850 Algorithmic denial of service via SpEL expressions (CWE-407)
CVE-2026-41851 Denial of service via an unbounded SpEL expression cache (CWE-770)
CVE-2026-41852 Arbitrary zero-argument method invocation in SpEL (CWE-863)

Pulsar only used Spring's cron scheduling, so it neither evaluated SpEL nor used AntPathMatcher, and was unlikely to be exploitable through those paths. Dropping the dependency nevertheless removes the flagged artifacts from the connector NARs and ends the need to track Spring CVEs at all.

Modifications

  • Replace ThreadPoolTaskScheduler + CronTrigger with cron-utils (CronType.SPRING53; 175 kB, Apache-2.0, slf4j-api its only dependency) and a self-rescheduling single-threaded ScheduledExecutorService. The next firing is derived from the completion time of the previous one, so firings never overlap — matching how Spring's CronTrigger behaved.
  • Lower-case leading-@ expressions before parsing: Spring matched the @daily style macros case-insensitively, cron-utils only accepts lower case.
  • Parse the expression in init(), so a malformed expression fails there rather than at the first scheduling attempt.
  • Drop the now unused spring-context from batch-data-generator (cron-utils is bundled into the NAR transitively) and spring-core from the broker test dependencies, replacing the single CollectionUtils.isEmpty() use in AdminApiHealthCheckTest with List.isEmpty().
  • Remove the spring version and both library aliases from the version catalog, and wire up the previously orphaned cron-utils version, bumped 9.1.6 -> 9.2.1 because CronType.SPRING53 requires 9.2.0+.

Backward compatibility of the cron dialect

Existing __CRON__ configurations must keep working, so the replacement was validated against Spring rather than assumed. A differential harness compared CronExpression (Spring 6.2.12) with cron-utils SPRING53 over 4,589,882 next-fire comparisons — 528 expressions x 8 time zones x 37 start instants x 6 chained steps:

  • No parsing differences. Six-field syntax, L / W / LW / # / ?, named and numeric day-of-week, all seven macros, whitespace and case all agree, and both reject the same malformed input with IllegalArgumentException.
  • 99.9944% identical firing times. All 255 differing results are in DST-observing zones, where an expression falling in a shifted or repeated hour can lose one firing per year. There were no differences in fixed-offset zones (UTC, Asia/Kolkata, America/Sao_Paulo).

Quartz was also evaluated and rejected: it cannot parse 15 of 17 of the expressions used here — including * * * * * * from BatchSourceTest — because it forbids specifying both day-of-month and day-of-week, and it numbers day-of-week 1=SUN against Spring's 1=MON, which would silently shift every numeric schedule by a day.

Verifying this change

  • Make sure that the change passes the CI checks.

This change added tests and can be verified as follows:

  • Adds CronTriggererTest (33 cases), covering the cron dialect, repeated scheduling, stop() behaviour, thread naming, non-overlapping firings and trigger-failure handling. The expected firing times were generated from Spring itself, so they pin down the previous behaviour.
  • Verified locally: CronTriggererTest green over 6 consecutive runs with retries disabled; quickCheck, sanityCheck, spotlessCheck, checkstyleMain, checkstyleTest all pass; BatchSourceExecutorTest, BatchSourceConfigParseTest, SourceConfigUtilsTest, TestCmdSources and AdminApiHealthCheckTest pass.
  • Confirmed the built pulsar-io-batch-data-generator NAR now bundles cron-utils-9.2.1.jar and no Spring jars.

Does this pull request potentially affect one of the following parts:

If the box was checked, please highlight the changes

  • Dependencies (add or upgrade a dependency)
  • The public API
  • The schema
  • The default values of configurations
  • The threading model
  • The binary protocol
  • The REST endpoints
  • The admin CLI options
  • The metrics
  • Anything that affects deployment

Dependency changes: removes org.springframework:spring-context and org.springframework:spring-core (6.2.12) from the build entirely; adds com.cronutils:cron-utils 9.2.1 (Apache-2.0) to pulsar-io-batch-discovery-triggerers. No LICENSE/NOTICE updates are needed — the server and shell distributions exclude .nar files, so the Spring jars were never listed there.

### Motivation

`CronTriggerer` was the only production use of the Spring Framework in the
Pulsar code base. It pulled `spring-context` and `spring-core` (3.3 MB) into the
batch-source connector NARs just to parse a cron expression and run a timer.

Removing the dependency also removes Pulsar's exposure to Spring Framework CVEs.
The pinned version, 6.2.12, falls within the affected range (6.2.0 -> 6.2.18.1)
of four CVEs published on 2026-06-09:

- CVE-2026-41848: denial of service via ReDoS in `AntPathMatcher`
- CVE-2026-41850: algorithmic denial of service via SpEL expressions
- CVE-2026-41851: denial of service via an unbounded SpEL expression cache
- CVE-2026-41852: arbitrary zero-argument method invocation in SpEL

Pulsar only used Spring's cron scheduling, so it neither evaluated SpEL nor used
`AntPathMatcher`, and was unlikely to be exploitable through those paths. Dropping
the dependency nevertheless removes the flagged artifacts from the connector NARs
and ends the need to track Spring CVEs at all.

### Modifications

- Replace `ThreadPoolTaskScheduler` + `CronTrigger` with cron-utils
  (`CronType.SPRING53`; 175 kB, Apache-2.0, `slf4j-api` its only dependency) and
  a self-rescheduling single-threaded `ScheduledExecutorService`. The next firing
  is derived from the completion time of the previous one, so firings never
  overlap, matching how Spring's `CronTrigger` behaved.
- Lower-case leading-`@` expressions before parsing: Spring matched the
  `@daily` style macros case-insensitively, cron-utils only accepts lower case.
- Parse the expression in `init()`, so a malformed expression fails there rather
  than at the first scheduling attempt.
- Drop the now unused `spring-context` from `batch-data-generator` (cron-utils
  is bundled into the NAR transitively) and `spring-core` from the broker test
  dependencies, replacing the single `CollectionUtils.isEmpty()` use in
  `AdminApiHealthCheckTest` with `List.isEmpty()`.
- Remove the `spring` version and both library aliases from the version catalog,
  and wire up the previously orphaned `cron-utils` version, bumped 9.1.6 ->
  9.2.1 because `CronType.SPRING53` requires 9.2.0+.

The cron dialect is preserved. A differential harness over 4,589,882 next-fire
comparisons (528 expressions x 8 zones x 37 start instants x 6 chained steps)
found no parsing differences and 99.9944% identical firing times. All 255
differing results are in DST-observing zones, where an expression falling in a
shifted or repeated hour can lose one firing per year; there were none in
fixed-offset zones.

### Verifying this change

Adds `CronTriggererTest`, covering the cron dialect, scheduling, stop
behaviour, thread naming and trigger-failure handling. Its expected firing
times were generated from Spring itself, so they pin down the previous
behaviour.

Assisted-by: Claude Code (Opus 5)
@lhotari lhotari added this to the 5.0.0-M2 milestone Aug 4, 2026
@merlimat
merlimat merged commit 534fd89 into apache:master Aug 4, 2026
44 checks passed
Radiancebobo pushed a commit to Radiancebobo/pulsar that referenced this pull request Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants