Flush VSA stage event upserts once the limit is passed

What does this MR do and why?

Analytics::CycleAnalytics::DataLoaderService#upsert_data_with_limit only flushed the pending stage event rows when their count was exactly UPSERT_LIMIT (1000). Each record adds one row per stage, so the count regularly stepped over 1000 without landing on it, and nothing was flushed until the end of the batch. In the namespace crons (IncrementalWorker, ReaggregationWorker), which load every distinct stage, one INSERT ... ON CONFLICT could then carry up to 500 records times the number of stages, on the primary, with no chance for the runtime check to interrupt it.

This MR flushes at or above the limit, so no upsert exceeds UPSERT_LIMIT - 1 plus the rows of one record. The spec lowers UPSERT_LIMIT to 3 and loads issues that add two rows each, which skips the limit on the old code (one upsert of all rows) and flushes twice on the new one.

MR 1 of https://gitlab.com/gitlab-org/gitlab/-/work_items/636175 (Project Bulkhead, Optimize row 23). The stage cap per hierarchy and the cron budgets follow in separate MRs.

References

How to set up and validate locally

  1. Lower the limit in a Rails console and load a group with several issue stages:

    stub = Analytics::CycleAnalytics::DataLoaderService
    stub.send(:remove_const, :UPSERT_LIMIT); stub.const_set(:UPSERT_LIMIT, 3)
    ActiveRecord::Base.logger = Logger.new($stdout)
    stub.new(namespace: Group.find_by_full_path('<group>'), model: Issue).execute
  2. Each INSERT INTO analytics_cycle_analytics_issue_stage_events carries at most UPSERT_LIMIT - 1 + <stages> rows. Before this change, a group whose issues add two rows each produced one INSERT with every row of the batch.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

🤖 Generated with Claude Code

Merge request reports

Loading
Loading