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
Iteration
page viaIssues
menu 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_constraint
took227.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)