Fix statement timeout in Duo Workflows credits cron worker
What does this MR do and why?
Ai::DuoWorkflows::FetchSessionCreditsCronWorker was failing on every 15-minute tick in production (gprd) with ActiveRecord::QueryCanceled / PG::QueryCanceled (statement timeout). This MR rewrites the worker's scan query so it can no longer time out, regardless of how far behind the sync cursor has fallen.
https://log.gprd.gitlab.net/app/r/s/v7eCE
Root cause
scan ran:
status IN (3,5,6,7,8) AND updated_at > cursor AND updated_at <= 2.hours.ago
ORDER BY updated_at, id
LIMIT 1000The supporting index, idx_workflows_status_updated_at_id, is (status, updated_at, id) — it leads on status. With an IN list on the leading column, PostgreSQL cannot emit rows in (updated_at, id) order directly, so it adds a Sort node and must materialize every matching row in the window before the LIMIT can apply. The LIMIT was effectively doing nothing.
This was self-reinforcing: the :duo_workflow_session_credits sync cursor only advances after a successful dispatch, so every timeout left the cursor in place and widened the next run's window, making the query even slower next time. It could not recover on its own, and the worker includes CronjobQueue, which disables retries, so nothing else recovered it either.
Also worth noting: the duo_workflow_session_credits_ingestion feature flag does not gate the scan — perform runs scan before any flag check, so turning the flag off would not have stopped the timeouts. Only Gitlab::ClickHouse.globally_enabled_for_analytics? and the suspend_click_house_data_ingestion pause control sit in front of it.
Kibana showed db_main_replica_duration_s of 15.048 out of a total job duration_s of 15.344 — one query burned the entire ~15s statement timeout. The backtrace pointed at ee/app/workers/ai/duo_workflows/fetch_session_credits_cron_worker.rb, in the scan method.
The fix: scan now issues one query branch per billable status (an equality condition on the leading index column) and combines them with from_union(remove_duplicates: false, remove_order: false), then applies the outer ORDER BY and LIMIT. Each branch also carries its own LIMIT. This lets PostgreSQL plan a Merge Append over five bounded Index Scans with no Sort, so the LIMIT actually stops the scan early and runtime no longer scales with how far behind the cursor is.
Supporting change: Ai::DuoWorkflows::Workflow.billable_status_values was extracted so the worker can iterate the individual status values. The with_billable_status scope became unused as a result and was removed; its specs were retargeted at .billable_status_values.
Query plan shape (verified locally on PostgreSQL 17.8; project MINIMUM_POSTGRES_VERSION is 17)
Before:Limit -> Sort (Sort Key: updated_at, id) -> Index Scan using idx_workflows_status_updated_at_id
Index Cond: status = ANY ('{3,5,6,7,8}') AND updated_at rangeAfter:
Limit -> Merge Append (Sort Key: updated_at, id) -> five child [Limit -> Index Scan] nodes, one per status
each Index Cond: status = N AND updated_at rangeDatabase review
This changes a query against duo_workflows_workflows, a large table, so this MR needs database review.
Before
| Before |
|---|
|
|
|
https://console.postgres.ai/gitlab/projects/gitlab-production-main/sessions/58273/commands/162941 |
After
| After |
|---|
|
|
|
https://console.postgres.ai/gitlab/projects/gitlab-production-main/sessions/58273/commands/162944 |
Deployment note
After this deploys, the stuck sync cursor does not jump forward instantly. It advances by one batch (up to MAX_ROWS_PER_RUN = 1000 rows) per 15-minute tick, starting from wherever it froze. Recovery is gradual catch-up, not immediate.
References
- https://gitlab.com/gitlab-org/gitlab/-/issues/630263 — related context, not resolved by this MR. That issue covers a different problem in the same worker (backfilling historical credits); this MR only addresses the statement-timeout/query-plan bug.
Screenshots or screen recordings
Not applicable — this MR has no UI change.
How to set up and validate locally
- To see the plan change directly, run
EXPLAINon the SQL generated byscanand confirm the plan usesMerge Appendrather thanSort. - Run the relevant specs (the worker spec is tagged
:click_house, so it needs the tag to run at all):bundle exec rspec ee/spec/workers/ai/duo_workflows/fetch_session_credits_cron_worker_spec.rb --tag click_housebundle exec rspec ee/spec/models/ai/duo_workflows/workflow_spec.rb
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.