Loading
Adds flow schedule execution
What does this MR do and why?
MR 6 (of 7) that addressses: #594180. The goal of the series: users will be able to run AI flows on a cron schedule.
This MR adds flow schedules execution.
The earlier MRs in the stack lets us create an Ai::FlowSchedule row with a next_run_at, but nothing ever reads or acts on it until now. This MR is what makes scheduling functionally real by introducing a dispatcher that watches for due schedules and a worker that actually runs them.
FlowScheduleWorker: cron dispatcher on a jittered cron (3-59/10 * * * *, matching the pipeline schedule load flattening pattern), gated by an instance-wide flag, and uses batching of 500 at a time with a 7s stagger as per implementation designRunFlowScheduleWorker: idempotent, deduplicated executor that advancesnext_run_atbefore executing (drops on failure; a failed tick is skipped, never retried) and records success/failure with 3-strike deactivationRunServicegains aflow_schedule:param and threads trigger metadata (trigger_source, trigger/schedule ids) through the catalog execution chain which is set at the workflow creation sincetrigger_sourceisattr_readonly- We also add an autonomous OAuth token branch in
WorkflowContextGenerationServicesince SA acts alone for autonomous triggers with no user scope
How to set up and validate locally
Prerequisites
- Rails console
- GDK and
letter_openerrunning:http://172.16.123.1:3000/rails/letter_opener
- Set up in Rails (create project, users, trigger and schedule):
1. Project, SA, membership and owner set up
# fetch or create project
project = Project.find(1000000)
# create SA
sa = begin
FactoryBot.create(:service_account,
username: "sa_#{SecureRandom.hex(6)}", email: "sa_#{SecureRandom.hex(6)}@example.com",
provisioned_by_group: begin
FactoryBot.create(:group, path: "group-#{SecureRandom.hex(6)}")
rescue ActiveRecord::RecordInvalid
retry
end)
rescue ActiveRecord::RecordInvalid
retry
end
# Add SA to project
project.add_developer(sa)
sa.refresh_authorized_projects(source: :test)
project.member?(sa) # => true
# Create owner and add to project
owner = project.owners_and_maintainers.first
if owner.nil?
owner = begin
FactoryBot.create(:user, username: "owner_#{SecureRandom.hex(6)}", email: "owner_#{SecureRandom.hex(6)}@example.com")
rescue ActiveRecord::RecordInvalid
retry
end
project.add_owner(owner)
end
2. FF and Duo set up
# enable feature flags
Feature.enable(:ai_flow_schedules)
Feature.enable(:autonomous_service_account_execution, project) # needed for :scheduled event type + RunService's composite-identity bypass
Gitlab::Llm::StageCheck.define_singleton_method(:available?) { |*| true }
# Class-level, not instance-level: the real run loads fresh User records deep inside
# the worker, so a singleton override on a local variable never applies to those.
User.class_eval do
def allowed_to_use?(*)
true
end
end
3. Trigger and schedule set up
# Create scheduled trigger
trigger = Ai::FlowTrigger.create!(
project: project,
user: sa,
event_types: [Ai::FlowTrigger::EVENT_TYPES[:scheduled]],
description: 'Nightly run',
config_path: 'duo/does-not-exist.yml', # guarantees a real failure on first run
filter: { 'scheduled' => { 'frequency' => 'DAILY', 'minute' => 30, 'hour' => 14 } }
)
# Fire trigger
Ai::FlowSchedules::SyncFromTriggerService.new(trigger: trigger).execute
schedule = trigger.flow_schedules.first
schedule.update_column(:next_run_at, 1.minute.ago)
4. Run and verify failure path (run everything in the same block)
result = Ai::FlowSchedule.find(schedule.id)
{
fresh: (Time.current - result.last_run_at < 30),
last_run_status: result.last_run_status,
last_run_error: result.last_run_error,
consecutive_failure_count: result.consecutive_failure_count,
active: result.active
}
# Expect fresh: true, last_run_error: "invalid or missing flow definition", consecutive_failure_count: 1.
5. Drive to 3-strike deactivation
2.times do
Ai::RunFlowScheduleWorker.new.perform(schedule.id, {})
end
result = Ai::FlowSchedule.find(schedule.id)
{
fresh: (Time.current - result.last_run_at < 30),
consecutive_failure_count: result.consecutive_failure_count,
active: result.active
}
Expect fresh: true, consecutive_failure_count: 3, active: false.
6. Confirm the deactivation notification email in http://localhost:3000/rails/letter_opener
7. Success / happy path: fix the flow config, reset and rerun in the same block
trigger.update!(config_path: 'duo/flow.yml')
content = "version: v1\nenvironment: ambient\nimage: alpine:latest\ncommands:\n - echo hello\ncomponents: []\nrouters: []\nflow: []\n"
unless project.repository.blob_data_at(project.repository.root_ref, 'duo/flow.yml')
project.repository.create_file(sa, 'duo/flow.yml', content,
message: 'Add flow config', branch_name: project.repository.root_ref)
end
fresh_schedule = Ai::FlowSchedule.find(schedule.id)
fresh_schedule.update!(active: true, consecutive_failure_count: 0, next_run_at: 1.minute.ago)
Ai::RunFlowScheduleWorker.new.perform(fresh_schedule.id, { 'scheduling' => true })
result = Ai::FlowSchedule.find(schedule.id)
{
fresh: (Time.current - result.last_run_at < 30),
last_run_status: result.last_run_status,
last_run_error: result.last_run_error
}
# Expect fresh: true, last_run_status: "success", last_run_error: nil.
8. Clean up
trigger.destroy # cascades to its flow_schedules
Related Stacked MRs (in order)
- Adds ai_flow_schedules table for scheduled AI f... (!250227 - merged)
- Add scheduled event type to AI flow triggers (!250805 - merged)
- Adds Flow Schedule Model (!250809 - merged)
- Add Flow Schedules API (!250821 - merged)
- Adds Flow Schedule Email Notifications (!250824 - merged)
- Adds flow schedule execution (!250838) (This MR)
- Adds FE changes for Flow Schedules (!250843)
DB Query
Up to date query
SELECT ai_flow_schedules.id, ai_flow_schedules.ai_flow_trigger_id, ai_flow_schedules.project_id
FROM ai_flow_schedules
WHERE active = TRUE AND next_run_at < now()
AND id > ( SELECT id FROM ai_flow_schedules WHERE active = TRUE AND next_run_at < now() ORDER BY id ASC LIMIT 1 OFFSET 499 )
ORDER BY id ASC LIMIT 500;Old query (ignore):
SELECT "ai_flow_schedules".*
FROM "ai_flow_schedules"
WHERE "ai_flow_schedules"."active" = TRUE
AND "ai_flow_schedules"."next_run_at" < now();Query Execution Plan
Old execution plan (w/out seeded data): https://console.postgres.ai/gitlab/projects/gitlab-production-main/sessions/56733/commands/161110- Execution plan with seeded data: https://console.postgres.ai/gitlab/projects/gitlab-production-main-v2/sessions/56764/commands/161176
References
Screenshots or screen recordings
| Before | After |
|---|---|
How to set up and validate locally
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.
Edited by Shola Quadri