Skip to content

Fix duplicate retention continuations for Waiting jobs - #11257

Open
Darrick (darjoo) wants to merge 3 commits into
mainfrom
darjoo-retention-continuation-scheduling
Open

Fix duplicate retention continuations for Waiting jobs#11257
Darrick (darjoo) wants to merge 3 commits into
mainfrom
darjoo-retention-continuation-scheduling

Conversation

@darjoo

@darjoo Darrick (darjoo) commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

What & why

Retention continuation scheduling only recognizes Ready and On Hold jobs. Once the dispatcher moves an existing continuation to Waiting, another limit-exceeded notification can create a duplicate.

Include Waiting in the pending-job lookup, but skip Restart for Waiting entries so their status and scheduled-task ID remain unchanged. Preserve the existing restart behavior for Ready and On Hold jobs. Extract the scheduling step into an internal procedure so its regression coverage does not depend on the overnight scheduling window.

Linked work

AB#649571

Azure DevOps Bug 649571

An approved GitHub issue has not been provided.

How I validated this

  • I read the full diff and it contains only changes I intended.
  • I built the affected app(s) locally with no new analyzer warnings.
  • I ran the change in Business Central and confirmed it behaves as expected.
  • I added or updated tests for the new behavior, or explained below why none are needed.

What I tested and the outcome (required — be specific: scenarios, commands, screenshots for UI changes)

  • Added four regression cases in Job Queue Entry Tests: repeated requests preserve a Waiting continuation and its task ID; Ready jobs restart; On Hold jobs restart; and a continuation is created when only a running retention job and an unrelated Waiting job exist. They use the existing task-scheduler mock and automatic rollback.
  • The repository's Test-ObjectIDsAreValid check passed for both modified codeunits, and git diff --check passed.
  • The AL build and Business Central tests were not run: Docker is unavailable, and no offline AL compiler was found.

Risk & compatibility

No schema, permission, telemetry, category, or scheduling-window changes. Waiting continuations remain under dispatcher control; existing Ready and On Hold restart behavior is unchanged. The extracted scheduling procedure is internal.

Recognize Waiting retention jobs as pending continuations without restarting them or replacing their scheduled-task IDs. Preserve Ready and On Hold restart behavior and add deterministic regression coverage.

AB#649571

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@darjoo
Darrick (darjoo) marked this pull request as ready for review September 9, 2026 09:31
@darjoo
Darrick (darjoo) requested a review from a team September 9, 2026 09:31
@darjoo
Darrick (darjoo) requested a review from a team as a code owner September 9, 2026 09:31
@github-actions github-actions Bot added the Team: Integrations GitHub request for Integrations area label Sep 9, 2026
@github-actions github-actions Bot added this to the Version 30.0 milestone Sep 9, 2026
Replace the separate existence check and bare lookup with a guarded FindFirst under UpdLock. Schedule a continuation when no pending entry matches, while preserving Waiting jobs without restarting them.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread src/Layers/W1/BaseApp/System/RetentionPolicy/RetentionPolicyJQ.Codeunit.al Outdated
Comment thread src/Layers/W1/BaseApp/System/RetentionPolicy/RetentionPolicyJQ.Codeunit.al Outdated
Use a ReadCommitted fast path for Waiting jobs, then guard the update-locked re-read before restarting actionable jobs. Clear stale record identity before creating a replacement continuation.

Correct the retention test fixture's attempt counter and remove an ignored using directive that caused AL0789 in Tests-Misc.

AB#649571

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
JobQueueEntry.ScheduleJobQueueEntryForLater(Codeunit::"Retention Policy JQ", CurrentDateTime(), JobQueueCategoryTok, '')
else begin
JobQueueEntry.SetFilter(Status, '%1|%2|%3', JobQueueEntry.Status::Ready, JobQueueEntry.Status::"On Hold", JobQueueEntry.Status::Waiting);
if JobQueueEntry.FindFirst() then begin

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Query}$

ScheduleContinuation() decides its outcome from two FindFirst() calls over a filter that matches Waiting, Ready, and On Hold entries together, but never establishes a deterministic priority among them. Job Queue Entry is not re-sorted here, so which status is returned first depends on key order, not on status. If a Waiting continuation and a Ready/On Hold continuation ever coexist for the same object, this code can restart the non-waiting row instead of honoring the existing Waiting continuation, leaving a duplicate pending job in place. Consider checking for a Waiting entry with its own filtered lookup before falling back to a separate locked lookup for restartable statuses, so the two cases can't race on row order.

