Use deferrable constraint to bulk update iterations
What does this MR do and why?
An iteration cadence is a container for iterations and has many iterations. Within an iteration cadence, iterations cannot have overlapping dates. Here's a short illustration:
| iteration 1 | iteration 2 | valid? |
|---|---|---|
| Apr 1 ~ 5 | Apr 6 ~ 10 | |
| Apr 1 ~ 5 | Apr 3 ~ 7 |
We validate and enforce that iterations don't have overlapping dates using AR validations and a PG exclusion constraint iteration_start_and_due_date_iterations_cadence_id_constraint.
Sometimes we want to defer the enforcement of the PG constraint to the end of a transaction when we need to bulk update iterations. For example, if an iteration's start date gets pushed to a later date we want to also reschedule the ensuing iterations. Currently, because we don't use a deferrable constraint, we have to carefully coordinate the bulk update of iterations and this adds to the code complexity (the bulk update is done in CreateIterationsInAdvanceService. See !85249 (comment 913642905).)
In this MR, we are setting the PG exclusion constraint iteration_start_and_due_date_iterations_cadence_id_constraint to be deferrable so we can bulk update iterations more easily.
No feature change is included in this MR. Moreover the affected part of the application code is behind a non-default FF :iteration_cadences.
How to test
If you want to see how iterations are updated in bulk:
-
Enable the feature flag
:iteration_cadences. -
Visit any group and navigate to
Iterationpage viaIssuesmenu on the left sidebar. -
Create a new cadence.
-
Edit the cadence duration to observe the bulk update.
cadence = Iterations::Cadence.last
Iterations::Cadences::CreateIterationsInAdvanceService.new(User.automation_bot, cadence).execute
Database review
Migration
- Dropping
iteration_start_and_due_date_iterations_cadence_id_constrainttook227.814 ms. - Adding the updated version of the same constraint (with
DEFERRABLE INITIALLY DEFERRED) took1133.898 ms.
(both on cold cache)
Updated bulk update query
CreateIterationsInAdvanceService class used to update iterations separately. Now they are updated at once inside a transaction.
Before: to update 10 iterations in bulk (10 is the upper limit), we currently run each of the ten updates in a separate transactions:
Only a single transaction shown for readability.
TRANSACTION (0.2ms) COMMIT /*application:sidekiq,correlation_id:01G0NMWCVBNXX1W17FZNSS5QJN,jid:82a7f1b57a9825463b1ddc30,endpoint_id:Iterations::Cadences::CreateIterationsWorker,db_config_name:main,line:/lib/gitlab/database.rb:356:in `commit'*/
↳ lib/gitlab/database.rb:356:in `commit'
Iteration Load (0.3ms) SELECT "sprints".* FROM "sprints" WHERE "sprints"."id" = 7296 LIMIT 1 /*application:sidekiq,correlation_id:01G0NMWCVBNXX1W17FZNSS5QJN,jid:82a7f1b57a9825463b1ddc30,endpoint_id:Iterations::Cadences::CreateIterationsWorker,db_config_name:main,line:/ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:139:in `block in update_existing_iterations!'*/
↳ config/initializers/forbid_sidekiq_in_transactions.rb:58:in `block in committed!'
TRANSACTION (0.1ms) BEGIN /*application:sidekiq,correlation_id:01G0NMWCVBNXX1W17FZNSS5QJN,jid:82a7f1b57a9825463b1ddc30,endpoint_id:Iterations::Cadences::CreateIterationsWorker,db_config_name:main,line:/ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:139:in `block in update_existing_iterations!'*/
↳ ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:139:in `block in update_existing_iterations!'
CACHE Route Load (0.0ms) SELECT "routes".* FROM "routes" WHERE "routes"."source_id" = 126 AND "routes"."source_type" = 'Namespace' LIMIT 1
↳ app/models/concerns/routable.rb:118:in `full_path'
Group Load (0.4ms) SELECT "namespaces"."id", "namespaces"."name", "namespaces"."path", "namespaces"."owner_id", "namespaces"."created_at", "namespaces"."updated_at", "namespaces"."type", "namespaces"."description", "namespaces"."avatar", "namespaces"."membership_lock", "namespaces"."share_with_group_lock", "namespaces"."visibility_level", "namespaces"."request_access_enabled", "namespaces"."ldap_sync_status", "namespaces"."ldap_sync_error", "namespaces"."ldap_sync_last_update_at", "namespaces"."ldap_sync_last_successful_update_at", "namespaces"."ldap_sync_last_sync_at", "namespaces"."description_html", "namespaces"."lfs_enabled", "namespaces"."parent_id", "namespaces"."shared_runners_minutes_limit", "namespaces"."repository_size_limit", "namespaces"."require_two_factor_authentication", "namespaces"."two_factor_grace_period", "namespaces"."cached_markdown_version", "namespaces"."project_creation_level", "namespaces"."runners_token", "namespaces"."file_template_project_id", "namespaces"."saml_discovery_token", "namespaces"."runners_token_encrypted", "namespaces"."custom_project_templates_group_id", "namespaces"."auto_devops_enabled", "namespaces"."extra_shared_runners_minutes_limit", "namespaces"."last_ci_minutes_notification_at", "namespaces"."last_ci_minutes_usage_notification_level", "namespaces"."subgroup_creation_level", "namespaces"."emails_disabled", "namespaces"."max_pages_size", "namespaces"."max_artifacts_size", "namespaces"."mentions_disabled", "namespaces"."default_branch_protection", "namespaces"."unlock_membership_to_ldap", "namespaces"."max_personal_access_token_lifetime", "namespaces"."push_rule_id", "namespaces"."shared_runners_enabled", "namespaces"."allow_descendants_override_disabled_shared_runners", "namespaces"."traversal_ids" FROM "namespaces" WHERE "namespaces"."type" = 'Group' AND "namespaces"."id" = 126 LIMIT 1 /*application:sidekiq,correlation_id:01G0NMWCVBNXX1W17FZNSS5QJN,jid:82a7f1b57a9825463b1ddc30,endpoint_id:Iterations::Cadences::CreateIterationsWorker,db_config_name:main,line:/ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:139:in `block in update_existing_iterations!'*/
↳ ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:139:in `block in update_existing_iterations!'
Iteration Exists? (0.3ms) SELECT 1 AS one FROM "sprints" WHERE "sprints"."iterations_cadence_id" = 8086 AND "sprints"."id" != 7295 AND (sprints.start_date <= '2022-06-05') AND (sprints.due_date >= '2022-05-16') LIMIT 1 /*application:sidekiq,correlation_id:01G0NMWCVBNXX1W17FZNSS5QJN,jid:82a7f1b57a9825463b1ddc30,endpoint_id:Iterations::Cadences::CreateIterationsWorker,db_config_name:main,line:/ee/app/models/ee/iteration.rb:281:in `dates_do_not_overlap'*/
↳ ee/app/models/ee/iteration.rb:281:in `dates_do_not_overlap'
Iteration Update (0.5ms) UPDATE "sprints" SET "updated_at" = '2022-04-15 03:39:48.455018', "start_date" = '2022-05-16', "due_date" = '2022-06-05', "cached_markdown_version" = 1900544, "title_html" = '', "description_html" = '' WHERE "sprints"."id" = 7295 /*application:sidekiq,correlation_id:01G0NMWCVBNXX1W17FZNSS5QJN,jid:82a7f1b57a9825463b1ddc30,endpoint_id:Iterations::Cadences::CreateIterationsWorker,db_config_name:main,line:/ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:139:in `block in update_existing_iterations!'*/
↳ ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:139:in `block in update_existing_iterations!'
(0.4ms) UPDATE sprints SET sequence=t.row_number
FROM (
SELECT id, row_number() OVER (ORDER BY start_date) FROM sprints
WHERE iterations_cadence_id = 8086
) as t
WHERE t.id=sprints.id AND (sprints.sequence IS DISTINCT FROM t.row_number)
/*application:sidekiq,correlation_id:01G0NMWCVBNXX1W17FZNSS5QJN,jid:82a7f1b57a9825463b1ddc30,endpoint_id:Iterations::Cadences::CreateIterationsWorker,db_config_name:main,line:/ee/app/models/iterations/cadence.rb:80:in `update_iteration_sequences'*/
↳ ee/app/models/iterations/cadence.rb:80:in `update_iteration_sequences'
TRANSACTION (0.2ms) COMMIT /*application:sidekiq,correlation_id:01G0NMWCVBNXX1W17FZNSS5QJN,jid:82a7f1b57a9825463b1ddc30,endpoint_id:Iterations::Cadences::CreateIterationsWorker,db_config_name:main,line:/lib/gitlab/database.rb:356:in `commit'*/
After: The updates are done in bulk in a single transaction.
Iteration Load (0.3ms) SELECT "sprints".* FROM "sprints" WHERE "sprints"."iterations_cadence_id" = 8086 AND (start_date > '2022-04-18') ORDER BY "sprints"."due_date" ASC /*application:sidekiq,correlation_id:01G0NMJKYDMT8X5FNJXGB5ZASW,jid:5d0300edbbd566365bb574e4,endpoint_id:Iterations::Cadences::CreateIterationsWorker,db_config_name:main,line:/ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:81:in `existing_iterations_in_advance'*/
↳ ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:81:in `existing_iterations_in_advance'
TRANSACTION (0.2ms) BEGIN /*application:sidekiq,correlation_id:01G0NMJKYDMT8X5FNJXGB5ZASW,jid:5d0300edbbd566365bb574e4,endpoint_id:Iterations::Cadences::CreateIterationsWorker,db_config_name:main,line:/ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:135:in `block (2 levels) in update_existing_iterations!'*/
↳ ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:135:in `block (2 levels) in update_existing_iterations!'
Iteration Update (0.5ms) UPDATE "sprints" SET "start_date" = '2022-04-25', "due_date" = '2022-05-08' WHERE "sprints"."id" = 7294 /*application:sidekiq,correlation_id:01G0NMJKYDMT8X5FNJXGB5ZASW,jid:5d0300edbbd566365bb574e4,endpoint_id:Iterations::Cadences::CreateIterationsWorker,db_config_name:main,line:/ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:135:in `block (2 levels) in update_existing_iterations!'*/
↳ ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:135:in `block (2 levels) in update_existing_iterations!'
Iteration Update (0.3ms) UPDATE "sprints" SET "start_date" = '2022-05-09', "due_date" = '2022-05-22' WHERE "sprints"."id" = 7295 /*application:sidekiq,correlation_id:01G0NMJKYDMT8X5FNJXGB5ZASW,jid:5d0300edbbd566365bb574e4,endpoint_id:Iterations::Cadences::CreateIterationsWorker,db_config_name:main,line:/ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:135:in `block (2 levels) in update_existing_iterations!'*/
↳ ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:135:in `block (2 levels) in update_existing_iterations!'
Iteration Update (0.4ms) UPDATE "sprints" SET "start_date" = '2022-05-23', "due_date" = '2022-06-05' WHERE "sprints"."id" = 7296 /*application:sidekiq,correlation_id:01G0NMJKYDMT8X5FNJXGB5ZASW,jid:5d0300edbbd566365bb574e4,endpoint_id:Iterations::Cadences::CreateIterationsWorker,db_config_name:main,line:/ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:135:in `block (2 levels) in update_existing_iterations!'*/
↳ ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:135:in `block (2 levels) in update_existing_iterations!'
Iteration Update (0.3ms) UPDATE "sprints" SET "start_date" = '2022-06-06', "due_date" = '2022-06-19' WHERE "sprints"."id" = 7297 /*application:sidekiq,correlation_id:01G0NMJKYDMT8X5FNJXGB5ZASW,jid:5d0300edbbd566365bb574e4,endpoint_id:Iterations::Cadences::CreateIterationsWorker,db_config_name:main,line:/ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:135:in `block (2 levels) in update_existing_iterations!'*/
↳ ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:135:in `block (2 levels) in update_existing_iterations!'
Iteration Update (0.3ms) UPDATE "sprints" SET "start_date" = '2022-06-20', "due_date" = '2022-07-03' WHERE "sprints"."id" = 7298 /*application:sidekiq,correlation_id:01G0NMJKYDMT8X5FNJXGB5ZASW,jid:5d0300edbbd566365bb574e4,endpoint_id:Iterations::Cadences::CreateIterationsWorker,db_config_name:main,line:/ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:135:in `block (2 levels) in update_existing_iterations!'*/
↳ ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:135:in `block (2 levels) in update_existing_iterations!'
Iteration Update (0.5ms) UPDATE "sprints" SET "start_date" = '2022-07-04', "due_date" = '2022-07-17' WHERE "sprints"."id" = 7299 /*application:sidekiq,correlation_id:01G0NMJKYDMT8X5FNJXGB5ZASW,jid:5d0300edbbd566365bb574e4,endpoint_id:Iterations::Cadences::CreateIterationsWorker,db_config_name:main,line:/ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:135:in `block (2 levels) in update_existing_iterations!'*/
↳ ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:135:in `block (2 levels) in update_existing_iterations!'
Iteration Update (1.3ms) UPDATE "sprints" SET "start_date" = '2022-07-18', "due_date" = '2022-07-31' WHERE "sprints"."id" = 7300 /*application:sidekiq,correlation_id:01G0NMJKYDMT8X5FNJXGB5ZASW,jid:5d0300edbbd566365bb574e4,endpoint_id:Iterations::Cadences::CreateIterationsWorker,db_config_name:main,line:/ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:135:in `block (2 levels) in update_existing_iterations!'*/
↳ ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:135:in `block (2 levels) in update_existing_iterations!'
Iteration Update (0.4ms) UPDATE "sprints" SET "start_date" = '2022-08-01', "due_date" = '2022-08-14' WHERE "sprints"."id" = 7301 /*application:sidekiq,correlation_id:01G0NMJKYDMT8X5FNJXGB5ZASW,jid:5d0300edbbd566365bb574e4,endpoint_id:Iterations::Cadences::CreateIterationsWorker,db_config_name:main,line:/ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:135:in `block (2 levels) in update_existing_iterations!'*/
↳ ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:135:in `block (2 levels) in update_existing_iterations!'
Iteration Update (0.3ms) UPDATE "sprints" SET "start_date" = '2022-08-15', "due_date" = '2022-08-28' WHERE "sprints"."id" = 7302 /*application:sidekiq,correlation_id:01G0NMJKYDMT8X5FNJXGB5ZASW,jid:5d0300edbbd566365bb574e4,endpoint_id:Iterations::Cadences::CreateIterationsWorker,db_config_name:main,line:/ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:135:in `block (2 levels) in update_existing_iterations!'*/
↳ ee/app/services/iterations/cadences/create_iterations_in_advance_service.rb:135:in `block (2 levels) in update_existing_iterations!'
TRANSACTION (0.2ms) COMMIT /*application:sidekiq,correlation_id:01G0NMJKYDMT8X5FNJXGB5ZASW,jid:5d0300edbbd566365bb574e4,endpoint_id:Iterations::Cadences::CreateIterationsWorker,db_config_name:main,line:/lib/gitlab/database.rb:356:in `commit'*/
Sample plan for a single update (provided as FYI, the query plan didn't change)
explain (analyze, buffers) UPDATE "sprints" SET "start_date" = '2022-08-01', "due_date" = '2022-08-14' WHERE "sprints"."id" = 1;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------
Update on sprints (cost=0.29..3.31 rows=1 width=322) (actual time=3.509..3.510 rows=0 loops=1)
Buffers: shared hit=29 read=3 dirtied=15
I/O Timings: read=2.604
-> Index Scan using sprints_pkey on sprints (cost=0.29..3.31 rows=1 width=322) (actual time=0.043..0.046 rows=1 loops=1)
Index Cond: (id = 1)
Buffers: shared hit=3
Planning Time: 0.518 ms
Execution Time: 3.604 ms
(8 rows)
Related to #354977 (closed)