You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@fineract.apache.org by "Arnold Galovics (Jira)" <ji...@apache.org> on 2026/10/11 20:13:00 UTC

[jira] [Created] (FINERACT-2923) Execute missed scheduler job runs after restart or resume

Arnold Galovics created FINERACT-2923:
-----------------------------------------

             Summary: Execute missed scheduler job runs after restart or resume
                 Key: FINERACT-2923
                 URL: https://issues.apache.org/jira/browse/FINERACT-2923
             Project: Apache Fineract
          Issue Type: Improvement
            Reporter: Arnold Galovics
            Assignee: Arnold Galovics


✅ Acceptance Criteria
• New property fineract.job.execute-missed-runs (env FINERACT_JOB_EXECUTE_MISSED_RUNS), default false. With it off, behaviour is unchanged.
• Boot: an active job whose stored next_run_time is in the past gets its trigger started at that time, so Quartz applies the misfire instruction. The default policy fires once.
• INCREASE_BUSINESS_DATE_BY_1_DAY uses MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY and fires once per missed run, one after another. INCREASE_COB_DATE_BY_1_DAY stays fire-once.
• Boot un-suspends a tenant with reset_scheduler_on_bootup before registering its jobs, so replayed fires are not vetoed.
• Resume (POST /scheduler?command=start) re-registers each active job with a past next_run_time the same way boot does and clears is_misfired. With the flag off, the existing is_misfired replay is unchanged.
• A job never runs concurrently with itself. Fires while suspended stay vetoed and do not advance next_run_time.
• SchedulerTriggerListener#triggerMisfired logs at INFO instead of ERROR.
• Unit tests with a real in-memory Quartz scheduler cover restart, suspend/resume, restart while suspended, and a multi-day outage (business date advances once per missed run).
• The property and the known limits are documented in batch-jobs.adoc.
 
📘 Background
• Quartz runs in memory per tenant (no QRTZ tables). Boot builds every trigger to start now, so nothing ever looks missed. A fire while suspended is vetoed, which Quartz counts as executed, not missed.
• job.next_run_time already records the first run that never happened: a vetoed fire never advances it. That is enough to drive Quartz's own misfire handling, with no persistent job store.
• The existing resume replay (execute_misfired_jobs) races with itself, see FINERACT-2918.
 
ℹ️ Notes
• Known limits: periodic Standing Instructions match an exact date, so skipped dates are lost. Retained Earning is wrong if the business date jumps over a year end in one step. Jobs outside COB date their postings on the catch-up day (due savings charges, transfer fee, dormancy, delinquency history). Report mailing sends one duplicate per missed run after a long gap.
• Checked per job (40 jobs, 1.15.0): fire-once is correct for nearly all of them. Exceptions: Increase Business Date (once per missed run), Loan COB and Working Capital COB (catch-up, see the COB ticket), Increase COB Date (must not be replayed per missed run while enable_automatic_cob_date_adjustment is on, which is the default).
• Depends on the reset-on-bootup property ticket for the cleanest boot ordering (same lines).
 
⚠️ Out of Scope
• COB catch-up on a replayed COB run (separate ticket).
• Keeping the scheduler suspended across restarts (separate ticket).
• A persistent Quartz job store, and a cross-job dependency or ordering concept.
 
🔧 Technical Notes
• JobRegisterServiceImpl#createTrigger: if the job is active and next_run_time is before now, CronTriggerFactoryBean#setStartTime(nextRunTime). With the flag on, INCREASE_BUSINESS_DATE_BY_1_DAY gets setMisfireInstruction(IGNORE_MISFIRE_POLICY), resolved via jobNameService like createJobDetail.
• JobRegisterServiceImpl#startScheduler: with the flag on, re-register active jobs with a past next_run_time (deleteJob, scheduleJob, saveOrUpdate) and clear is_misfired.
• JobSchedulerServiceImpl#onApplicationEvent: move the reset_scheduler_on_bootup block above the job loop.
• Quartz's RAMJobStore misfire threshold is the 5 second default, so a gap shorter than that simply fires late.
• Code is identical on 1.15.0 and develop apart from Spring Batch 6 imports.
• Tests cannot go in integration-tests: it runs one long-lived server and cannot restart it.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)