feat(jobs): client-Java @Scheduled/JobHandler jobs on the shared Quartz scheduler — visible in the Jobs perspective + cluster-safe - #6375
Merged
Conversation
…Quartz scheduler - visible + monitored in the Jobs perspective, cluster-safe Previously ScheduledClassConsumer scheduled client-Java jobs on a private in-JVM Spring ThreadPoolTaskScheduler: they executed but were invisible to the Jobs perspective/job log and fired once PER NODE in a cluster - unlike JS .job/scheduled.ts jobs, which register through the Quartz jobs engine. Now each JobHandler bean / @scheduled method is registered as a real Job definition (engine 'java', handler = FQN [+ '#method']) persisted via JobService and scheduled via JobsManager on the shared clustered scheduler. At cron time the jobs engine dispatches back through the new JavaJobExecutor SPI (implemented in engine-java) to run the client bean, wrapped in the same JobLog triggered/finished/failed as every other engine. Consequences: the job shows in the Jobs perspective with a run log, and fires once cluster-wide. - engine-jobs: JavaJobExecutor SPI + a 'java' branch in JobExecutionService; JobSynchronizer skips runtime-registered rows (RUNTIME_LOCATION_PREFIX) so they are not reaped as registry orphans. - engine-java: depends on engine-jobs; JavaJobExecutorImpl resolves+invokes the client bean; ScheduledClassConsumer registers/unregisters Job rows instead of using a private scheduler; ComponentContainer.instanceOfClassName. - IT: JavaJobDecoratorSampleProjectIT also asserts the jobs appear in /services/jobs (engine 'java') and survive a synchronization pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sert around throwing findByName The registration runs off any tenant thread (class load), so scheduling a 'defined'-group job (which derives a tenant-prefixed name) needs an explicit tenant context - register/unregister per tenant exactly as the JS .job synchronizer does, so the job body runs tenant-scoped at fire time. Also findByName throws (not null) when absent - handle it as a fresh row. Verified: JavaJobDecoratorSampleProjectIT green - jobs execute, appear in /services/jobs with engine 'java', and survive a synchronization pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| * Register one job (a JobHandler class or a single @Scheduled method) as a Job on the shared | ||
| * scheduler. | ||
| */ | ||
| private void register(List<String> sink, String fqn, String handler, String expression) { |
delchev
added a commit
that referenced
this pull request
Aug 9, 2026
…6625) #6375 made client-Java `@Scheduled` / `JobHandler` jobs first-class `Job` definitions on the shared Quartz scheduler, so they are listed, monitored and manageable in the Jobs perspective like any `.job`. One half of that was still missing: the Jobs perspective's Trigger action. `JobService.trigger` — the manual path behind `POST /services/jobs/trigger` — ran the handler through the JavaScript code runner unconditionally, ignoring the job's `engine`. A client-Java job's handler is a class name, not a repository path to a JS module, so triggering one answered 500 while the same job ran fine on its cron. The dispatch now lives in exactly one place, `JobHandlerRunner`, used by both ways a job can start: the scheduled fire (`JobExecutionService`) and the manual trigger. That is the actual defect — the two dispatches were written out separately and drifted. The engine-java guide's job bullet was also still describing the private `ThreadPoolTaskScheduler` that #6375 replaced; it now records the shared Quartz registration and this single dispatch point. Verified against the client-Java job sample: `JavaJobDecoratorSampleProjectIT` triggers a listed `engine: java` job over REST and fails on the pre-fix code with "Expected status code <200> but was <500>", passing with the fix. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Client-Java scheduled jobs — a
@ComponentimplementingJobHandler(cron()/run()) or a@Scheduled-annotated method — were scheduled byScheduledClassConsumeron a private in-JVM SpringThreadPoolTaskScheduler. They executed, but:/services/jobs) — no row, no run log;JobLogtriggered/finished/failed entries;This diverged from JS
.job/scheduled.tsjobs, which register through the Quartz jobs engine and get all of the above.Fix
Register each client-Java job as a real
Jobdefinition (enginejava, handler = the client FQN, optionally#method) viaJobService+JobsManageron the shared clustered Quartz scheduler, per tenant (mirroring the JS job synchronizer). At fire time the jobs engine dispatches back through a newJavaJobExecutorSPI (implemented inengine-java) to run the client bean, wrapped in the sameJobLogas every engine.Result: client-Java jobs now appear in the Jobs perspective with a run log and fire once cluster-wide.
JavaJobExecutorSPI; ajavabranch inJobExecutionService(keeps jobs → javascript layering — the impl is looked up viaObjectProvider);JobSynchronizerskips runtime-registered rows (RUNTIME_LOCATION_PREFIX) so they are not reaped as registry orphans.JavaJobExecutorImplresolves + invokes the client bean (ComponentContainer.instanceOfClassName);ScheduledClassConsumerregisters/unregistersJobrows per tenant instead of using a private scheduler.Test
JavaJobDecoratorSampleProjectIT(self-interface + method-level sample) now additionally asserts the jobs appear inGET /services/jobswith enginejavaand survive a forced synchronization pass. Green locally:Tests run: 1, Failures: 0.Notes / follow-ups
JobsManageruses QuartzCronScheduleBuilder(7-field Quartz), whereas the old private path used SpringCronTrigger(6-field). Expressions already used the Quartz?form in samples; worth a line in the SDK docs.