Skip to content

fix(backgroundjob): don't log ERROR for stale job rows of removed apps (#35589)#41644

Open
DeepDiver1975 wants to merge 1 commit into
masterfrom
fix/issue-35589-stale-job-log
Open

fix(backgroundjob): don't log ERROR for stale job rows of removed apps (#35589)#41644
DeepDiver1975 wants to merge 1 commit into
masterfrom
fix/issue-35589-stale-job-log

Conversation

@DeepDiver1975

Copy link
Copy Markdown
Member

Summary

Fixes #35589 — the background-job runner logged an ERROR-level stack trace on every cron run for stale job rows whose class no longer exists (the "SyncJob does not exist" log spam).

JobList::buildJob() caught the QueryException from \OC::$server->query($row['class']) and always called logException(..., ERROR) before checking whether the class even exists. A stale oc_jobs row left behind by a removed or disabled app therefore produced a recurring ERROR-level stack trace, even though the job was correctly skipped (return null).

Change

Check class_exists() first inside the catch:

  • Class exists but failed to resolve as a service → genuine, actionable DI failure → still logged at ERROR via logException() (unchanged).
  • Class no longer exists (stale row from a removed/disabled app or old version) → logged once at DEBUG with a concise message, then skipped.

Control flow and return values are unchanged (new $class() fallback when the class exists; return null for stale rows).

Tests

Updates the existing JobListTest::testUnknownJobLogsExceptiontestUnknownJobDoesNotLogException: a missing-class job must not call logException and instead logs at debug. (The old test asserted the now-removed ERROR behavior.)

Tagged @group DB (existing class). php -l clean; full PHPUnit not run in the preparation environment.

Note: owncloud/core is in maintenance mode; targets installations on classic ownCloud 10.x.


🤖 This PR was prepared by the Claude Code review agent from the analysis of #35589. Please review carefully before merging.

JobList::buildJob() logged a full ERROR-level stack trace via
logException() whenever a job row referenced a class that no longer
exists — e.g. a stale oc_jobs row left behind by a removed or disabled
app (the "SyncJob does not exist" spam in issue #35589). The job was
already correctly skipped (return null), but the noisy ERROR log
recurred on every cron run for a benign, expected condition.

Check class_exists() first: a class that exists but fails to resolve as
a service is a genuine, actionable DI failure and still logs at ERROR via
logException(); a class that no longer exists (stale row) is now logged
once at DEBUG and skipped. Control flow and return values are unchanged.

Updates the existing JobListTest case accordingly: a missing-class job
must not call logException and instead logs at debug.

Fixes #35589

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
@update-docs

update-docs Bot commented Jun 21, 2026

Copy link
Copy Markdown

Thanks for opening this pull request! The maintainers of this repository would appreciate it if you would create a changelog item based on your changes.

@DeepDiver1975 DeepDiver1975 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🤖 Automated code review by Claude Code review agent

Overview

Fixes #35589 — the cron background-job runner logged an ERROR-level stack trace on every run for stale oc_jobs rows whose class no longer exists (removed/disabled apps). The QueryException catch in JobList::buildJob() is restructured to check class_exists() first: a class that exists but fails DI resolution still logs at ERROR via logException(); a class that no longer exists logs once at DEBUG and returns null. The existing test is renamed testUnknownJobLogsExceptiontestUnknownJobDoesNotLogException to assert the new behavior. +19/-4 across 2 files, targeting master.

Correctness

  • Control flow / return values unchanged. The class-exists branch is byte-for-byte equivalent (logException then new $class()); the missing-class branch still return null. The only behavioral change is downgrading the missing-class log from ERROR (always) to DEBUG (missing only) — exactly the intended fix. The outer AutoloadNotAllowedException handler is untouched. Correct.
  • DEBUG log call is valid. $this->logger is typed ILogger (constructor param). ILogger::debug($message, array $context = []) — the two-argument call debug('Background job class ... skipping', ['app' => 'core']) matches the signature.
  • No silent failure introduced. Genuine DI failures (class present, service unresolvable) keep their ERROR-level logException, so actionable failures remain visible. Only the benign stale-row case is quieted. Good.
  • Minor (non-blocking): the message embeds raw $row['class'] from the DB. For a fix targeting log spam this is fine and consistent with surrounding code, just noting the value is attacker-uncontrolled-but-DB-sourced.

Tests

  • addWrongJob() inserts the class literal 'wrong job title'. listJobs() selects all rows and calls buildJob() on each, so the stale row reaches the catch: \OC::$server->query('wrong job title') throws QueryException, class_exists('wrong job title') is false (contains a space — no such class), so the DEBUG branch fires and null is returned. Path confirmed.
  • Assertions match the implementation precisely: logExceptionnever() (the whole point of the fix); debugatLeastOnce() with stringContains('wrong job title') (the message contains $row['class'] = 'wrong job title') and exact context ['app' => 'core'] (matches the code). The test will PASS.
  • No other test in the file asserts the old ERROR/logException behavior — the single prior occurrence is the renamed test. No stale assertion is left to break.
  • atLeastOnce() (rather than once()) is the right choice — it tolerates other rows without over-constraining.

Caveat: tagged @group DB and full PHPUnit was not run in the prep environment (per PR description). The logic and mock expectations check out by inspection, but a real DB test run is the final confirmation.

Verdict

approve-with-nits. The fix is correct, minimal, behavior-preserving except for the intended ERROR→DEBUG downgrade, and the rewritten test accurately reflects and would pass against the new code. No blocking issues. The only outstanding item is running the @group DB PHPUnit suite to confirm green, which CI should cover.

@phil-davis

Copy link
Copy Markdown
Contributor

@jvillafanez @DeepDiver1975 is there some way to get Claude to create a changelog file when it creates a PR?

Otherwise, we keep having to push an extra commit with the changelog, and then CI runs all over again.

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.

Backgroundjob error messages: SyncJob does not exist

3 participants