Repository: quartznet/quartznet (2026-03-29T13:50:11Z)

Issue:
RefireImmediately with JobChaining(The job referenced by the trigger does not exist)
I use JobChainingJobListener to connect several Jobs in Scheduler and then start first job.
So I add first Job with trigger like 
`scheduler.ScheduleJob(firstJob, SimpleTrigger);`
and all other jobs I add like
```
jobListener.AddJobChainLink(previousJob.Key, currentJob.Key);
scheduler.AddJob(currentJob, true);
```

It works fine, after **JobWasExecuted** in **jobListener** jobs fire one by one with a chain.

Now I want to use **RefireImmediately** in my jobs like
```
catch (Exception ex)
{
	_logger.Error(ExecuteThrownExceptionMessage, nameof(context.JobDetail.JobType), ex.Message);
	var exc = new JobExecutionException(ex)
	{
		RefireImmediately = true
	};
	throw exc;
}
```

I made environment condition so my first job fails and _reFires_ several times. 
After some tries **Execute** works properly and **JobWasExecuted** in **jobListener** starts.
But when its time to fire next job in the chain we get **"The job referenced by the trigger does not exist"**.
In **RAMJobStore.cs** 
```
if (RetrieveJobInternal(newTrigger.JobKey) == null)
{
                    throw new JobPersistenceException("The job (" + newTrigger.JobKey +
                                                      ") referenced by the trigger does not exist.");
}
```

I do not quite understand how **RefireImmediately** works and why it has problem with the chaining next job.


Discussion:

Patch:
diff --git a/src/Quartz/Core/JobRunShell.cs b/src/Quartz/Core/JobRunShell.cs
index 709f1181b..66dd7ab91 100644
--- a/src/Quartz/Core/JobRunShell.cs
+++ b/src/Quartz/Core/JobRunShell.cs
@@ -244,34 +244,10 @@ public virtual async Task Run(CancellationToken cancellationToken = default)
                 activity.Stop(timeProvider.GetUtcNow(), jobExEx);
                 instrumentation.EndJobExecute(jec.JobRunTime, jobExEx);
 
-                // notify all job listeners
-                if (!await NotifyJobListenersComplete(qs, jec, jobExEx, cancellationToken).ConfigureAwait(false))
-                {
-                    var failureInstCode = SchedulerInstruction.NoInstruction;
-
-                    // update the trigger to determine the correct instruction, even when listener notification fails
-                    try
-                    {
-                        failureInstCode = trigger.ExecutionComplete(jec, jobExEx);
-                        if (logger.IsEnabled(LogLevel.Debug))
-                        {
-                            logger.LogDebug("Trigger instruction : {InstCode}", failureInstCode);
-                        }
-                    }
-                    catch (Exception e)
-                    {
-                        // If this happens, there's a bug in the trigger...
-                        SchedulerException se = new SchedulerException("Trigger threw an unhandled exception.", e);
-                        await qs.NotifySchedulerListenersError("Please report this error to the Quartz developers.", se, cancellationToken).ConfigureAwait(false);
-                    }
-
-                    await qs.NotifyJobStoreJobComplete(trigger, jobDetail, failureInstCode, cancellationToken).ConfigureAwait(false);
-                    break;
-                }
-
                 instCode = SchedulerInstruction.NoInstruction;
 
-                // update the trigger
+                // update the trigger — must happen before listener notifications
+                // so we know whether to refire (and skip notifications) or complete
                 try
                 {
                     instCode = trigger.ExecutionComplete(jec, jobExEx);
@@ -287,6 +263,33 @@ public virtual async Task Run(CancellationToken cancellationToken = default)
                     await qs.NotifySchedulerListenersError("Please report this error to the Quartz developers.", se, cancellationToken).ConfigureAwait(false);
                 }
 
+                // re-Execute job — skip listener notifications so that listeners like
+                // JobChainingJobListener don't see intermediate refire attempts as completions (#663)
+                if (instCode == SchedulerInstruction.ReExecuteJob)
+                {
+                    if (logger.IsEnabled(LogLevel.Debug))
+                    {
+                        logger.LogDebug("Rescheduling trigger to reexecute");
+                    }
+                    jec.IncrementRefireCount();
+                    try
+                    {
+                        Complete(successfulExecution: false);
+                    }
+                    catch (SchedulerException se)
+                    {
+                        await qs.NotifySchedulerListenersError($"Error executing Job {jec.JobDetail.Key}: couldn't finalize execution.", se, cancellationToken).ConfigureAwait(false);
+                    }
+                    continue;
+                }
+
+                // notify all job listeners
+                if (!await NotifyJobListenersComplete(qs, jec, jobExEx, cancellationToken).ConfigureAwait(false))
+                {
+                    await qs.NotifyJobStoreJobComplete(trigger, jobDetail, instCode, cancellationToken).ConfigureAwait(false);
+                    break;
+                }
+
                 // notify all trigger listeners
                 if (!await NotifyTriggerListenersComplete(qs, jec, instCode, cancellationToken).ConfigureAwait(false))
                 {
@@ -308,24 +311,6 @@ public virtual async Task Run(CancellationToken cancellationToken = default)
                     await qs.NotifyJobStoreJobComplete(trigger, jobDetail, instCode, cancellationToken).ConfigureAwait(false);
                     break;
                 }