Agent judgement — not directly backed by a BCQuality knowledge article.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.41.6

Handled := true;
end;

internal procedure ScheduleContinuation()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Security}$

Extracting the scheduling logic into an internal procedure (ScheduleContinuation) exposes retention-job scheduling to every companion app listed in BaseApp's internalsVisibleTo, but the procedure no longer enforces the caller's session, user-invoked, or time-window guards that gated the original subscriber-inline logic. Access = Internal is API hygiene, not an authorization boundary, so a privileged scheduling operation reached this way should either stay non-exposed or re-validate its preconditions at the new entry point.

Knowledge:

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.41.6


RetentionPolicyLog.LogInfo(RetentionPolicyLogCategory::"Retention Policy - Schedule", RescheduleOnLimitExceededLbl);

ScheduleContinuation();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Telemetry}$

The info log emitted just before calling ScheduleContinuation() ("The job queue entry was scheduled to run again") is written unconditionally, before the actual outcome is known. After this refactor, ScheduleContinuation() has three distinct outcomes: create a new job entry, restart an existing Ready/On Hold entry, or no-op because a Waiting entry already exists. In the no-op/Waiting branch nothing is (re)scheduled, so the log line records an outcome that didn't happen. Move the log call inside ScheduleContinuation() (or add branch-specific messages) so telemetry reflects what was actually done.

Agent judgement — not directly backed by a BCQuality knowledge article.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.41.6

[Test]
[TransactionModel(TransactionModel::AutoRollback)]
[Scope('OnPrem')]
procedure RetentionContinuationPreservesWaitingJob()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$\textbf{🟡\ Medium\ Severity\ —\ Testing}$

The new retention-continuation tests cover a Waiting-only scenario and a Ready/On-Hold-only scenario, but none seeds a Waiting entry together with a Ready or On Hold entry for the same object. That mixed-state case is exactly the risky one for this change, since ScheduleContinuation()'s FindFirst() over all three statuses is not guaranteed to return the Waiting row first. Add a test that creates both entries at once and asserts the Waiting job is preserved and the other entry is left untouched (not restarted, not duplicated).

Agent judgement — not directly backed by a BCQuality knowledge article.

👍 useful · ❤️ especially valuable · 👎 wrong - reply with why · AL review agent v1.41.6

@alexei-dobriansky

Copy link
Copy Markdown
Contributor

Good Sense Reviewer - Round 1

Recommendation: Request Changes

What this PR does

This change makes retention continuation scheduling treat a Waiting job as pending work, and it extracts the scheduling step into ScheduleContinuation() so tests can call it directly. The main path now preserves an existing Waiting task and still restarts Ready or On Hold jobs. One edge case remains: the lookup mixes Waiting with restartable statuses, so row order can decide the branch when both kinds exist.

Problem-solution fit

Fit: Partial

The change matches the reported duplicate-continuation problem and covers the main Waiting, Ready, On Hold, and no-pending-job cases. It does not fully guard the mixed pending-state case where a Waiting entry and a restartable entry are both present for the same retention job.

Suggestions

S1 (🔴 High): Check Waiting before restartable jobs
Check for a Waiting retention job before restarting any Ready or On Hold job. The current mixed-status lookup can return a restartable row even when a Waiting continuation already exists. That can keep duplicate retention jobs queued instead of preserving the Waiting continuation as the single pending job.

Risk assessment and necessity

Risk: The changed branch controls retention policy continuation scheduling. A wrong branch can keep extra jobs queued for the same codeunit, causing redundant retention processing and confusing task state. The change is internal and has no schema or public API effect.

Necessity: The change is needed because a Waiting continuation means work is already queued. Treating it as missing or secondary can create or preserve redundant jobs during limit-exceeded runs; the scope is otherwise narrow and the tests cover the main states.


[AI-PR-REVIEW] version=1 promptVersion=4 system=github pr=11257 round=1 by=alexei-dobriansky at=2026-09-09T22:21:59Z lastSha=aeea331c6531d4648f2115d395bf44df47d92387 reviewKey=6c2cf7abae5376349e1f6af47cabafeb1718df8e077b2c54d4c8217dfca10a1a suggestions=S1@b90ee02f

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Team: Integrations GitHub request for Integrations area

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants