Backport of 'Stop event purges from moving the stale scan cursor' to 19.5

What does this MR do and why?

This MR backports !262572 (merged) to 19-5-stable-ee.

The MR is a git cherry-pick -x of the squash commit 796359ba. The pick applied with no conflicts. The patch is identical to the default branch commit (same git patch-id).

What is backported

Security::PurgeScansService stores a keyset cursor in Redis. The stale purge cron (Security::Scans::PurgeWorker, every 3 hours) uses this cursor to continue from the last scan that it purged. The backport makes these changes:

  1. Only the stale path (purge_stale_records) writes the cursor. The event path (purge_by_build_ids) does not write it.
  2. When a stale run ends with :drained, the service resets the cursor. The next cron run starts again from the oldest stale scan.
  3. When a stale run ends with :work_remaining or :halted, the service keeps the cursor.

Why

Before this change, both paths wrote the same cursor. The scans of an event purge are not in order with the stale set. An event purge moved the cursor to one of its own scans, which is usually newer than the old stale scans. The next cron run started after that scan and reported :drained. The old stale scans before the cursor stayed in the security_scans table, and the cron did not purge them.

On 19.5, artifact deletes trigger event purges, so the stale cron can skip stale scans. The stale scans then stay in the table.

The related confidential issue https://gitlab.com/gitlab-org/gitlab/-/work_items/633838 has the details.

Risk

  • After the upgrade, no manual Redis change is necessary. The cron continues from the current cursor until a run drains. That run resets the cursor, and the next cron tick starts from the oldest stale scan.
  • On an instance with many skipped scans, that tick starts a catch-up chain. Each run stops at MAX_RUNTIME (3 minutes) or MAX_STALE_SCANS_SIZE (200,000 scans), and the worker enqueues itself again until the backlog is gone.
  • To slow the catch-up when the database is under pressure, enable the security_scans_purge_db_health_check ops flag. It is off by default.

Default branch status

Item Value
Default branch MR !262572 (merged)
Squash commit 796359ba
Merged 2026-10-20
First deployed to GitLab.com production 2026-10-21

Testing on 19-5-stable-ee

I ran these specs on the backport commit, on top of 19-5-stable-ee:

Spec Result
ee/spec/services/security/purge_scans_service_spec.rb and ee/spec/workers/security/scans/purge_worker_spec.rb 36 examples, 0 failures

RuboCop found no offenses in the 2 changed files.

MR acceptance checklist

This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.

  • This MR is backporting a bug fix, documentation update, or spec fix, previously merged in the default branch.
  • The MR that fixed the bug on the default branch has been deployed to GitLab.com (not applicable for documentation or spec changes).
  • The MR title is descriptive (e.g. "Backport of 'title of default branch MR'"). This is important, since the title will be copied to the patch blog post.
  • Required labels have been applied to this merge request
  • This MR has been approved by a maintainer (only one approval is required).
  • Ensure the E2E Omnibus GitLab EE pipeline (triggered by thee2e:test-on-omnibus-ee job) has succeeded. If it has failed (or shows warnings), investigate the failures. If you determine the failures are unrelated, you may proceed. If you need assistance investigating, request help in the #s_developer_experience Slack channel to confirm the failures are unrelated to the merge request.

Note to the merge request author and maintainer

If you have questions about the patch release process, please:

Edited by Gregory Havenga

Merge request reports

Loading
Loading