-                // update job/trigger or re-Execute job
-                if (instCode == SchedulerInstruction.ReExecuteJob)
-                {
-                    if (logger.IsEnabled(LogLevel.Debug))
-                    {
-                        logger.LogDebug("Rescheduling trigger to reexecute");
-                    }
-                    jec.IncrementRefireCount();
-                    try
-                    {
-                        Complete(successfulExecution: false);
-                    }
-                    catch (SchedulerException se)
-                    {
-                        await qs.NotifySchedulerListenersError($"Error executing Job {jec.JobDetail.Key}: couldn't finalize execution.", se, cancellationToken).ConfigureAwait(false);
-                    }
-                    continue;
-                }
 
                 try
                 {


Test patch:
diff --git a/src/Quartz.Tests.Unit/Core/RefireImmediatelyJobChainingTest.cs b/src/Quartz.Tests.Unit/Core/RefireImmediatelyJobChainingTest.cs
new file mode 100644
index 000000000..b8b972abc
--- /dev/null
+++ b/src/Quartz.Tests.Unit/Core/RefireImmediatelyJobChainingTest.cs
@@ -0,0 +1,139 @@
+using System.Collections.Specialized;
+
+using Quartz.Impl;
+using Quartz.Listener;
+
+namespace Quartz.Tests.Unit.Core;
+
+/// <summary>
+/// Regression test for #663: RefireImmediately with JobChainingJobListener
+/// should not trigger the chained job until the refiring job finally completes.
+/// </summary>
+[NonParallelizable]
+public sealed class RefireImmediatelyJobChainingTest
+{
+    [Test]
+    public async Task ChainedJob_ShouldNotFire_UntilRefiringJobCompletes()
+    {
+        // Track execution order
+        RefireTrackingJob.Reset();
+        ChainedTrackingJob.Reset();
+
+        var properties = new NameValueCollection
+        {
+            ["quartz.scheduler.instanceName"] = "RefireChainTest",
+            ["quartz.serializer.type"] = TestConstants.DefaultSerializerType
+        };
+        var schedulerFactory = new StdSchedulerFactory(properties);
+        var scheduler = await schedulerFactory.GetScheduler();
+
+        try
+        {
+            // Set up job chaining: refireJob -> chainedJob
+            var chainingListener = new JobChainingJobListener("testChain");
+            chainingListener.AddJobChainLink(
+                new JobKey("refireJob", "test"),
+                new JobKey("chainedJob", "test"));
+            scheduler.ListenerManager.AddJobListener(chainingListener);
+
+            // refireJob: throws RefireImmediately twice, then succeeds on 3rd attempt
+            var refireJob = JobBuilder.Create<RefireTrackingJob>()
+                .WithIdentity("refireJob", "test")
+                .Build();
+
+            var refireTrigger = TriggerBuilder.Create()
+                .WithIdentity("refireTrigger", "test")
+                .ForJob(refireJob)
+                .StartNow()
+                .Build();
+
+            // chainedJob: records when it executes
+            var chainedJob = JobBuilder.Create<ChainedTrackingJob>()
+                .WithIdentity("chainedJob", "test")
+                .StoreDurably()
+                .Build();
+
+            await scheduler.AddJob(chainedJob, true);
+            await scheduler.ScheduleJob(refireJob, refireTrigger);
+
+            await scheduler.Start();
+
+            // Wait for both jobs to complete
+            Assert.That(ChainedTrackingJob.Completed.Wait(TimeSpan.FromSeconds(10)), Is.True,
+                "Chained job should have completed within 10 seconds");
+
+            // Verify: refireJob executed 3 times (2 refires + 1 final)
+            Assert.That(RefireTrackingJob.ExecutionCount, Is.EqualTo(3),
+                "RefireJob should have executed 3 times (2 refires + final success)");
+
+            // Verify: chainedJob executed exactly once
+            Assert.That(ChainedTrackingJob.ExecutionCount, Is.EqualTo(1),
+                "ChainedJob should have executed exactly once, not on each refire attempt");
+
+            // Verify: chainedJob started AFTER refireJob completed all executions
+            Assert.That(ChainedTrackingJob.FirstExecutionTicks, Is.GreaterThanOrEqualTo(RefireTrackingJob.LastExecutionTicks),
+                "ChainedJob should not start until refireJob finishes all refire attempts");
+        }
+        finally
+        {
+            await scheduler.Shutdown(true).ConfigureAwait(false);
+        }
+    }
+}
+
+public sealed class RefireTrackingJob : IJob
+{
+    private static int executionCount;
+    private static long lastExecutionTicks;
+
+    public static int ExecutionCount => executionCount;
+    public static long LastExecutionTicks => Interlocked.Read(ref lastExecutionTicks);
+
+    public static void Reset()
+    {
+        executionCount = 0;
+        Interlocked.Exchange(ref lastExecutionTicks, 0);
+    }
+
+    public ValueTask Execute(IJobExecutionContext context)
+    {
+        var count = Interlocked.Increment(ref executionCount);
+        Interlocked.Exchange(ref lastExecutionTicks, DateTimeOffset.UtcNow.UtcTicks);
+
+        if (count < 3)
+        {
+            throw new JobExecutionException(new Exception("Retry"), refireImmediately: true);
+        }
+
+        return default;
+    }
+}
+
+public sealed class ChainedTrackingJob : IJob
+{
+    private static int executionCount;
+    private static long firstExecutionTicks;
+    private static readonly ManualResetEventSlim completed = new ManualResetEventSlim(false);
+
+    public static int ExecutionCount => executionCount;
+    public static long FirstExecutionTicks => Interlocked.Read(ref firstExecutionTicks);
+    public static ManualResetEventSlim Completed => completed;
+
+    public static void Reset()
+    {
+        executionCount = 0;
+        Interlocked.Exchange(ref firstExecutionTicks, 0);
+        completed.Reset();
+    }
+
+    public ValueTask Execute(IJobExecutionContext context)
+    {
+        var count = Interlocked.Increment(ref executionCount);
+        if (count == 1)
+        {
+            Interlocked.Exchange(ref firstExecutionTicks, DateTimeOffset.UtcNow.UtcTicks);
+        }
+        completed.Set();
+        return default;
+    }
+}